From 77750bb36b3e93b29134cf6191d2cdbd55636c47 Mon Sep 17 00:00:00 2001 From: chsami Date: Fri, 14 Nov 2025 04:41:50 +0100 Subject: [PATCH 01/42] feat(transports): add climbing interactions for woodcutting guild --- .../client/plugins/microbot/shortestpath/transports.tsv | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv index aa5f7602a2a..607a382c68c 100644 --- a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv +++ b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv @@ -5190,4 +5190,7 @@ 2310 2919 0 2167 9308 0 Enter;Cave;40736 2167 9308 0 2310 2919 0 Exit;Opening;40737 2763 2951 0 2763 2951 1 Climb-up;Ladder;16683 -2763 2952 1 2763 2951 0 Climb-down;Ladder;16679 \ No newline at end of file +2763 2952 1 2763 2951 0 Climb-down;Ladder;16679 +# woodcutting guild +1574 3483 1 1575 3483 0 Climb-down;Rope ladder;28858 +1575 3483 0 1574 3483 1 Climb-up;Rope ladder;28857 Rope ladder 28857 \ No newline at end of file From b5ea8dcc6da35991c77b83f375ac0ed42e58a246 Mon Sep 17 00:00:00 2001 From: chsami Date: Fri, 14 Nov 2025 05:55:52 +0100 Subject: [PATCH 02/42] feat(tileobjects): implement tile object querying and caching --- .../microbot/api/IEntityQueryable.java | 13 + .../api/tileobject/Rs2TileObjectCache.java | 67 +++++ .../tileobject/Rs2TileObjectQueryable.java | 83 ++++++ .../tileobject/models/Rs2TileObjectModel.java | 264 ++++++++++++++++++ .../api/tileobject/models/TileObjectType.java | 9 + 5 files changed, 436 insertions(+) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/IEntityQueryable.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/Rs2TileObjectCache.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/Rs2TileObjectQueryable.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/models/Rs2TileObjectModel.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/models/TileObjectType.java 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..94dc3117940 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/IEntityQueryable.java @@ -0,0 +1,13 @@ +package net.runelite.client.plugins.microbot.api; + +public interface IEntityQueryable, E> { + Q where(java.util.function.Predicate predicate); + E first(); + E nearest(); + E nearest(int maxDistance); + E withName(String name); + E withNames(String...names); + E withId(int id); + E withIds(int...ids); + java.util.List toList(); +} 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..e68e411224f --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/Rs2TileObjectCache.java @@ -0,0 +1,67 @@ +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.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<>(); + + var tileValues = Microbot.getClient().getTopLevelWorldView().getScene().getTiles()[Microbot.getClient().getTopLevelWorldView().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..7c443447001 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/Rs2TileObjectQueryable.java @@ -0,0 +1,83 @@ +package net.runelite.client.plugins.microbot.api.tileobject; + +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.api.IEntityQueryable; +import net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +public final class Rs2TileObjectQueryable + implements IEntityQueryable { + + private Stream source; + + public Rs2TileObjectQueryable() { + this.source = Rs2TileObjectCache.getObjectsStream(); + } + + @Override + public Rs2TileObjectQueryable where(java.util.function.Predicate predicate) { + source = source.filter(predicate); + return this; + } + + @Override + public Rs2TileObjectModel first() { + return source.findFirst().orElse(null); + } + + @Override + public Rs2TileObjectModel nearest() { + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc == null) { + return null; + } + + return source + .min(java.util.Comparator.comparingInt( + o -> o.getWorldLocation().distanceTo(playerLoc))) + .orElse(null); + } + + @Override + public Rs2TileObjectModel nearest(int maxDistance) { + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc == null) { + return null; + } + + return source + .filter(x -> x.getWorldLocation().distanceTo(playerLoc) <= maxDistance) + .min(java.util.Comparator.comparingInt( + o -> o.getWorldLocation().distanceTo(playerLoc))) + .orElse(null); + } + + @Override + public Rs2TileObjectModel withName(String name) { + return source.filter(x -> x.getName().toLowerCase() == name.toLowerCase()).findFirst().orElse(null); + } + + @Override + public Rs2TileObjectModel withNames(String... names) { + return null; + } + + @Override + public Rs2TileObjectModel withId(int id) { + return null; + } + + @Override + public Rs2TileObjectModel withIds(int... ids) { + return null; + } + + @Override + public java.util.List toList() { + return source.collect(Collectors.toList()); + } +} 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..218ba4ffe5c --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/models/Rs2TileObjectModel.java @@ -0,0 +1,264 @@ +package net.runelite.client.plugins.microbot.api.tileobject.models; + +import lombok.Getter; +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.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 net.runelite.client.plugins.microbot.util.walker.Rs2Walker; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; + +import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; + + +public class Rs2TileObjectModel implements TileObject { + + 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() { + return tileObject.getWorldLocation(); + } + + 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(); + } + + public ObjectComposition getObjectComposition() { + return Microbot.getClientThread().invoke(() -> { + ObjectComposition composition = Microbot.getClient().getObjectDefinition(tileObject.getId()); + if(composition.getImpostorIds() != null) + { + composition = composition.getImpostor(); + } + return composition; + }); + } + + /** + * 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) { + if (Microbot.getClient().getLocalPlayer().getWorldLocation().distanceTo(getWorldLocation()) > 51) { + Microbot.log("Object with id " + getId() + " is not close enough to interact with. Walking to the object...."); + Rs2Walker.walkTo(getWorldLocation()); + return false; + } + + 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) { + Microbot.log("Failed to interact with object " + 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, param1, menuAction.getId(), getId(), -1, action, objName, 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) { + Microbot.log("Failed to interact with object " + ex.getMessage()); + } + + 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 +} From 77975d485d8d56b92226604ccdcceca2ff864e0b Mon Sep 17 00:00:00 2001 From: chsami Date: Fri, 14 Nov 2025 06:02:31 +0100 Subject: [PATCH 03/42] feat(tileobjects): enhance querying with case-insensitive name and ID filters --- .../tileobject/Rs2TileObjectQueryable.java | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) 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 index 7c443447001..875460f3996 100644 --- 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 @@ -1,6 +1,7 @@ package net.runelite.client.plugins.microbot.api.tileobject; import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.api.IEntityQueryable; import net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel; import net.runelite.client.plugins.microbot.util.player.Rs2Player; @@ -58,22 +59,36 @@ public Rs2TileObjectModel nearest(int maxDistance) { @Override public Rs2TileObjectModel withName(String name) { - return source.filter(x -> x.getName().toLowerCase() == name.toLowerCase()).findFirst().orElse(null); + return source.filter(x -> x.getName().equalsIgnoreCase(name)).findFirst().orElse(null); } @Override public Rs2TileObjectModel withNames(String... names) { - return null; + return Microbot.getClientThread().invoke(() -> source.filter(x -> { + for (String name : names) { + if (x.getName().equalsIgnoreCase(name)) { + return true; + } + } + return false; + }).findFirst().orElse(null)); } @Override public Rs2TileObjectModel withId(int id) { - return null; + return source.filter(x -> x.getId() == id).findFirst().orElse(null); } @Override public Rs2TileObjectModel withIds(int... ids) { - return null; + return source.filter(x -> { + for (int id : ids) { + if (x.getId() == id) { + return true; + } + } + return false; + }).findFirst().orElse(null); } @Override From 04c2a3d45afa08e3be4fb2a718d2b769f0376645 Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 15 Nov 2025 07:42:59 +0100 Subject: [PATCH 04/42] fix(transports): correct rope ladder climb-up entry formatting --- .../client/plugins/microbot/shortestpath/transports.tsv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv index 607a382c68c..530e838b97f 100644 --- a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv +++ b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv @@ -5193,4 +5193,4 @@ 2763 2952 1 2763 2951 0 Climb-down;Ladder;16679 # woodcutting guild 1574 3483 1 1575 3483 0 Climb-down;Rope ladder;28858 -1575 3483 0 1574 3483 1 Climb-up;Rope ladder;28857 Rope ladder 28857 \ No newline at end of file +1575 3483 0 1574 3483 1 Climb-up;Rope ladder;28857 "" "" \ No newline at end of file From d03fa174771feafa713bbed5b30e2809f4e78983 Mon Sep 17 00:00:00 2001 From: chsami Date: Fri, 14 Nov 2025 04:41:50 +0100 Subject: [PATCH 05/42] feat(transports): add climbing interactions for woodcutting guild --- .../client/plugins/microbot/shortestpath/transports.tsv | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv index aa5f7602a2a..607a382c68c 100644 --- a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv +++ b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv @@ -5190,4 +5190,7 @@ 2310 2919 0 2167 9308 0 Enter;Cave;40736 2167 9308 0 2310 2919 0 Exit;Opening;40737 2763 2951 0 2763 2951 1 Climb-up;Ladder;16683 -2763 2952 1 2763 2951 0 Climb-down;Ladder;16679 \ No newline at end of file +2763 2952 1 2763 2951 0 Climb-down;Ladder;16679 +# woodcutting guild +1574 3483 1 1575 3483 0 Climb-down;Rope ladder;28858 +1575 3483 0 1574 3483 1 Climb-up;Rope ladder;28857 Rope ladder 28857 \ No newline at end of file From 401fe461ed7ed7e805af7c4b4510d0d66cdf2481 Mon Sep 17 00:00:00 2001 From: chsami Date: Fri, 14 Nov 2025 05:55:52 +0100 Subject: [PATCH 06/42] feat(tileobjects): implement tile object querying and caching --- .../microbot/api/IEntityQueryable.java | 13 + .../api/tileobject/Rs2TileObjectCache.java | 67 +++++ .../tileobject/Rs2TileObjectQueryable.java | 83 ++++++ .../tileobject/models/Rs2TileObjectModel.java | 264 ++++++++++++++++++ .../api/tileobject/models/TileObjectType.java | 9 + 5 files changed, 436 insertions(+) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/IEntityQueryable.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/Rs2TileObjectCache.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/Rs2TileObjectQueryable.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/models/Rs2TileObjectModel.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/models/TileObjectType.java 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..94dc3117940 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/IEntityQueryable.java @@ -0,0 +1,13 @@ +package net.runelite.client.plugins.microbot.api; + +public interface IEntityQueryable, E> { + Q where(java.util.function.Predicate predicate); + E first(); + E nearest(); + E nearest(int maxDistance); + E withName(String name); + E withNames(String...names); + E withId(int id); + E withIds(int...ids); + java.util.List toList(); +} 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..e68e411224f --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/Rs2TileObjectCache.java @@ -0,0 +1,67 @@ +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.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<>(); + + var tileValues = Microbot.getClient().getTopLevelWorldView().getScene().getTiles()[Microbot.getClient().getTopLevelWorldView().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..7c443447001 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/Rs2TileObjectQueryable.java @@ -0,0 +1,83 @@ +package net.runelite.client.plugins.microbot.api.tileobject; + +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.api.IEntityQueryable; +import net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +public final class Rs2TileObjectQueryable + implements IEntityQueryable { + + private Stream source; + + public Rs2TileObjectQueryable() { + this.source = Rs2TileObjectCache.getObjectsStream(); + } + + @Override + public Rs2TileObjectQueryable where(java.util.function.Predicate predicate) { + source = source.filter(predicate); + return this; + } + + @Override + public Rs2TileObjectModel first() { + return source.findFirst().orElse(null); + } + + @Override + public Rs2TileObjectModel nearest() { + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc == null) { + return null; + } + + return source + .min(java.util.Comparator.comparingInt( + o -> o.getWorldLocation().distanceTo(playerLoc))) + .orElse(null); + } + + @Override + public Rs2TileObjectModel nearest(int maxDistance) { + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc == null) { + return null; + } + + return source + .filter(x -> x.getWorldLocation().distanceTo(playerLoc) <= maxDistance) + .min(java.util.Comparator.comparingInt( + o -> o.getWorldLocation().distanceTo(playerLoc))) + .orElse(null); + } + + @Override + public Rs2TileObjectModel withName(String name) { + return source.filter(x -> x.getName().toLowerCase() == name.toLowerCase()).findFirst().orElse(null); + } + + @Override + public Rs2TileObjectModel withNames(String... names) { + return null; + } + + @Override + public Rs2TileObjectModel withId(int id) { + return null; + } + + @Override + public Rs2TileObjectModel withIds(int... ids) { + return null; + } + + @Override + public java.util.List toList() { + return source.collect(Collectors.toList()); + } +} 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..218ba4ffe5c --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileobject/models/Rs2TileObjectModel.java @@ -0,0 +1,264 @@ +package net.runelite.client.plugins.microbot.api.tileobject.models; + +import lombok.Getter; +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.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 net.runelite.client.plugins.microbot.util.walker.Rs2Walker; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; + +import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; + + +public class Rs2TileObjectModel implements TileObject { + + 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() { + return tileObject.getWorldLocation(); + } + + 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(); + } + + public ObjectComposition getObjectComposition() { + return Microbot.getClientThread().invoke(() -> { + ObjectComposition composition = Microbot.getClient().getObjectDefinition(tileObject.getId()); + if(composition.getImpostorIds() != null) + { + composition = composition.getImpostor(); + } + return composition; + }); + } + + /** + * 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) { + if (Microbot.getClient().getLocalPlayer().getWorldLocation().distanceTo(getWorldLocation()) > 51) { + Microbot.log("Object with id " + getId() + " is not close enough to interact with. Walking to the object...."); + Rs2Walker.walkTo(getWorldLocation()); + return false; + } + + 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) { + Microbot.log("Failed to interact with object " + 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, param1, menuAction.getId(), getId(), -1, action, objName, 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) { + Microbot.log("Failed to interact with object " + ex.getMessage()); + } + + 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 +} From 6e18b7dd13f96d47a6d9e201b51c44e6d12f37f9 Mon Sep 17 00:00:00 2001 From: chsami Date: Fri, 14 Nov 2025 06:02:31 +0100 Subject: [PATCH 07/42] feat(tileobjects): enhance querying with case-insensitive name and ID filters --- .../tileobject/Rs2TileObjectQueryable.java | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) 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 index 7c443447001..875460f3996 100644 --- 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 @@ -1,6 +1,7 @@ package net.runelite.client.plugins.microbot.api.tileobject; import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.api.IEntityQueryable; import net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel; import net.runelite.client.plugins.microbot.util.player.Rs2Player; @@ -58,22 +59,36 @@ public Rs2TileObjectModel nearest(int maxDistance) { @Override public Rs2TileObjectModel withName(String name) { - return source.filter(x -> x.getName().toLowerCase() == name.toLowerCase()).findFirst().orElse(null); + return source.filter(x -> x.getName().equalsIgnoreCase(name)).findFirst().orElse(null); } @Override public Rs2TileObjectModel withNames(String... names) { - return null; + return Microbot.getClientThread().invoke(() -> source.filter(x -> { + for (String name : names) { + if (x.getName().equalsIgnoreCase(name)) { + return true; + } + } + return false; + }).findFirst().orElse(null)); } @Override public Rs2TileObjectModel withId(int id) { - return null; + return source.filter(x -> x.getId() == id).findFirst().orElse(null); } @Override public Rs2TileObjectModel withIds(int... ids) { - return null; + return source.filter(x -> { + for (int id : ids) { + if (x.getId() == id) { + return true; + } + } + return false; + }).findFirst().orElse(null); } @Override From c5c487ec38136348b6d8d093be3073a45df7fbf1 Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 15 Nov 2025 07:42:59 +0100 Subject: [PATCH 08/42] fix(transports): correct rope ladder climb-up entry formatting --- .../client/plugins/microbot/shortestpath/transports.tsv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv index 607a382c68c..530e838b97f 100644 --- a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv +++ b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv @@ -5193,4 +5193,4 @@ 2763 2952 1 2763 2951 0 Climb-down;Ladder;16679 # woodcutting guild 1574 3483 1 1575 3483 0 Climb-down;Rope ladder;28858 -1575 3483 0 1574 3483 1 Climb-up;Rope ladder;28857 Rope ladder 28857 \ No newline at end of file +1575 3483 0 1574 3483 1 Climb-up;Rope ladder;28857 "" "" \ No newline at end of file From 2163f45ffc9f126b9c0d095257ecb529363bbc9b Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 15 Nov 2025 07:51:54 +0100 Subject: [PATCH 09/42] remove caching and scheduler --- docs/scheduler/README.md | 195 - docs/scheduler/api/schedulable-plugin.md | 796 ---- docs/scheduler/combat-lock-examples.md | 307 -- docs/scheduler/condition-manager.md | 424 -- .../conditions/location-conditions.md | 136 - .../conditions/logical-conditions.md | 192 - docs/scheduler/conditions/npc-conditions.md | 125 - .../conditions/resource-conditions.md | 177 - docs/scheduler/conditions/skill-conditions.md | 230 -- docs/scheduler/conditions/time-conditions.md | 128 - docs/scheduler/defining-conditions.md | 584 --- .../plugin-schedule-entry-finished-event.md | 175 - .../plugin-schedule-entry-soft-stop-event.md | 153 - .../scheduler/plugin-schedule-entry-merged.md | 753 ---- docs/scheduler/plugin-writers-guide.md | 626 --- .../scheduler/predicate-condition-examples.md | 257 -- docs/scheduler/roadmap.md | 152 - docs/scheduler/schedulable-example-plugin.md | 364 -- docs/scheduler/scheduler-plugin.md | 1009 ----- docs/scheduler/tasks/README.md | 123 - .../tasks/enhanced-schedulable-plugin-api.md | 237 -- docs/scheduler/tasks/plugin-writers-guide.md | 225 - .../tasks/requirements-integration.md | 259 -- docs/scheduler/tasks/requirements-system.md | 197 - .../scheduler/tasks/task-management-system.md | 218 - docs/scheduler/user-guide.md | 469 --- .../client/plugins/microbot/Microbot.java | 14 - .../plugins/microbot/MicrobotConfig.java | 12 - .../plugins/microbot/MicrobotOverlay.java | 286 -- .../plugins/microbot/MicrobotPlugin.java | 146 +- .../client/plugins/microbot/Script.java | 6 - .../GroundItemFilterPreset.java | 141 - .../rs2cachedebugger/NpcFilterPreset.java | 111 - .../rs2cachedebugger/ObjectFilterPreset.java | 159 - .../rs2cachedebugger/RenderStyle.java | 32 - .../Rs2CacheDebuggerConfig.java | 908 ---- .../Rs2CacheDebuggerGroundItemOverlay.java | 157 - .../Rs2CacheDebuggerInfoPanel.java | 326 -- .../Rs2CacheDebuggerNpcOverlay.java | 168 - .../Rs2CacheDebuggerObjectOverlay.java | 231 -- .../Rs2CacheDebuggerPlugin.java | 637 --- .../LocationStartNotificationOverlay.java | 145 - .../VoxPlugins/schedulable/example/README.md | 499 --- .../example/SchedulableExampleConfig.java | 889 ---- .../example/SchedulableExampleOverlay.java | 139 - .../example/SchedulableExamplePlugin.java | 978 ----- ...bleExamplePrePostScheduleRequirements.java | 640 --- ...chedulableExamplePrePostScheduleTasks.java | 186 - .../example/SchedulableExampleScript.java | 495 --- .../example/enums/SpellbookOption.java | 49 - .../example/enums/UnifiedLocation.java | 190 - .../breakhandler/BreakHandlerOverlay.java | 17 +- .../breakhandler/BreakHandlerScript.java | 3 +- .../MicrobotPluginManager.java | 27 +- .../pluginscheduler/SchedulerConfig.java | 336 -- .../pluginscheduler/SchedulerInfoOverlay.java | 345 -- .../pluginscheduler/SchedulerPlugin.java | 3667 ----------------- .../pluginscheduler/SchedulerState.java | 115 - .../api/SchedulablePlugin.java | 687 --- .../pluginscheduler/condition/Condition.java | 304 -- .../condition/ConditionManager.java | 2691 ------------ .../condition/ConditionType.java | 44 - .../condition/location/AreaCondition.java | 153 - .../condition/location/LocationCondition.java | 278 -- .../condition/location/PositionCondition.java | 248 -- .../condition/location/RegionCondition.java | 149 - .../condition/location/readme.md | 28 - .../serialization/AreaConditionAdapter.java | 81 - .../PositionConditionAdapter.java | 76 - .../serialization/RegionConditionAdapter.java | 79 - .../location/ui/LocationConditionUtil.java | 831 ---- .../condition/logical/AndCondition.java | 245 -- .../condition/logical/LockCondition.java | 167 - .../condition/logical/LogicalCondition.java | 1504 ------- .../condition/logical/NotCondition.java | 263 -- .../condition/logical/OrCondition.java | 234 -- .../condition/logical/PredicateCondition.java | 196 - .../condition/logical/enums/UpdateOption.java | 28 - .../LogicalConditionAdapter.java | 117 - .../serialization/NotConditionAdapter.java | 69 - .../condition/npc/NpcCondition.java | 46 - .../condition/npc/NpcKillCountCondition.java | 470 --- .../pluginscheduler/condition/npc/ReadMe.md | 14 - .../NpcKillCountConditionAdapter.java | 77 - .../resource/BankItemCountCondition.java | 364 -- .../resource/GatheredResourceCondition.java | 645 --- .../resource/InventoryItemCountCondition.java | 383 -- .../condition/resource/LootItemCondition.java | 884 ---- .../resource/ProcessItemCondition.java | 804 ---- .../condition/resource/README.md | 30 - .../condition/resource/ResourceCondition.java | 397 -- .../BankItemCountConditionAdapter.java | 76 - .../GatheredResourceConditionAdapter.java | 106 - .../InventoryItemCountConditionAdapter.java | 90 - .../LootItemConditionAdapter.java | 84 - .../ProcessItemConditionAdapter.java | 147 - .../ResourceConditionAdapter.java | 66 - .../ui/ResourceConditionPanelUtil.java | 1405 ------- .../condition/skill/SkillCondition.java | 422 -- .../condition/skill/SkillLevelCondition.java | 450 -- .../condition/skill/SkillXpCondition.java | 455 -- .../SkillLevelConditionAdapter.java | 85 - .../SkillXpConditionAdapter.java | 84 - .../skill/ui/SkillConditionPanelUtil.java | 662 --- .../condition/time/DayOfWeekCondition.java | 1057 ----- .../condition/time/IntervalCondition.java | 781 ---- .../time/SingleTriggerTimeCondition.java | 281 -- .../condition/time/TimeCondition.java | 487 --- .../condition/time/TimeWindowCondition.java | 1292 ------ .../condition/time/enums/RepeatCycle.java | 41 - .../DayOfWeekConditionAdapter.java | 125 - .../time/serialization/DurationAdapter.java | 37 - .../IntervalConditionAdapter.java | 258 -- .../time/serialization/LocalDateAdapter.java | 32 - .../time/serialization/LocalTimeAdapter.java | 33 - .../SingleTriggerTimeConditionAdapter.java | 103 - .../serialization/TimeConditionAdapter.java | 57 - .../TimeWindowConditionAdapter.java | 217 - .../time/ui/TimeConditionPanelUtil.java | 1492 ------- .../time/util/TimeConditionUtil.java | 730 ---- .../condition/ui/ConditionConfigPanel.java | 2883 ------------- .../ui/callback/ConditionUpdateCallback.java | 53 - .../renderer/ConditionTreeCellRenderer.java | 290 -- .../ui/util/ConditionConfigPanelUtil.java | 868 ---- .../condition/varbit/VarbitCondition.java | 559 --- .../condition/varbit/VarbitUtil.java | 249 -- .../serialization/VarbitConditionAdapter.java | 135 - .../varbit/ui/VarbitConditionPanelUtil.java | 940 ----- .../config/ScheduleEntryConfigManager.java | 612 --- .../config/ui/HotkeyButton.java | 89 - .../ui/ScheduleEntryConfigManagerPanel.java | 619 --- .../event/ExecutionResult.java | 93 - ...ginScheduleEntryMainTaskFinishedEvent.java | 55 - ...ginScheduleEntryPostScheduleTaskEvent.java | 38 - ...uleEntryPostScheduleTaskFinishedEvent.java | 53 - ...uginScheduleEntryPreScheduleTaskEvent.java | 41 - ...duleEntryPreScheduleTaskFinishedEvent.java | 53 - .../PluginScheduleEntrySoftStopEvent.java | 45 - .../model/PluginScheduleEntry.java | 3584 ---------------- ...sientAndNonSerializableFieldsStrategy.java | 47 - .../serialization/ScheduledSerializer.java | 205 - .../adapter/ConditionManagerAdapter.java | 105 - .../adapter/ConditionTypeAdapter.java | 303 -- .../adapter/PluginScheduleEntryAdapter.java | 275 -- .../adapter/ZonedDateTimeAdapter.java | 36 - .../adapter/config/AlphaAdapter.java | 30 - .../config/ConfigDescriptorAdapter.java | 87 - .../adapter/config/ConfigGroupAdapter.java | 41 - .../config/ConfigInformationAdapter.java | 40 - .../adapter/config/ConfigItemAdapter.java | 90 - .../config/ConfigItemDescriptorAdapter.java | 107 - .../adapter/config/ConfigSectionAdapter.java | 62 - .../ConfigSectionDescriptorAdapter.java | 51 - .../adapter/config/RangeAdapter.java | 47 - .../adapter/config/UnitsAdapter.java | 40 - .../tasks/AbstractPrePostScheduleTasks.java | 1102 ----- .../microbot/pluginscheduler/tasks/README.md | 263 -- .../ExamplePrePostScheduleRequirements.java | 151 - .../examples/ExamplePrePostScheduleTasks.java | 206 - ...PrePostScheduleTasksOverlayComponents.java | 480 --- .../PrePostScheduleRequirements.java | 1513 ------- .../data/ItemRequirementCollection.java | 1444 ------- .../requirements/enums/OrRequirementMode.java | 20 - .../requirements/enums/RequirementMode.java | 25 - .../enums/RequirementPriority.java | 21 - .../requirements/enums/RequirementType.java | 64 - .../tasks/requirements/enums/TaskContext.java | 7 - .../registry/RequirementRegistry.java | 2833 ------------- .../InventorySetupRequirement.java | 172 - .../requirements/requirement/Requirement.java | 235 -- .../requirement/SpellbookRequirement.java | 520 --- .../collection/LootRequirement.java | 600 --- .../conditional/ConditionalRequirement.java | 522 --- .../conditional/OrderedRequirement.java | 427 -- .../item/InventorySetupPlanner.java | 3097 -------------- .../requirement/item/ItemRequirement.java | 1947 --------- .../item/RunePouchRequirement.java | 328 -- .../requirement/location/LocationOption.java | 148 - .../location/LocationRequirement.java | 759 ---- .../location/ResourceLocationOption.java | 145 - .../logical/LogicalRequirement.java | 879 ---- .../requirement/logical/OrRequirement.java | 543 --- .../requirement/shop/ShopItemRequirement.java | 217 - .../requirement/shop/ShopRequirement.java | 2572 ------------ .../shop/models/CancelledOfferState.java | 171 - .../shop/models/MultiItemConfig.java | 57 - .../shop/models/ShopOperation.java | 9 - .../util/ConditionalRequirementBuilder.java | 232 -- .../util/RequirementSelector.java | 314 -- .../requirements/util/RequirementSolver.java | 347 -- .../tasks/state/FulfillmentStep.java | 72 - .../tasks/state/TaskExecutionState.java | 441 -- .../ui/PrePostScheduleTasksInfoPanel.java | 258 -- .../tasks/ui/RequirementsStatusPanel.java | 434 -- .../tasks/ui/TaskExecutionStatePanel.java | 289 -- .../ui/Antiban/AntibanDialogWindow.java | 53 - .../ui/Antiban/AntibanWindowManager.java | 76 - .../PrioritySpinnerEditor.java | 113 - .../ScheduleFormPanel.java | 1339 ------ .../ScheduleTableModel.java | 17 - .../ScheduleTablePanel.java | 2038 --------- .../ui/SchedulerInfoPanel.java | 1494 ------- .../pluginscheduler/ui/SchedulerPanel.java | 792 ---- .../pluginscheduler/ui/SchedulerWindow.java | 999 ----- .../ui/components/DatePickerPanel.java | 254 -- .../ui/components/DateRangePanel.java | 160 - .../ui/components/DateTimePickerPanel.java | 88 - .../ui/components/InitialDelayPanel.java | 156 - .../ui/components/IntervalPickerPanel.java | 583 --- .../components/SingleDateTimePickerPanel.java | 136 - .../ui/components/TimePickerPanel.java | 184 - .../ui/components/TimeRangePanel.java | 184 - .../ui/layout/DynamicFlowLayout.java | 300 -- .../ui/util/SchedulerUIUtils.java | 155 - .../pluginscheduler/ui/util/UIUtils.java | 515 --- .../util/PluginFilterUtil.java | 531 --- .../util/SchedulerPluginUtil.java | 1223 ------ .../pathfinder/PathfinderConfig.java | 36 +- .../plugins/microbot/util/bank/Rs2Bank.java | 356 -- .../microbot/util/cache/CacheMode.java | 41 - .../util/cache/MemorySizeCalculator.java | 514 --- .../plugins/microbot/util/cache/Rs2Cache.java | 1356 ------ .../microbot/util/cache/Rs2CacheManager.java | 1184 ------ .../util/cache/Rs2GroundItemCache.java | 787 ---- .../microbot/util/cache/Rs2NpcCache.java | 502 --- .../microbot/util/cache/Rs2ObjectCache.java | 604 --- .../microbot/util/cache/Rs2QuestCache.java | 669 --- .../microbot/util/cache/Rs2SkillCache.java | 523 --- .../util/cache/Rs2SpiritTreeCache.java | 688 ---- .../util/cache/Rs2VarPlayerCache.java | 423 -- .../microbot/util/cache/Rs2VarbitCache.java | 507 --- .../microbot/util/cache/model/SkillData.java | 90 - .../util/cache/model/SpiritTreeData.java | 226 - .../microbot/util/cache/model/VarbitData.java | 138 - .../cache/overlay/HoverInfoContainer.java | 142 - .../cache/overlay/Rs2BaseCacheOverlay.java | 215 - .../cache/overlay/Rs2CacheInfoBoxOverlay.java | 154 - .../cache/overlay/Rs2CacheOverlayManager.java | 229 - .../overlay/Rs2GroundItemCacheOverlay.java | 645 --- .../cache/overlay/Rs2NpcCacheOverlay.java | 451 -- .../cache/overlay/Rs2ObjectCacheOverlay.java | 841 ---- .../serialization/CacheSerializable.java | 35 - .../CacheSerializationManager.java | 757 ---- .../cache/serialization/QuestAdapter.java | 54 - .../serialization/QuestStateAdapter.java | 54 - .../cache/serialization/SkillAdapter.java | 54 - .../cache/serialization/SkillDataAdapter.java | 88 - .../serialization/SpiritTreeDataAdapter.java | 147 - .../serialization/SpiritTreePatchAdapter.java | 53 - .../serialization/VarbitDataAdapter.java | 147 - .../util/cache/strategy/CacheOperations.java | 95 - .../cache/strategy/CacheUpdateStrategy.java | 64 - .../util/cache/strategy/PredicateQuery.java | 48 - .../util/cache/strategy/QueryCriteria.java | 15 - .../util/cache/strategy/QueryStrategy.java | 29 - .../util/cache/strategy/ValueWrapper.java | 35 - .../entity/GroundItemUpdateStrategy.java | 536 --- .../strategy/entity/NpcUpdateStrategy.java | 390 -- .../strategy/entity/ObjectUpdateStrategy.java | 720 ---- .../farming/SpiritTreeUpdateStrategy.java | 385 -- .../strategy/simple/QuestUpdateStrategy.java | 387 -- .../strategy/simple/SkillUpdateStrategy.java | 82 - .../simple/VarPlayerUpdateStrategy.java | 103 - .../strategy/simple/VarbitUpdateStrategy.java | 103 - .../util/cache/util/LogOutputMode.java | 24 - .../util/cache/util/Rs2CacheLoggingUtils.java | 492 --- .../cache/util/Rs2GroundItemCacheUtils.java | 1480 ------- .../util/cache/util/Rs2NpcCacheUtils.java | 1185 ------ .../util/cache/util/Rs2ObjectCacheUtils.java | 1423 ------- .../microbot/util/farming/SpiritTree.java | 5 - .../util/farming/SpiritTreeHelper.java | 248 -- .../microbot/util/player/Rs2Player.java | 7 +- .../util/world/WorldHoppingConfig.java | 36 - .../microbot/pluginscheduler/area_map.png | Bin 17509 -> 0 bytes .../pluginscheduler/calendar-icon.png | Bin 1078 -> 0 bytes .../microbot/pluginscheduler/chronometer.png | Bin 23542 -> 0 bytes .../microbot/pluginscheduler/clock.png | Bin 53520 -> 0 bytes .../microbot/pluginscheduler/delete.png | Bin 7842 -> 0 bytes .../microbot/pluginscheduler/location_map.png | Bin 49758 -> 0 bytes .../pluginscheduler/logic-gate-and.png | Bin 5314 -> 0 bytes .../pluginscheduler/logic-gate-or.png | Bin 5218 -> 0 bytes .../microbot/pluginscheduler/loot_icon.png | Bin 892 -> 0 bytes .../microbot/pluginscheduler/not-equal.png | Bin 19767 -> 0 bytes .../microbot/pluginscheduler/old-watch.png | Bin 5617 -> 0 bytes .../microbot/pluginscheduler/padlock.png | Bin 15064 -> 0 bytes .../microbot/pluginscheduler/position.png | Bin 30852 -> 0 bytes .../microbot/pluginscheduler/region.png | Bin 65293 -> 0 bytes .../microbot/pluginscheduler/ungroup.png | Bin 2553 -> 0 bytes 288 files changed, 35 insertions(+), 112952 deletions(-) delete mode 100644 docs/scheduler/README.md delete mode 100644 docs/scheduler/api/schedulable-plugin.md delete mode 100644 docs/scheduler/combat-lock-examples.md delete mode 100644 docs/scheduler/condition-manager.md delete mode 100644 docs/scheduler/conditions/location-conditions.md delete mode 100644 docs/scheduler/conditions/logical-conditions.md delete mode 100644 docs/scheduler/conditions/npc-conditions.md delete mode 100644 docs/scheduler/conditions/resource-conditions.md delete mode 100644 docs/scheduler/conditions/skill-conditions.md delete mode 100644 docs/scheduler/conditions/time-conditions.md delete mode 100644 docs/scheduler/defining-conditions.md delete mode 100644 docs/scheduler/event/plugin-schedule-entry-finished-event.md delete mode 100644 docs/scheduler/event/plugin-schedule-entry-soft-stop-event.md delete mode 100644 docs/scheduler/plugin-schedule-entry-merged.md delete mode 100644 docs/scheduler/plugin-writers-guide.md delete mode 100644 docs/scheduler/predicate-condition-examples.md delete mode 100644 docs/scheduler/roadmap.md delete mode 100644 docs/scheduler/schedulable-example-plugin.md delete mode 100644 docs/scheduler/scheduler-plugin.md delete mode 100644 docs/scheduler/tasks/README.md delete mode 100644 docs/scheduler/tasks/enhanced-schedulable-plugin-api.md delete mode 100644 docs/scheduler/tasks/plugin-writers-guide.md delete mode 100644 docs/scheduler/tasks/requirements-integration.md delete mode 100644 docs/scheduler/tasks/requirements-system.md delete mode 100644 docs/scheduler/tasks/task-management-system.md delete mode 100644 docs/scheduler/user-guide.md delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/GroundItemFilterPreset.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/NpcFilterPreset.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/ObjectFilterPreset.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/RenderStyle.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerConfig.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerGroundItemOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerInfoPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerNpcOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerObjectOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerPlugin.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/LocationStartNotificationOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/README.md delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleConfig.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePlugin.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePrePostScheduleRequirements.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePrePostScheduleTasks.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleScript.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/enums/SpellbookOption.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/enums/UnifiedLocation.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/SchedulerConfig.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/SchedulerInfoOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/SchedulerPlugin.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/SchedulerState.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/api/SchedulablePlugin.java delete mode 100755 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/Condition.java delete mode 100755 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ConditionManager.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ConditionType.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/AreaCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/LocationCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/PositionCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/RegionCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/readme.md delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/AreaConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/PositionConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/RegionConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/ui/LocationConditionUtil.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/AndCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/LockCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/LogicalCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/NotCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/OrCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/PredicateCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/enums/UpdateOption.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/serialization/LogicalConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/serialization/NotConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/NpcCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/NpcKillCountCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/ReadMe.md delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/serialization/NpcKillCountConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/BankItemCountCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/GatheredResourceCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/InventoryItemCountCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/LootItemCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ProcessItemCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/README.md delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ResourceCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/BankItemCountConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/GatheredResourceConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/InventoryItemCountConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/LootItemConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/ProcessItemConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/ResourceConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ui/ResourceConditionPanelUtil.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillCondition.java delete mode 100755 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillLevelCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillXpCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/serialization/SkillLevelConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/serialization/SkillXpConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/ui/SkillConditionPanelUtil.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/DayOfWeekCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/IntervalCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/SingleTriggerTimeCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/TimeCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/TimeWindowCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/enums/RepeatCycle.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/DayOfWeekConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/DurationAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/IntervalConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/LocalDateAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/LocalTimeAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/SingleTriggerTimeConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/TimeConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/TimeWindowConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/ui/TimeConditionPanelUtil.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/util/TimeConditionUtil.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/ConditionConfigPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/callback/ConditionUpdateCallback.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/renderer/ConditionTreeCellRenderer.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/util/ConditionConfigPanelUtil.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/VarbitCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/VarbitUtil.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/serialization/VarbitConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/ui/VarbitConditionPanelUtil.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ScheduleEntryConfigManager.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ui/HotkeyButton.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ui/ScheduleEntryConfigManagerPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/ExecutionResult.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryMainTaskFinishedEvent.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPostScheduleTaskEvent.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPostScheduleTaskFinishedEvent.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPreScheduleTaskEvent.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPreScheduleTaskFinishedEvent.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntrySoftStopEvent.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/model/PluginScheduleEntry.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/ExcludeTransientAndNonSerializableFieldsStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/ScheduledSerializer.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ConditionManagerAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ConditionTypeAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/PluginScheduleEntryAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ZonedDateTimeAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/AlphaAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigDescriptorAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigGroupAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigInformationAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigItemAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigItemDescriptorAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigSectionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigSectionDescriptorAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/RangeAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/UnitsAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/AbstractPrePostScheduleTasks.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/README.md delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleRequirements.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleTasks.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/overlay/PrePostScheduleTasksOverlayComponents.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/PrePostScheduleRequirements.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/data/ItemRequirementCollection.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/OrRequirementMode.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementMode.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementPriority.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementType.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/TaskContext.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/registry/RequirementRegistry.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/InventorySetupRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/Requirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/SpellbookRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/collection/LootRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/conditional/ConditionalRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/conditional/OrderedRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/InventorySetupPlanner.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/ItemRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/RunePouchRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/LocationOption.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/LocationRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/ResourceLocationOption.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/logical/LogicalRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/logical/OrRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/ShopItemRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/ShopRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/CancelledOfferState.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/MultiItemConfig.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/ShopOperation.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/ConditionalRequirementBuilder.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSelector.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSolver.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/state/FulfillmentStep.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/state/TaskExecutionState.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/PrePostScheduleTasksInfoPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/RequirementsStatusPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/TaskExecutionStatePanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/Antiban/AntibanDialogWindow.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/Antiban/AntibanWindowManager.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/PrioritySpinnerEditor.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleFormPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleTableModel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleTablePanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerInfoPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerWindow.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DatePickerPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DateRangePanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DateTimePickerPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/InitialDelayPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/IntervalPickerPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/SingleDateTimePickerPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/TimePickerPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/TimeRangePanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/layout/DynamicFlowLayout.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/util/SchedulerUIUtils.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/util/UIUtils.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/util/PluginFilterUtil.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/util/SchedulerPluginUtil.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/CacheMode.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/MemorySizeCalculator.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2Cache.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2CacheManager.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2GroundItemCache.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2NpcCache.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2ObjectCache.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2QuestCache.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2SkillCache.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2SpiritTreeCache.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2VarPlayerCache.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2VarbitCache.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/SkillData.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/SpiritTreeData.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/VarbitData.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/HoverInfoContainer.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2BaseCacheOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2CacheInfoBoxOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2CacheOverlayManager.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2GroundItemCacheOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2NpcCacheOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2ObjectCacheOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/CacheSerializable.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/CacheSerializationManager.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/QuestAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/QuestStateAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SkillAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SkillDataAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SpiritTreeDataAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SpiritTreePatchAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/VarbitDataAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/CacheOperations.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/CacheUpdateStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/PredicateQuery.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/QueryCriteria.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/QueryStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/ValueWrapper.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/GroundItemUpdateStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/NpcUpdateStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/ObjectUpdateStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/farming/SpiritTreeUpdateStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/QuestUpdateStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/SkillUpdateStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/VarPlayerUpdateStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/VarbitUpdateStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/LogOutputMode.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2CacheLoggingUtils.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2GroundItemCacheUtils.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2NpcCacheUtils.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2ObjectCacheUtils.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/farming/SpiritTreeHelper.java delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/area_map.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/calendar-icon.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/chronometer.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/clock.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/delete.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/location_map.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/logic-gate-and.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/logic-gate-or.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/loot_icon.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/not-equal.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/old-watch.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/padlock.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/position.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/region.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/ungroup.png diff --git a/docs/scheduler/README.md b/docs/scheduler/README.md deleted file mode 100644 index ec9084f7638..00000000000 --- a/docs/scheduler/README.md +++ /dev/null @@ -1,195 +0,0 @@ -# Plugin Scheduler System - -## Overview - -The Plugin Scheduler is a sophisticated system that allows for the automatic scheduling and management of plugins based on various conditions. It provides a flexible framework for defining when plugins should start and stop, using a powerful condition-based approach. - -## User Guides - -- **[User Guide](user-guide.md)**: Comprehensive guide on using the Scheduler Plugin UI -- **[Defining Conditions](defining-conditions.md)**: Detailed instructions on setting up start and stop conditions - -## Key Components - -The Plugin Scheduler system consists of several key components: - -1. **[SchedulerPlugin](scheduler-plugin.md)**: The main plugin that manages the scheduling of other plugins. -2. **[PluginScheduleEntry](plugin-schedule-entry-merged.md)**: Represents a scheduled plugin with start and stop conditions. -3. **[ConditionManager](conditions/README.md)**: Manages logical conditions for plugin scheduling in a hierarchical structure. -4. **[Condition](conditions/README.md)**: The base interface for all conditions that determine when plugins should run. -5. **[SchedulablePlugin](schedulable-plugin.md)**: Interface that plugins must implement to be schedulable by the Scheduler. - -## Making Your Plugin Schedulable - -To make your plugin schedulable by the Plugin Scheduler, follow these steps: - - - -1. **Implement the `SchedulablePlugin` interface**: - ```java - public class MyPlugin extends Plugin implements SchedulablePlugin { - // Plugin implementation... - } - ``` - -2. **Define stop conditions**: - ```java - @Override - public LogicalCondition getStopCondition() { - // Create conditions that determine when your plugin should stop - OrCondition orCondition = new OrCondition(); - - // Add time-based condition to stop after 30 minutes - orCondition.addCondition(IntervalCondition.createRandomized( - Duration.ofMinutes(25), - Duration.ofMinutes(30) - )); - - // Add inventory-based condition to stop when inventory is full - // Add other conditions as needed - - return orCondition; - } - ``` - -3. **Define optional start conditions**: - ```java - @Override - public LogicalCondition getStartCondition() { - // Create conditions that determine when your plugin can start - // Return null if the plugin can start anytime - } - ``` - -4. **Implement the soft stop event handler**: - ```java - @Override - @Subscribe - public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - if (event.getPlugin() == this) { - // Save state if needed - // Clean up resources - } - } - ``` - -For complete details about implementing the SchedulablePlugin interface, see the [SchedulablePlugin documentation](schedulable-plugin.md). - -## Condition System - -The heart of the Plugin Scheduler is its condition system, which allows for complex logic to determine when plugins should start and stop. Conditions can be combined using logical operators (AND/OR) to create sophisticated scheduling rules. - -### Condition Types - -The system supports various types of conditions, each serving a specific purpose: - -1. **[Time Conditions](conditions/time-conditions.md)**: Schedule plugins based on time-related factors such as intervals, specific times, or day of week. -2. **[Skill Conditions](conditions/skill-conditions.md)**: Trigger plugins based on skill levels or experience. -3. **[Resource Conditions](conditions/resource-conditions.md)**: Manage plugins based on inventory items, gathered resources, or loot. -4. **[Location Conditions](conditions/location-conditions.md)**: Control plugins based on player position or area. -5. **[NPC Conditions](conditions/npc-conditions.md)**: Trigger plugins based on NPC-related events. -6. **[Logical Conditions](conditions/logical-conditions.md)**: Combine other conditions using logical operators (AND, OR, NOT). - -### Lock Condition - -The `LockCondition` is a special condition that can prevent a plugin from being stopped during critical operations: - -```java -// In your plugin's getStopCondition method: -LockCondition lockCondition = new LockCondition("Critical banking operation in progress", true); -AndCondition andCondition = new AndCondition(); -andCondition.addCondition(orCondition); // Other stop conditions -andCondition.addCondition(lockCondition); // Add the lock condition -return andCondition; -``` - -You can then control the lock in your plugin code: - -```java -// Lock to prevent stopping during critical operations -lockCondition.lock(); - -// Critical operation here... - -// Unlock when safe to stop -lockCondition.unlock(); -``` - -The lock condition ensures that your plugin won't be interrupted during critical operations, such as banking, trading, or complex interactions that should not be interrupted. - -## Start and Stop Conditions - -Each scheduled plugin can have both start and stop conditions: - -- **Start Conditions**: Determine when a plugin should be activated. -- **Stop Conditions**: Determine when a plugin should be deactivated. - -These conditions operate independently, allowing for flexible plugin lifecycle management. The PluginScheduleEntry class manages these conditions through separate ConditionManager instances. - -## Plugin Scheduling Events - -The scheduler uses events to communicate with plugins about their lifecycle: - -- **[Plugin Schedule Entry Soft Stop Event](plugin-schedule-entry-soft-stop-event.md)**: Sent by the scheduler to request plugins to stop gracefully -- **[Plugin Schedule Entry Finished Event](plugin-schedule-entry-finished-event.md)**: Sent by plugins to notify the scheduler they've completed their task - -## Usage Examples - -### Basic Scheduling - -```java -// Schedule a plugin to run every 30 minutes -PluginScheduleEntry entry = new PluginScheduleEntry( - "MyPlugin", - Duration.ofMinutes(30), - true, // enabled - true // allow random scheduling -); -``` - -### Advanced Condition-Based Scheduling - -```java -// Create a schedule entry -PluginScheduleEntry entry = new PluginScheduleEntry("MyPlugin", true); - -// Add a time window condition (run between 9 AM and 5 PM) -entry.addStartCondition(new TimeWindowCondition( - LocalTime.of(9, 0), - LocalTime.of(17, 0) -)); - -// Add a stop condition (stop when inventory is full) -entry.addStopCondition(new InventoryItemCountCondition( - ItemID.ANY, - 28, - -)); - -// Register the scheduled entry -schedulerPlugin.registerScheduledPlugin(entry); -``` - -## Example Implementation - -For a complete example of a schedulable plugin, see the [SchedulableExamplePlugin](schedulable-example-plugin.md), which demonstrates all aspects of making a plugin work with the scheduler system. - -## Further Documentation - -For more detailed information about each component, refer to the specific documentation files: - -- [SchedulerPlugin](scheduler-plugin.md) -- [PluginScheduleEntry](plugin-schedule-entry-merged.md) -- [SchedulablePlugin](api/schedulable-plugin.md) -- [Plugin Schedule Entry Soft Stop Event](plugin-schedule-entry-soft-stop-event.md) -- [Plugin Schedule Entry Finished Event](plugin-schedule-entry-finished-event.md) -- [SchedulableExamplePlugin](schedulable-example-plugin.md) - -For condition-specific documentation: - -- [Time Conditions](conditions/time-conditions.md) -- [Skill Conditions](conditions/skill-conditions.md) -- [Resource Conditions](conditions/resource-conditions.md) -- [Location Conditions](conditions/location-conditions.md) -- [NPC Conditions](conditions/npc-conditions.md) -- [Logical Conditions](conditions/logical-conditions.md) \ No newline at end of file diff --git a/docs/scheduler/api/schedulable-plugin.md b/docs/scheduler/api/schedulable-plugin.md deleted file mode 100644 index e62328e3684..00000000000 --- a/docs/scheduler/api/schedulable-plugin.md +++ /dev/null @@ -1,796 +0,0 @@ -# SchedulablePlugin Interface - -## Overview - -The `SchedulablePlugin` interface is the core integration point between standard RuneLite plugins and the Microbot Plugin Scheduler system. It defines a contract that plugins must implement to participate in the automated scheduling system, allowing them to be started and stopped based on configurable conditions while maintaining control over their execution lifecycle. - -This interface leverages the event-based architecture of RuneLite to enable communication between the scheduler and plugins, with methods that define start and stop conditions, handle state transitions, and provide mechanisms for critical section protection. - -## Interface Architecture - -The `SchedulablePlugin` interface is designed with a combination of required methods that must be implemented by each plugin and default methods that provide standardized behavior. This approach allows plugins to focus on their specific scheduling requirements while inheriting common functionality from the interface. - -The interface follows these core design principles: - -1. **Condition-based Scheduling**: Uses logical conditions to determine when plugins should start or stop -2. **Event-driven Communication**: Relies on RuneLite's event system for lifecycle notifications -3. **Graceful Termination**: Provides mechanisms for both soft and hard stops -4. **Critical Section Protection**: Implements a locking system to prevent interruption during sensitive operations - -## Key Methods - -### Essential Methods for Implementation - -#### `void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event)` - -This method handles the post-schedule task event from the scheduler, which is triggered when stop conditions are met or when manual stop is initiated. While the SchedulablePlugin interface provides a default no-op implementation, plugins should override this method to ensure they stop gracefully, preserving state and cleaning up resources before terminating. - -The implementation is expected to: - -1. Verify the event is targeted at this specific plugin -2. Save any current state if needed -3. Clean up resources and close interfaces -4. Schedule the actual stop on the RuneLite client thread -5. Disable and stop the plugin through the Microbot plugin manager - -### Start and Stop Condition Methods - -#### `LogicalCondition getStartCondition()` - -Defines when a plugin is eligible to start. The default implementation returns an empty `AndCondition`, meaning the plugin can start at any time if no specific condition is provided. Plugins can override this to specify precise starting conditions like time windows, player locations, or game states. - -The scheduler evaluates this condition before starting the plugin. If it returns `null` or the condition is met, the plugin is eligible to start. - -#### `LogicalCondition getStopCondition()` - -Specifies when a plugin should stop running. The default implementation returns an empty `AndCondition`, meaning the plugin would run indefinitely unless manually stopped. Plugins should override this to define appropriate stop conditions, which might include: - -- Time limits or specific time windows -- Resource counts (items collected, XP gained) -- Player state (inventory full, health low) -- Game state (logged out, in combat) - -The scheduler continuously monitors these conditions while the plugin runs. - -### State Management Methods - -#### `void onStopConditionCheck()` - -This method is called periodically (approximately once per second) while the plugin is running, just before the stop conditions are evaluated. Its purpose is to allow plugins to update any dynamic state information that might affect condition evaluation. - -This is particularly useful when conditions depend on changing game state, such as inventory contents or skill levels. Plugins can use this hook to keep condition evaluation accurate without having to implement separate timer logic. - -#### `void reportFinished(String reason, boolean success)` - -Provides a way for plugins to proactively indicate completion without waiting for stop conditions to trigger. This method posts a `PluginScheduleEntryMainTaskFinishedEvent` that notifies the scheduler the plugin has finished its task. - -The implementation handles various edge cases: - -- If the scheduler isn't loaded, it directly stops the plugin -- If no plugin is currently running in the scheduler, it stops itself -- If another plugin is running, it gracefully handles the mismatch - -This method is commonly used when a plugin has met its objective (like completing a quest) or encountered a situation where it cannot continue (like running out of resources). - -#### `boolean allowHardStop()` - -Indicates whether a plugin supports being forcibly terminated if it doesn't respond to a soft stop request. The default implementation returns `false`, meaning plugins will only be stopped gracefully. Plugins can override this to allow hard stops in specific situations. - -### Lock Management Methods - -The interface includes a comprehensive locking system that prevents plugins from being stopped during critical operations. - -#### `LockCondition getLockCondition(Condition stopConditions)` - -Retrieves the lock condition associated with a plugin's stop conditions. The default implementation recursively searches through the condition structure to find a `LockCondition` instance. - -#### `boolean isLocked(Condition stopConditions)` - -Checks if the plugin is currently locked, preventing it from being stopped. - -#### `boolean lock(Condition stopConditions)` - -Activates the lock to prevent the plugin from being stopped. Returns `true` if successful. - -#### `boolean unlock(Condition stopConditions)` - -Deactivates the lock, allowing the plugin to be stopped when conditions are met. Returns `true` if successful. - -#### `Boolean toggleLock(Condition stopConditions)` - -Toggles the lock state and returns the new state (`true` for locked, `false` for unlocked). - -## Stop Mechanisms - -Plugins can be stopped through various mechanisms: - -1. **Manual Stop**: User explicitly stops the plugin - - Appears as `StopReason.MANUAL_STOP` - - Highest priority, will always attempt to stop - - Flow: User Interface → SchedulerPlugin.forceStopCurrentPluginScheduleEntry() → PluginScheduleEntry.stop() → Plugin stops - -2. **Plugin Finished**: Plugin self-reports completion using `reportFinished()` - - Appears as `StopReason.PLUGIN_FINISHED` - - Indicates normal completion - - Flow: Plugin.reportFinished() → PluginScheduleEntryMainTaskFinishedEvent → SchedulerPlugin.onPluginScheduleEntryMainTaskFinishedEvent() → PluginScheduleEntry.stop() → Plugin stops - -3. **Stop Conditions Met**: When plugin or user-defined stop conditions are satisfied - - Appears as `StopReason.SCHEDULED_STOP` - - Follows the soft-stop/hard-stop pattern - - Flow: SchedulerPlugin.checkCurrentPlugin() → PluginScheduleEntry.checkConditionsAndStop() → PluginScheduleEntry.softStop() → PluginScheduleEntryPostScheduleTaskEvent → Plugin.onPluginScheduleEntryPostScheduleTaskEvent() → Plugin stops - -4. **Error**: An exception occurs in the plugin - - Appears as `StopReason.ERROR` - - Immediate stop without soft-stop sequence - - Flow: Exception → PluginScheduleEntry.setLastStopReasonType(ERROR) → Plugin disabled → SchedulerPlugin returns to SCHEDULING state - -5. **Hard Stop**: Forced termination after soft-stop timeout - - Appears as `StopReason.HARD_STOP` - - Last resort when plugin doesn't respond to soft-stop - - Flow: Timeout after soft-stop → PluginScheduleEntry.hardStop() → Microbot.stopPlugin() → Plugin forcibly terminated - -## Integration with Scheduler Architecture - -The `SchedulablePlugin` interface integrates with the broader scheduler architecture in several key ways: - -1. **Plugin Registry**: The scheduler maintains a registry of `PluginScheduleEntry` objects, each referencing a `Plugin` that implements `SchedulablePlugin`. - -2. **Condition Management**: The scheduler continuously evaluates both start and stop conditions through a `ConditionManager` that separates plugin-defined conditions from user-defined ones. - -3. **Event Communication**: The scheduler posts events like `PluginScheduleEntryPostScheduleTaskEvent` to initiate plugin stops, and receives events like `PluginScheduleEntryMainTaskFinishedEvent` when plugins self-report completion. - -4. **Lifecycle Management**: The scheduler controls when plugins are enabled or disabled based on their schedule and conditions, but delegates the actual stopping process to the plugins themselves through the interface methods. - -The relationship can be visualized as follows: - -```ascii -┌─────────────────────┐ schedules ┌───────────────────┐ -│ SchedulerPlugin ├────────────────────â”Ī PluginScheduleEntry│ -│ (Orchestrator) │ │ (Data Model) │ -└─────────┮───────────┘ └─────────┮─────────┘ - │ │ - │ manages │ - │ │ - ▾ ▾ -┌─────────────────────┐ implements ┌───────────────────────┐ -│ Regular RuneLite │◄───────────────â”Ī SchedulablePlugin │ -│ Plugin │ │ (API) │ -└─────────────────────┘ └───────────────────────┘ -``` - -When the scheduler is running: - -1. The `SchedulerPlugin` periodically checks each registered `PluginScheduleEntry` -2. If a plugin implements `SchedulablePlugin`, its conditions are retrieved and evaluated -3. The `SchedulerPlugin` makes decisions about starting/stopping based on these conditions -4. Events are sent back to the plugin through interface methods like `onPluginScheduleEntryPostScheduleTaskEvent` - -## Plugin Conditions vs. User Conditions - -An important concept in the scheduler system is the distinction between plugin conditions and user conditions: - -### Plugin Conditions - -- **Source**: Defined programmatically by implementing `getStartCondition()` and `getStopCondition()` -- **Purpose**: Express the plugin's intrinsic requirements and business logic -- **Control**: Controlled by the plugin developer -- **Example**: A mining plugin might define "stop when inventory is full" as a plugin condition because it's fundamental to the plugin's functionality -- **Default Behavior**: When a plugin doesn't define specific conditions (returns empty `AndCondition`), it has no inherent restrictions on when it can start or stop - -### User Conditions - -- **Source**: Added through UI or configuration by the end user -- **Purpose**: Express user preferences and personalization -- **Control**: Controlled by the end user -- **Example**: A user might add "only run between 8pm-2am" as a user condition because it's their preferred play time -- **Default Behavior**: If no user conditions are defined, the plugin will run continuously until manually stopped - -### How They Work Together - -The `PluginScheduleEntry` class maintains both sets of conditions using separate logical structures: - -1. **Start Logic**: Plugin AND User start conditions must be met for the plugin to start -2. **Stop Logic**: Plugin OR User stop conditions must be met for the plugin to stop - -This gives both the plugin developer and the end user appropriate control while ensuring proper plugin operation: - -- The plugin can't start unless both the plugin requirements AND user preferences are satisfied -- The plugin will stop if EITHER the plugin determines it should stop OR the user's conditions determine it should stop - -### Full User Control Scenario - -When a plugin implements `SchedulablePlugin` but doesn't override `getStartCondition()` or `getStopCondition()` (or returns empty conditions), the execution is fully controlled by user-defined conditions: - -1. **Starting**: The plugin can start any time, but only when user-defined start conditions are met -2. **Stopping**: The plugin will only stop when user-defined stop conditions are met or the user manually stops it -3. **Self-Reporting**: Even without defined conditions, the plugin can still use `reportFinished()` to signal completion - -This design allows plugins to be made schedulable with minimal implementation effort while still giving users complete control over when they run. - -## Implementation Guidelines - -### Lock Condition Management - -Critical operations in plugins should be protected with the locking mechanism: - -1. Create a `LockCondition` in your stop condition structure -2. Call `lock()` before entering critical sections -3. Always call `unlock()` in a finally block to ensure the lock is released -4. Avoid long-running operations while locked, as this prevents the scheduler from stopping the plugin - -### Condition State Updates - -For plugins with dynamic stop conditions: - -1. Override `onStopConditionCheck()` to update condition state -2. Keep these updates lightweight and focused -3. Avoid heavy computation or network operations -4. Update counters, flags, or other simple state variables - -### Graceful Stopping - -When implementing the stop event handler: - -1. Check that the event is intended for this plugin -2. Save any critical state information -3. Close any open interfaces or dialogs -4. Release resources and cancel any pending operations -5. Use the client thread to ensure thread safety - -## Event-Driven Communication Mechanism - -The Plugin Scheduler system relies heavily on RuneLite's event bus for communication between components. Two primary events facilitate this communication: - -### 1. PluginScheduleEntryPostScheduleTaskEvent - -This event represents a request from the scheduler to a plugin asking it to gracefully stop execution. - -**Flow:** - -1. **Event Creation:** When stop conditions are met, PluginScheduleEntry.softStop() creates the event -2. **Event Posting:** The event is posted to the RuneLite EventBus -3. **Event Handling:** The target plugin's onPluginScheduleEntryPostScheduleTaskEvent() method is called -4. **Response:** The plugin performs cleanup operations and stops itself - -**Example:** - -```java -// Inside PluginScheduleEntry.softStop() -Microbot.getClientThread().runOnSeperateThread(() -> { - Microbot.getEventBus().post(new PluginScheduleEntryPostScheduleTaskEvent(plugin, ZonedDateTime.now())); - return false; -}); - -// Inside the plugin implementation -@Subscribe -@Override -public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - if (event.getPlugin() == this) { - // Cleanup operations - Microbot.getClientThread().invokeLater(() -> { - Microbot.stopPlugin(this); - }); - } -} -``` - -### 2. PluginScheduleEntryMainTaskFinishedEvent - -This event allows plugins to proactively inform the scheduler that they have completed their task and should be stopped. - -**Flow:** - -1. **Event Creation:** Plugin calls reportFinished() when its task is complete -2. **Validation:** reportFinished() verifies the scheduler is active and this plugin is the current running plugin -3. **Event Posting:** A PluginScheduleEntryMainTaskFinishedEvent is posted to the RuneLite EventBus -4. **Event Handling:** SchedulerPlugin.onPluginScheduleEntryMainTaskFinishedEvent() processes the event -5. **Plugin Stopping:** The scheduler initiates a stop with PLUGIN_FINISHED reason - -**Example:** - -```java -// Inside the plugin when a task is complete -reportFinished("Mining task completed - inventory full", true); - -// Inside the reportFinished() method -Microbot.getEventBus().post(new PluginScheduleEntryMainTaskFinishedEvent( - (Plugin) this, - "Plugin [" + this.getClass().getSimpleName() + "] finished: " + reason, - success -)); - -// Inside the SchedulerPlugin -@Subscribe -public void onPluginScheduleEntryMainTaskFinishedEvent(PluginScheduleEntryMainTaskFinishedEvent event) { - if (currentPlugin != null && event.getPlugin() == currentPlugin.getPlugin()) { - // Stop the plugin with the success state from the event - currentPlugin.stop(event.isSuccess(), StopReason.PLUGIN_FINISHED, event.getReason()); - } -} -``` - -### Self-Reporting Plugin Completion - -The `reportFinished()` method is a key component that allows plugins to proactively signal completion. It works as follows: - -1. **Validation Checks:** - - Verifies the SchedulerPlugin is loaded - - Confirms there's a current plugin running - - Ensures the current plugin matches this plugin instance - -2. **Event Creation and Posting:** - - Creates a PluginScheduleEntryMainTaskFinishedEvent with: - - Reference to the plugin - - Formatted reason message - - Success status flag - - Posts the event to the RuneLite EventBus - -3. **Fallback Handling:** - - If validation checks fail, the plugin stops itself directly - - This ensures plugins can always stop themselves, even if the scheduler isn't functioning properly - -The self-reporting mechanism gives plugins control over their lifecycle while still maintaining the scheduler's orchestration role. - -## Technical Notes - -### Thread Safety Considerations - -The `SchedulablePlugin` interface methods may be called from different threads: - -- Events are typically dispatched on the RuneLite event thread -- Condition checks are called from the scheduler's timer thread -- Plugin operations should be performed on the client thread for game state interactions - -Proper thread management is essential for stable operation. Use `Microbot.getClientThread().invokeLater()` for game state interactions. - -### Serialization Impact - -The `PluginScheduleEntry` class handles serialization of plugin schedules, but plugin instances themselves are marked as `transient`. This means: - -1. Any plugin-specific state must be managed separately -2. Plugin references are re-established when schedules are loaded -3. Condition objects are serialized and deserialized, so they should be designed with serialization in mind - - -## Best Practices - -When implementing the `SchedulablePlugin` interface, consider the following best practices: - -1. **Clear Conditions**: Define explicit start and stop conditions that clearly express your plugin's requirements. - -2. **Respect Soft Stops**: Implement `onPluginScheduleEntryPostScheduleTaskEvent` to clean up resources properly. - -3. **Use Locks Carefully**: Lock your plugin only during critical operations that should not be interrupted. - -4. **Self-Report Completion**: Use `reportFinished()` when your plugin naturally completes its task. - -5. **Handle Time Appropriately**: Include time-based conditions in your stop conditions to ensure your plugin doesn't run indefinitely. - -6. **Update Conditions**: Use `onStopConditionCheck()` to refresh dynamic conditions based on changing game state. - -## Component Relationships - -The `SchedulablePlugin` interface is part of a larger system that enables automatic scheduling of plugins. Here's how the components interact: - -### System Architecture - -```ascii -┌───────────────────────────────┐ -│ SchedulerPlugin │ -│ (Central Orchestrator) │ -├───────────────────────────────â”Ī -│ - Manages scheduling cycle │ -│ - Handles state transitions │ -│ - Evaluates conditions │ -│ - Starts & stops plugins │ -└─────────────────┮─────────────┘ - │ manages - │ multiple - ▾ -┌───────────────────────────────┐ -│ PluginScheduleEntry │ -│ (Data Model) │ -├───────────────────────────────â”Ī -│ - Stores config & state │ -│ - Tracks execution metrics │ -│ - Contains conditions │ -│ - References plugin instance │ -└─────────────────┮─────────────┘ - │ references - │ - ▾ -┌───────────────────────────────┐ ┌───────────────────────────┐ -│ Plugin implementing │ │ │ -│ SchedulablePlugin │◄────────â”Ī Regular RuneLite │ -│ (Plugin API Contract) │implements│ Plugin │ -├───────────────────────────────â”Ī ├───────────────────────────â”Ī -│ - Defines start/stop logic │ │ - Standard RuneLite │ -│ - Handles events │ │ plugin functionality │ -│ - Reports completion │ │ │ -└───────────────────────────────┘ └───────────────────────────┘ -``` - -### Component Responsibilities - -#### 1. SchedulerPlugin (Orchestrator) - -- **Primary Role:** Central controller that manages the entire scheduling system -- **Responsibilities:** - - Maintains the scheduler's state machine (16 distinct states) - - Executes the scheduling algorithm to determine which plugin runs next - - Processes condition evaluations to start/stop plugins - - Manages integration with other systems (BreakHandler, AutoLogin) - - Provides UI for configuration and monitoring - -#### 2. PluginScheduleEntry (Data Model) - -- **Primary Role:** Container for plugin scheduling configuration and execution state -- **Responsibilities:** - - Stores start and stop conditions for a specific plugin - - Tracks execution metrics (run count, duration, last run time) - - Maintains plugin priority and randomization settings - - Records state information (enabled/disabled, running/stopped) - - Handles watchdog functionality to monitor plugin execution - -#### 3. SchedulablePlugin (API Contract) - -- **Primary Role:** Interface implemented by plugins to participate in scheduling -- **Responsibilities:** - - Defines plugin-specific start and stop conditions - - Responds to scheduler events (start request, soft stop, hard stop) - - Reports task completion back to the scheduler - - Protects critical sections during execution - - Provides hooks for condition evaluation - -### Data Flow Between Components - -1. **Registration Flow:** - - ```text - Plugin implements SchedulablePlugin → User configures in UI → - SchedulerPlugin creates PluginScheduleEntry → Entry stored in scheduler - ``` - -2. **Startup Flow:** - - ```text - SchedulerPlugin evaluates conditions → Matches found → SchedulerPlugin references - PluginScheduleEntry → Entry points to Plugin → Plugin starts - ``` - -3. **Stopping Flow:** - - ```text - Stop conditions met → SchedulerPlugin posts event → - Plugin's onPluginScheduleEntryPostScheduleTaskEvent handler called → - Plugin stops gracefully → PluginScheduleEntry updated - ``` - -4. **Self-Completion Flow:** - - ```text - Plugin determines it's finished → Plugin calls reportFinished() → - SchedulerPlugin processes finish event → Updates PluginScheduleEntry → - Selects next plugin - ``` - -### Integration Points - -1. **Condition System Integration:** - - - `SchedulablePlugin.getStartCondition()` - Plugin defines when it can start - - `SchedulablePlugin.getStopCondition()` - Plugin defines when it should stop - - These combine with user-configured conditions in the PluginScheduleEntry - -2. **Event System Integration:** - - - `PluginScheduleEntryPostScheduleTaskEvent` - Sent from scheduler to plugin requesting stop - - `PluginScheduleEntryMainTaskFinishedEvent` - Sent from plugin to scheduler reporting completion - -3. **State Protection Integration:** - - - `SchedulablePlugin.lockPlugin()` - Prevents scheduler from stopping during critical operations - - `SchedulablePlugin.unlockPlugin()` - Releases lock when safe to stop - -## Practical Implementation - -When implementing the `SchedulablePlugin` interface in your plugin, consider the following workflow: - -1. **Define Start and Stop Conditions:** - - ```java - @Override - public LogicalCondition getStartCondition() { - AndCondition startConditions = new AndCondition(); - startConditions.addCondition(new TimeWindowCondition( - LocalTime.of(9, 0), LocalTime.of(17, 0) - )); - startConditions.addCondition(new InventoryItemCountCondition( - ItemID.COINS, 1000, ComparisonType.MORE_THAN - )); - return startConditions; - } - - @Override - public LogicalCondition getStopCondition() { - OrCondition stopConditions = new OrCondition(); - stopConditions.addCondition(new InventoryItemCountCondition( - ItemID.DRAGON_BONES, 28 - )); - stopConditions.addCondition(new PlayerHealthCondition( - 10, ComparisonType.LESS_THAN_OR_EQUAL - )); - return stopConditions; - } - ``` - -2. **Implement Stop Event Handler:** - - ```java - @Subscribe - public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - // Ensure this event is for our plugin - if (event.getPlugin() != this) { - return; - } - - // Save state if needed - saveCurrentProgress(); - - // Log the stop reason - log.info("Plugin stopping due to: " + event.getReason()); - - // Allow the plugin to be stopped - Microbot.stopPlugin(this); - } - ``` - -3. **Report Completion When Done:** - - ```java - private void checkTaskCompletion() { - if (isTaskComplete()) { - // Report we're done to the scheduler - reportFinished("Task completed successfully", true); - } else if (isTaskFailed()) { - reportFinished("Task failed: out of resources - inventory setup dont match", false); //USE with CAUTION only if you want your plugin is not started agin by the scheduler plugin can not be started again by the scheduler until it is enable again by the user in the scheduler plan (UI) - } - } - ``` - -4. **Protect Critical Sections:** - - ```java - @Override - public void onGameTick() { - try { - // Lock to prevent interruption during critical operation - lockPlugin(); - - // Perform sensitive operation that shouldn't be interrupted - performBankTransaction(); - } finally { - // Always unlock when done - unlockPlugin(); - } - } - ``` - -## How the Scheduler Selects the Next Plugin - -The Plugin Scheduler uses a sophisticated algorithm to determine which plugin to run next. Understanding this algorithm helps when configuring your plugin's schedule settings. - -### Multi-Factor Selection Algorithm - -```text -START SELECTION - Filter out disabled plugins - Filter out plugins currently running - Group remaining plugins by priority (highest first) - - FOR EACH priority group: - Split into non-default and default plugins - - IF non-default plugins exist: - Evaluate start conditions for each plugin - Separate into "can start" and "cannot start" groups - IF "can start" group is not empty: - IF randomization enabled: - Apply weighted random selection based on run count - ELSE: - Select first plugin - ELSE: - Continue to next sub-group - - IF default plugins exist AND no non-default plugin selected: - Evaluate start conditions for each plugin - Separate into "can start" and "cannot start" groups - IF "can start" group is not empty: - IF randomization enabled: - Apply weighted random selection based on run count - ELSE: - Select first plugin - ELSE: - Continue to next priority group - - IF no plugin selected: - Return null (scheduler will enter BREAK state) - ELSE: - Return selected plugin -END SELECTION -``` - -### Selection Factors (in order of importance) - -1. **Plugin Priority**: Higher priority plugins (larger number) are always evaluated first -2. **Plugin Type**: Non-default plugins take precedence over default plugins -3. **Start Conditions**: Only plugins whose start conditions are met are considered -4. **Randomization Setting**: Controls whether selection within a group is deterministic or random -5. **Run Count Balance**: Less frequently run plugins get higher weighting during random selection - -### Weighting Formula for Random Selection - -When randomization is enabled, plugins are selected using a weighted algorithm: - -```java -// For each plugin in the eligible group -double weight = BASE_WEIGHT; - -// Adjust based on how often this plugin has run compared to others -if (averageRunCount > 0 && plugin.getRunCount() < averageRunCount) { - // Plugin has run less than average, increase its chance of selection - double runCountRatio = (double) plugin.getRunCount() / averageRunCount; - weight += (1.0 - runCountRatio) * RUN_COUNT_WEIGHT; -} - -// Plugins with higher priority get a slight boost even within their priority group -weight += (plugin.getPriority() - baseGroupPriority) * PRIORITY_BONUS; - -// Add weight to selection map -weightMap.put(plugin, weight); -``` - -This approach ensures all plugins get fair execution time while still respecting priorities. - -## State Transition Flow and Condition Evaluation - -The scheduler manages a complex state machine that determines when and how plugins are executed. Here's how your plugin interacts with this state machine: - -### Key State Transitions - -1. **SCHEDULING → STARTING_PLUGIN**: - - Triggered when: Your plugin is selected to run next - - Requirements: All start conditions must be met - - Actions: Scheduler enables your plugin and starts watching it - -2. **STARTING_PLUGIN → RUNNING_PLUGIN**: - - Triggered when: Your plugin activates successfully - - Requirements: Plugin starts without errors - - Actions: Scheduler begins monitoring stop conditions - -3. **RUNNING_PLUGIN → SOFT_STOPPING_PLUGIN**: - - Triggered when: Any stop condition is met OR your plugin calls reportFinished() - - Actions: Scheduler sends PluginScheduleEntryPostScheduleTaskEvent to your plugin - -4. **SOFT_STOPPING_PLUGIN → HARD_STOPPING_PLUGIN**: - - Triggered when: Your plugin doesn't stop within timeout period - - Actions: Scheduler forcibly disables the plugin - -### Condition Evaluation Cycle - -```text -While in RUNNING_PLUGIN state: - Every ~1 second: - Call plugin.onStopConditionCheck() to refresh condition state - Evaluate plugin's stop conditions - Evaluate user-defined stop conditions - IF any condition is true: - Transition to SOFT_STOPPING_PLUGIN - Send soft stop event to plugin - ELSE: - Continue monitoring -``` - -This dual-layer condition system (plugin-defined + user-defined) provides maximum flexibility while maintaining plugin control over its execution criteria. - -## Best Practices for SchedulablePlugin Implementation - -To make the most of the Scheduler system, follow these guidelines when implementing `SchedulablePlugin`: - -1. **Use Specific Conditions**: Define clear, specific conditions that accurately represent when your plugin should start and stop. - - ```java - // Good: Specific condition - new InventoryItemCountCondition(ItemID.DRAGON_BONES, 28) - - // Avoid: Overly general condition - new TimeElapsedCondition(Duration.ofMinutes(30)) - ``` - -2. **Implement Graceful Stopping**: Always handle the `onPluginScheduleEntryPostScheduleTaskEvent` properly to ensure your plugin can be stopped cleanly. - - ```java - @Subscribe - public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - if (event.getPlugin() != this) { - return; - } - - // Save progress - savePluginState(); - - // Release resources - cleanupResources(); - - // Stop the plugin - Microbot.stopPlugin(this); - } - ``` - -3. **Use Critical Section Protection**: Lock the plugin during critical operations to prevent interruption. - - ```java - // Banking is a critical operation that shouldn't be interrupted - try { - lockPlugin(); - performBankingOperation(); - } finally { - unlockPlugin(); - } - ``` - -4. **Self-Report Completion**: Use `reportFinished()` when your plugin completes its task or encounters a situation where it should stop. - - ```java - if (isInventoryFull() && task.isComplete()) { - reportFinished("Task completed successfully", true); - return; - } - - if (isOutOfSupplies()) { - reportFinished("Out of required supplies", false); - return; - } - ``` - -5. **Update Condition State**: Implement `onStopConditionCheck()` to refresh any dynamic condition state. - - ```java - @Override - public void onStopConditionCheck() { - // Update our cached values that conditions might use - this.currentPlayerHealth = getPlayerHealth(); - this.nearbyEnemyCount = countNearbyEnemies(); - } - ``` - -6. **Design for Recovery**: Make your plugin resilient to interruptions by saving state periodically. - -7. **Balance Priorities**: Set appropriate priorities for your plugin to ensure it runs when most appropriate. - - - High priority (50+): Critical scripts that should run first - - Medium priority (20-49): Standard task scripts - - Low priority (1-19): Background or maintenance scripts - - Default priority (0): Last to run - -8. **Consider Randomization**: Enable randomization for most plugins to create more natural bot behavior patterns. - -## Conclusion - -The `SchedulablePlugin` interface serves as the foundation of Microbot's advanced plugin scheduling system, enabling sophisticated automation workflows with natural-looking behavior patterns. By implementing this interface, your plugins become part of an intelligent ecosystem that can coordinate multiple activities, respect constraints, and adapt to changing conditions. - -Key benefits of using the scheduler system include: - -- Reduced detection risk through coordinated breaks and natural activity patterns -- Lower development burden by leveraging shared infrastructure for timing and conditions -- More sophisticated automation flows by chaining plugins together -- Better user experience through consistent configuration and monitoring - -For comprehensive examples of `SchedulablePlugin` implementations, refer to the following reference plugins: - -- `WoodcuttingPlugin`: Demonstrates basic resource gathering with simple conditions -- `FletchingPlugin`: Shows workflow stages with progress reporting -- `CombatPlugin`: Illustrates complex condition structures and critical section protection - -These reference implementations provide patterns you can adapt for your own plugins to integrate seamlessly with the scheduler system. - diff --git a/docs/scheduler/combat-lock-examples.md b/docs/scheduler/combat-lock-examples.md deleted file mode 100644 index 3d064619172..00000000000 --- a/docs/scheduler/combat-lock-examples.md +++ /dev/null @@ -1,307 +0,0 @@ -## LockCondition Usage in Combat Plugins - -Combat and bossing plugins often require careful management of the LockCondition to prevent interruption during critical combat sequences. This section provides examples and best practices for using LockCondition in combat scenarios. - -### Example: Boss Fight Lock Management - -For a boss fight plugin, you would typically want to lock the plugin when the fight begins and unlock it when the fight ends, similar to how the GotrPlugin manages lock state: - -```java -public class BossPlugin extends Plugin implements SchedulablePlugin { - - private LockCondition lockCondition; - private LogicalCondition stopCondition = null; - - @Override - public LogicalCondition getStopCondition() { - if (this.stopCondition == null) { - this.stopCondition = createStopCondition(); - } - return this.stopCondition; - } - - private LogicalCondition createStopCondition() { - // Create a lock condition specifically for boss combat - this.lockCondition = new LockCondition("Locked during boss fight"); //ensure unlock on shutdown of the plugin ! - - // Create stop conditions - OrCondition stopTriggers = new OrCondition(); - - // Add various stop conditions (resource depletion, time limit, etc.) - stopTriggers.addCondition(new InventoryItemCountCondition("Prayer potion", 0, true)); - // NOTE: HealthPercentCondition is not yet implemented - this is a placeholder - // stopTriggers.addCondition(new HealthPercentCondition(15)); - - // Use SingleTriggerTimeCondition for a time limit (60 minutes from now) - stopTriggers.addCondition(SingleTriggerTimeCondition.afterDelay(60 * 60)); // 60 minutes in seconds - - // Combine the stop triggers with the lock condition using AND logic - // This ensures the plugin won't stop if locked, even if other conditions are met - AndCondition andCondition = new AndCondition(); - andCondition.addCondition(stopTriggers); - andCondition.addCondition(lockCondition); - - return andCondition; - } - - @Subscribe - public void onChatMessage(ChatMessage chatMessage) { - if (chatMessage.getType() != ChatMessageType.GAMEMESSAGE) { - return; - } - - String message = chatMessage.getMessage(); - - // Lock the plugin when the boss fight begins - if (message.contains("You've entered the boss arena.") || - message.contains("The boss has appeared!")) { - if (lockCondition != null) { - lockCondition.lock(); - log.debug("Boss fight started - locked plugin"); - } - } - // Unlock the plugin when the boss fight ends - else if (message.contains("Congratulations, you've defeated the boss!") || - message.contains("You have been defeated.")) { - if (lockCondition != null) { - lockCondition.unlock(); - log.debug("Boss fight ended - unlocked plugin"); - } - } - } - - @Subscribe - public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - if (event.getPlugin() == this) { - log.info("Scheduler requesting plugin shutdown"); - - // Setup a scheduled task to check if it's safe to exit - ScheduledExecutorService exitExecutor = Executors.newSingleThreadScheduledExecutor(); - exitExecutor.scheduleWithFixedDelay(() -> { - try { - // Check if we're in a critical phase (boss fight) - if (lockCondition != null && lockCondition.isLocked()) { - log.info("Cannot exit during boss fight - waiting for fight to end"); - return; // Try again later - } - - // Safe to exit, perform cleanup - log.info("Safe to exit - performing cleanup"); - leaveBossArea(); // Method to safely teleport away or exit - - // Stop the plugin - Microbot.getClientThread().invokeLater(() -> { - Microbot.stopPlugin(this); - return true; - }); - - // Shutdown the executor - exitExecutor.shutdown(); - } catch (Exception ex) { - log.error("Error during safe exit", ex); - // Force stop in case of error - Microbot.getClientThread().invokeLater(() -> { - Microbot.stopPlugin(this); - return true; - }); - - // Shutdown the executor - exitExecutor.shutdown(); - } - }, 0, 2, java.util.concurrent.TimeUnit.SECONDS); - } - } - - /** - * Safely leaves the boss area, e.g., via teleport or exit portal - */ - private void leaveBossArea() { - // Implementation to safely leave the boss area - // This might involve clicking an exit portal, using a teleport item, etc. - } -} -``` - -### Combat-Specific Lock Patterns - -When developing combat plugins, consider these lock patterns: - -1. **Phase-Based Locking**: Lock during specific boss phases that shouldn't be interrupted - -```java -@Subscribe -public void onNpcChanged(NpcChanged event) { - NPC npc = event.getNpc(); - - // Detect phase change on the boss - if (npc.getId() == BOSS_ID && npc.getAnimation() == PHASE_CHANGE_ANIM) { - // Lock during the special phase - lockCondition.lock(); - - // Schedule unlock after the phase should be complete - ScheduledExecutorService phaseExecutor = Executors.newSingleThreadScheduledExecutor(); - phaseExecutor.schedule(() -> { - lockCondition.unlock(); - phaseExecutor.shutdown(); - }, 30, java.util.concurrent.TimeUnit.SECONDS); - } -} -``` - -2. **Special Attack Locking**: Lock during special attack execution - -```java -private void executeSpecialAttack() { - try { - // Lock before starting the special attack sequence - lockCondition.lock(); - - // Perform special attack actions - Rs2Combat.toggleSpecialAttack(true); - Rs2Equipment.equipItem("Dragon dagger(p++)", "Wield"); - Rs2Npc.interact(targetNpc, "Attack"); - - // Wait for animation to complete - Global.sleepUntil(() -> Rs2Player.getAnimationId() == -1, 3000); - - // Switch back to main weapon - Rs2Equipment.equipItem("Abyssal whip", "Wield"); - } finally { - // Always unlock when the sequence is complete - lockCondition.unlock(); - } -} -``` - -3. **Prayer Flicking Lock**: Lock during prayer flicking sequences - -```java -private void startPrayerFlicking() { - // Lock while the prayer flicking routine is active - lockCondition.lock(); - - ScheduledExecutorService flickingExecutor = Executors.newSingleThreadScheduledExecutor(); - prayerFlickingFuture = flickingExecutor.scheduleAtFixedRate(() -> { - try { - // Detect if we should stop flicking - // Note: HealthPercentCondition is not implemented yet, this is just an example - // This would need to be replaced with actual health checking logic - if (/* Rs2Player.getHealthPercent() < 25 || */ !inCombat()) { - stopPrayerFlicking(); - return; - } - - // Toggle the appropriate prayers based on boss attacks - toggleProtectionPrayers(); - } catch (Exception e) { - log.error("Error in prayer flicking", e); - stopPrayerFlicking(); - } - }, 0, 600, java.util.concurrent.TimeUnit.MILLISECONDS); -} - -private void stopPrayerFlicking() { - if (prayerFlickingFuture != null) { - prayerFlickingFuture.cancel(false); - prayerFlickingFuture = null; - } - - // Turn off prayers - Rs2Prayer.toggle(Prayer.PROTECT_FROM_MAGIC, false); - Rs2Prayer.toggle(Prayer.PROTECT_FROM_MISSILES, false); - Rs2Prayer.toggle(Prayer.PROTECT_FROM_MELEE, false); - - // Unlock after prayer flicking stops - lockCondition.unlock(); -} -``` - -### Best Practices for Combat Lock Management - -1. **Always use try-finally blocks** when manually locking to ensure the lock is released even if exceptions occur: - -```java -try { - lockCondition.lock(); - // Critical combat sequence -} finally { - lockCondition.unlock(); -} -``` - -2. **Keep lock scopes narrow** - only lock during truly critical operations: - -```java -// BAD: Locking for the entire method -public void doBossFight() { - lockCondition.lock(); - setupGear(); - walkToBoss(); - fightBoss(); // Only this part is truly critical - collectLoot(); - lockCondition.unlock(); -} - -// GOOD: Locking only the critical part -public void doBossFight() { - setupGear(); - walkToBoss(); - - try { - lockCondition.lock(); - fightBoss(); // Only lock during the actual fight - } finally { - lockCondition.unlock(); - } - - collectLoot(); -} -``` - -3. **Use meaningful lock reason messages** to aid debugging: - -```java -// BAD -this.lockCondition = new LockCondition(); //ensure unlock on shutdown of the plugin ! - -// GOOD -this.lockCondition = new LockCondition("Locked during Zulrah's blue phase"); //ensure unlock on shutdown of the plugin ! -``` - -4. **Consider timeout mechanisms** for locks that might get stuck: - -```java -// Set a maximum lock duration for safety -final long MAX_LOCK_DURATION = 120_000; // 2 minutes -long lockStartTime = System.currentTimeMillis(); - -try { - lockCondition.lock(); - - while (bossStillFighting()) { - // Combat logic... - - // Safety timeout check - if (System.currentTimeMillis() - lockStartTime > MAX_LOCK_DURATION) { - log.warn("Lock timeout exceeded - forcing unlock"); - break; - } - - Global.sleep(100); - } -} finally { - lockCondition.unlock(); -} -``` - -5. **Take advantage of LockCondition constructor options**: - -```java -// Create a locked condition from the start -this.lockCondition = new LockCondition("Locked during combat sequence", true); //ensure unlock on shutdown of the plugin ! - -// Later when it's safe to unlock -this.lockCondition.unlock(); -``` - -By following these patterns, your combat plugins will safely integrate with the scheduler system while ensuring critical combat sequences are never interrupted at dangerous moments. diff --git a/docs/scheduler/condition-manager.md b/docs/scheduler/condition-manager.md deleted file mode 100644 index 45ef8c6478c..00000000000 --- a/docs/scheduler/condition-manager.md +++ /dev/null @@ -1,424 +0,0 @@ -# ConditionManager - -## Overview - -The `ConditionManager` class is a sophisticated component of the Plugin Scheduler system responsible for handling the hierarchical structure of conditions that determine when plugins should start and stop. It manages two separate condition hierarchies - one for plugin-defined conditions and another for user-defined conditions - and provides methods to evaluate, combine, and manipulate these conditions. - -This class serves as the "brain" of the condition evaluation system, determining whether a plugin should start or stop based on complex logical structures of conditions. - -## Class Definition - -```java -@Slf4j -public class ConditionManager implements AutoCloseable { - // Condition hierarchies - private LogicalCondition pluginCondition; - private LogicalCondition userLogicalCondition; - - // Event handling - private final EventBus eventBus; - private boolean eventsRegistered; - - // Watchdog system - private boolean watchdogsRunning; - private UpdateOption currentWatchdogUpdateOption; - private long currentWatchdogInterval; - private Supplier currentWatchdogSupplier; - - // Other implementation details... -} -``` - -## Key Features - -### Dual Condition Hierarchy System - -The `ConditionManager` maintains two separate hierarchical structures: - -1. **Plugin Conditions**: Defined programmatically by the plugin through the `SchedulablePlugin` interface - - Controlled by the plugin developer - - Typically express the plugin's business logic requirements - - Cannot be modified by the user through the UI - -2. **User Conditions**: Defined by the end-user through configuration - - Controlled by the user - - Express user preferences about when the plugin should run - - Can be modified through the UI - -When evaluating the complete condition set, the plugin conditions and user conditions are combined using configurable logic (typically AND): - -``` -Final Condition = Plugin Conditions AND User Conditions -``` - -This means both sets of conditions must be satisfied for the final result to be satisfied, giving both the developer and user appropriate control. - -### Hierarchical Logical Structure - -Each condition hierarchy can contain complex nested structures of logical operators (AND, OR, NOT) and various condition types: - -``` -UserLogicalCondition (AND) -├── TimeWindowCondition -├── InventoryItemCountCondition -└── OrCondition - ├── SkillLevelCondition - └── PlayerInAreaCondition -``` - -This allows for sophisticated condition combinations like: -- "Execute only between 8pm-10pm AND when inventory isn't full OR when in the mining area" - -### Logic Types - -The `ConditionManager` supports two primary logic types: - -1. **Require All**: All conditions must be met (AND logic) - ```java - conditionManager.setRequireAll(); // Use AND logic - ``` - -2. **Require Any**: Any condition can be met (OR logic) - ```java - conditionManager.setRequireAny(); // Use OR logic - ``` - -### Condition Management Methods - -The class provides various methods to manipulate and query its condition structures: - -```java -// Add a user condition -public void addUserCondition(Condition condition) { - ensureUserLogicalExists(); - userLogicalCondition.addCondition(condition); -} - -// Remove a condition from both hierarchies -public boolean removeCondition(Condition condition) { - boolean removed = false; - if (userLogicalCondition != null) { - removed |= userLogicalCondition.removeCondition(condition); - } - if (pluginCondition != null) { - removed |= pluginCondition.removeCondition(condition); - } - return removed; -} - -// Check if a condition exists in either hierarchy -public boolean containsCondition(Condition condition) { - ensureUserLogicalExists(); - // Check user conditions - if (userLogicalCondition.contains(condition)) { - return true; - } - // Check plugin conditions - return pluginCondition != null && pluginCondition.contains(condition); -} -``` - -### Condition Evaluation - -The `ConditionManager` provides methods to evaluate whether its conditions are satisfied: - -```java -// Check if all conditions (both plugin and user) are met -public boolean areAllConditionsMet() { - // Check if plugin conditions are met (or if none exist) - boolean pluginConditionsMet = !hasPluginConditions() || arePluginConditionsMet(); - - // Check if user conditions are met (or if none exist) - boolean userConditionsMet = !hasUserConditions() || areUserConditionsMet(); - - // Both must be satisfied - return pluginConditionsMet && userConditionsMet; -} - -// Check only plugin-defined conditions -public boolean arePluginConditionsMet() { - if (pluginCondition == null || pluginCondition.getConditions().isEmpty()) { - return true; // No plugin conditions means this requirement is satisfied - } - return pluginCondition.isSatisfied(); -} - -// Check only user-defined conditions -public boolean areUserConditionsMet() { - if (userLogicalCondition == null || userLogicalCondition.getConditions().isEmpty()) { - return true; // No user conditions means this requirement is satisfied - } - return userLogicalCondition.isSatisfied(); -} -``` - -### Condition Watchdog System - -The `ConditionManager` includes a sophisticated watchdog system that periodically updates conditions from a supplier function (typically from the plugin itself): - -```java -/** - * Schedules a periodic task to update conditions from the given supplier. - * This allows plugins to dynamically update their conditions based on changing game state. - */ -public ScheduledFuture scheduleConditionWatchdog( - Supplier conditionSupplier, - long checkIntervalMillis, - UpdateOption updateOption -) { - // Store the current settings - this.currentWatchdogSupplier = conditionSupplier; - this.currentWatchdogInterval = checkIntervalMillis; - this.currentWatchdogUpdateOption = updateOption; - - // Create and schedule the watchdog task - ScheduledFuture future = SHARED_WATCHDOG_EXECUTOR.scheduleAtFixedRate( - () -> { - try { - // Get the latest condition from the supplier - LogicalCondition newCondition = conditionSupplier.get(); - if (newCondition != null) { - // Update the plugin condition with the new one - updatePluginCondition(newCondition, updateOption); - } - } catch (Exception e) { - log.error("Error in condition watchdog", e); - } - }, - checkIntervalMillis, // Initial delay - checkIntervalMillis, // Periodic interval - TimeUnit.MILLISECONDS - ); - - // Track the future so it can be canceled later - watchdogFutures.add(future); - watchdogsRunning = true; - - return future; -} -``` - -This watchdog system enables plugins to dynamically update their conditions based on changing game state, supporting more sophisticated automation patterns. - -### Condition Update Options - -The watchdog system supports several update strategies for merging new conditions with existing ones: - -1. **ADD_ONLY**: Only add new conditions, never remove existing ones -2. **REMOVE_ONLY**: Only remove conditions that no longer exist -3. **SYNC**: Fully synchronize the condition structure with the new one (default) -4. **REPLACE**: Replace the entire condition structure with the new one - -```java -/** - * Updates the plugin condition structure using the specified update option. - */ -public boolean updatePluginCondition(LogicalCondition newCondition, UpdateOption option) { - if (newCondition == null) { - return false; - } - - switch (option) { - case ADD_ONLY: - // Only add new conditions, don't remove existing ones - return mergeAddOnly(pluginCondition, newCondition); - - case REMOVE_ONLY: - // Only remove conditions that are no longer present - return mergeRemoveOnly(pluginCondition, newCondition); - - case SYNC: - // Full synchronization - add new ones, remove old ones - return synchronizeConditions(pluginCondition, newCondition); - - case REPLACE: - // Complete replacement - setPluginCondition(newCondition); - return true; - - default: - // Default to sync behavior - return synchronizeConditions(pluginCondition, newCondition); - } -} -``` - -### Event System Integration - -The `ConditionManager` integrates with RuneLite's event system to update conditions based on in-game events: - -```java -/** - * Registers event listeners with the RuneLite event bus to receive relevant game events. - * This enables conditions to update based on game state changes. - */ -public void registerEvents() { - if (!eventsRegistered) { - eventBus.register(this); - eventsRegistered = true; - log.debug("Registered condition event listeners"); - } -} - -/** - * Unregisters event listeners to prevent receiving events when not needed. - */ -public void unregisterEvents() { - if (eventsRegistered) { - eventBus.unregister(this); - eventsRegistered = false; - log.debug("Unregistered condition event listeners"); - } -} - -/** - * Example of an event handler that updates conditions based on game events - */ -@Subscribe -public void onGameTick(GameTick event) { - // Update all conditions with the latest game state - for (Condition condition : getConditions()) { - condition.update(); - } -} -``` - -This event integration ensures conditions are kept up-to-date with the current game state. - -### Time Condition Management - -The `ConditionManager` provides special handling for time-based conditions, which have unique properties like future trigger times: - -```java -/** - * Gets the next scheduled trigger time from all time conditions. - * This is the earliest time when any time condition would be satisfied. - */ -public Optional getCurrentTriggerTime() { - List timeConditions = getAllTimeConditions(); - if (timeConditions.isEmpty()) { - return Optional.empty(); - } - - // Find the earliest trigger time among all time conditions - return timeConditions.stream() - .map(TimeCondition::getCurrentTriggerTime) - .filter(Optional::isPresent) - .map(Optional::get) - .min(ZonedDateTime::compareTo); -} - -/** - * Checks if all time-only conditions would be satisfied, ignoring non-time conditions. - * This is useful for diagnostic purposes to determine if time conditions are blocking execution. - */ -public boolean wouldBeTimeOnlySatisfied() { - // Check if we have any time conditions - List timeConditions = getAllTimeConditions(); - if (timeConditions.isEmpty()) { - return true; // No time conditions means they're not blocking - } - - // Check if all time conditions are satisfied - return timeConditions.stream().allMatch(Condition::isSatisfied); -} -``` - -### Progress Tracking - -The `ConditionManager` can calculate progress toward conditions being met: - -```java -/** - * Gets the overall progress percentage toward the next trigger time. - * Useful for UI display of how close a plugin is to running. - */ -public double getProgressTowardNextTrigger() { - // Implementation to calculate progress between 0-100% -} - -/** - * Gets the percentage of conditions that are currently satisfied. - */ -public double getFullConditionProgress() { - List conditions = getConditions(); - if (conditions.isEmpty()) { - return 100.0; // No conditions means 100% done - } - - // Calculate percentage of satisfied conditions - long satisfiedCount = conditions.stream() - .filter(Condition::isSatisfied) - .count(); - - return (satisfiedCount * 100.0) / conditions.size(); -} -``` - -## Relationship with PluginScheduleEntry - -Each `PluginScheduleEntry` contains two `ConditionManager` instances: - -1. `startConditionManager`: Manages conditions that determine when to start the plugin -2. `stopConditionManager`: Manages conditions that determine when to stop the plugin - -The `PluginScheduleEntry` uses these managers to evaluate: -- Whether the plugin is "due to run" (should start) -- Whether the plugin should stop -- The next scheduled activation time -- Progress toward starting or stopping - -```java -// In PluginScheduleEntry -public boolean isDueToRun() { - // Check if we're already running - if (isRunning()) { - return false; - } - - // Check if start conditions are met - return startConditionManager.areAllConditionsMet(); -} - -public boolean shouldStop() { - if (!isRunning()) { - return false; - } - - // Check if stop conditions are met - return stopConditionManager.areAllConditionsMet(); -} -``` - -## Best Practices - -When working with `ConditionManager`: - -1. **Appropriate Logic Type**: Choose the appropriate logic type (AND/OR) based on your requirements - - Use AND (requireAll) when all conditions must be satisfied - - Use OR (requireAny) when any condition can trigger the action - -2. **Performance Considerations**: Avoid excessive condition nesting and large condition trees - - Very complex condition structures can impact performance - - Use logical grouping to optimize evaluation - -3. **Condition Registration**: Ensure conditions are registered with the appropriate manager - - User conditions should be added via `addUserCondition()` - - Plugin conditions should be set via `setPluginCondition()` or `updatePluginCondition()` - -4. **Resource Cleanup**: Always call `close()` when the manager is no longer needed - - This ensures watchdog tasks are properly canceled - - Event listeners are unregistered - -5. **Watchdog Usage**: Use watchdogs judiciously - - Frequent updates can impact performance - - Consider appropriate update intervals based on your plugin's needs - -6. **Condition Synchronization**: Choose the right update option for your use case - - `SYNC` for complete condition synchronization - - `ADD_ONLY` to preserve existing conditions while adding new ones - - `REPLACE` when you want to completely reset the condition structure - -## Summary - -The `ConditionManager` class is the sophisticated "brain" behind the Plugin Scheduler's condition evaluation system. By maintaining separate hierarchies for user and plugin conditions, it creates a powerful but flexible framework for defining when plugins should start and stop. Its integration with the RuneLite event system, watchdog capabilities, and hierarchical logical structure enable complex automation patterns that can adapt to the changing game state. diff --git a/docs/scheduler/conditions/location-conditions.md b/docs/scheduler/conditions/location-conditions.md deleted file mode 100644 index 301997926a4..00000000000 --- a/docs/scheduler/conditions/location-conditions.md +++ /dev/null @@ -1,136 +0,0 @@ -# Location Conditions - -Location conditions in the Plugin Scheduler system allow plugins to be controlled based on the player's position in the game world. - -## Overview - -Location conditions monitor the player's physical location in the game, enabling plugins to respond to position-based triggers. These conditions are useful for area-specific automation, region-based task scheduling, and creating location-aware plugin behaviors. - -## Available Location Conditions - -### PositionCondition - -The `PositionCondition` checks if the player is at a specific tile position in the game world. - -**Usage:** -```java -// Satisfied when the player is at the Grand Exchange center tile -PositionCondition condition = new PositionCondition( - new WorldPoint(3165, 3487, 0) // GE center coordinates -); -``` - -**Key features:** -- Checks for exact position matching -- Can include or ignore the plane/level coordinate -- Can specify a tolerance radius to create a small area around the target position -- Useful for precise location triggers - -### AreaCondition - -The `AreaCondition` checks if the player is within a defined area in the game world. - -**Usage:** -```java -// Satisfied when the player is in the Grand Exchange area -AreaCondition condition = new AreaCondition( - new WorldArea(3151, 3473, 30, 30, 0) // GE area -); -``` - -**Key features:** -- Defines rectangular areas using WorldArea -- Can cover multiple planes/levels -- Useful for monitoring presence in towns, dungeons, or training areas -- Can be inverted to check if player is outside an area - -## Common Features of Location Conditions - -All location conditions provide core functionality for position-based checks: - -- `isSatisfied()`: Determines if the player's current position satisfies the condition -- `getDescription()`: Returns a human-readable description of the location condition -- `reset()`: Refreshes any cached location data -- Various configuration options for making the check more or less strict - -## Using Location Conditions as Start Conditions - -When used as start conditions, location conditions can trigger plugins when the player enters specific areas: - -```java -PluginScheduleEntry entry = new PluginScheduleEntry("MyPlugin", true); - -// Start the plugin when the player enters the Wilderness -entry.addStartCondition(new AreaCondition( - new WorldArea(2944, 3520, 448, 448, 0) // Wilderness area -)); -``` - -## Using Location Conditions as Stop Conditions - -Location conditions are also useful as stop conditions to deactivate plugins when the player leaves or enters an area: - -```java -// Stop the plugin when the player returns to a safe area (e.g., Lumbridge) -entry.addStopCondition(new AreaCondition( - new WorldArea(3206, 3208, 30, 30, 0) // Lumbridge area -)); -``` - -## Combining with Logical Conditions - -Location conditions can be combined with logical conditions to create more complex location-based rules: - -```java -// Create a logical NOT condition -NotCondition notInSafeArea = new NotCondition( - new AreaCondition(new WorldArea(3206, 3208, 30, 30, 0)) // Lumbridge area -); - -// Create a logical AND condition -AndCondition dangerousCondition = new AndCondition(); - -// Require being in the wilderness -dangerousCondition.addCondition(new AreaCondition( - new WorldArea(2944, 3520, 448, 448, 0) // Wilderness area -)); - -// AND not being in a safe spot -dangerousCondition.addCondition(notInSafeArea); - -// Add this combined condition as a start condition -entry.addStartCondition(dangerousCondition); -``` - -## Creating Complex Area Monitoring - -For complex regions that cannot be represented by a single rectangle, multiple area conditions can be combined using logical OR: - -```java -// Create a logical OR condition for multiple areas -OrCondition bankingAreas = new OrCondition(); - -// Add various bank areas -bankingAreas.addCondition(new AreaCondition( - new WorldArea(3207, 3215, 10, 10, 0) // Lumbridge bank -)); -bankingAreas.addCondition(new AreaCondition( - new WorldArea(3180, 3433, 15, 15, 0) // Varrock West bank -)); -bankingAreas.addCondition(new AreaCondition( - new WorldArea(3251, 3420, 10, 10, 0) // Varrock East bank -)); -bankingAreas.addCondition(new AreaCondition( - new WorldArea(3088, 3240, 10, 10, 0) // Draynor bank -)); - -// Add this combined condition to stop a plugin when in any bank -entry.addStopCondition(bankingAreas); -``` - -## Event Integration - -Location conditions integrate with the RuneLite event system to track changes in real-time: - -- `GameTick`: Periodically checks the player's position against the condition -- Efficient position comparison to minimize performance impact \ No newline at end of file diff --git a/docs/scheduler/conditions/logical-conditions.md b/docs/scheduler/conditions/logical-conditions.md deleted file mode 100644 index 9f460131310..00000000000 --- a/docs/scheduler/conditions/logical-conditions.md +++ /dev/null @@ -1,192 +0,0 @@ -# Logical Conditions - -Logical conditions are a powerful feature of the Plugin Scheduler that enable the combination of other conditions using logical operators. - -## Overview - -Logical conditions allow for the creation of complex conditional expressions by combining simpler conditions. They are essential for creating sophisticated scheduling rules based on multiple factors. - -## Available Logical Conditions - -### AndCondition - -The `AndCondition` requires that all of its child conditions are satisfied for the condition itself to be satisfied. - -**Usage:** -```java -// Create an AND condition -AndCondition condition = new AndCondition(); - -// Add child conditions -condition.addCondition(new InventoryItemCountCondition(ItemID.COINS, 1000, )); -condition.addCondition(new SkillLevelCondition(Skill.ATTACK, 60, )); -``` - -**Key features:** -- Requires all child conditions to be satisfied -- Can contain any type of condition, including other logical conditions -- Returns the minimum progress percentage among child conditions - -### OrCondition - -The `OrCondition` is satisfied if any of its child conditions are satisfied. - -**Usage:** -```java -// Create an OR condition -OrCondition condition = new OrCondition(); - -// Add child conditions -condition.addCondition(new TimeWindowCondition(LocalTime.of(9, 0), LocalTime.of(12, 0))); -condition.addCondition(new TimeWindowCondition(LocalTime.of(14, 0), LocalTime.of(17, 0))); -``` - -**Key features:** -- Requires at least one child condition to be satisfied -- Useful for creating alternative paths to satisfy a condition -- Returns the maximum progress percentage among child conditions - -### NotCondition - -The `NotCondition` inverts the result of its child condition. - -**Usage:** -```java -// Create a NOT condition -NotCondition condition = new NotCondition( - new AreaCondition(new WorldArea(3200, 3200, 50, 50, 0)) -); -``` - -**Key features:** -- Inverts the satisfaction state of the wrapped condition -- Progress percentage is inverted (100 - child progress) -- Useful for creating negative conditions like "not in an area" or "no items in inventory" - -### LockCondition - -The `LockCondition` is a special logical condition that remains satisfied once it becomes satisfied, regardless of the subsequent state of its child condition. - -**Usage:** -```java -// Create a lock condition that stays satisfied once the player reaches level 70 -LockCondition condition = - new SkillLevelCondition(Skill.MINING, 70); -``` - -**Key features:** -- "Locks" to true once satisfied -- Useful for one-way transitions or milestone achievements -- Can be reset if needed - -### PredicateCondition - -The `PredicateCondition` is a versatile logical condition that evaluates a Java Predicate function against a game state. It combines a manual lock mechanism with dynamic state evaluation. - -**Usage:** -```java -// Create a predicate condition that checks if the player is in an agility course region -Predicate notInAgilityRegion = player -> { - if (player == null) return true; - int playerRegionId = player.getWorldLocation().getRegionID(); - return !courseRegionIds.contains(playerRegionId); -}; - -// Create the predicate condition with a descriptive reason -PredicateCondition condition = new PredicateCondition<>( - "Player is currently in an agility course", // reason for the lock - notInAgilityRegion, // the predicate to evaluate - () -> client.getLocalPlayer(), // supplier of the state to check - "Player is not in an agility course region" // description of the predicate -); -``` - -**Key features:** -- Combines a traditional lock mechanism with dynamic predicate evaluation -- Supports any type of game state through generic type parameter -- Only satisfied when both the lock is unlocked AND the predicate evaluates to true -- Extremely flexible for complex game state evaluation -- Perfect for state-driven plugin control logic - -## Common Features of Logical Conditions - -All logical conditions implement the `LogicalCondition` interface, which extends the base `Condition` interface and provides additional functionality for managing child conditions: - -- `addCondition(Condition)`: Adds a child condition -- `removeCondition(Condition)`: Removes a child condition -- `getConditions()`: Returns all child conditions -- `contains(Condition)`: Checks if a specific condition is contained in the logical structure - -## Using Logical Conditions as Start Conditions - -When used as start conditions, logical conditions provide complex rules for when a plugin should be activated: - -```java -PluginScheduleEntry entry = new PluginScheduleEntry("MyPlugin", true); - -// Create a logical structure for starting conditions -OrCondition startCondition = new OrCondition(); - -// Add multiple time windows -startCondition.addCondition(new TimeWindowCondition( - LocalTime.of(9, 0), - LocalTime.of(12, 0) -)); -startCondition.addCondition(new TimeWindowCondition( - LocalTime.of(14, 0), - LocalTime.of(17, 0) -)); - -entry.addStartCondition(startCondition); -``` - -## Using Logical Conditions as Stop Conditions - -When used as stop conditions, logical conditions define complex rules for when a plugin should be deactivated: - -```java -// Create a logical structure for stop conditions -AndCondition stopCondition = new AndCondition(); - -// Stop when inventory is full AND player is in a safe area -stopCondition.addCondition(new InventoryItemCountCondition( - ItemID.ANY, - 28, - -)); -stopCondition.addCondition(new AreaCondition( - new WorldArea(3200, 3200, 50, 50, 0) -)); - -entry.addStopCondition(stopCondition); -``` - -## Creating Complex Nested Logical Structures - -Logical conditions can be nested to create complex conditional expressions: - -```java -// Main structure is OR -OrCondition complexCondition = new OrCondition(); - -// First branch: AND condition -AndCondition firstBranch = new AndCondition(); -firstBranch.addCondition(new TimeWindowCondition(LocalTime.of(9, 0), LocalTime.of(17, 0))); -firstBranch.addCondition(new SkillLevelCondition(Skill.MINING, 60, )); - -// Second branch: AND condition with a NOT -AndCondition secondBranch = new AndCondition(); -secondBranch.addCondition(new TimeWindowCondition(LocalTime.of(20, 0), LocalTime.of(23, 59))); -secondBranch.addCondition(new NotCondition( - new AreaCondition(new WorldArea(3200, 3200, 50, 50, 0)) -)); - -// Add branches to main structure -complexCondition.addCondition(firstBranch); -complexCondition.addCondition(secondBranch); - -// Add to schedule entry -entry.addStartCondition(complexCondition); -``` - -This creates a structure that means: "Run the plugin if it's between 9 AM and 5 PM AND mining level is 60+, OR if it's between 8 PM and midnight AND the player is not in the specified area." \ No newline at end of file diff --git a/docs/scheduler/conditions/npc-conditions.md b/docs/scheduler/conditions/npc-conditions.md deleted file mode 100644 index 75ced92342d..00000000000 --- a/docs/scheduler/conditions/npc-conditions.md +++ /dev/null @@ -1,125 +0,0 @@ -# NPC Conditions - -NPC conditions in the Plugin Scheduler system allow plugins to be controlled based on NPC-related events and states in the game. - -## Overview - -NPC conditions monitor interactions with Non-Player Characters in the game, enabling plugins to respond to NPC presence, combat state, and other NPC-related factors. These conditions are particularly useful for combat automation, quest helpers, and NPC interaction scripts. - -## Available NPC Conditions - -### NpcCondition - -The `NpcCondition` monitors the presence, proximity, or state of specific NPCs in the game. - -**Usage:** -```java -// Satisfied when an NPC with ID 3080 (Moss giant) is within visibility range -NpcCondition condition = new NpcCondition( - 3080, // NPC ID to check - true // Whether the NPC must be present to satisfy the condition -); -``` - -**Key features:** -- Monitors for specific NPC IDs or name patterns -- Can check for NPC presence or absence -- Can validate distance to the NPC -- Supports combat state checking (in combat, health percentage) -- Updates dynamically as NPCs spawn, despawn, or change state - -## Common Features of NPC Conditions - -All NPC conditions provide core functionality for NPC-based checks: - -- `isSatisfied()`: Determines if the current NPC state satisfies the condition -- `getDescription()`: Returns a human-readable description of the NPC condition -- `reset()`: Refreshes any cached NPC data -- Various configuration options for specifying which NPCs to monitor and what states to check - -## Using NPC Conditions as Start Conditions - -When used as start conditions, NPC conditions can trigger plugins when specific NPCs appear or enter a certain state: - -```java -PluginScheduleEntry entry = new PluginScheduleEntry("MyPlugin", true); - -// Start the plugin when a Rock Crab appears -entry.addStartCondition(new NpcCondition( - "Rock Crab", // NPC name to check (can also use ID) - true, // NPC must be present - 15 // Within 15 tiles -)); -``` - -## Using NPC Conditions as Stop Conditions - -NPC conditions are also valuable as stop conditions to deactivate plugins based on NPC state: - -```java -// Stop the plugin when the target NPC dies or despawns -entry.addStopCondition(new NpcCondition( - 3080, // NPC ID (Moss giant) - false // NPC must NOT be present (i.e., has despawned) -)); - -// OR stop when the player is no longer in combat with any NPC -entry.addStopCondition(new NotCondition( - new NpcCondition().inCombat(true) -)); -``` - -## Combining with Logical Conditions - -NPC conditions can be combined with logical conditions to create more complex NPC-related rules: - -```java -// Create a logical AND condition -AndCondition combatCondition = new AndCondition(); - -// Require being in combat with an NPC -combatCondition.addCondition(new NpcCondition().inCombat(true)); - -// AND the NPC's health is below 25% -combatCondition.addCondition(new NpcCondition().healthPercentageLessThan(25)); - -// Add this combined condition as a start condition for a finishing move plugin -entry.addStartCondition(combatCondition); -``` - -## Advanced NPC Monitoring - -For more complex NPC monitoring scenarios, multiple conditions can be combined: - -```java -// Create a logical OR condition for multiple NPC types -OrCondition dragonTargets = new OrCondition(); - -// Add various dragon types -dragonTargets.addCondition(new NpcCondition( - "Blue dragon", true, 20 -)); -dragonTargets.addCondition(new NpcCondition( - "Red dragon", true, 20 -)); -dragonTargets.addCondition(new NpcCondition( - "Green dragon", true, 20 -)); -dragonTargets.addCondition(new NpcCondition( - "Black dragon", true, 20 -)); - -// Add this combined condition to start a dragon-fighting plugin -entry.addStartCondition(dragonTargets); -``` - -## Event Integration - -NPC conditions integrate with the RuneLite event system to track changes in real-time: - -- `NpcSpawned`: Detects when new NPCs appear in the game -- `NpcDespawned`: Detects when NPCs are removed from the game -- `NpcChanged`: Detects when NPC properties or appearance changes -- `InteractingChanged`: Detects when the player starts or stops interacting with an NPC -- `HitsplatApplied`: Monitors damage dealt to NPCs -- `GameTick`: Periodically validates NPC state \ No newline at end of file diff --git a/docs/scheduler/conditions/resource-conditions.md b/docs/scheduler/conditions/resource-conditions.md deleted file mode 100644 index 4ec13a828c9..00000000000 --- a/docs/scheduler/conditions/resource-conditions.md +++ /dev/null @@ -1,177 +0,0 @@ -# Resource Conditions - -Resource conditions in the Plugin Scheduler system allow plugins to be controlled based on the player's inventory, gathered resources, and item interactions. - -## Overview - -Resource conditions monitor various aspects of item and resource management in the game. They can track inventory contents, gathered resources, processed items, and loot drops, making them particularly useful for automation related to skilling, combat, and resource collection. - -## Available Resource Conditions - -### InventoryItemCountCondition - -The `InventoryItemCountCondition` monitors the quantity of specified items in the player's inventory. - -**Usage:** -```java -// Satisfied when inventory contains at least 1000 coins -InventoryItemCountCondition condition = new InventoryItemCountCondition( - ItemID.COINS, // Item ID to check - 1000, // Quantity - // Comparison operator -); -``` - -**Key features:** -- Monitors specific item IDs or any item (using ItemID.ANY) -- Supports various comparison types (equals, greater than, less than, etc.) -- Updates dynamically as inventory contents change -- Can track progress toward target quantities - -### GatheredResourceCondition - -The `GatheredResourceCondition` tracks resources that the player has gathered (like ore from mining or logs from woodcutting). - -**Usage:** -```java -// Satisfied when player has gathered 100 yew logs -GatheredResourceCondition condition = new GatheredResourceCondition( - ItemID.YEW_LOGS, // Resource item ID - 100, // Target quantity - -); -``` - -**Key features:** -- Tracks the total amount of a resource gathered over time -- Persists count even if items are banked, dropped, or used -- Useful for long-term gathering goals -- Can be reset if needed - -### ProcessItemCondition - -The `ProcessItemCondition` monitors items that have been processed (like ores smelted into bars or logs made into bows). - -**Usage:** -```java -// Satisfied when player has processed 50 yew logs into yew longbows -ProcessItemCondition condition = new ProcessItemCondition( - ItemID.YEW_LOGS, // Input item ID - ItemID.YEW_LONGBOW, // Output item ID - 50, // Target quantity - -); -``` - -**Key features:** -- Tracks the transformation of one item into another -- Monitors both input and output items -- Useful for crafting, smithing, and other processing skills -- Can be configured to track multiple possible outputs - -### LootItemCondition - -The `LootItemCondition` monitors items that have been looted from the ground. - -**Usage:** -```java -// Satisfied when player has looted 10 dragon bones -LootItemCondition condition = new LootItemCondition( - ItemID.DRAGON_BONES, // Item ID to track - 10, // Target quantity - -); -``` - -**Key features:** -- Specifically tracks items picked up from the ground -- Distinguishes between looted items and other inventory additions -- Useful for monster drop tracking -- Can be configured to track specific areas or with item filters - -## Common Features of Resource Conditions - -All resource conditions implement the `ResourceCondition` interface, which extends the base `Condition` interface and provides additional functionality: - -- `getProgressPercentage()`: Returns the progress toward the condition goal as a percentage -- `reset()`: Resets the tracking counters to zero -- `getTrackedQuantity()`: Returns the current tracked quantity -- `getTargetQuantity()`: Returns the target quantity needed to satisfy the condition - -## Using Resource Conditions as Start Conditions - -When used as start conditions, resource conditions can trigger plugins based on inventory state or gathered resources: - -```java -PluginScheduleEntry entry = new PluginScheduleEntry("MyPlugin", true); - -// Start the plugin when inventory contains at least 1000 coins -entry.addStartCondition(new InventoryItemCountCondition( - ItemID.COINS, - 1000, - -)); -``` - -## Using Resource Conditions as Stop Conditions - -Resource conditions are particularly powerful as stop conditions for plugins: - -```java -// Stop when inventory is full -entry.addStopCondition(new InventoryItemCountCondition( - ItemID.ANY, - 28, - -)); - -// OR stop when a specific goal is reached -entry.addStopCondition(new GatheredResourceCondition( - ItemID.YEW_LOGS, - 1000, - -)); -``` - -## Combining with Logical Conditions - -Resource conditions can be combined with logical conditions to create complex resource management rules: - -```java -// Create a logical OR condition -OrCondition stopConditions = new OrCondition(); - -// Stop when inventory is full -stopConditions.addCondition(new InventoryItemCountCondition( - ItemID.ANY, - 28, - -)); - -// OR when the player has gathered 1000 yew logs -stopConditions.addCondition(new GatheredResourceCondition( - ItemID.YEW_LOGS, - 1000, - -)); - -// OR when the player has crafted 500 yew longbows -stopConditions.addCondition(new ProcessItemCondition( - ItemID.YEW_LOGS, - ItemID.YEW_LONGBOW, - 500, - -)); - -// Add the combined stop conditions to the plugin schedule -entry.addStopCondition(stopConditions); -``` - -## Progress Tracking and Event Integration - -Resource conditions integrate with the RuneLite event system to track changes in real-time: - -- `ItemContainerChanged`: Updates inventory item counts -- `ItemSpawned`/`ItemDespawned`: Monitors ground items for looting -- `MenuOptionClicked`: Detects item processing actions -- `GameTick`: Periodically validates condition state \ No newline at end of file diff --git a/docs/scheduler/conditions/skill-conditions.md b/docs/scheduler/conditions/skill-conditions.md deleted file mode 100644 index 655f33ab53e..00000000000 --- a/docs/scheduler/conditions/skill-conditions.md +++ /dev/null @@ -1,230 +0,0 @@ -# Skill Conditions - -Skill conditions in the Plugin Scheduler system allow plugins to be controlled based on the player's skill levels and experience points. - -## Overview - -Skill conditions monitor the player's progress in various skills, allowing plugins to respond to skill-related milestones and achievements. These conditions can be used to automate skill training, set goals, and manage plugin schedules based on skill progress. - -## Available Skill Conditions - -### SkillLevelCondition - -The `SkillLevelCondition` monitors the player's actual level in a specific skill. - -**Usage:** -```java -// Satisfied when player has at least level 70 Mining -SkillLevelCondition condition = new SkillLevelCondition( - Skill.MINING, // The skill to monitor - 70 // Target level -); - -// Satisfied when player gains 5 levels in Attack (relative) -SkillLevelCondition relativeCondition = SkillLevelCondition.createRelative( - Skill.ATTACK, // The skill to monitor - 5 // Target level gain -); - -// Satisfied when player reaches a random level between 70-80 in Mining -SkillLevelCondition randomizedCondition = SkillLevelCondition.createRandomized( - Skill.MINING, // The skill to monitor - 70, // Minimum target level - 80 // Maximum target level -); -``` - -**Key features:** -- Monitors any skill in the game -- Can track total level using `Skill.OVERALL` -- Supports absolute level targets (reach a specific level) -- Supports relative level targets (gain X levels from current) -- Can use randomization within a min/max range -- Updates dynamically as skill levels change -- Provides progress tracking toward target levels - -### SkillXpCondition - -The `SkillXpCondition` monitors the player's experience points in a specific skill. - -**Usage:** -```java -// Absolute XP goal: Satisfied when player has at least 1,000,000 XP in Woodcutting -SkillXpCondition condition = new SkillXpCondition( - Skill.WOODCUTTING, // The skill to monitor - 1_000_000 // Target XP (absolute) -); - -// Relative XP goal: Satisfied when player gains 50,000 XP from the starting point -SkillXpCondition relativeCondition = SkillXpCondition.createRelative( - Skill.WOODCUTTING, // The skill to monitor - 50_000 // Target XP gain -); - -// Randomized XP goal: Satisfied when player reaches a random XP between 1M-1.5M -SkillXpCondition randomizedCondition = SkillXpCondition.createRandomized( - Skill.WOODCUTTING, // The skill to monitor - 1_000_000, // Minimum target XP - 1_500_000 // Maximum target XP -); - -// Randomized relative XP goal: Satisfied when player gains a random amount of XP -// between 50K-100K from starting point -SkillXpCondition relativeRandomCondition = SkillXpCondition.createRelativeRandomized( - Skill.WOODCUTTING, // The skill to monitor - 50_000, // Minimum XP gain - 100_000 // Maximum XP gain -); -``` - -**Key features:** -- Monitors precise XP values rather than levels -- Useful for tracking progress between levels -- Can be used to set specific XP goals -- Provides accurate progress percentage toward XP targets -- Supports both absolute XP targets (reach a specific XP amount) -- Supports relative XP targets (gain X XP from current) -- Can use randomization within a min/max range for both absolute and relative targets - -## Common Features of Skill Conditions - -All skill conditions implement the `SkillCondition` interface, which extends the base `Condition` interface and provides additional functionality: - -- `getProgressPercentage()`: Returns the progress toward the target level or XP as a percentage -- `reset()`: Resets any cached skill data -- `getSkill()`: Returns the skill being monitored -- `getTargetValue()`: Returns the target level or XP value - -## Using Skill Conditions as Start Conditions - -When used as start conditions, skill conditions can trigger plugins based on skill achievements: - -```java -PluginScheduleEntry entry = new PluginScheduleEntry("MyPlugin", true); - -// Start the plugin when the player reaches level 70 in Mining -entry.addStartCondition(new SkillLevelCondition( - Skill.MINING, - 70 -)); -``` - -## Using Skill Conditions as Stop Conditions - -Skill conditions can be used as stop conditions to end a plugin's execution when a skill goal is reached: - -```java -// Stop when the player reaches level 80 in Mining -entry.addStopCondition(new SkillLevelCondition( - Skill.MINING, - 80 -)); - -// OR stop when the player gains 100,000 XP in Mining -entry.addStopCondition(SkillXpCondition.createRelative( - Skill.MINING, - 100_000 -)); -``` - -## Tracking Relative Changes - -Both `SkillXpCondition` and `SkillLevelCondition` support tracking relative changes, which is useful for setting goals based on progress from the current state rather than absolute values: - -```java -// Satisfied when the player gains 50,000 XP in total from when the condition was created -SkillXpCondition condition = SkillXpCondition.createRelative( - Skill.OVERALL, // Track total XP across all skills - 50_000 // Target XP gain -); - -// Satisfied when the player gains 5 levels in Mining from when the condition was created -SkillLevelCondition levelCondition = SkillLevelCondition.createRelative( - Skill.MINING, // The skill to monitor - 5 // Target level gain -); -``` - -## Combining with Logical Conditions - -Skill conditions can be combined with logical conditions to create complex skill-based rules: - -```java -// Create a logical AND condition -AndCondition skillGoals = new AndCondition(); - -// Require level 70 in Mining -skillGoals.addCondition(new SkillLevelCondition( - Skill.MINING, - 70 -)); - -// AND level 70 in Smithing -skillGoals.addCondition(new SkillLevelCondition( - Skill.SMITHING, - 70 -)); - -// Add these combined requirements as a start condition -entry.addStartCondition(skillGoals); -``` - -## Multi-Skill Training Scenarios - -For multi-skill training scenarios, skill conditions can be configured to monitor several skills: - -```java -// Create a logical OR condition for alternative training paths -OrCondition trainingGoals = new OrCondition(); - -// Path 1: Mining to level 80 -trainingGoals.addCondition(new SkillLevelCondition( - Skill.MINING, - 80 -)); - -// Path 2: Fishing to level 80 -trainingGoals.addCondition(new SkillLevelCondition( - Skill.FISHING, - 80 -)); - -// Path 3: Woodcutting to level 80 -trainingGoals.addCondition(new SkillLevelCondition( - Skill.WOODCUTTING, - 80 -)); - -// Add these alternative goals as a stop condition -entry.addStopCondition(trainingGoals); -``` - -## Event Integration - -Skill conditions integrate with the RuneLite event system to track changes in real-time: - -- `StatChanged`: Updates skill levels and XP values when they change -- `GameTick`: Periodically validates condition state - -## Performance Optimizations - -The `SkillCondition` base class includes several optimizations for improved performance: - -- **Static Caching**: Skill levels and XP values are cached in static maps to minimize client thread calls -- **Throttled Updates**: Updates are throttled to prevent excessive client thread operations -- **Icon Caching**: Skill icons are cached to improve UI rendering performance -- **Single Source of Truth**: All skill-related conditions use the same cached skill data -- **Efficient Event Handling**: Only relevant skill updates trigger condition recalculation - -Example using the cached data: - -```java -// Get cached skill data without requiring client thread call -int currentLevel = SkillCondition.getSkillLevel(Skill.MINING); -long currentXp = SkillCondition.getSkillXp(Skill.MINING); -int totalLevel = SkillCondition.getTotalLevel(); -long totalXp = SkillCondition.getTotalXp(); - -// Force an update of all skill data (throttled to prevent performance issues) -SkillCondition.forceUpdate(); -``` \ No newline at end of file diff --git a/docs/scheduler/conditions/time-conditions.md b/docs/scheduler/conditions/time-conditions.md deleted file mode 100644 index 4520b6233b5..00000000000 --- a/docs/scheduler/conditions/time-conditions.md +++ /dev/null @@ -1,128 +0,0 @@ -# Time Conditions - -Time conditions are a fundamental part of the Plugin Scheduler system that allow plugins to be scheduled based on various time-related factors. - -## Overview - -Time conditions enable plugins to run at specific times, intervals, or within time windows. They are essential for automating plugin execution based on real-world time constraints. - -## Available Time Conditions - -### IntervalCondition - -The `IntervalCondition` allows plugins to run at regular time intervals. - -**Usage:** -```java -// Run every 30 minutes -IntervalCondition condition = new IntervalCondition(Duration.ofMinutes(30)); -``` - -**Key features:** -- Flexible interval specification using Java's `Duration` class -- Configurable randomization to add variation to the timing -- Reset capability to restart the interval countdown - -### SingleTriggerTimeCondition - -The `SingleTriggerTimeCondition` triggers once at a specific date and time. - -**Usage:** -```java -// Trigger at a specific date and time -ZonedDateTime triggerTime = ZonedDateTime.of(2025, 4, 18, 15, 0, 0, 0, ZoneId.systemDefault()); -SingleTriggerTimeCondition condition = new SingleTriggerTimeCondition(triggerTime); -``` - -**Key features:** -- One-time execution at a precise moment -- Cannot trigger again after the specified time has passed -- Progress tracking toward the trigger time - -### TimeWindowCondition - -The `TimeWindowCondition` allows plugins to run within specific time windows on a daily basis. - -**Usage:** -```java -// Run between 9 AM and 5 PM -TimeWindowCondition condition = new TimeWindowCondition( - LocalTime.of(9, 0), // Start time - LocalTime.of(17, 0) // End time -); -``` - -**Key features:** -- Daily recurring time windows -- Configurable start and end times -- Support for windows that span midnight - -### DayOfWeekCondition - -The `DayOfWeekCondition` restricts plugin execution to specific days of the week. - -**Usage:** -```java -// Run only on weekends -Set weekendDays = EnumSet.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY); -DayOfWeekCondition condition = new DayOfWeekCondition(weekendDays); -``` - -**Key features:** -- Selection of multiple days of the week -- Combines with other time conditions to create complex schedules - -## Common Features of Time Conditions - -All time conditions implement the `TimeCondition` interface, which extends the base `Condition` interface and provides additional time-specific functionality: - -- `getCurrentTriggerTime()`: Returns the next time this condition will be satisfied -- `getDurationUntilNextTrigger()`: Returns the time remaining until the next trigger -- `hasTriggered()`: Indicates if a one-time condition has already triggered -- `canTriggerAgain()`: Determines if the condition can trigger again in the future - -## Using Time Conditions as Start Conditions - -When used as start conditions, time conditions determine when a plugin should be activated: - -```java -PluginScheduleEntry entry = new PluginScheduleEntry("MyPlugin", true); -// Run every 2 hours -entry.addStartCondition(new IntervalCondition(Duration.ofHours(2))); -``` - -## Using Time Conditions as Stop Conditions - -When used as stop conditions, time conditions control when a plugin should be deactivated: - -```java -// Stop after running for 30 minutes -entry.addStopCondition(new SingleTriggerTimeCondition( - ZonedDateTime.now().plusMinutes(30) -)); -``` - -## Combining Time Conditions - -Time conditions can be combined with logical conditions to create complex scheduling rules: - -```java -// Create a logical AND condition -AndCondition timeRules = new AndCondition(); - -// Add time window (9 AM to 5 PM) -timeRules.addCondition(new TimeWindowCondition( - LocalTime.of(9, 0), - LocalTime.of(17, 0) -)); - -// Add day of week condition (weekdays only) -Set weekdays = EnumSet.of( - DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, DayOfWeek.FRIDAY -); -timeRules.addCondition(new DayOfWeekCondition(weekdays)); - -// Add the combined time rules as a start condition -entry.addStartCondition(timeRules); -``` \ No newline at end of file diff --git a/docs/scheduler/defining-conditions.md b/docs/scheduler/defining-conditions.md deleted file mode 100644 index 6e5e6f4eba4..00000000000 --- a/docs/scheduler/defining-conditions.md +++ /dev/null @@ -1,584 +0,0 @@ -# Defining Conditions in the Scheduler UI - -This guide provides detailed instructions on how to use the condition configuration panels in the Plugin Scheduler UI to define start and stop conditions for your plugins. - -## Understanding the Condition Panel -The condition configuration panel is the heart of the scheduler's power, allowing you to create sophisticated logic that determines when plugins start and stop. Think of it as programming your character's behavior without writing code. - -### Panel Components - -The panel consists of four main sections: - -1. **Condition Category Dropdown**: Select the type of condition you want to create - - Time: For scheduling based on real-world time - - Skill: For conditions based on your character's skills and XP - - Resource: For inventory, item collection, and processing conditions - - Location: For position-based conditions in the game world - - Varbit: For game state variables and collection log entries - -2. **Condition Type Selection**: Each category has several specific condition types - - Shows only relevant condition types based on your selected category - - Provides tooltips explaining each condition type's purpose - -3. **Parameter Configuration**: Customize the condition with specific values - - Different condition types have different parameter forms - - Includes helpful controls like time pickers, dropdown menus, and numeric inputs - -4. **Logical Structure Tree**: Visual representation of your condition structure - - Shows how conditions are combined with AND/OR logic - - Allows selecting, grouping, and organizing conditions - - Displays satisfaction status with visual indicators - -![Condition Panel Overview](../img/condition-panel-overview.png) - -### Creating Your First Condition - -1. Select the relevant tab (Start Conditions or Stop Conditions) -2. Choose a condition category from the dropdown -3. Select a specific condition type -4. Configure the parameters -5. Click "Add" to add it to your condition structure -6. The condition appears in the tree view, where you can organize it - -## Condition Categories and Types - -### Time Conditions - -Time conditions are among the most commonly used and versatile conditions in the scheduler. They allow you to control when plugins start and stop based on real-world time factors. - -#### Available Time Condition Types - -| Condition Type | Description | Real-World Examples | -|----------------|-------------|-------------| -| **Time Duration** | Runs for a specific amount of time | "Run for 3 hours then stop" (good as a stop condition) | -| **Time Window** | Runs during specific hours each day | "Only run between 8am-10pm when I'm at my computer" | -| **Not In Time Window** | Runs outside specific hours | "Don't run during work hours (9am-5pm)" | -| **Specific Time** | Runs at an exact date/time | "Run exactly at 8:00pm on Tuesdays" | -| **Day of Week** | Runs on specific days | "Run on weekends only" | -| **Interval** | Runs at regular time intervals | "Run every 2 hours" or "Take a 15-minute break every hour" | - -#### Practical Examples - -**Example 1: Setting Up a Time Window for Evening Play** - -This is perfect for limiting your botting to specific hours: - -1. Select "Time" from the Category dropdown -2. Select "Time Window" from the Type dropdown -3. Set the start time to 6:00 PM (when you get home) -4. Set the end time to 11:00 PM (before bed) -5. Optional: Enable randomization for more human-like timing -6. Click "Add" to add the condition - -Now your plugins will only run during your evening hours and automatically stop at night. - -**Example 2: Creating a Playtime Limit** - -To limit how long a plugin runs (great for stop conditions): - -1. Select "Time" from the Category dropdown -2. Select "Time Duration" from the Type dropdown -3. Set the duration to your desired play time (e.g., 2 hours) -4. Enable randomization for natural variation (e.g., 1h45m to 2h15m) -5. Click "Add" to add the condition - -This ensures your plugin won't run for too long, preventing detection and burnout. - -**Example 3: Setting Up Weekend-Only Gaming** - -For those who only want to bot on weekends: - -1. Select "Time" from the Category dropdown -2. Select "Day of Week" from the Type dropdown -3. Check only "Saturday" and "Sunday" in the day selector -4. Click "Add" to add the condition - -Combine this with other time conditions for even more control, like "Run on weekends between 10am and 8pm". - -### Skill Conditions - -Skill conditions allow you to create automation based on your character's skills and experience progress. They're perfect for training goals and level-based activities. - -#### Available Skill Condition Types - -| Condition Type | Description | Strategic Uses | -|----------------|-------------|---------------| -| Skill Level | Sets absolute or relative level targets | Stop when reaching a milestone level | -| Skill XP Goal | Sets XP targets rather than levels | Get precise control over training sessions | -| Skill Level Required | Sets minimum requirements | Safeguard activities that require certain levels | - -#### Practical Skill Condition Examples - -**Example 1: Setting a Level-Based Training Goal** - -To create a condition that stops a plugin when you reach a specific level: - -1. Select "Skill" from the Category dropdown -2. Select "Skill Level" from the Type dropdown -3. Choose your skill from the dropdown (e.g., "Fishing") -4. Select either: - - Absolute level: Specific level to reach (e.g., 70) - - Relative level: Levels to gain from current (e.g., +5 levels) -5. Optional: Enable randomization for the target level to vary behavior -6. Click "Add" to add the condition - -This creates a natural stopping point for your training session, perfect for goal-oriented skilling. - -**Example 2: Setting an XP-Based Training Session** - -For more precise control over training duration: - -1. Select "Skill" from the Category dropdown -2. Select "Skill XP Goal" from the Type dropdown -3. Choose your skill from the dropdown -4. Enter your XP target (e.g., 50,000 XP) -5. Select whether this is absolute or relative to current XP -6. Click "Add" to add the condition - -XP-based conditions give you finer control than level-based ones, especially at higher levels where levels take longer to achieve. - -**Example 3: Level Requirement Safety Check** - -To ensure you have the required level before attempting an activity: - -1. Select "Skill" from the Category dropdown -2. Select "Skill Level Required" from the Type dropdown -3. Choose the required skill (e.g., "Agility") -4. Set the minimum level needed (e.g., 60 for Seers Village course) -5. Click "Add" to add the condition - -This prevents your plugin from starting activities you don't have the levels for, avoiding failures and wasted time. - -**Pro Tip:** Combine skill conditions with time conditions to create balanced training sessions. For example: "Train Woodcutting until level 70 OR until 2 hours have passed." - -### Resource Conditions - -Resource conditions are essential for inventory management and gathering activities. They let you create rules based on items, resources, and inventory state. - -#### Available Resource Condition Types - -| Condition Type | Description | Common Use Cases | -|----------------|-------------|-----------------| -| Item Collection | Tracks collected items over time | Stop after gathering 1000 feathers | -| Process Items | Tracks items processed/created | Stop after smithing 500 cannonballs | -| Gather Resources | Tracks gathered raw resources | Stop after mining 200 iron ore | -| Inventory Item Count | Checks current inventory state | Start when inventory is empty, stop when full | -| Bank Item Count | Checks items in bank | Stop when bank has 10,000 vials | -| Loot Item | Checks for specific drops | Keep killing until specific drop obtained | -| Item Required | Checks for required items | Only start if you have antipoison | - -#### Practical Resource Condition Examples - -**Example 1: Setting Up an Efficient Woodcutting Session** - -To create a stop condition that triggers when you've cut 500 logs or your inventory is full: - -1. Select "Resource" from the Category dropdown -2. Select "Gather Resources" from the Type dropdown -3. Enter "Yew logs" in the item name field -4. Set the target amount to 500 -5. Click "Add" to add this condition -6. Add another condition for inventory: - - Select "Resource" category again - - Select "Inventory Item Count" type - - Enter "28" for full inventory - - Set the comparison to "greater than or equal to" - - Click "Add" -7. Ensure these are in an OR relationship in the condition tree - -Now your plugin will stop when either you've gathered 500 logs OR your inventory becomes full. - -**Example 2: Setting Up a Profitable Crafting Session** - -For a plugin that runs until you've crafted a specific number of items: - -1. Select "Resource" from the Category dropdown -2. Select "Process Items" from the Type dropdown -3. Enter "Gold bracelet" in the item name field -4. Set the target amount to 100 -5. Enable "Track profits" if you want to monitor profitability -6. Click "Add" to add the condition - -This will track how many items you've crafted and automatically stop once you reach your goal. - -**Example 3: Required Items Safety Check** - -To ensure you have the necessary items before starting a dangerous activity: - -1. Select "Resource" from the Category dropdown -2. Select "Item Required" from the Type dropdown -3. Enter "Super antipoison" in the item name field -4. Set the minimum amount to 2 -5. Click "Add" to add this condition -6. Add more required items as needed using AND logic - -This creates a safety check that prevents your plugin from starting unless you have all the required items. - -### Location Conditions - -Location conditions allow you to create rules based on where your character is in the game world. These are essential for region-specific activities and location-based task switching. - -#### Available Location Condition Types - -| Condition Type | Description | Strategic Uses | -|----------------|-------------|----------------| -| Position | Based on exact coordinates with radius | Create specific action spots | -| Area | Based on a rectangular area | Define larger working zones | -| Region | Based on game region IDs | Control activities by game area | -| Distance | Based on distance from a point | Create proximity-based triggers | -| Not In Area | Inverted area condition | Create exclusion zones | - -#### Practical Location Condition Examples - -**Example 1: Setting Up a Mining Location** - -To create a condition that only runs your plugin when you're at a specific mining site: - -1. Navigate to the desired mining location in-game -2. Select "Location" from the Category dropdown -3. Select "Area" from the Type dropdown -4. Click "Capture Current Area" to use your current position -5. Adjust the area size using the sliders (make it large enough to cover the entire mining area) -6. Click "Add" to add the condition - -This ensures your mining plugin only runs when you're actually at the mining site, preventing it from activating in inappropriate locations. - -**Example 2: Creating a Safe Zone** - -To create a condition that stops a plugin when you enter a dangerous area: - -1. Select "Location" from the Category dropdown -2. Select "Not In Area" from the Type dropdown -3. Define the area you consider safe (e.g., a bank or city) -4. Click "Add" to add the condition - -This works as a safety measure - the plugin will only run when you're in the defined safe area and automatically stop if you leave it. - -**Example 3: Bank-Proximity Condition** - -For creating a condition that activates when you're near a bank: - -1. Stand near your preferred bank -2. Select "Location" from the Category dropdown -3. Select "Position" from the Type dropdown -4. Click "Use Current Position" -5. Set an appropriate radius (e.g., 15 tiles to cover the bank area) -6. Click "Add" to add the condition - -This is perfect for banking plugins that should only activate when you're actually near a bank, preventing premature attempts to bank while still gathering resources. - -**Pro Tip:** Combine location conditions with resource conditions for complete automation cycles. For example: "Stop mining when inventory is full OR player is no longer in the mining area." - -### NPC Conditions - -NPC conditions allow you to create rules based on interactions with non-player characters in the game. They're particularly useful for combat activities, boss fights, and NPC-dependent tasks. - -#### Available NPC Condition Types - -| Condition Type | Description | Strategic Uses | Notes | -|----------------|-------------|----------------|-------| -| NPC Kill Count | Tracks how many of a specific NPC you've killed | Stop after killing 100 goblins | May have tracking issues in multi-combat areas | - -> **Note**: Currently, only the NPC Kill Count condition is fully implemented. Future versions may include NPC Presence and NPC Interaction conditions. - -#### Practical NPC Condition Examples - -**Example 1: Basic Kill Count Goal** - -For a plugin that runs until you've killed a certain number of NPCs: - -1. Select "NPC" from the Category dropdown -2. Select "Kill Count" from the Type dropdown -3. Enter the NPC name (e.g., "Goblin") -4. Set the target count (e.g., 50) -5. Click "Add" to add the condition - -The plugin will track NPC interactions and count kills until the target is reached. - -**Example 2: Multi-NPC Kill Goals with Logical Conditions** - -For more complex hunting goals that involve multiple NPC types: - -1. Create a kill count condition for the first NPC type -2. Create a kill count condition for the second NPC type -3. Use logical operators to combine them: - - AND: Must kill both target counts (e.g., "Kill 50 goblins AND 30 rats") - - OR: Killing either target count is sufficient (e.g., "Kill 50 goblins OR 30 rats") - -**Advanced Features:** - -- **Name Pattern Matching**: Use regular expressions to match similar NPCs (e.g., "Goblin.*" matches all goblin variants) -- **Randomized Goals**: Set min/max ranges for more varied play patterns -- **Progress Tracking**: View detailed statistics including kill rate and completion percentage -- **Interaction Detection**: Accurately tracks which NPCs you're engaged with - -### Varbit Conditions - -Varbit conditions relate to game state variables and track internal game values, allowing you to create rules based on quests, minigames, collection logs, and other game state information. - -#### Available Varbit Condition Types - -| Condition Type | Description | Use Case | -|----------------|-------------|----------| -| Collection Log - Bosses | Based on boss collection log entries | "Stop after collecting all GWD items" | -| Collection Log - Minigames | Based on minigame collection log entries | "Run until Tempoross log is complete" | -| Custom Varbit | Track a specific varbit or varp ID | "Wait until quest state changes" | - -#### Advanced Varbit Features - -- **Relative or Absolute Values**: Compare against absolute values or relative changes from the starting state -- **Comparison Operators**: Use equals, not equals, greater than, less than, etc. -- **Randomization**: Set min/max target ranges for more varied behavior - -#### Working with Varbit Conditions - -Varbit conditions provide powerful ways to track game progress but require some technical understanding: - -1. **Finding Varbit IDs**: Use developer tools to identify the relevant varbit ID for your condition -2. **Understanding Value Ranges**: Most varbits use values 0-1 for off/on states, but some track counts or progress -3. **Test Thoroughly**: Always test your varbit conditions before relying on them for automation - -## Creating Complex Logical Conditions - -The true power of the scheduler comes from creating sophisticated logic by combining multiple conditions. Think of this as building "if-then" statements that determine when your plugins run. - -### Understanding Logical Operators - -Before we dive in, let's understand the basic logical operators: - -- **AND**: All conditions must be true (like saying "I'll only go fishing IF I have bait AND I have a fishing rod") -- **OR**: Any condition can be true (like saying "I'll stop fishing IF my inventory is full OR it's been 2 hours") -- **NOT**: Inverts a condition (like saying "Run the plugin when I'm NOT in the Wilderness") -- **LOCK**: Prevents a plugin from stopping while the lock is active (critical for combat and dangerous activities) - -### Using AND Logic (All Conditions) - -Use AND logic when you want a plugin to start/stop only when ALL specified conditions are met: - -1. Add your first condition (e.g., "Time Window: 6PM-10PM") -2. In the condition tree, select the root node -3. Click "Convert to AND" (the icon changes to show AND logic) -4. Add your second condition (e.g., "Location: Mining Guild") -5. Add any additional conditions - -**Real-World Example: Safe Mining Training** -``` -AND -├── TimeWindow(6:00 PM to 10:00 PM) -├── Location(Mining Guild) -└── InventoryItemCount(Pickaxe) >= 1 -``` -This setup ensures your plugin only runs in the evening, when you're in the Mining Guild, and have a pickaxe. - -### Using OR Logic (Any Conditions) - -Use OR logic when you want a plugin to start/stop when ANY of the specified conditions are met: - -1. Add your first condition (e.g., "Inventory Full") -2. In the condition tree, ensure it's set to OR (this is the default) -3. Add your second condition (e.g., "Time Duration: 2 hours") -4. Add any additional conditions - -**Real-World Example: Smart Fishing Stop Condition** -``` -OR -├── InventoryItemCount(Raw fish) >= 28 -├── TimeDuration(2 hours) -└── PlayerHealth < 20% -``` -This will stop your fishing plugin when your inventory fills OR you've been fishing for 2 hours OR your health gets dangerously low. - -### Using NOT Logic (Negate Conditions) - -Use NOT logic when you want to invert a condition's result: - -1. Select the condition in the tree -2. Click the "Negate" button (usually shown as a "!" icon) -3. The condition will be inverted and shown with "NOT" in the description - -**Real-World Example: Avoid Dangerous Times** - -```text -NOT(TimeWindow(2:00 AM to 6:00 AM)) -``` - -This creates a condition that's true except during the specified hours, helping you avoid playing during suspicious times. - -### Creating Nested Conditions - -For more complex logic, you can create nested condition groups that combine AND and OR logic: - -1. Add several conditions -2. Select a subset of those conditions in the tree -3. Click "Group AND" or "Group OR" to create a sub-group -4. The sub-group will now behave as a single condition within the parent group - -**Real-World Example: Advanced Skilling Strategy** - -```text -AND -├── OR -│ ├── Location(Varrock Mine) -│ └── Location(Al Kharid Mine) -└── AND - ├── InventoryItemCount(Total) < 28 - └── NOT(NearbyPlayer > 5) -``` - -This complex condition translates to: "Run the plugin when I'm at either mining location AND my inventory isn't full AND there aren't too many players nearby." - -### Visual Indicators in the Logical Tree - -The condition tree provides visual feedback about your logical structure: - -- **Connecting lines**: Show the relationship between conditions -- **Icons**: Indicate AND/OR/NOT relationships -- **Checkmarks/X marks**: Show which conditions are currently satisfied -- **Highlighting**: Indicates which condition is selected for editing - -Remember that well-designed logical structures can create extremely sophisticated automation patterns without requiring any actual coding! - -## Understanding One-Time vs. Recurring Conditions - -When configuring conditions, it's important to understand which conditions trigger just once and which can trigger repeatedly. This affects how your plugins will behave over extended periods. - -### One-Time Conditions - -One-time conditions are triggers that once satisfied, will stay satisfied forever. They're perfect for milestone events and permanent changes. - -**Examples of one-time conditions:** - -- **Specific Time condition**: Triggers once exactly at 8:00 PM on June 15 -- **Skill Level condition**: Triggers once when Woodcutting reaches level 70 -- **Item Collection condition**: Triggers once after collecting 1000 feathers -- **Collection Log condition**: Triggers once after completing a boss collection - -**How to identify one-time conditions:** - -- Look for the "One-time" label in the schedule table -- These conditions use absolute values rather than ranges or states -- They typically represent achievement of a specific goal - -**Strategic use:** -One-time conditions are excellent for progression-based automation. For example, you might set up a series of plugins that activate as you reach certain milestones, automatically moving your character from one training method to the next as you level up. - -### Recurring Conditions - -Recurring conditions can trigger multiple times as their state changes between satisfied and unsatisfied. - -**Examples of recurring conditions:** - -- **Time Window condition**: Triggers daily during set hours (e.g., 6PM-10PM) -- **Interval condition**: Triggers repeatedly at set intervals (e.g., every 2 hours) -- **Inventory Item Count**: Can trigger repeatedly as inventory fills and empties -- **Location condition**: Triggers each time you enter or leave an area - -**How they work:** -These conditions can switch between active and inactive states multiple times. For example, a Time Window condition for 6PM-10PM will be: - -- Inactive from midnight until 6PM -- Active from 6PM to 10PM -- Inactive from 10PM to midnight -- Active again at 6PM the next day - -**Strategic use:** -Use recurring conditions to create cyclical behavior patterns. For example, combine an inventory condition with a location condition to create a gathering cycle: gather resources until inventory is full, bank when near a bank, repeat when inventory is empty. - -## Indicators and Visualization - -The condition panel provides visual feedback on condition status: - -- **Green checkmark (✓)**: Condition is currently satisfied -- **Red X (✗)**: Condition is not currently satisfied -- **Lightning bolt (⚡)**: Condition is currently relevant to the plugin's state - -The condition tree visualizes the logical structure of your conditions, allowing you to see at a glance how they're organized. - -## Tips for Effective Condition Configuration - -1. **Start simple**: Begin with just one or two conditions and add complexity gradually -2. **Test thoroughly**: Always test your conditions with controlled parameters first -3. **Use the condition tree**: View the logical structure to ensure it matches your intent -4. **Monitor condition status**: Watch the indicators to see if conditions are behaving as expected -5. **Combine time with other conditions**: For example, "Run for 2 hours OR until inventory is full" -6. **Use nested logical groups**: For complex scenarios like "Run when (at location A OR location B) AND (have antipoison)" - -## Common Issues and Solutions - -### Issue: Plugin never starts - -- **Possible cause**: Start conditions are too restrictive -- **Solution**: Check if all required conditions are being met, or switch from AND to OR logic - -### Issue: Plugin never stops - -- **Possible cause**: Stop conditions are never met or are incorrectly configured -- **Solution**: Add a time-based fallback stop condition - -### Issue: Plugin starts or stops at unexpected times - -- **Possible cause**: Logical structure (AND/OR) is not what you intended -- **Solution**: Review the condition tree and restructure as needed - -### Issue: Condition status indicators don't match expectations - -- **Possible cause**: The condition parameters don't match the current game state -- **Solution**: Verify the current game state and adjust the condition parameters - -## Advanced Condition Techniques - -### Using Lock Conditions - -Lock conditions prevent a plugin from stopping during critical operations: - -1. Add your regular stop conditions -2. Add a LockCondition for critical operations -3. In your plugin code, activate the lock when needed -4. The plugin won't stop while the lock is active, even if stop conditions are met - -### Time Randomization - -For more human-like behavior: - -1. When creating time conditions, use the randomization options -2. For intervals, set a min/max range instead of exact times -3. For time windows, consider adding random variations to start/end times - -### Progressive Conditions - -Create conditions that change as the plugin runs: - -1. Use the plugin's `onStopConditionCheck()` method -2. Modify conditions based on progress -3. This allows for dynamic behavior adaptation - -## Condition Reliability Disclaimer - -> **Important**: Not all conditions have been thoroughly tested in all scenarios. The following list indicates the confidence level in each condition type's reliability: -> -> ### High Confidence (Thoroughly Tested) -> - **Time Conditions**: Well-tested and reliable, though some features users want may be missing (feedback appreciated) -> - **Skill Conditions**: Thoroughly tested and reliable -> - **Location Conditions**: Well-tested in most common areas -> - **Logical Conditions**: Thoroughly tested (AND, OR, NOT operators) -> - **Lock Conditions**: Thoroughly tested -> - **Item Collection Conditions** (from Resource Conditions): Well-tested for most common items -> -> ### Medium Confidence (Tested but May Have Edge Cases) -> - **Varbit Conditions**: Tested for collection log entries and common game variables -> - **Bank Item Conditions**: May have edge cases with certain items -> - **Process Item Conditions**: May have tracking issues with certain processing methods -> -> ### Lower Confidence (Newer Implementations) -> - **NPC Conditions**: Implementation is still being refined, particularly for multi-combat areas -> - **NPC Kill Count Conditions**: May not track kills accurately in crowded areas or specific circumstances -> -> If you encounter any issues with condition reliability, please report them with specific details to help improve the system. For critical tasks, we recommend using the high confidence conditions or adding fallback conditions (such as time limits). - -## Conclusion - -Mastering the condition configuration system is key to effectively using the Plugin Scheduler. With proper condition setup, you can create sophisticated automation plans that respond intelligently to game states, time factors, and resource availability. - -For more information on how conditions fit into the overall Scheduler workflow, including default plugins, priorities, and the "Allow Continue" setting, see the [Plugin Scheduler User Guide](user-guide.md). - -For implementation details about specific condition types, see the [API documentation for conditions](api/conditions/). diff --git a/docs/scheduler/event/plugin-schedule-entry-finished-event.md b/docs/scheduler/event/plugin-schedule-entry-finished-event.md deleted file mode 100644 index 78cb19a01fa..00000000000 --- a/docs/scheduler/event/plugin-schedule-entry-finished-event.md +++ /dev/null @@ -1,175 +0,0 @@ -# Plugin Schedule Entry Finished Event - -## Overview - -The `PluginScheduleEntryMainTaskFinishedEvent` is a crucial component of the Plugin Scheduler system's inter-component communication mechanism. This event is fired when a plugin self-reports that it has completed its assigned task and is ready to be stopped by the scheduler. - -## Class Structure - -```java -@Getter -public class PluginScheduleEntryMainTaskFinishedEvent { - private final Plugin plugin; - private final ZonedDateTime finishDateTime; - private final String reason; - private final boolean success; - - // Constructor implementations -} -``` - -## Key Features - -### Plugin Identification - -The event carries a reference to the specific `Plugin` instance that has completed: - -```java -private final Plugin plugin; -``` - -This allows the scheduler to correctly identify which scheduled plugin has finished, even when multiple plugins might be active or scheduled. - -### Timestamp Tracking - -The event includes a precise timestamp of when the plugin completed its work: - -```java -private final ZonedDateTime finishDateTime; -``` - -This timestamp is used by the scheduler for logging, debugging, and calculating statistics about plugin execution times. - -### Success Reporting - -The event includes a boolean flag indicating whether the plugin completed successfully: - -```java -private final boolean success; -``` - -This allows the scheduler to distinguish between different types of completion: -- `true`: The plugin completed its task as expected and terminated normally -- `false`: The plugin encountered an issue that prevented it from completing its task but was still able to gracefully report its status - -### Reason Documentation - -The event provides a human-readable explanation of why the plugin finished: - -```java -private final String reason; -``` - -This reason string can be used for: -- User interface display -- Logging and diagnostics -- Decision-making about future scheduling attempts - -## Technical Details - -### Event Propagation - -This event is sent through the RuneLite EventBus system: - -```java -// Inside a plugin that wants to report completion -Microbot.getEventBus().post(new PluginScheduleEntryMainTaskFinishedEvent( - this, // The plugin itself - "Inventory full", // Reason for finishing - true // Was successful -)); -``` - -### Constructors - -The class provides two constructor options: - -```java -// Constructor with explicit timestamp -public PluginScheduleEntryMainTaskFinishedEvent( - Plugin plugin, - ZonedDateTime finishDateTime, - String reason, - boolean success -) - -// Constructor using current time as the finish time -public PluginScheduleEntryMainTaskFinishedEvent( - Plugin plugin, - String reason, - boolean success -) -``` - -The second constructor is a convenience method that automatically captures the current time. - -### Immutability - -All fields in the event are marked as `final`, ensuring the event is immutable once created. This prevents potential issues with event data being modified during propagation. - -## Usage Example - -### Inside a Plugin - -A plugin can report its completion using code like this: - -```java -// When the plugin has completed its task successfully -Microbot.getEventBus().post(new PluginScheduleEntryMainTaskFinishedEvent( - this, - "Target level reached", - true -)); - -// Or when the plugin needs to stop due to an issue -Microbot.getEventBus().post(new PluginScheduleEntryMainTaskFinishedEvent( - this, - "Unable to find target NPC", - false -)); -``` - -### Using the ConditionProvider Interface - -For plugins that implement the `ConditionProvider` interface, a convenience method is provided: - -```java -// Inside a plugin that implements ConditionProvider -@Override -public void onSkillLevelReached(int level) { - if (level >= targetLevel) { - reportFinished("Target level " + level + " reached", true); - } -} -``` - -The `reportFinished` method internally creates and posts the `PluginScheduleEntryMainTaskFinishedEvent`. - -### In the Scheduler - -The scheduler listens for these events and processes them: - -```java -@Subscribe -public void onPluginScheduleEntryMainTaskFinishedEvent(PluginScheduleEntryMainTaskFinishedEvent event) { - if (currentPlugin != null && currentPlugin.getPlugin() == event.getPlugin()) { - log.info("Plugin {} reported finished: {} (success={})", - currentPlugin.getName(), - event.getReason(), - event.isSuccess()); - - // Stop the plugin and record the finish reason - currentPlugin.setStopReason( - event.isSuccess() ? StopReason.PLUGIN_FINISHED : StopReason.ERROR); - forceStopCurrentPluginScheduleEntry(); - } -} -``` - -## Relationship to Other Components - -The `PluginScheduleEntryMainTaskFinishedEvent` works alongside other events in the scheduler system: - -- It differs from `PluginScheduleEntryPostScheduleTaskEvent`, which is sent by the scheduler to request that a plugin stop -- It is typically used after the plugin has responded to a soft stop request or has independently determined it should stop -- It provides feedback to the scheduler about the plugin's state at the end of execution \ No newline at end of file diff --git a/docs/scheduler/event/plugin-schedule-entry-soft-stop-event.md b/docs/scheduler/event/plugin-schedule-entry-soft-stop-event.md deleted file mode 100644 index 9d7706b7634..00000000000 --- a/docs/scheduler/event/plugin-schedule-entry-soft-stop-event.md +++ /dev/null @@ -1,153 +0,0 @@ -# Plugin Schedule Entry Soft Stop Event - -## Overview - -The `PluginScheduleEntryPostScheduleTaskEvent` signals the start of post-schedule tasks for a plugin. It is emitted when the scheduler transitions a schedule entry out of its main task phase (e.g., stop conditions met, user-initiated stop, or scheduler shutdown) so that coordinated cleanup/post actions can run. - -## Class Structure - -```java -@Getter -public class PluginScheduleEntryPostScheduleTaskEvent { - private final Plugin plugin; - private final ZonedDateTime stopDateTime; - - public PluginScheduleEntryPostScheduleTaskEvent(Plugin plugin, ZonedDateTime stopDateTime) { - this.plugin = plugin; - this.stopDateTime = stopDateTime; - } -} -``` - -## Key Features - -### Plugin Identification - -The event carries a reference to the specific `Plugin` instance that should stop: - -```java -private final Plugin plugin; -``` - -This allows the event to specifically target the plugin that needs to be stopped, enabling precise communication even in a multi-plugin environment. - -### Timestamp Tracking - -The event includes a timestamp indicating when the stop request was issued: - -```java -private final ZonedDateTime stopDateTime; -``` - -This timestamp serves several purposes: -- Records when the stop decision was made for logging and analytics -- Enables time-based escalation if the plugin doesn't respond promptly -- Provides context to the plugin about the timing of the stop request - -## Technical Details - -### Event Propagation - -This event is sent through the RuneLite EventBus system: - -```java -// Inside the scheduler when a plugin should be stopped -Microbot.getEventBus().post(new PluginScheduleEntryPostScheduleTaskEvent( - plugin, // The plugin to stop - ZonedDateTime.now() // Current time -)); -``` - -### Soft Stop Concept - -The term "soft stop" in the event name is significant: - -1. It indicates that this is a request for the plugin to stop gracefully, not an immediate termination command -2. It gives the plugin an opportunity to: - - Complete critical operations in progress - - Save any necessary state - - Clean up resources - - Reach a safe termination point - -### Immutability - -All fields in the event are marked as `final`, ensuring the event is immutable once created. This prevents potential issues with event data being modified during propagation. - -## Usage Example - -### In the Scheduler - -The scheduler creates and posts this event when a plugin's stop conditions are met: - -```java -private void softStopPlugin(PluginScheduleEntry entry) { - Plugin plugin = entry.getPlugin(); - if (plugin != null) { - log.debug("Sending soft stop request to plugin {}", entry.getName()); - - // Post the event to the EventBus - Microbot.getEventBus().post(new PluginScheduleEntryPostScheduleTaskEvent( - plugin, - ZonedDateTime.now() - )); - - // Set state to indicate stopping is in progress - entry.setStopReason(StopReason.CONDITIONS_MET); - currentState = SchedulerState.STOPPING; - - // Schedule hard stop fallback if plugin doesn't respond in time - if (entry.allowHardStop()) { - scheduleHardStopFallback(entry, Duration.ofSeconds(30)); - } - } -} -``` - -### In a Plugin Implementing ConditionProvider - -Plugins that implement the `ConditionProvider` interface can handle the event: - -```java -@Subscribe -@Override -public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - // Only respond if this event is targeted at this plugin - if (event.getPlugin() == this) { - log.info("Received soft stop request, cleaning up and stopping..."); - - // Perform necessary cleanup - saveCurrentProgress(); - closeOpenInterfaces(); - - // Actually stop the plugin - Microbot.stopPlugin(this); - } -} -``` - -## Relationship to Other Components - -The `PluginScheduleEntryPostScheduleTaskEvent` is part of a coordinated stop sequence: - -1. **Trigger**: Stop conditions are met, manual stop requested, or scheduler is shutting down -2. **Soft Stop**: The scheduler sends `PluginScheduleEntryPostScheduleTaskEvent` to request graceful termination -3. **Plugin Response**: The plugin performs cleanup and either: - - Stops itself directly using `Microbot.stopPlugin(this)` - - Posts a `PluginScheduleEntryMainTaskFinishedEvent` to report completion -4. **Hard Stop Fallback**: If the plugin doesn't respond within a timeout period and is marked as hard-stoppable, the scheduler may forcibly terminate it - -## Best Practices - -### For Plugin Developers - -1. **Implement the Event Handler**: Ensure your plugin properly handles the `PluginScheduleEntryPostScheduleTaskEvent` if it implements `ConditionProvider` -2. **Respond Promptly**: Complete cleanup operations quickly to avoid being hard-stopped -3. **Check Target Plugin**: Always verify that the event is targeting your plugin before processing it -4. **Report Completion**: Consider posting a `PluginScheduleEntryMainTaskFinishedEvent` to provide more context about the stop reason - -### For Scheduler Implementation - -1. **Timeout Management**: Set appropriate timeouts for plugins to respond to soft stop requests -2. **Escalation Path**: Define clear escalation when plugins don't respond to soft stops -3. **Stop Context**: Provide useful context in the event for why the plugin is being stopped -4. **Validation**: Verify that the plugin is actually running before sending a stop event \ No newline at end of file diff --git a/docs/scheduler/plugin-schedule-entry-merged.md b/docs/scheduler/plugin-schedule-entry-merged.md deleted file mode 100644 index 539dfd72472..00000000000 --- a/docs/scheduler/plugin-schedule-entry-merged.md +++ /dev/null @@ -1,753 +0,0 @@ -# Plugin Schedule Entry - -## Overview - -The `PluginScheduleEntry` class is the core component of the Plugin Scheduler system, serving as the central data structure that connects plugins with their scheduling configuration. Each entry represents a scheduled plugin and encapsulates all the information needed to determine when a plugin should start and stop, as well as tracking its execution state and history. - -This class acts as the bridge between the scheduler orchestrator (`SchedulerPlugin`), the plugin API (`SchedulablePlugin`), and the user interface, managing both user-defined conditions and plugin-provided conditions through separate `ConditionManager` instances. - -## Class Definition - -```java -@Data -@AllArgsConstructor -@Getter -@Slf4j -public class PluginScheduleEntry implements AutoCloseable { - // Plugin reference and metadata - private transient Plugin plugin; - private String name; - private boolean enabled; - private boolean allowContinue = true; - private boolean hasStarted = false; - - // Condition management - private ConditionManager startConditionManager; - private ConditionManager stopConditionManager; - - // Scheduling properties - private boolean allowRandomScheduling = true; - private int runCount = 0; - private int priority = 0; // Higher numbers = higher priority - private boolean isDefault = false; - - // State tracking - private String lastStopReason; - private boolean lastRunSuccessful; - private boolean onLastStopUserConditionsSatisfied = false; - private boolean onLastStopPluginConditionsSatisfied = false; - private StopReason lastStopReasonType = StopReason.NONE; - private Duration lastRunDuration = Duration.ZERO; - private ZonedDateTime lastRunStartTime; - private ZonedDateTime lastRunEndTime; - - // Stop reason enumeration - public enum StopReason { - NONE("None"), - MANUAL_STOP("Manually Stopped"), - PLUGIN_FINISHED("Plugin Finished"), - ERROR("Error"), - SCHEDULED_STOP("Scheduled Stop"), - INTERRUPTED("Interrupted"), - HARD_STOP("Hard Stop"); - - private final String description; - - // Implementation details... - } - - // Other implementation details... -} -``` - -## Key Features - -### Plugin Reference and Identification - -Each `PluginScheduleEntry` maintains a reference to the actual RuneLite `Plugin` instance it schedules, along with a name for display and identification purposes. The plugin reference is marked as `transient` to ensure it's not serialized when the schedule entry is saved to configuration. - -### Scheduling Properties - -The class includes several properties that directly affect how a plugin gets scheduled: - -#### Priority System - -```java -private int priority = 0; // Higher numbers = higher priority -``` - -The `priority` field is used to determine which plugins are considered first during scheduling: - -- Higher numbers indicate higher priority -- Plugins with the same priority are grouped together for scheduling decisions -- When multiple plugins are due to run at the same time, the highest priority plugins are considered first - -The scheduler will always prefer to run the highest priority plugins first. Only if there are multiple plugins with the same priority level will other factors like the default status, randomization, or weighted selection be considered. - -#### Default Plugin Status - -```java -private boolean isDefault = false; // Flag to indicate if this is a default plugin -``` - -The `isDefault` flag marks a plugin as a "default" plugin that can be preempted by non-default plugins: - -- Default plugins are lower-priority in the overall scheduling system -- Non-default plugins are always preferred over default plugins with the same priority -- The Scheduler can be configured to stop default plugins when a non-default plugin is about to run -- This allows users to run certain critical plugins at scheduled times without interference - -The scheduler configuration includes settings like `prioritizeNonDefaultPlugins` which can automatically stop default plugins when a non-default plugin is scheduled to run soon. - -#### Random Scheduling Support - -```java -private boolean allowRandomScheduling = true; // Whether this plugin can be randomly scheduled -``` - -The `allowRandomScheduling` flag controls whether a plugin participates in the weighted random selection process: - -- When `true` (the default), the plugin can be selected randomly among other plugins with the same priority -- When `false`, the plugin will be strictly scheduled based on its trigger time and priority -- Non-randomizable plugins are always prioritized over randomizable plugins with the same priority - -This property is particularly important for plugins that must run at exact times (like plugins that perform critical actions at specific game events), as setting `allowRandomScheduling = false` will ensure they run exactly when scheduled, without any randomization. - -#### Run Count Tracking - -```java -private int runCount = 0; // Track how many times this plugin has been run -``` - -The `runCount` property keeps track of how many times the plugin has been executed and is used for weighted selection: - -- Plugins that have run less frequently are given a higher weight in the weighted selection algorithm -- This ensures a balanced distribution of execution time among different plugins -- The weight calculation is: `weight = (maxRuns - runCount + 1)` where `maxRuns` is the highest run count among all plugins - -### Dual Condition Management System - -#### User Conditions vs. Plugin Conditions - -The class uses two distinct sets of conditions, each managed by separate `ConditionManager` instances: - -1. **User Conditions**: Defined through the UI by the end user. These are conditions the user configures to determine when a plugin should start or stop. - - Added via `addStartCondition()` and `addStopCondition()` methods - - Persist across client sessions - - Can be modified through the UI - -2. **Plugin Conditions**: Defined programmatically by implementing the `SchedulablePlugin` interface. These are conditions defined within the plugin's own code. - - Provided via `getStartCondition()` and `getStopCondition()` methods - - Typically define the plugin's own business logic around when it should run - - Cannot be modified through the UI - -Both types of conditions are evaluated separately with their own logical rules, then combined for the final decision: - -```java -// For start conditions -private boolean areUserStartConditionsMet() { - if (startConditionManager.getUserConditions().isEmpty()) { - return true; - } - return startConditionManager.areUserConditionsMet(); -} - -private boolean arePluginStartConditionsMet() { - if (startConditionManager.getPluginConditions().isEmpty()) { - return true; - } - return startConditionManager.arePluginConditionsMet(); -} - -// Both must be satisfied -public boolean isDueToRun() { - // ... other checks ... - if (areUserStartConditionsMet() && arePluginStartConditionsMet()) { - return true; - } - return false; -} -``` - -The same pattern applies to stop conditions. This dual approach provides flexibility while maintaining control - users can automate plugins according to their needs, but plugins can still enforce their own operational requirements. - -#### Condition Evaluation Logic - -The combination of user and plugin conditions follows these rules: - -- **Start Logic**: `(User Start Conditions AND Plugin Start Conditions)` - - Both sets must be satisfied for the plugin to start - - If either set is empty, it's treated as automatically satisfied - -- **Stop Logic**: `(User Stop Conditions OR Plugin Stop Conditions)` - - Either set being satisfied is sufficient to stop the plugin - - If both sets are empty, the plugin won't stop automatically - -This provides a balance of control between the user and the plugin developer. - -### Stop Mechanism - -The `PluginScheduleEntry` class supports several ways a plugin can be stopped, tracked through the `StopReason` enum: - -| Stop Reason | Description | Initiated By | -|-------------|-------------|------------| -| `NONE` | Plugin hasn't stopped (still running or never started) | - | -| `MANUAL_STOP` | The user manually stopped the plugin | User | -| `PLUGIN_FINISHED` | The plugin self-reported completion through `reportFinished()` | Plugin | -| `ERROR` | An error occurred during plugin execution | System | -| `SCHEDULED_STOP` | The plugin was stopped due to its scheduled stop conditions being met | Scheduler | -| `INTERRUPTED` | Plugin was interrupted (e.g., client shutdown) | System | -| `HARD_STOP` | Plugin was forcibly terminated after not responding to a soft stop | Scheduler | - -#### The Stopping Process - -The stopping process follows a sophisticated pattern: - -1. **Stop Initiation**: When conditions are met or user triggers a stop, the `stopInitiated` flag is set and the stop process begins -2. **Soft Stop**: A `PluginScheduleEntryPostScheduleTaskEvent` is sent to the plugin, allowing it to perform cleanup operations -3. **Grace Period**: The plugin gets time to clean up (based on `softStopRetryInterval`) -4. **Stop Monitoring**: A separate monitoring thread tracks the stopping process -5. **Hard Stop**: If the plugin doesn't respond within `hardStopTimeout`, a forced stop occurs - -```java -private void softStop(boolean successfulRun) { - // Set flags to track stop process - stopInitiated = true; - stopInitiatedTime = ZonedDateTime.now(); - lastStopAttemptTime = stopInitiatedTime; - - // Create and post the stop event - PluginScheduleEntryPostScheduleTaskEvent stopEvent = new PluginScheduleEntryPostScheduleTaskEvent( - this, - isRunning(), - areUserDefinedStopConditionsMet(), - arePluginStopConditionsMet(), - lastStopReasonType - ); - - // Post event to notify the plugin - Microbot.getEventBus().post(stopEvent); - - // Start monitoring thread to track stop progress - startStopMonitoringThread(successfulRun); -} -``` - -#### Stop Monitoring - -A dedicated monitoring thread watches the plugin during the stopping process: - -```java -private void startStopMonitoringThread(boolean successfulRun) { - if (isMonitoringStop) { - return; - } - - isMonitoringStop = true; - - stopMonitorThread = new Thread(() -> { - try { - // Keep checking until the stop completes or is abandoned - while (stopInitiated && isMonitoringStop) { - // Check if plugin has stopped running - if (!isRunning()) { - // Plugin has stopped, update state and exit loop - stopInitiated = false; - hasStarted = false; - // ...other cleanup... - break; - } - - // Check every 300ms to be responsive but not wasteful - Thread.sleep(300); - } - } catch (InterruptedException e) { - // Thread was interrupted, just exit - } finally { - isMonitoringStop = false; - } - }); - - stopMonitorThread.setName("StopMonitor-" + name); - stopMonitorThread.setDaemon(true); // Don't prevent JVM exit - stopMonitorThread.start(); -} -``` - -This ensures that plugins have a chance to clean up resources before being terminated while still preventing hung processes. - -### Condition Management Methods - -The class provides methods to manipulate its start and stop conditions: - -```java -// Add a start condition to determine when the plugin should be activated -public void addStartCondition(Condition condition) { - startConditionManager.addUserCondition(condition); -} - -// Add a stop condition to determine when the plugin should be deactivated -public void addStopCondition(Condition condition) { - stopConditionManager.addUserCondition(condition); -} -``` - -### Condition Watchdogs - -Condition watchdogs are scheduled tasks that periodically update conditions from the plugin. This is particularly useful for dynamic conditions that need to be re-evaluated regularly: - -```java -public boolean scheduleConditionWatchdogs(long checkIntervalMillis, UpdateOption updateOption) { - if (!(this.plugin instanceof SchedulablePlugin)) { - return false; - } - - SchedulablePlugin schedulablePlugin = (SchedulablePlugin) this.plugin; - - // Create suppliers that get the current plugin conditions - Supplier startConditionSupplier = - () -> schedulablePlugin.getStartCondition(); - - Supplier stopConditionSupplier = - () -> schedulablePlugin.getStopCondition(); - - // Schedule the watchdogs - startConditionWatchdogFuture = startConditionManager.scheduleConditionWatchdog( - startConditionSupplier, - checkIntervalMillis, - updateOption - ); - - stopConditionWatchdogFuture = stopConditionManager.scheduleConditionWatchdog( - stopConditionSupplier, - checkIntervalMillis, - updateOption - ); - - return true; -} -``` - -This mechanism allows plugins to provide fresh conditions at runtime, enabling adaptive behavior based on changing game states. - -### Plugin Lock Mechanism - -A sophisticated locking system allows plugins to temporarily prevent being stopped during critical operations: - -```java -public boolean isLocked() { - if (!(plugin instanceof SchedulablePlugin)) { - return false; - } - - SchedulablePlugin schedulablePlugin = (SchedulablePlugin) plugin; - return schedulablePlugin.isLocked(null); -} - -// SchedulablePlugin interface provides these methods: -// boolean lock(Condition stopCondition); -// boolean unlock(Condition stopCondition); -``` - -This is crucial for operations that should not be interrupted, such as trading, banking, or other sensitive activities. - -### Progress Tracking - -The class includes methods to track and report the progress of both start and stop conditions: - -```java -// Get progress percentage toward stop conditions being met -public double getStopConditionProgress() { - return stopConditionManager.getFullConditionProgress(); -} - -// Get progress percentage toward start conditions being met -public double getStartConditionProgress() { - return startConditionManager.getFullConditionProgress(); -} -``` - -### Timing Calculation - -Methods are provided to calculate: - -- When the plugin is next scheduled to run -- The duration until the next scheduled execution -- Whether the plugin is due to run based on its conditions -- Total and average runtime statistics -- Randomized intervals for more natural scheduling - -```java -// Get the next time this plugin is scheduled to run -public Optional getCurrentStartTriggerTime() { - return startConditionManager.getCurrentTriggerTime(); -} - -// Check if the plugin is due to run based on its conditions -public boolean isDueToRun() { - // Check basic preconditions - if (isRunning() || !hasAnyStartConditions()) { - return false; - } - - // For diagnostic purposes, we may log detailed condition information - if (Microbot.isDebug()) { - String diagnosticInfo = diagnoseStartConditions(); - log.debug("\n[isDueToRun] - \n"+diagnosticInfo); - } - - // Check if all start conditions are met (combining both user and plugin conditions) - return startConditionManager.areAllConditionsMet(); -} - -// Get a user-friendly string showing when the plugin will next run -public String getNextStartTriggerTimeString() { - Optional triggerTime = getCurrentStartTriggerTime(); - if (triggerTime.isPresent()) { - ZonedDateTime now = ZonedDateTime.now(); - Duration until = Duration.between(now, triggerTime.get()); - if (until.isNegative()) { - return "Due now"; - } - return formatDuration(until) + " from now"; - } - return "Not scheduled"; -} -``` - -## The Plugin Scheduling Algorithm - -The plugin scheduling process in the `SchedulerPlugin` uses several key properties from `PluginScheduleEntry` to determine which plugin to run next: - -1. **Basic Filters**: - - Only enabled plugins are considered - - Only plugins that are due to run (conditions satisfied) are considered - - Plugins with `stopInitiated` are excluded - -2. **Priority-Based Selection**: - - Plugins are first filtered by the highest `priority` value - - Among equally high priority plugins, non-default plugins (`isDefault = false`) are preferred - -3. **Random vs. Non-Random Selection**: - - Non-randomizable plugins (`allowRandomScheduling = false`) are always preferred over randomizable ones - - Non-randomizable plugins are strictly ordered by their trigger times (earliest first) - -4. **Weighted Selection for Randomizable Plugins**: - - For randomizable plugins with the same priority and default status, a weighted selection is applied - - Plugins with lower `runCount` values receive higher weights - - The weight formula: `weight = (maxRuns - plugin's runCount + 1)` - - This creates a balanced distribution, with less-frequently run plugins having a higher chance of selection - -5. **Final Selection**: - - If multiple plugins have the exact same trigger time and other factors, a stable sort by name and object identity is used - -### Non-Default Plugin Prioritization - -The scheduler can be configured to interrupt or prevent default plugins from running when a non-default plugin is scheduled soon: - -```java -// In SchedulerPlugin.java -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 the next due plugin is a default plugin, don't start it - if (nextDuePlugin.isPresent() && nextDuePlugin.get().isDefault()) { - log.info("Not starting default plugin '{}' because non-default plugin '{}' is scheduled within {} minutes", - nextDuePlugin.get().getCleanName(), - upcomingNonDefault.getCleanName(), - nonDefaultPluginLookAheadMinutes); - return; - } -} -``` - -This mechanism ensures that high-priority, non-default plugins can run as scheduled without being blocked by long-running default plugins. - -## Integration with SchedulablePlugin - -When a plugin implements the `SchedulablePlugin` interface, `PluginScheduleEntry` detects this and interacts with it differently: - -1. Retrieves plugin-defined start and stop conditions -2. Sets up condition watchdogs to periodically refresh those conditions -3. Routes stop events to the plugin so it can handle cleanup -4. Respects the plugin's lock status when stopping -5. Tracks whether the plugin self-reports completion - -This creates a strong relationship between the plugin and its schedule entry, allowing for sophisticated scheduling behaviors. - -```java -// Check if the plugin implements SchedulablePlugin -public boolean isSchedulablePlugin() { - return this.plugin instanceof SchedulablePlugin; -} - -// Register plugin conditions -private boolean registerPluginStartingConditions(UpdateOption updateOption) { - if (!(this.plugin instanceof SchedulablePlugin)) { - return false; - } - - SchedulablePlugin provider = (SchedulablePlugin) plugin; - LogicalCondition pluginLogic = provider.getStartCondition(); - - if (pluginLogic != null) { - return startConditionManager.updatePluginCondition(pluginLogic, updateOption); - } - - return false; -} -``` - -## Usage Example - -Here's an extended example showing how to create and configure a `PluginScheduleEntry` with scheduling properties: - -```java -// Create a high-priority, non-default schedule entry for a critical plugin -PluginScheduleEntry criticalEntry = new PluginScheduleEntry( - criticalPlugin, // Plugin instance - "Critical Task", // Display name - true // Enabled -); -criticalEntry.setPriority(10); // High priority (default is 0) -criticalEntry.setDefault(false); // Not a default plugin -criticalEntry.setAllowRandomScheduling(false); // Must run at exact scheduled time - -// Add start condition (when the plugin should activate) -criticalEntry.addStartCondition(new TimeWindowCondition( - LocalTime.of(12, 0), - LocalTime.of(12, 30) -)); - -// Create a low-priority default plugin that can be randomized -PluginScheduleEntry defaultEntry = new PluginScheduleEntry( - backgroundPlugin, // Plugin instance - "Background Task", // Display name - true // Enabled -); -defaultEntry.setPriority(0); // Low priority -defaultEntry.setDefault(true); // This is a default plugin -defaultEntry.setAllowRandomScheduling(true); // Can be randomly scheduled - -// Register with the scheduler -schedulerPlugin.addScheduledPlugin(criticalEntry); -schedulerPlugin.addScheduledPlugin(defaultEntry); -``` - -In this example, the critical task will always run at exactly 12:00-12:30, while the background task will run based on weighted selection when no critical tasks are scheduled. - -## Internal Lifecycle - -The internal plugin lifecycle follows this pattern: - -1. **Creation**: Plugin schedule entry is created and configured -2. **Registration**: Entry is registered with the `SchedulerPlugin` -3. **Start Check**: Periodically checks if start conditions are met -4. **Activation**: When conditions are met, plugin is started -5. **Monitoring**: While running, stop conditions are evaluated -6. **Deactivation**: When stop conditions are met, stopping process begins -7. **Cleanup**: Plugin gets opportunity to clean up before full stop -8. **Reset**: Conditions are reset for the next activation cycle - -## Key Methods - -### Starting and Stopping - -```java -// Start the plugin -public boolean start(boolean logConditions) { - if (getPlugin() == null || !this.isEnabled() || isRunning()) { - return false; - } - - // Log conditions if requested - if (logConditions) { - logStartConditionsWithDetails(); - logStopConditionsWithDetails(); - } - - // Reset conditions if needed - if (!this.allowContinue || lastStopReasonType != StopReason.INTERRUPTED) { - resetStopConditions(); - } - - // Set state and start the plugin - this.lastRunStartTime = ZonedDateTime.now(); - Microbot.startPlugin(plugin); - return true; -} - -// Check if stop conditions are met -public boolean shouldStop() { - if (!isRunning()) { - return false; - } - - // Check if the plugin is locked - if (isLocked()) { - return false; - } - - // Check both plugin and user conditions - return arePluginStopConditionsMet() && areUserDefinedStopConditionsMet(); -} -``` - -### Condition Evaluation - -```java -// Check if plugin-defined stop conditions are met -private boolean arePluginStopConditionsMet() { - if (stopConditionManager.getPluginConditions().isEmpty()) { - return true; - } - return stopConditionManager.arePluginConditionsMet(); -} - -// Check if user-defined stop conditions are met -private boolean areUserDefinedStopConditionsMet() { - if (stopConditionManager.getUserConditions().isEmpty()) { - return true; - } - return stopConditionManager.areUserConditionsMet(); -} -``` - -### Condition Validation and Optimization - -```java -// Validates stop conditions structure and logs issues -private void validateStopConditions() { - LogicalCondition stopLogical = getStopConditionManager().getFullLogicalCondition(); - if (stopLogical != null) { - List issues = stopLogical.validateStructure(); - if (!issues.isEmpty()) { - log.warn("Validation issues found in stop conditions for '{}':", name); - for (String issue : issues) { - log.warn(" - {}", issue); - } - } - } -} - -// Optimizes condition structures by flattening unnecessary nesting -private void optimizeConditionStructures() { - LogicalCondition startLogical = getStartConditionManager().getFullLogicalCondition(); - if (startLogical != null) { - boolean optimized = startLogical.optimizeStructure(); - if (optimized) { - log.debug("Optimized start condition structure for '{}'", name); - } - } - - LogicalCondition stopLogical = getStopConditionManager().getFullLogicalCondition(); - if (stopLogical != null) { - boolean optimized = stopLogical.optimizeStructure(); - if (optimized) { - log.debug("Optimized stop condition structure for '{}'", name); - } - } -} -``` - -## Stop Flags and Monitoring - -The `stopInitiated` flag is a crucial part of the stopping process, indicating that the plugin is in the process of being stopped. This flag is set in the `softStop()` method when the stop process begins and cleared when the plugin is fully stopped: - -```java -// Set when stop process begins -stopInitiated = true; -stopInitiatedTime = ZonedDateTime.now(); - -// Checked by monitoring thread -while (stopInitiated && isMonitoringStop) { - if (!isRunning()) { - // Plugin has stopped, clear the flag - stopInitiated = false; - break; - } - Thread.sleep(300); -} -``` - -The stop monitoring thread continuously checks if the plugin has stopped running and updates the state once the stop is complete. If the plugin doesn't stop within the configured timeout, a hard stop may be initiated. - -### Hard Stop Fallback - -If a plugin doesn't respond to a soft stop within the configured timeout, a hard stop is performed: - -```java -private void hardStop(boolean successfulRun) { - log.warn("Performing hard stop on plugin '{}'", name); - - // Force stop the plugin - Microbot.stopPlugin(plugin); - - // Update state - lastStopReasonType = StopReason.HARD_STOP; - lastStopReason = "Plugin did not respond to soft stop and was forcibly terminated"; - stopInitiated = false; - hasStarted = false; - - // ...additional cleanup... -} -``` - -## Best Practices - -When working with `PluginScheduleEntry`: - -1. Prefer setting both start and stop conditions for predictable behavior -2. Use plugin-defined conditions for essential business logic -3. Allow user-defined conditions for customization -4. Consider plugin safety with appropriate lock usage during critical operations -5. Use the soft-stop mechanism to ensure your plugin can clean up properly -6. Make good use of the `reportFinished()` method to signal natural completion -7. Set appropriate priority levels - use higher values only for truly critical plugins -8. Mark essential time-sensitive plugins as `allowRandomScheduling = false` -9. Use the `isDefault` flag for background plugins that can be interrupted - -## Advanced Features - -The class includes several advanced features: - -- Condition validation and optimization -- Detailed logging and diagnostics -- Time condition randomization for bot detection prevention -- Support for complex logical hierarchies through the `LogicalCondition` framework -- Serialization support for configuration persistence - -## Relationship with SchedulerPlugin - -The `PluginScheduleEntry` class works closely with the `SchedulerPlugin` class, which acts as the orchestrator for the entire scheduling system: - -1. **SchedulerPlugin** manages a collection of `PluginScheduleEntry` instances -2. It periodically checks each entry to determine if it should start or stop -3. It handles the scheduling of breaks between plugin executions -4. It manages the overall scheduler state (IDLE, EXECUTING, STOPPING, etc.) -5. It provides methods to add, remove, and configure scheduled plugins - -This separation of concerns allows the `PluginScheduleEntry` to focus on the state and management of an individual plugin while the `SchedulerPlugin` handles the higher-level orchestration. - -```java -// In SchedulerPlugin: -public void checkPluginsToStart() { - for (PluginScheduleEntry entry : scheduledPlugins) { - if (entry.isDueToRun()) { - entry.start(true); - } - } -} - -public void checkPluginsToStop() { - for (PluginScheduleEntry entry : getRunningScheduledPlugins()) { - if (entry.shouldStop()) { - entry.initiateStop(PluginScheduleEntry.StopReason.SCHEDULED_STOP, "Stop conditions met", true); - } - } -} -``` diff --git a/docs/scheduler/plugin-writers-guide.md b/docs/scheduler/plugin-writers-guide.md deleted file mode 100644 index e68e8bd842e..00000000000 --- a/docs/scheduler/plugin-writers-guide.md +++ /dev/null @@ -1,626 +0,0 @@ -# Plugin Writer's Guide for the Scheduler Infrastructure - -## Introduction - -This guide provides comprehensive information for plugin developers who want to make their plugins compatible with the Plugin Scheduler system. By implementing the `SchedulablePlugin` interface, your plugin can take advantage of sophisticated scheduling capabilities, including condition-based starting and stopping,5. **Document Conditions**: Make sure your condition implementations have clear descriptions that explain what they do. - -6. **Test Thoroughly**: Test your plugin with the scheduler under various scenarios to ensure it behaves as expected. - -7. **Use LockCondition for Critical Operations**: Always protect critical operations with a LockCondition, especially in combat contexts. See [Combat Lock Examples](combat-lock-examples.md) for detailed patterns used in bossing plugins. - -## Example Implementation: GotrPluginrity-based execution, and integration with the scheduler's user interface. - -## Understanding the SchedulablePlugin Interface - -The `SchedulablePlugin` interface is the cornerstone of the scheduler infrastructure. It defines the contract that plugins must follow to work with the scheduler system. - -### Core Methods - -```java -public interface SchedulablePlugin { - // Required methods - LogicalCondition getStartCondition(); - LogicalCondition getStopCondition(); - void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event); - - // Optional methods with default implementations - void onStopConditionCheck(); - void reportFinished(String reason, boolean success); - boolean allowHardStop(); - ConfigDescriptor getConfigDescriptor(); - // Lock-related methods - boolean isLocked(Condition stopConditions); - boolean lock(Condition stopConditions); - boolean unlock(Condition stopConditions); - Boolean toggleLock(Condition stopConditions); -} -``` - -Each method plays a specific role in how your plugin interacts with the scheduler: - -1. **getStartCondition()**: Defines when your plugin is eligible to start. -2. **getStopCondition()**: Defines when your plugin should terminate. -3. **onPluginScheduleEntryPostScheduleTaskEvent()**: Handles graceful shutdown requests from the scheduler. -4. **onStopConditionCheck()**: Hook for updating condition state before evaluation. -5. **reportFinished()**: Allows the plugin to self-report task completion. -6. **allowHardStop()**: Indicates if the plugin can be forcibly terminated. -7. **Lock methods**: Prevent the plugin from being stopped during critical operations. -8. **getConfigDescriptor()**: Provides the scheduler with configuration information. - -## Step-by-Step Implementation Guide - -### Step 1: Implement the SchedulablePlugin Interface - -```java -@PluginDescriptor( - name = "My Schedulable Plugin", - description = "A plugin that works with the scheduler", - tags = {"microbot", "scheduler"}, - enabledByDefault = false -) -@Slf4j -public class MyPlugin extends Plugin implements SchedulablePlugin { - // Plugin implementation... -} -``` - -### Step 2: Define Stop Conditions - -The stop condition determines when your plugin should terminate. This is a required implementation: - -```java -@Override -public LogicalCondition getStopCondition() { - // Create a logical condition structure for when the plugin should stop - OrCondition orCondition = new OrCondition(); - - // Create a lock condition to prevent stopping during critical operations , and the break handler for taking a break - LockCondition lockCondition = new LockCondition("Locked during critical operation", true); //ensure unlock on shutdown of the plugin ! - - // Add your specific conditions - orCondition.addCondition(new TimeCondition(30, TimeUnit.MINUTES)); - orCondition.addCondition(new InventoryFullCondition()); - - // Combine with lock condition using AND logic - AndCondition andCondition = new AndCondition(); - andCondition.addCondition(orCondition); - andCondition.addCondition(lockCondition); - - return andCondition; -} -``` - -Real-world example from GotrPlugin: - -```java -@Override -public LogicalCondition getStopCondition() { - if (this.stopCondition == null) { - this.stopCondition = createStopCondition(); - } - return this.stopCondition; -} - -private LogicalCondition createStopCondition() { - if (this.lockCondition == null) { - this.lockCondition = new LockCondition("Locked because the Plugin " + getName() + " is in a critical operation", true); //ensure unlock on shutdown of the plugin ! - } - - AndCondition andCondition = new AndCondition(); - andCondition.addCondition(lockCondition); - return andCondition; -} -``` - -The GotrPlugin example shows a minimal implementation that only uses a lock condition. This is because the Guardians of the Rift minigame has its own natural start and end points, and the plugin uses the lock condition to prevent the scheduler from stopping it during an active game. - -### Step 3: Define Start Conditions (Optional) - -If you want to restrict when your plugin can start, implement the `getStartCondition()` method: - -```java -@Override -public LogicalCondition getStartCondition() { - // Create a logical condition for start conditions - OrCondition startCondition = new OrCondition(); - - // Add conditions based on your requirements - startCondition.addCondition(new LocationCondition( - "Grand Exchange", - 20 - )); - - return startCondition; -} -``` - -If you don't need specific start conditions (the plugin can start anytime), you can use the default implementation which returns a simple `AndCondition`. - -### Step 4: Implement the Soft Stop Handler - -The soft stop handler is essential for graceful shutdown. It's triggered when the scheduler determines that your plugin should stop: - -```java -@Override -@Subscribe -public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - if (event.getPlugin() == this) { - log.info("Scheduler requesting plugin shutdown"); - - // Perform any necessary cleanup - saveState(); - - // Schedule the actual stop on the client thread - Microbot.getClientThread().invokeLater(() -> { - Microbot.stopPlugin(this); - return true; - }); - } -} -``` - -Real-world example from GotrPlugin: - -```java -@Subscribe -public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - if (event.getPlugin() == this) { - Microbot.log("Scheduler about to turn off Guardians of the Rift"); - - if (exitScheduledFuture != null) { - return; // Exit task is already scheduled - } - - exitScheduledFuture = exitExecutorService.scheduleWithFixedDelay(() -> { - try { - if (lockCondition != null && lockCondition.isLocked()) { - Microbot.log("Exiting GOTR - waiting for the game to end"); - sleep(10000); - return; - } - gotrScript.shutdown(); - sleepUntil(() -> !gotrScript.isRunning(), 10000); - GotrScript.leaveMinigame(); - - Microbot.log("Successfully exited GOTR - stopping plugin"); - Microbot.getClientThread().invokeLater(() -> { - Microbot.stopPlugin(this); - return true; - }); - } catch (Exception ex) { - Microbot.log("Error during safe exit: " + ex.getMessage()); - Microbot.getClientThread().invokeLater(() -> { - Microbot.stopPlugin(this); - return true; - }); - } - }, 0, 500, TimeUnit.SECONDS); - } -} -``` - -The GotrPlugin example demonstrates a more sophisticated approach: - -1. It schedules a periodic check to see if it's safe to exit -2. It respects the lock condition to avoid stopping during an active game -3. It performs proper cleanup with `gotrScript.shutdown()` -4. It actively leaves the minigame area before stopping -5. It handles exceptions gracefully - -### Step 5: Using the Lock Condition - -The lock condition is a powerful feature that prevents your plugin from being stopped during critical operations: - -```java -// Creating the lock condition -this.lockCondition = new LockCondition("Locked during critical operation", true; //ensure unlock on shutdown of the plugin ! - -// Locking before a critical operation -lockCondition.lock(); -try { - // Perform critical operation that shouldn't be interrupted - performBankTransaction(); -} finally { - // Always unlock when the critical operation is complete - lockCondition.unlock(); -} -``` - -Real-world example from GotrPlugin: - -```java -// During chat message handling -if (msg.contains("The rift becomes active!")) { - if (Microbot.isPluginEnabled(BreakHandlerPlugin.class)) { - BreakHandlerScript.setLockState(true); - } - GotrScript.nextGameStart = Optional.empty(); - GotrScript.timeSincePortal = Optional.of(Instant.now()); - GotrScript.isFirstPortal = true; - GotrScript.state = GotrState.ENTER_GAME; - if (lockCondition != null) { - lockCondition.lock(); - } -} -else if (msg.toLowerCase().contains("closed the rift!") || msg.toLowerCase().contains("The great guardian was defeated!")) { - if (Microbot.isPluginEnabled(BreakHandlerPlugin.class)) { - Global.sleep(Rs2Random.randomGaussian(2000, 300)); - BreakHandlerScript.setLockState(false); - } - if (lockCondition != null) { - lockCondition.unlock(); - } - GotrScript.shouldMineGuardianRemains = true; -} -``` - -The GotrPlugin example shows how to: - -1. Lock the plugin when the GOTR minigame starts -2. Unlock it when the minigame ends -3. Integrate with other systems like the break handler - -### Step 6: Reporting Task Completion - -If your plugin can determine when it has completed its task, it should report this to the scheduler: - -```java -// Report successful completion -reportFinished("Task completed successfully", true); - -// Report unsuccessful completion -reportFinished("Failed to complete task: insufficient resources", false); -``` - -The scheduler will handle this report and update the plugin's status accordingly. - -### Step 7: Providing Configuration Information - -If your plugin has configurable options that should be accessible from the scheduler interface, implement the `getConfigDescriptor()` method: - -```java -@Override -public ConfigDescriptor getConfigDescriptor() { - if (Microbot.getConfigManager() == null) { - return null; - } - MyPluginConfig conf = Microbot.getConfigManager().getConfig(MyPluginConfig.class); - return Microbot.getConfigManager().getConfigDescriptor(conf); -} -``` - -Real-world example from GotrPlugin: - -```java -@Override -public ConfigDescriptor getConfigDescriptor() { - if (Microbot.getConfigManager() == null) { - return null; - } - GotrConfig conf = Microbot.getConfigManager().getConfig(GotrConfig.class); - return Microbot.getConfigManager().getConfigDescriptor(conf); -} -``` - -## Condition Types and Logical Structures - -### Common Condition Types - -The scheduler provides various condition types for different scenarios: - -1. **Time Conditions**: - - `TimeCondition`: Stops after a specified duration - - `IntervalCondition`: Runs at specific intervals - - `TimeWindowCondition`: Runs within specific time windows - - `DayOfWeekCondition`: Runs on specific days of the week - - `SingleTriggerTimeCondition`: Runs once at a specific time - -2. **Resource Conditions**: - - `InventoryItemCountCondition`: Stops when inventory contains a specific number of items - - `BankItemCountCondition`: Stops based on bank contents - - `GatheredResourceCondition`: Tracks gathered resources - - `LootItemCondition`: Tracks looted items - - `ProcessItemCondition`: Tracks item processing (crafting, smithing, etc.) - -3. **Location Conditions**: - - `AreaCondition`: Checks if player is in a specific area - - `RegionCondition`: Checks if player is in a specific region - - `PositionCondition`: Checks if player is at a specific position - -4. **Skill Conditions**: - - `SkillLevelCondition`: Stops when a skill reaches a level - - `SkillXpCondition`: Stops after gaining a certain amount of XP - -5. **NPC Conditions**: - - `NpcKillCountCondition`: Stops after killing a number of NPCs - -6. **Varbit Conditions**: - - `VarbitCondition`: Tracks game state variables - -7. **Logical Conditions**: - - `AndCondition`: Combines conditions with AND logic - - `OrCondition`: Combines conditions with OR logic - - `NotCondition`: Inverts a condition - - `LockCondition`: Special condition for preventing plugin termination - -### Creating Logical Structures - -Conditions can be combined using logical operators: - -1. **AND Logic**: All conditions must be satisfied - -```java -AndCondition andCondition = new AndCondition(); -andCondition.addCondition(conditionA); -andCondition.addCondition(conditionB); -``` - -2. **OR Logic**: Any condition can be satisfied - -```java -OrCondition orCondition = new OrCondition(); -orCondition.addCondition(conditionA); -orCondition.addCondition(conditionB); -``` - -3. **NOT Logic**: Inverts a condition - -```java -NotCondition notCondition = new NotCondition(conditionA); -``` - -4. **Complex Logic**: Conditions can be nested - -```java -// (A OR B) AND C -AndCondition root = new AndCondition(); -OrCondition orGroup = new OrCondition(); - -orGroup.addCondition(conditionA); -orGroup.addCondition(conditionB); - -root.addCondition(orGroup); -root.addCondition(conditionC); -``` - -## Best Practices - -1. **Always Use Lock Conditions**: Include a lock condition in your stop condition structure to prevent your plugin from being stopped during critical operations. - -2. **Handle Soft Stops Gracefully**: Implement proper cleanup in your `onPluginScheduleEntryPostScheduleTaskEvent` method. - -3. **Use the Client Thread**: Always stop your plugin on the client thread to avoid synchronization issues. - -4. **Report Completion**: Use `reportFinished()` when your plugin completes its task, rather than letting it run until stop conditions are met. - -5. **Provide Clear Configuration**: Implement `getConfigDescriptor()` to make your plugin's configuration accessible from the scheduler. - -6. **Document Conditions**: Make sure your condition implementations have clear descriptions that explain what they do. - -7. **Test Thoroughly**: Test your plugin with the scheduler under various scenarios to ensure it behaves as expected. - -## Example Implementation: GotrPlugin - -The Guardians of the Rift plugin (GotrPlugin) is a real-world example of a schedulable plugin. It demonstrates several best practices: - -1. **Minimal Stop Condition**: Uses only a lock condition since the minigame has natural start and end points. - -2. **Lock Management**: Locks the plugin during active games and unlocks it when games end. - -3. **Safe Shutdown**: Implements a sophisticated shutdown procedure that: - - Checks if it's safe to exit - - Completes necessary cleanup - - Leaves the minigame area - - Handles exceptions - -4. **Integration with Other Systems**: Works with the break handler to coordinate breaks. - -5. **Config Descriptor**: Provides configuration information to the scheduler. - -By following these patterns, your plugin can work seamlessly with the scheduler system, providing a better user experience and more reliable operation. - -## Reference - -For more information, see the following resources: - -- [Scheduler User Guide](user-guide.md): How to use the scheduler from a user perspective -- [Defining Conditions](defining-conditions.md): Detailed information on condition types -- [API Documentation: SchedulablePlugin](api/schedulable-plugin.md): Full API reference for the SchedulablePlugin interface -- [Combat Lock Examples](combat-lock-examples.md): Examples of using LockCondition in combat and bossing plugins - -## Location-Based Conditions - -The scheduler provides robust location-based conditions that can be used to start or stop plugins based on the player's position in the game world. This section covers how to leverage these powerful tools. - -### Working with LocationCondition Utilities - -The `LocationCondition` abstract class provides several utility methods for creating location-based conditions: - -#### 1. Bank Location Conditions - -You can create conditions that trigger when the player is near a specific bank: - -```java -// Create a condition that is satisfied when the player is at the Grand Exchange -Condition atBankCondition = LocationCondition.atBank(BankLocation.GRAND_EXCHANGE, 20); - -// This condition will be satisfied when the player is within 20 tiles of the GE bank -``` - -#### 2. Working with Multiple Points - -Sometimes, you want a condition that triggers at any of several points: - -```java -// Define multiple points where the player may be -WorldPoint[] importantLocations = new WorldPoint[] { - new WorldPoint(3222, 3218, 0), // Lumbridge - new WorldPoint(3165, 3485, 0), // Grand Exchange - new WorldPoint(2964, 3378, 0) // Falador -}; - -// Create a condition that is satisfied at any of these points -Condition atAnyPointCondition = LocationCondition.atAnyPoint( - "At a major city", - importantLocations, - 10 // Within 10 tiles of any point -); -``` - -#### 3. Creating Area Conditions - -For rectangular areas, you can use the `createArea` utility method: - -```java -// Create a condition for a rectangular area centered around a point -WorldPoint center = new WorldPoint(3222, 3218, 0); // Lumbridge -AreaCondition lumbridgeAreaCondition = LocationCondition.createArea( - "Lumbridge Center", - center, - 20, // Width in tiles - 20 // Height in tiles -); -``` - -#### 4. Working with Multiple Areas - -You can also create conditions that check if the player is in any of several areas: - -```java -// Define multiple areas -WorldArea[] trainingAreas = new WorldArea[] { - new WorldArea(3207, 3206, 30, 30, 0), // Lumbridge cows - new WorldArea(3244, 3295, 20, 15, 0) // Varrock east mine -}; - -// Create a condition that is satisfied in any of these areas -Condition inAnyAreaCondition = LocationCondition.inAnyArea( - "At training location", - trainingAreas -); -``` - -Alternatively, you can define areas using coordinate arrays: - -```java -// Define areas using coordinate arrays [x1, y1, x2, y2, plane] -int[][] areaDefs = new int[][] { - {3207, 3206, 3237, 3236, 0}, // Lumbridge cows - {3244, 3295, 3264, 3310, 0} // Varrock east mine -}; - -// Create a condition that is satisfied in any of these areas -Condition inAnyAreaCondition = LocationCondition.inAnyArea( - "At training location", - areaDefs -); -``` - -### Practical Use Cases for Location Conditions - -Location conditions can serve various purposes: - -1. **Start Conditions**: Define where a plugin can start - ```java - @Override - public LogicalCondition getStartCondition() { - // Only start the woodcutting plugin when in a woodcutting area - return (LogicalCondition) LocationCondition.inAnyArea( - "At woodcutting location", - new int[][] { - {3163, 3415, 3173, 3425, 0}, // Varrock west trees - {3040, 3308, 3055, 3323, 0} // Falador trees - } - ); - } - ``` - -2. **Stop Conditions**: Define where a plugin should stop - ```java - OrCondition stopCondition = new OrCondition(); - - // Stop if inventory is full OR player leaves the mining area - stopCondition.addCondition(new InventoryFullCondition()); - - NotCondition notInMiningArea = new NotCondition( - LocationCondition.inAnyArea( - "Mining area", - new int[][] {{3027, 9733, 3055, 9747, 0}} // Mines - ) - ); - stopCondition.addCondition(notInMiningArea); - ``` - -3. **Safety Checks**: Prevent dangerous activities - ```java - // Don't allow stop in dangerous areas - LockCondition lockCondition = new LockCondition("In wilderness", true); //ensure unlock on shutdown of the plugin ! - - // Lock when entering wilderness - if (LocationCondition.inAnyArea( - "Wilderness", - new int[][] {{3008, 3525, 3071, 3589, 0}} - ).isSatisfied()) { - lockCondition.lock(); - } - ``` - - - -4. **Arceuus script**: using condition-based locking : -```java -private LogicalCondition createStopCondition() { - // Import required classes - import java.util.Arrays; - import java.util.List; - - - // Create location conditions for Dense Runestone and Blood Altar - // NOTE: Update these coordinates with the actual in-game coordinates - LocationCondition atDenseRunestone = new AreaCondition("At Dense Runestone", 1760, 3850, 1780, 3870, 0); - LogicalCondition notAtDenseRunestone = new NotCondition(atDenseRunestone); - LocationCondition atBloodAltar = new AreaCondition("At Blood Altar", 1710, 3820, 1730, 3840, 0); - - - - // Option 1: Using createAndCondition helper method for more readable code - // Create a list of items to check (both Dark essence types) - List darkEssenceItems = Arrays.asList("Dark essence fragments", "Dark essence block"); - - // Create an AND condition that checks if both items have count >=1 - // Each condition is satisfied when the item count is >=1 (using NOT to invert the default behavior item count <1) - LogicalCondition noDarkEssence = new AndCondition(); - for (String itemName : darkEssenceItems) { - NotCondition noItem = new NotCondition( - new InventoryItemCountCondition(itemName, 1, true) - ); - noDarkEssence.addCondition(noItem); - } - - // Option 2: Using a single regex pattern to match both item types (more efficient) - // This creates a condition that checks if there are any Dark essence items (fragments or blocks), with count >=1, count all matching items - InventoryItemCountCondition hasAnyDarkEssence = new InventoryItemCountCondition( - "Dark essence.*", 1, true); // Regex pattern to match both item types - - // Invert the condition to check if there are NO dark essence items in the inventory - NotCondition noDarkEssenceItems = new NotCondition(hasAnyDarkEssence); - - // Use an ANDCondition for being at the Blood Altar with any dark essence item - AndCondition atBloodAltarWithEssence = new AndCondition(); - atBloodAltarWithNoEssence.addCondition(atBloodAltar); - atBloodAltarWithNoEssence.addCondition(hasAnyDarkEssence); // Using Option 2: the regex pattern approach - // we can invert it, so the condition is true if we are not at the blood alter or we dont have any dark essence items - LogicalCondition notAtBloodAltarOrNoDarkEssenceItems = new NotCondition(atBloodAltarWithEssence); - // Alternatively, using Option 3: createOrCondition helper for multiple items with count>=1 - List darkEssenceTypes = Arrays.asList("Dark essence fragments", "Dark essence block"); - LogicalCondition hasDarkEssenceAlt = InventoryItemCountCondition.createOrCondition( - darkEssenceTypes, 1, 1, true); - - - // Create the stop condition, so we can stop when we are not at the runestone and (we are not at Blood Altar or we have no essences - LocationCondition logicalStopCondition = new AndCondition(); - logicalStopCondition.addCondition(notAtDenseRunestone); - logicalStopCondition.addCondition(notAtBloodAltarOrNoDarkEssenceItems); - return logicalStopCondition; -} -``` \ No newline at end of file diff --git a/docs/scheduler/predicate-condition-examples.md b/docs/scheduler/predicate-condition-examples.md deleted file mode 100644 index cf87564ff13..00000000000 --- a/docs/scheduler/predicate-condition-examples.md +++ /dev/null @@ -1,257 +0,0 @@ -# PredicateCondition Examples - -This document provides practical examples of how to use the `PredicateCondition` class in your plugins to create dynamic stop conditions based on the game state. - -## Overview - -The `PredicateCondition` is a powerful extension of `LockCondition` that combines: -1. A manual lock mechanism (inherited from `LockCondition`) -2. A Java Predicate for evaluating dynamic game states - -This makes it perfect for creating stop conditions that depend on the current state of the game rather than static values. - -## Basic Implementation Pattern - -The general pattern for implementing a `PredicateCondition` is: - -```java -// Create the predicate function that evaluates game state -Predicate myPredicate = gameState -> { - // Logic to evaluate the game state - return someBoolean; // true if condition is satisfied -}; - -// Create a supplier that provides the current state -Supplier stateSupplier = () -> { - // Return the current state to be evaluated - return currentState; -}; - -// Create the PredicateCondition -PredicateCondition condition = new PredicateCondition<>( - "Human-readable reason for locking", - true, // with break handler lock, so during the predicate is true we cane take a break - myPredicate, - stateSupplier, - "Description of what the predicate checks" -); -``` - -## Example 1: Region-Based Agility Plugin - -This example demonstrates how to use `PredicateCondition` to stop an agility plugin when the player leaves an agility course region: - -```java -public class MicroAgilityPlugin extends Plugin implements SchedulablePlugin { - private PredicateCondition notInCourseCondition; - private LockCondition lockCondition; - private LogicalCondition stopCondition = null; - private final Set courseRegionIds = new HashSet<>(); - - @Inject - private Client client; - - /** - * Initialize the list of region IDs where agility courses are located - */ - private void initializeCourseRegions() { - // Add region IDs for all agility courses - courseRegionIds.add(9781); // Gnome Stronghold - courseRegionIds.add(12338); // Draynor Village - courseRegionIds.add(13105); // Al Kharid - // ... other course regions - } - - /** - * Set up the predicate condition that will be used to determine if the player is in an agility course - */ - private void setupPredicateCondition() { - // This predicate checks if the player is in an agility course region - Predicate notInAgilityCourse = player -> { - if (player == null) return true; // If player is null, condition is satisfied (safer to stop) - - boolean playerPlayerIsNotInCourse = ((Microbot.getClient().getLocalPlayer()!=null && !Rs2Player.isInteracting()) && index == 0 && (AgilityPlugin.getMarksOfGrace() == null || AgilityPlugin.getMarksOfGrace().isEmpty())); - // Return true if player is NOT in a course (condition to stop is satisfied) - return playerPlayerIsNotInCourse; - }; - - // Create the predicate condition - notInCourseCondition = new PredicateCondition<>( - "Player is currently in an agility course", - true, // with break handler lock, so during the predicate is true we cane take a break - notInAgilityCourse, - () -> Rs2Player.getLocalPlayer(), // provider - "Player is not in an agility course region" - ); - } - - @Override - public LogicalCondition getStopCondition() { - if (this.stopCondition == null) { - this.stopCondition = createStopCondition(); - } - return this.stopCondition; - } - - private LogicalCondition createStopCondition() { - if (this.lockCondition == null) { - this.lockCondition = new LockCondition("Locked because the Agility Plugin is in a critical operation", true); //ensure unlock on shutdown of the plugin ! - } - - // Setup course regions if not already done - if (courseRegionIds.isEmpty()) { - initializeCourseRegions(); - } - - // Setup predicate condition if not already done - if (notInCourseCondition == null) { - setupPredicateCondition(); - } - - // Combine the lock condition and the predicate condition with AND logic - AndCondition andCondition = new AndCondition(); - andCondition.addCondition(lockCondition); - andCondition.addCondition(notInCourseCondition); - return andCondition; - } -} -``` - -## Example 2: Combat State Monitoring - -This example shows how to use `PredicateCondition` to track if a player is in combat: - -```java -public class CombatPluginExample extends Plugin implements SchedulablePlugin { - private PredicateCondition notInCombatCondition; - private LockCondition lockCondition; - - @Inject - private Client client; - - private void setupCombatCondition() { - Predicate notInCombat = player -> { - if (player == null) return true; - - // Check if the player is in combat - boolean inCombat = player.getInteracting() != null || - (player.getHealthScale() > 0 && - System.currentTimeMillis() - player.getLastCombatTime() < 5000); - - // Return true if NOT in combat (condition to stop is satisfied) - return !inCombat; - }; - - notInCombatCondition = new PredicateCondition<>( - "Player is currently in combat", - notInCombat, - () -> client.getLocalPlayer(), - "Player is not in combat" - ); - } - - @Override - public LogicalCondition getStopCondition() { - if (lockCondition == null) { - lockCondition = new LockCondition("Critical combat operation in progress", true); - } - - if (notInCombatCondition == null) { - setupCombatCondition(); - } - - AndCondition stopCondition = new AndCondition(); - stopCondition.addCondition(lockCondition); - stopCondition.addCondition(notInCombatCondition); - - return stopCondition; - } -} -``` - -## Example 3: Multiple State Conditions - -This example demonstrates combining multiple predicate conditions: - -```java -public class FishingPluginExample extends Plugin implements SchedulablePlugin { - private PredicateCondition notFishingCondition; - private PredicateCondition inventoryFullCondition; - private LockCondition lockCondition; - - @Inject - private Client client; - - private void setupConditions() { - // Check if the player is not fishing - Predicate notFishing = player -> { - if (player == null) return true; - return player.getAnimation() != FISHING_ANIMATION; - }; - - // Check if inventory is full - Predicate inventoryFull = player -> { - if (player == null) return false; - return client.getItemContainer(InventoryID.INVENTORY).size() >= 28; - }; - - notFishingCondition = new PredicateCondition<>( - "Player is actively fishing", - notFishing, - () -> client.getLocalPlayer(), - "Player is not currently fishing" - ); - - inventoryFullCondition = new PredicateCondition<>( - "Inventory has space", - inventoryFull, - () -> client.getLocalPlayer(), - "Inventory is full" - ); - } - - @Override - public LogicalCondition getStopCondition() { - if (lockCondition == null) { - lockCondition = new LockCondition("Critical fishing operation in progress", true); - } - - if (notFishingCondition == null || inventoryFullCondition == null) { - setupConditions(); - } - - // Create a structure: (Lock AND (NotFishing OR InventoryFull)) - OrCondition fishingOrInventoryCondition = new OrCondition(); - fishingOrInventoryCondition.addCondition(notFishingCondition); - fishingOrInventoryCondition.addCondition(inventoryFullCondition); - - AndCondition stopCondition = new AndCondition(); - stopCondition.addCondition(lockCondition); - stopCondition.addCondition(fishingOrInventoryCondition); - - return stopCondition; - } -} -``` - -## Best Practices - -When using `PredicateCondition`, follow these best practices: - -1. **Safety Checks**: Always handle null values in your predicates to prevent NullPointerExceptions. - -2. **Clear Descriptions**: Provide meaningful descriptions for your predicate conditions, as these will be shown in the UI. - -3. **Logical Grouping**: Use logical conditions (AND/OR) to group predicate conditions with other conditions in meaningful ways. - -4. **State Suppliers**: Create efficient state suppliers that provide only the necessary game state for evaluation. - -5. **Locking Logic**: Remember that the condition is only satisfied when both the lock is unlocked AND the predicate returns true. - -6. **Performance**: Keep predicate evaluation efficient as it may be called frequently. - -7. **Debugging**: Use `Microbot.log()` in your predicates during development to debug condition evaluation. - -## Conclusion - -The `PredicateCondition` class provides a powerful way to create dynamic stop conditions based on the current game state. By leveraging Java Predicates, you can create sophisticated conditions that respond to the game environment in real-time, making your plugins more intelligent and responsive. diff --git a/docs/scheduler/roadmap.md b/docs/scheduler/roadmap.md deleted file mode 100644 index 3e8d5c7344a..00000000000 --- a/docs/scheduler/roadmap.md +++ /dev/null @@ -1,152 +0,0 @@ -# SchedulerPlugin: Development Roadmap - -## Short-term Priorities - -- **Community-driven Fixes** - - Implement bug fixes based on community feedback - - Address stability issues reported by early adopters - - Improve error handling and diagnostic messaging - -- **Documentation Enhancements** - - Complete comprehensive guides for all condition types - - Create video tutorials demonstrating scheduler setup and usage - - Provide more code examples for common scheduling scenarios - - Add troubleshooting section to documentation - -- **Review Response** - - Address code review comments from community members - - Implement suggested API improvements for better integration with existing plugins - - Enhance testing coverage for edge cases and failure modes - -## Medium-term Plans - -### Utility Framework for Schedulable Plugins - -- **Uility and Base Class** - - Develop a utility base class - - Provide common lifecycle hooks and helper methods - - Include default implementations for common scheduling patterns - -- **Impelentation of Pre-Schedule Task Framework** - - **Resource Acquisition System** - - Automated Grand Exchange purchasing functionality - - Automated collection of items - - Automated shopping of items - - Bank item withdrawal/preparation based on configurable templates - - Tool and equipment verification and acquisition - - **Location Management** - - Travel to appropriate starting locations before main task execution - - Fallback handling when target locations are unreachable - - **Inventory Setup Automation** - - Configure and validate inventory setups before starting primary task - - Handle edge cases like missing items - -- **Post-Schedule Task Framework** - - **Resource Management** - - Automated selling of gathered resources on Grand Exchange - - Intelligent price determination based on market conditions - - Bank organization and item categorization - - **Cleanup Operations** - - Return to safe locations after task completion - - Store valuable equipment to prevent loss - - Log detailed statistics about completed operations - - **Notification System** - - Discord/Telegram integration for completion notifications - - Configurable alerts based on success/failure states - - Detailed reports on resources gathered, skills gained, etc. - -- **Graceful Stopping Support** - - **State Persistence Framework - Just an Idea** - - Save critical state during interruptions - - Resume capability from last known good state (location,...) - - Progress tracking with persistent checkpoints - - **Safety Mechanism Implementations** - - Transition to safe areas before stopping - - Complete partial operations before full shutdown - - Banking valuable items before exit - - -### Plugin Integration - -- **Existing Plugins** - - Convert popular plugins to use the scheduling framework - - Update woodcutting, mining, and fishing plugins with schedulable support - - Implement combat scripts with safety condition integration - -### UI Enhancements - -- **Schedule Management GUI** - - Visual timeline showing planned plugin execution - - Better Conflict detection and resolution for overlapping schedules - - Schedule templates and sharing capability - - Improve Condition visualization and editing tools - - -## Long-term Enhancements - -### Advanced Condition Framework - -- **Machine Learning Integration** - - Train models to recognize optimal stopping conditions - - Implement predictive scheduling based on historical performance - - Auto-adjust parameters based on success/failure patterns - -- **Extended Condition Types** - - **Market Conditions** - - Start/stop based on Grand Exchange prices - - Algorithmic trading strategies with configurable parameters - - Support for price trend detection and forecasting - - **Server Conditions** - - Player density monitoring to avoid crowded areas - - World-hopping integration based on optimal conditions - - Server performance metrics to avoid high-lag situations - - **Account Progression Conditions** - - Quest completion dependencies - - Achievement diary stage requirements - - Total skill level milestones - -- **Condition Chain System** - - Sequential condition evaluation with dependencies - - Milestone-based progression between different plugins - - Complex workflow orchestration across multiple plugins - -### Advanced GUI Features - -- **Data Visualization** - - Interactive charts showing resource collection rates - - Performance analytics across different schedules - - Heat maps of player activity and resource distribution - -- **Remote Management** - - Web interface for monitoring and controlling schedules - - Mobile companion app for notifications and basic control - - Cross-account scheduling and coordination - -- **Community Integration** - - Schedule sharing platform for common tasks - - Upvoting system for effective condition combinations - - Community benchmarks for plugin performance - -### Ecosystem Extensions - -- **Integration with External Tools** - - Prayer/HP monitoring via companion services - - Network condition monitoring for stability - - Anti-ban pattern enhancements through scheduling variability - -- **Environment-aware Scheduling** - - Adapt to in-game events and seasonal activities - - Dynamic resource targeting based on current game economy - - Account-specific optimization based on stats and equipment - -## Implementation Approach - -The development will follow an iterative approach, with regular releases that incrementally add functionality. Community feedback will be actively sought after each significant feature addition to ensure the system meets real-world needs. - -Priority will be given to features that: -1. Improve stability and reliability -2. Enhance user experience for non-technical players -3. Provide valuable automation to complex multi-step tasks -4. Support intelligent decision-making based on game state - -This roadmap is subject to change based on community feedback, game updates, and shifting priorities within the development team. \ No newline at end of file diff --git a/docs/scheduler/schedulable-example-plugin.md b/docs/scheduler/schedulable-example-plugin.md deleted file mode 100644 index 11b4ae2c096..00000000000 --- a/docs/scheduler/schedulable-example-plugin.md +++ /dev/null @@ -1,364 +0,0 @@ -# SchedulableExamplePlugin - -## Overview - -The `SchedulableExamplePlugin` is a reference implementation demonstrating how to create plugins that work with the Plugin Scheduler system. It showcases various types of conditions for both starting and stopping a plugin, as well as proper implementation of the `SchedulablePlugin` interface. - -## Key Features - -1. **Comprehensive Condition Examples**: Demonstrates all major condition types: - - Time-based conditions - - Resource gathering conditions - - Item looting conditions - - NPC kill count conditions - - Process item conditions (crafting/smithing) - - Location-based conditions - -2. **Manual Testing Capabilities**: Includes hotkeys to manually trigger events for testing: - - Finish plugin event trigger - - Lock condition toggling - - Custom area definition for location-based conditions - -3. **Start Condition Examples**: Shows how to restrict plugin activation to specific locations: - - Bank-based start conditions - - Custom area start conditions - -## Making Your Plugin Schedulable - -### Step 1: Plugin Declaration - -```java -@PluginDescriptor( - name = "Schedulable Example", - description = "Designed for use with the scheduler and testing its features", - tags = {"microbot", "woodcutting", "combat", "scheduler", "condition"}, - enabledByDefault = false -) -@Slf4j -public class SchedulableExamplePlugin extends Plugin implements SchedulablePlugin { - // Plugin implementation... -} -``` - -A plugin becomes schedulable by implementing the `SchedulablePlugin` interface. - -### Step 2: Implement SchedulablePlugin - -The `SchedulablePlugin` interface requires implementation of key methods: - -```java -public interface SchedulablePlugin { - LogicalCondition getStartCondition(); - LogicalCondition getStopCondition(); - void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event); - - // Optional methods with default implementations - void onStopConditionCheck(); - void reportFinished(String reason, boolean success); -} -``` - -### Step 3: Define Stop Conditions - -```java -@Override -public LogicalCondition getStopCondition() { - // Create an OR condition - stop when ANY of the enabled conditions are met - OrCondition orCondition = new OrCondition(); - - // Create a lock condition for manual prevention of stopping - this.lockCondition = new LockCondition("Locked because the Plugin is in a critical operation", true); - - // Add enabled conditions based on configuration - if (config.enableTimeCondition()) { - orCondition.addCondition(createTimeCondition()); - } - - if (config.enableLootItemCondition()) { - orCondition.addCondition(createLootItemCondition()); - } - - // Add more conditions... - - // Combine with lock condition using AND logic - AndCondition andCondition = new AndCondition(); - andCondition.addCondition(orCondition); - andCondition.addCondition(lockCondition); - return andCondition; -} -``` - -### Step 4: Define Start Conditions (Optional) - -```java -@Override -public LogicalCondition getStartCondition() { - // Only create start conditions if enabled - if (!config.enableLocationStartCondition()) { - return null; // null means plugin can start anytime - } - - // Create a logical condition for start conditions - LogicalCondition startCondition = new OrCondition(); - - // Add conditions based on configuration - if (config.locationStartType() == LocationStartType.BANK) { - // Bank-based start condition - BankLocation selectedBank = config.bankStartLocation(); - int distance = config.bankDistance(); - - // Create condition using bank location - Condition bankCondition = LocationCondition.atBank(selectedBank, distance); - ((OrCondition) startCondition).addCondition(bankCondition); - } - else if (config.locationStartType() == LocationStartType.CUSTOM_AREA) { - // Custom area start condition logic - // ... - } - - return startCondition; -} -``` - -### Step 5: Implement Soft Stop Handler - -```java -@Override -@Subscribe -public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - // Save state before stopping - if (event.getPlugin() == this) { - WorldPoint currentLocation = null; - if (Microbot.isLoggedIn()) { - currentLocation = Rs2Player.getWorldLocation(); - } - Microbot.getConfigManager().setConfiguration("SchedulableExample", "lastLocation", currentLocation); - log.info("Scheduling stop for plugin: {}", event.getPlugin().getClass().getSimpleName()); - - // Schedule the stop operation on the client thread - Microbot.getClientThread().invokeLater(() -> { - try { - Microbot.getPluginManager().setPluginEnabled(this, false); - Microbot.getPluginManager().stopPlugin(this); - } catch (Exception e) { - log.error("Error stopping plugin", e); - } - }); - } -} -``` - -The `onPluginScheduleEntryPostScheduleTaskEvent` method is triggered when the Plugin Scheduler determines that a plugin's stop conditions have been met and requests the plugin to gracefully shut down. This implementation follows best practices for safely stopping a plugin: - -1. **State Preservation**: First saves the current player location to configuration for later use. -2. **Thread Safety**: Uses `Microbot.getClientThread().invokeLater()` to ensure the plugin is stopped on the client thread, avoiding concurrency issues. -3. **Clean Shutdown**: Disables the plugin and then properly stops it using `Microbot.getPluginManager().stopPlugin(this)`. - -#### Alternative Stopping Methods - -You can also directly stop a plugin using: - -```java -// Direct method to stop a plugin -Microbot.stopPlugin(this); -``` - -This is useful in situations where you need an immediate shutdown response, but be careful to ensure you've performed any necessary cleanup operations first. - -### Understanding the Plugin Shutdown Process - -The scheduler-managed shutdown process follows this sequence: - -1. **Trigger**: Stop conditions are met or manual stop requested -2. **Soft Stop Request**: The scheduler sends `PluginScheduleEntryPostScheduleTaskEvent` to the plugin -3. **Plugin Cleanup**: The plugin performs necessary cleanup operations -4. **Graceful Termination**: The plugin stops itself using one of the following methods: - - `Microbot.getPluginManager().stopPlugin(this)` - - `Microbot.stopPlugin(this)` -5. **Completion Reporting**: Optionally, the plugin can report detailed completion status using: - ```java - reportFinished("Task completed successfully", true); - ``` - -This approach ensures that plugins can safely save their state and perform cleanup operations before being terminated, preserving data integrity and preventing issues that could arise from abrupt termination. - -## Understanding SchedulableExampleConfig - -The `SchedulableExampleConfig` interface provides a comprehensive configuration system for the example plugin, demonstrating how to create configurable conditions: - -### Config Sections - -The config is organized into logical sections: - -```java -@ConfigSection( - name = "Start Conditions", - description = "Conditions for when the plugin is allowed to start", - position = 0 -) -String startConditionSection = "startConditions"; - -@ConfigSection( - name = "Stop Conditions", - description = "Conditions for when the plugin should stop", - position = 101 -) -String stopSection = "stopConditions"; - -// More specific condition sections... -``` - -### Condition Configuration Groups - -Each condition type has its own configuration group: - -1. **Location Start Conditions**: Control where the plugin can start - - Bank location selection - - Custom area definition with hotkey - - Area radius configuration - -2. **Time Conditions**: Control duration-based stopping - - Min/max runtime settings - - Randomized intervals - -3. **Loot Item Conditions**: Stop based on collected items - - Item name patterns (supports regex) - - Min/max item count targets - - Logical operators (AND/OR) - - Note item inclusion settings - -4. **Resource Conditions**: Stop based on gathered resources - - Resource type patterns - - Min/max count settings - - Logical operators - -5. **Process Item Conditions**: Stop based on items processed - - Source/target item tracking - - Tracking mode selection - - Min/max processed items - -6. **NPC Conditions**: Stop based on NPC kill counts - - NPC name patterns - - Min/max kill targets - - Per-NPC or total counting options - -7. **Debug Options**: Test functionality without running the full plugin - - Manual finish hotkey - - Success reporting - - Lock condition toggle - -## Testing Conditions - -The `SchedulableExamplePlugin` includes features specifically designed for testing the condition system: - -### Manual Condition Triggers - -```java -// HotkeyListener for testing PluginScheduleEntryMainTaskFinishedEvent -private final HotkeyListener finishPluginHotkeyListener = new HotkeyListener(() -> config.finishPluginHotkey()) { - @Override - public void hotkeyPressed() { - String reason = config.finishReason(); - boolean success = config.reportSuccessful(); - log.info("Manually triggering plugin finish: reason='{}', success={}", reason, success); - reportFinished(reason, success); - } -}; - -// HotkeyListener for toggling the lock condition -private final HotkeyListener lockConditionHotkeyListener = new HotkeyListener(() -> config.lockConditionHotkey()) { - @Override - public void hotkeyPressed() { - boolean newState = toggleLock(currentCondition); - log.info("Lock condition toggled: {}", newState ? "LOCKED - " + config.lockDescription() : "UNLOCKED"); - } -}; -``` - -### Location-Based Start Conditions - -```java -// HotkeyListener for the area marking -private final HotkeyListener areaHotkeyListener = new HotkeyListener(() -> config.areaMarkHotkey()) { - @Override - public void hotkeyPressed() { - toggleCustomArea(); - } -}; - -private void toggleCustomArea() { - // Logic to mark the current player position as center of a custom area - // or to clear a previously defined area -} -``` - -## Understanding the LockCondition - -The `LockCondition` is a special condition used to prevent a plugin from being stopped during critical operations, even if other stop conditions are met. - -### How It Works - -```java -public class LockCondition implements Condition { - private final AtomicBoolean locked = new AtomicBoolean(false); - private final String reason; - - // Constructor and methods... - - @Override - public boolean isSatisfied() { - // If locked, the condition is NOT satisfied, which prevents stopping - return !isLocked(); - } -} -``` - -The lock condition works by returning `false` for `isSatisfied()` when locked, which prevents the stop condition from being met when used with AND logic. - -### Usage in the Example Plugin - -```java -// Create the lock condition -this.lockCondition = new LockCondition("Locked because the Plugin is in a critical operation", true); - -// Add it to the condition structure with AND logic -AndCondition andCondition = new AndCondition(); -andCondition.addCondition(orCondition); // Other stop conditions -andCondition.addCondition(lockCondition); // Lock condition -``` - -### When to Use Lock Condition - -The lock condition should be used during critical operations where stopping the plugin might leave game state inconsistent: - -- **Banking operations**: Prevent stopping mid-transaction -- **Trading**: Ensure trades complete fully -- **Complex combat sequences**: Avoid stopping during multi-step attacks -- **Item processing**: Complete crafting/smithing operations fully - -### Testing the Lock Condition - -The example plugin provides a hotkey to test locking and unlocking: - -```java -private final HotkeyListener lockConditionHotkeyListener = new HotkeyListener(() -> config.lockConditionHotkey()) { - @Override - public void hotkeyPressed() { - boolean newState = toggleLock(currentCondition); - log.info("Lock condition toggled: {}", newState ? "LOCKED - " + config.lockDescription() : "UNLOCKED"); - } -}; -``` - -## Summary - -The `SchedulableExamplePlugin` serves as a comprehensive demonstration of how to create plugins that work with the Plugin Scheduler system. The conditions it implements are designed for testing purposes but illustrate the patterns needed for real-world scheduling scenarios: - -- Time-based scheduling using intervals -- Resource gathering targets -- Item processing goals -- NPC kill counts -- Location-based activation -- Manual intervention capabilities - -By studying this example plugin, both plugin developers and script writers can understand how to make their own plugins schedulable, creating more sophisticated automation workflows that operate according to complex rules and conditions. \ No newline at end of file diff --git a/docs/scheduler/scheduler-plugin.md b/docs/scheduler/scheduler-plugin.md deleted file mode 100644 index efdddfa3944..00000000000 --- a/docs/scheduler/scheduler-plugin.md +++ /dev/null @@ -1,1009 +0,0 @@ -# Scheduler Plugin - -## Overview - -The `SchedulerPlugin` class is the central orchestrator of the Plugin Scheduler system. It manages the automated execution of plugins based on configurable conditions and schedules, providing a comprehensive framework for automating RuneLite plugins in a controlled, prioritized, and natural-appearing manner. - -## Component Relationships - -The scheduler system consists of three main components that work together: - -| Component | Role | Responsibility | -|-----------|------|----------------| -| `SchedulerPlugin` | **Orchestrator** | Coordinates the overall scheduling process, state transitions, and integration with other systems | -| `PluginScheduleEntry` | **Data Model** | Holds configuration and execution state for each scheduled plugin | -| `SchedulablePlugin` | **Interface** | Implemented by RuneLite plugins to define start/stop conditions and handle events | - -### Component Interaction Flow - -```ascii -┌────────────────┐ registers ┌──────────────────┐ -│ │◄──────────────────────â”Ī │ -│ │ │ │ -│ SchedulerPlugin schedules/evaluates │ PluginScheduleEntry │ -│ │─────────────────────â–ķ│ │ -│ │ │ │ -└────────┮───────┘ └────────┮─────────┘ - │ │ - │ manages │ references - │ │ - ▾ ▾ -┌────────────────┐ ┌──────────────────┐ -│ │ │ │ -│ Regular RuneLite│ implements │ SchedulablePlugin │ -│ Plugin │◄─────────────────────â”Ī (API) │ -│ │ │ │ -└────────────────┘ └──────────────────┘ -``` - -## Configuration Options - -The SchedulerPlugin offers extensive configuration options organized into four main sections: - -### Control Settings - -Controls the core behavior of the scheduler: - -| Setting | Description | Default | -|---------|-------------|---------| -| Soft Stop Retry (seconds) | Time between attempts to gracefully stop a plugin | 60 seconds | -| Enable Hard Stop | When enabled, forcibly stops plugins if they don't respond to soft stop | Disabled | -| Hard Stop Timeout (seconds) | Time to wait before forcing a hard stop | 0 seconds | -| Manual Start Threshold (minutes) | Minimum time until next scheduled plugin before manual start is allowed | 1 minute | -| Prioritize Non-Default Plugins | Stop default plugins when non-default plugins are due soon | Enabled | -| Non-Default Plugin Look-Ahead (minutes) | Time window to check for upcoming non-default plugins | 1 minute | -| Notifications On | Enable notifications for scheduler events | Disabled | - -### Conditions Settings - -Controls how the scheduler enforces conditions: - -| Setting | Description | Default | -|---------|-------------|---------| -| Enforce Stop Conditions | Prompt before running plugins without time-based stop conditions | Enabled | -| Dialog Timeout (seconds) | Time before the 'No Stop Conditions' dialog auto-closes | 30 seconds | -| Config Timeout (seconds) | Time to wait for user to add stop conditions before canceling | 60 seconds | - -### Log-In Settings - -Controls login behavior of scheduled plugins: - -| Setting | Description | Default | -|---------|-------------|---------| -| Enable Auto Log In | Enable auto-login before starting a plugin | Disabled | -| Auto Log In World | World to log into (0 for random) | 0 | -| World Type | Type of world to log into (0: F2P, 1: P2P, 2: Any) | 2 (Any) | -| Auto Log Out on Stop | Automatically log out when stopping the scheduler | Disabled | - -### Break Settings - -Controls break behavior between plugin executions: - -| Setting | Description | Default | -|---------|-------------|---------| -| BreakHandler on Start | Automatically enable BreakHandler when starting a plugin | Enabled | -| Break During Wait | Take breaks when waiting for the next scheduled plugin | Enabled | -| Min Break Duration (minutes) | Minimum duration of breaks between schedules | 2 minutes | -| Max Break Duration (minutes) | Maximum duration of breaks between schedules | 2 minutes | -| Log Out During A Break | Automatically log out during breaks | Disabled | -| Use Play Schedule | Enable play schedule to control when scheduler is active | Disabled | -| Play Schedule | Select pre-defined play schedule pattern | Medium Day | - -## User Interface - -The SchedulerPlugin provides a comprehensive user interface for managing scheduled plugins through several key components: - -### Main Scheduler Window - -The `SchedulerWindow` is the primary interface for managing the plugin scheduler. It contains: - -- **Schedule Tab**: Displays a table of all scheduled plugins and their status -- **Start Conditions Tab**: Configure when plugins should start running -- **Stop Conditions Tab**: Configure when running plugins should stop -- **Information Panel**: Shows real-time scheduler status and statistics - -### Schedule Table Panel - -The `ScheduleTablePanel` displays all scheduled plugins with the following information: - -- Plugin name -- Schedule type -- Next run time -- Start/Stop conditions -- Priority -- Enabled status -- Run count - -Special visual indicators help identify: -- Currently running plugin (purple highlight) -- Next scheduled plugin (amber highlight) -- Plugins with met/unmet conditions (green/red indicators) - -### Schedule Form Panel - -The `ScheduleFormPanel` allows users to add, edit, and remove scheduled plugins. It provides: - -- Plugin selection dropdown -- Time condition configuration -- Priority setting -- Randomization options -- Default plugin status toggle -- Allow continue after interruption option - -When editing an existing plugin, additional statistics are shown: -- Total runs -- Last run time -- Last run duration -- Last stop reason - -### Condition Configuration - -The condition configuration interface allows users to create complex logical conditions for starting and stopping plugins: - -- **Time Conditions**: Specific times, intervals, time windows, days of week -- **Game State Conditions**: Player status, inventory contents, skill levels -- **Logical Operators**: AND, OR, NOT for combining conditions -- **Lock Conditions**: Prevent plugins from stopping during critical operations - -## Plugin Schedule Entry Model - -The `PluginScheduleEntry` class is the core data model representing a scheduled plugin: - -### Key Properties - -| Property | Description | -|----------|-------------| -| `name` | Name of the plugin to schedule | -| `enabled` | Whether this schedule entry is active | -| `allowRandomScheduling` | Whether this plugin can be scheduled randomly | -| `isDefault` | Whether this is a default plugin (lower priority) | -| `priority` | Numeric priority (higher values = higher priority) | -| `allowContinue` | Whether to resume after interruption | -| `startConditions` | Logical conditions that determine when plugin starts | -| `stopConditions` | Logical conditions that determine when plugin stops | - -### Statistics Tracking - -Each entry tracks comprehensive statistics: -- Run count -- Last run time -- Last run duration -- Last stop reason -- Success/failure status - -### Stop Reason Types - -The system tracks why plugins stop: -- `NONE`: Not stopped yet -- `MANUAL_STOP`: User manually stopped the plugin -- `PLUGIN_FINISHED`: Plugin completed its task normally -- `ERROR`: Error occurred while running -- `SCHEDULED_STOP`: Stop conditions were met -- `INTERRUPTED`: Externally interrupted -- `HARD_STOP`: Forcibly stopped after timeout - -## Adding New Schedule Entries - -New schedule entries can be added through the `ScheduleFormPanel`: - -1. **Select Plugin**: Choose a plugin from the dropdown menu -2. **Set Priority**: Adjust the priority spinner (higher values = higher priority) -3. **Configure Time Conditions**: Choose from: - - Run Default: Use plugin's built-in schedule - - Run at Specific Time: Run at a particular time of day - - Run at Interval: Run at regular intervals - - Run in Time Window: Run during specific hours - - Run on Day of Week: Run on particular days -4. **Optional Settings**: - - Random Scheduling: Allow the scheduler to choose this plugin randomly - - Default Plugin: Set as a default (lower priority) plugin - - Time-based Stop: Configure automatic stop after running for some time - - Allow Continue: Resume after interruption -5. **Click Add**: Add the plugin to the schedule - -## State Management and Execution Flow - -The SchedulerPlugin implements a sophisticated state machine that manages the entire plugin scheduling life cycle through the `SchedulerState` enum. - -### State Categories and Relationships - -The scheduler's states can be organized into four functional categories: - -#### 1. Initialization States -- **UNINITIALIZED**: Initial state before the plugin is ready -- **INITIALIZING**: Loading required dependencies and preparing to run -- **READY**: Fully initialized and waiting for user activation - -#### 2. Active Scheduling States -- **SCHEDULING**: Actively monitoring schedules -- **STARTING_PLUGIN**: Beginning execution of a scheduled plugin -- **RUNNING_PLUGIN**: Plugin is currently executing -- **SOFT_STOPPING_PLUGIN**: Gracefully requesting a plugin to stop -- **HARD_STOPPING_PLUGIN**: Forcefully stopping a plugin - -#### 3. Waiting States -- **WAITING_FOR_LOGIN**: Waiting for user login before starting a plugin -- **LOGIN**: Currently in the process of logging in -- **WAITING_FOR_STOP_CONDITION**: Waiting for user to configure stop conditions -- **WAITING_FOR_SCHEDULE**: Waiting for the next scheduled plugin -- **BREAK**: Taking a configured break between plugin executions -- **PLAYSCHEDULE_BREAK**: Taking a break based on play schedule settings - -#### 4. Control States -- **HOLD**: Scheduler manually paused by user -- **ERROR**: An error occurred that prevents normal operation - -### State Transitions Diagram - -```ascii -┌───────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ INITIALIZATION STATES │ -├───────────────┐ ┌───────────────┐ ┌───────────────┐ │ -│ UNINITIALIZED │────────────────â–ķ│ INITIALIZING │────────────────â–ķ│ READY │ │ -└───────────────┘ └───────────────┘ └───────┮───────┘ │ - │ │ -┌──────────────────────────────────────────────────────────────────┐ │ │ -│ WAITING STATES │ │ │ -│ ┌────────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ -│ │WAITING_FOR_ │◀──────â–ķ│ LOGIN │──────â–ķ│WAITING_FOR_│ │ │ │ -│ │ LOGIN │ └────────────┘ │ STOP │ │ │ │ -│ └─────────┮──────┘ │ CONDITION │ │ │ │ -│ │ └─────┮──────┘ │ │ │ -│ ┌─────────▾──────┐ ┌────────────┐ │ │ │ │ -│ │WAITING_FOR_ │ │ BREAK │◀───────────┘ │ │ │ -│ │ SCHEDULE │◀────────â”Ī │ │ │ │ -│ └─────────┮──────┘ └─────┮──────┘ │ │ │ -│ │ │ │ │ │ -│ ┌─────────▾──────┐ ┌─────▾──────┐ │ │ │ -│ │PLAYSCHEDULE_ │ │ SCHEDULING │◀──────────────────────┾────────┘ │ -│ │ BREAK │◀────────â”Ī │ │ │ -│ └───────────────┮┘ └─────┮──────┘ │ │ -└───────────────────────────────────┘ │ │ - │ │ -┌───────────────────────────────────────────────────────┐ │ │ -│ ACTIVE SCHEDULING STATES │ │ │ -│ ┌────────────────┐ │ │ │ -│ │ STARTING_PLUGIN│◀───────────┾──────────┘ │ -│ └────────┮───────┘ │ │ -│ │ │ │ -│ ┌────────▾───────┐ │ ┌────────────────────────────────┐ -│ │ RUNNING_PLUGIN │ │ │ CONTROL STATES │ -│ └────────┮───────┘ │ │ ┌────────────┐ ┌──────────┐ │ -│ │ │ │ │ HOLD │ │ ERROR │ │ -│ ┌────────▾───────┐ │ │ └────────────┘ └──────────┘ │ -│ │SOFT_STOPPING_ │ │ └────────────────────────────────┘ -│ │ PLUGIN │ │ -│ └────────┮───────┘ │ -│ │ │ -│ ┌────────▾───────┐ │ -│ │HARD_STOPPING_ │ │ -│ │ PLUGIN │ │ -│ └────────────────┘ │ -└───────────────────────────────────────────────────────┘ -``` - -### State Descriptions and Transition Logic - -| State | Description | Entry Conditions | Exit Conditions | Helper Methods | -|-------|-------------|------------------|-----------------|----------------| -| UNINITIALIZED | Default state when plugin is loaded but not ready | Initial state | Transitions to INITIALIZING when plugin starts | isInitializing() | -| INITIALIZING | Loading required plugins and setting up | From UNINITIALIZED on startup | Transitions to READY when dependencies are loaded | isInitializing() | -| READY | Ready but not actively scheduling | After successful initialization | Transitions to SCHEDULING when user activates scheduler | !isSchedulerActive() | -| SCHEDULING | Actively monitoring for plugins to run | From READY when activated, or after breaks/waiting | To WAITING states or STARTING_PLUGIN | isSchedulerActive(), isWaiting() | -| STARTING_PLUGIN | In process of starting a plugin | When conditions are met to start a plugin | To RUNNING_PLUGIN when started successfully | isAboutStarting() | -| RUNNING_PLUGIN | A scheduled plugin is currently running | After plugin successfully starts | To SOFT_STOPPING_PLUGIN when stop conditions met | isActivelyRunning() | -| WAITING_FOR_LOGIN | Plugin needs login before running | When plugin requires login but user is not logged in | To LOGIN when login process begins | isAboutStarting() | -| LOGIN | Currently logging in | From WAITING_FOR_LOGIN | To STARTING_PLUGIN after successful login | N/A | -| WAITING_FOR_STOP_CONDITION | Awaiting user to add stop conditions | When plugin has no stop conditions | To STARTING_PLUGIN when conditions added | isAboutStarting() | -| SOFT_STOPPING_PLUGIN | Requesting plugin to stop gracefully | When stop conditions are met | To HARD_STOPPING_PLUGIN on timeout or successful stop | isStopping() | -| HARD_STOPPING_PLUGIN | Forcing plugin to stop | After soft stop timeout or when hard stop requested | To SCHEDULING or ERROR | isStopping() | -| BREAK | Taking scheduled break between plugins | After plugin completes and break is needed | To SCHEDULING when break duration completes | isWaiting(), isBreaking() | -| PLAYSCHEDULE_BREAK | Break due to play schedule settings | When outside allowed play hours | To SCHEDULING when entering allowed play hours | isWaiting(), isBreaking() | -| WAITING_FOR_SCHEDULE | Waiting for next scheduled plugin | When no plugins are ready to run currently | To SCHEDULING when schedule time approaches | isWaiting() | -| ERROR | Error occurred during scheduling | On exception or plugin error | After error is acknowledged | N/A | -| HOLD | Scheduler manually paused | User requested pause | When user resumes scheduling | N/A | - -### State Helper Methods - -The `SchedulerState` enum provides helper methods to classify states into functional groups, making it easier to check the scheduler's current status: - -```java -// Returns true if the scheduler is active (not in initialization, hold, or error states) -public boolean isSchedulerActive() { - return this != SchedulerState.UNINITIALIZED && - this != SchedulerState.INITIALIZING && - this != SchedulerState.ERROR && - this != SchedulerState.HOLD && - this != SchedulerState.READY; -} - -// Returns true if a plugin is currently running -public boolean isActivelyRunning() { - return isSchedulerActive() && - (this == SchedulerState.RUNNING_PLUGIN); -} - -// Returns true if scheduler is about to start a plugin -public boolean isAboutStarting() { - return this == SchedulerState.STARTING_PLUGIN || - this == SchedulerState.WAITING_FOR_STOP_CONDITION || - this == SchedulerState.WAITING_FOR_LOGIN; -} - -// Returns true if scheduler is waiting between plugin executions -public boolean isWaiting() { - return isSchedulerActive() && - (this == SchedulerState.SCHEDULING || - this == SchedulerState.WAITING_FOR_SCHEDULE || - this == SchedulerState.BREAK || - this == SchedulerState.PLAYSCHEDULE_BREAK); -} - -// Returns true if scheduler is in a break state -public boolean isBreaking() { - return (this == SchedulerState.BREAK || - this == SchedulerState.PLAYSCHEDULE_BREAK); -} - -// Returns true if scheduler is stopping a plugin -public boolean isStopping() { - return this == SchedulerState.SOFT_STOPPING_PLUGIN || - this == SchedulerState.HARD_STOPPING_PLUGIN; -} - -// Returns true if scheduler is initializing -public boolean isInitializing() { - return this == SchedulerState.INITIALIZING || - this == SchedulerState.UNINITIALIZED; -} -``` - -## Class Structure - -```java -@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 { - // Dependencies, state variables, configuration - // Methods for plugin lifecycle management and scheduling -} -``` - -## Key Features - -### Plugin Lifecycle Management - -The `SchedulerPlugin` manages the complete lifecycle of scheduled plugins: - -- **Registration**: Allows plugins to be registered with the scheduler through `PluginScheduleEntry` objects -- **Activation**: Starts plugins when their start conditions are met, respecting priority and scheduling properties -- **Monitoring**: Tracks running plugins and continuously evaluates their stop conditions -- **Deactivation**: Implements both soft and hard stop mechanisms when stop conditions are met -- **Persistence**: Saves and loads scheduled plugin configurations across client sessions - -### Advanced Scheduling Algorithm - -The scheduler implements a sophisticated algorithm to determine which plugins to run and prioritize: - -```java -/** - * Schedules the next plugin based on priority and timing rules - */ -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("Not starting default plugin '{}' because non-default plugin '{}' is scheduled within {} 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 - if (isOnBreak()) { - log.info("Interrupting active break to start scheduled plugin: {}", selected.get().getCleanName()); - interruptBreak(); - } - - // Start the selected plugin - log.info("Starting scheduled plugin: {}", selected.get().getCleanName()); - startPluginScheduleEntry(selected.get()); -} -``` - -The plugin selection algorithm incorporates several factors and methods: - -1. **Priority**: Higher priority plugins are always considered first -2. **Default Status**: Non-default plugins can be prioritized over default plugins -3. **Time Window Forecasting**: The scheduler looks ahead for upcoming non-default plugins -4. **Run Count Balance**: For randomizable plugins, uses weighted selection based on run counts - -```java -/** - * Selects a plugin using weighted random selection. - * Plugins with lower run counts have higher probability of being selected. - */ -private PluginScheduleEntry selectPluginWeighted(List plugins) { - // Return the only plugin if there's just one - if (plugins.size() == 1) { - return plugins.get(0); - } - - // Calculate weights - plugins with lower run counts get higher weights - // Find the maximum run count - int maxRuns = plugins.stream() - .mapToInt(PluginScheduleEntry::getRunCount) - .max() - .orElse(0); - - // Add 1 to avoid division by zero and ensure all plugins have some chance - maxRuns = maxRuns + 1; - - // Calculate weights - double[] weights = new double[plugins.size()]; - double totalWeight = 0; - - for (int i = 0; i < plugins.size(); i++) { - weights[i] = maxRuns - plugins.get(i).getRunCount() + 1; - totalWeight += weights[i]; - } - - // Select based on weighted probability - double randomValue = Math.random() * totalWeight; - double weightSum = 0; - for (int i = 0; i < plugins.size(); i++) { - weightSum += weights[i]; - if (randomValue < weightSum) { - return plugins.get(i); - } - } - - // Fallback - return plugins.get(0); -} -``` - -### State-Based Decision Making - -The scheduler uses the state machine to make intelligent decisions about plugin execution, with different behavior based on the current state: - -#### 1. State-Dependent UI Updates -The UI reflects the current state with appropriate colors and messages: -- Active states (RUNNING_PLUGIN) show green indicators -- Warning states (STOPPING_PLUGIN) show orange indicators -- Error states show red indicators -- Break states show blue indicators - -#### 2. State-Based Priority Handling -The scheduler prioritizes actions differently based on the current state: -- During SCHEDULING, it evaluates which plugin should run next -- During BREAK states, it calculates appropriate break durations -- During WAITING states, it monitors for conditions to transition - -#### 3. State Transition Guards -Transitions between states have guards that ensure proper flow: -- Cannot transition directly from ERROR to RUNNING_PLUGIN -- HARD_STOPPING_PLUGIN only follows SOFT_STOPPING_PLUGIN -- STARTING_PLUGIN must precede RUNNING_PLUGIN - -This state-based design creates a robust system that can handle complex scheduling scenarios while maintaining proper execution flow. - -### Seamless Integration with Core Systems - -The scheduler deeply integrates with other Microbot systems to create a cohesive automation experience: - -#### Break Handler Integration - -The scheduler integrates with the BreakHandler plugin in several ways: - -```java -/** - * Starts a short break until the next plugin is scheduled to run - */ -private boolean startBreakBetweenSchedules(boolean logout, - int minBreakDurationMinutes, int maxBreakDurationMinutes) { - if (!isBreakHandlerEnabled()) { - return false; - } - if (BreakHandlerScript.isLockState()) - BreakHandlerScript.setLockState(false); - - // Check if we're outside play schedule - if (config.usePlaySchedule() && config.playSchedule().isOutsideSchedule()) { - Duration untilNextSchedule = config.playSchedule().timeUntilNextSchedule(); - log.info("Outside play schedule. Next schedule in: {}", 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); - - // Wait for break to become active - sleepUntil(() -> BreakHandlerScript.isBreakActive(), 1000); - - setState(SchedulerState.PLAYSCHEDULE_BREAK); - return true; - } - - // Configure duration for regular break - // Determine break duration and start break handler - - return true; -} -``` - -#### Auto Login Integration - -The scheduler manages the login process through a dedicated monitoring thread: - -```java -/** - * Starts a thread to monitor login state and process login when needed - */ -private void startLoginMonitoringThread() { - if (loginMonitor != null && loginMonitor.isAlive()) { - return; - } - - setState(SchedulerState.WAITING_FOR_LOGIN); - - loginMonitor = new Thread(() -> { - try { - // Don't continue if auto login is disabled - if (!config.autoLogIn()) { - log.info("Auto login is disabled"); - setState(SchedulerState.SCHEDULING); - return; - } - - // Wait a moment for the game client to be ready - sleep(1000); - - // Set state to indicate login attempt is in progress - setState(SchedulerState.LOGIN); - - // Determine world selection logic - int worldNumber = config.autoLogInWorld(); - int worldType = config.worldType(); - - // Attempt login with configured settings - AutoLoginPlugin autoLogin = injector.getInstance(AutoLoginPlugin.class); - autoLogin.requestLogin(); - - // Wait for login to complete - boolean loggedIn = sleepUntil(() -> Login.isLoggedIn(), 60000); - - if (loggedIn) { - setState(SchedulerState.SCHEDULING); - } else { - setState(SchedulerState.ERROR); - } - } catch (Exception e) { - log.error("Error in login monitor thread", e); - setState(SchedulerState.ERROR); - } - }); - - loginMonitor.setName("Login-Monitor"); - loginMonitor.setDaemon(true); - loginMonitor.start(); -} - -### Two-Tiered Plugin Stop Mechanism - -The scheduler implements a sophisticated stop mechanism with both soft and hard stop options: - -```java -/** - * Initiates a forced stop process for the current plugin. - * Used when a plugin needs to be stopped immediately or when soft stop fails. - * - * @param successful Whether the plugin completed successfully before stopping - */ -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); - } - 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(); -} -``` - -The actual stop monitoring logic is implemented in the `PluginScheduleEntry` class: - -```java -/** - * Starts a monitor thread that oversees the plugin stopping process - * - * @param successfulRun Whether the plugin run was successful - */ -private void startStopMonitoringThread(boolean successfulRun) { - // Don't start a new thread if one is already running - if (isMonitoringStop) { - return; - } - - isMonitoringStop = true; - - stopMonitorThread = new Thread(() -> { - log.info("Stop monitoring thread started for plugin '" + name + "'"); - - try { - // Keep checking until the stop completes or is abandoned - while (stopInitiated && isMonitoringStop) { - // Check if plugin has stopped running - if (!isRunning()) { - // Plugin has stopped successfully - if (scheduleEntryConfigManager != null) { - scheduleEntryConfigManager.setScheduleMode(false); - } - - // Update conditions for next run - if (successfulRun) { - resetStartConditions(); - } else { - setEnabled(false); - } - - // Reset stop state - stopInitiated = false; - hasStarted = false; - break; - } - - Thread.sleep(300); // Check every 300ms - } - } catch (InterruptedException e) { - // Thread was interrupted, just exit - } finally { - isMonitoringStop = false; - } - }); - - stopMonitorThread.setName("StopMonitor-" + name); - stopMonitorThread.setDaemon(true); // Use daemon thread to not prevent JVM exit - stopMonitorThread.start(); -} -``` - -This approach allows plugins to clean up resources and save state during a soft stop, while ensuring they eventually stop even if unresponsive. - -### Comprehensive UI and User Experience - -The scheduler provides an intuitive interface for managing scheduled plugins: - -```java -@Inject -private ClientToolbar clientToolbar; - -private NavigationButton navButton; -private SchedulerPanel panel; -private SchedulerWindow schedulerWindow; - -/** - * Initializes the scheduler UI components and navigation - */ -private void initializeUI() { - 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); - - // Initialize the main scheduler window - schedulerWindow = new SchedulerWindow(this); -} -``` - -The UI allows users to: - -- Add, edit, and remove scheduled plugins -- Configure start and stop conditions through an intuitive condition builder -- View detailed plugin execution history and statistics -- Manually control plugin execution with start, stop, and pause options - -## Plugin Selection and Scheduling Algorithm - -The scheduler implements a sophisticated multi-factor algorithm to determine which plugins to run and when. - -### Plugin Selection Process - -1. **Scheduling Cycle**: - - The scheduler runs a periodic check approximately every second via `checkSchedule()` - - During each cycle, it evaluates if the current plugin should be stopped with `checkCurrentPlugin()` - - If no plugin is running, it selects the next plugin to run with `scheduleNextPlugin()` - -2. **Selection Algorithm**: - ```text - START - Check if any non-default plugins are scheduled soon (within lookup window) - If a non-default plugin is upcoming and current plugin is default: - Don't start any default plugin, wait for the non-default plugin - Get next plugin that's due to run via getNextScheduledPluginEntry() - If on a break, interrupt it to start the selected plugin - Start the selected plugin with startPluginScheduleEntry() - END - ``` - -3. **Selection Factors** (in order of precedence): - - **Plugin Priority**: Higher priority plugins are always evaluated first - - **Plugin Type**: Non-default plugins take precedence over default plugins - - **Start Conditions**: Plugins with met start conditions are selected first - - **Randomization**: For equal priority plugins, weighted random selection - - **Run Balance**: Plugins run less frequently get higher weighting - -### Condition Evaluation Model - -The scheduler uses a sophisticated condition evaluation model with different rules for start vs. stop: - -```text -For Starting a Plugin: -Plugin Start Conditions AND User Start Conditions must ALL be true - -For Stopping a Plugin: -Plugin Stop Conditions OR User Stop Conditions - ANY being true triggers stop -``` - -This condition model gives both plugins and users appropriate control: - -- Plugins cannot start unless both their requirements and user preferences allow -- Either the plugin or user can trigger a stop when needed - -## Break System and Play Schedule - -The scheduler implements two complementary break systems to create natural-appearing behavior patterns. - -### Core Break System - -The break system controls automated breaks between plugin executions: - -1. **Break Duration Calculation**: - - Minimum break duration from config (default: 2 minutes) - - Maximum break duration from config (default: 2 minutes, configurable up to 60) - - The `startBreakBetweenSchedules` method handles break initialization - -2. **Break Triggers**: - - After plugin completion - - When no plugins are running and no plugins are due to run soon - - When outside allowed play schedule hours - - Based on configured break frequency - -3. **Break Management**: - - **Break Initiation**: Uses `BreakHandlerScript.breakDuration` to set break length - - **Break Interruption**: Can interrupt breaks for high-priority plugins using `interruptBreak()` - - **Break State Tracking**: Uses dedicated states (BREAK, PLAYSCHEDULE_BREAK) - -### Play Schedule System - -The play schedule controls when the scheduler is allowed to run plugins: - -1. **Schedule Configuration**: - - Each day can have different allowed play hours - - Multiple time windows can be configured per day - - Randomization can be applied to window boundaries - -2. **Schedule Behavior**: - - Outside allowed hours: Scheduler enters PLAYSCHEDULE_BREAK state - - Approaching end of window: Current plugin may be stopped - - Beginning of window: Scheduler resumes normal operation - - Window transitions: Can trigger login/logout actions - -### Integration with BreakHandler Plugin - -The scheduler integrates with the standalone BreakHandler plugin: - -1. **Coordination Mechanism**: - - BreakHandler signals when breaks begin/end - - Scheduler respects BreakHandler's break state - - Login/logout settings synchronized between systems - - Break statistics shared for consistent behavior - -2. **Break Handler Settings Management**: - - ```java - /** - * Saves current BreakHandler settings before modifying them - * for scheduler operation, and restores original settings - * when scheduler is disabled. - */ - private void syncBreakHandlerSettings() { - // Save original settings - savedBreakHandlerLogoutSetting = getBreakHandlerSetting("logout"); - - // Apply scheduler settings - if (config.breakHandlerForceLogout()) { - setBreakHandlerSetting("logout", true); - } - } - ``` - -## Implementation Strategies - -### Main Schedule Check Logic - -The scheduler uses a main scheduling check that runs approximately every second: - -```java -private void checkSchedule() { - // Skip checking if in certain states - if (SchedulerState.LOGIN == currentState || - SchedulerState.WAITING_FOR_LOGIN == currentState || - SchedulerState.HARD_STOPPING_PLUGIN == currentState || - SchedulerState.SOFT_STOPPING_PLUGIN == currentState || - currentState == SchedulerState.HOLD) { - 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()) { - // Check if login is needed - PluginScheduleEntry nextPluginWith = null; - PluginScheduleEntry nextPluginPossible = getNextScheduledPluginEntry(false, null).orElse(null); - - // Skip breaks if min break duration is 0 - int minBreakDuration = config.minBreakDuration(); - if (minBreakDuration == 0) { - minBreakDuration = 1; - nextPluginWith = getNextScheduledPluginEntry(true, null).orElse(null); - } else { - minBreakDuration = Math.max(1, minBreakDuration); - // Get the next scheduled plugin within minBreakDuration - nextPluginWith = getNextScheduledPluginWithinTime( - Duration.ofMinutes(minBreakDuration)); - } - - // Handle login requirements - if (nextPluginWith == null && - nextPluginPossible != null && - !nextPluginPossible.hasOnlyTimeConditions() - && !isOnBreak() && !Microbot.isLoggedIn()) { - startLoginMonitoringThread(); - return; - } - - // Determine if we should schedule next plugin or take a break - if (nextPluginWith != null && canRunNow()) { - scheduleNextPlugin(); - } else { - // Take a break if nothing is running - startBreak(); - } - } -} -``` - -### Plugin Management - -The actual management of plugin lifecycle is handled through: - -1. **Starting Plugins**: `startPluginScheduleEntry(PluginScheduleEntry entry)` -2. **Stopping Plugins**: `forceStopCurrentPluginScheduleEntry(boolean successful)` -3. **Checking Conditions**: `checkCurrentPlugin()` evaluates stop conditions - -### User Interface Functions - -The scheduler provides UI controls through: - -- **Force Start**: `forceStartPluginScheduleEntry(PluginScheduleEntry entry)` -- **Stop**: `forceStopCurrentPluginScheduleEntry(boolean successful)` -- **Pause/Resume**: `setSchedulerState(SchedulerState.HOLD)` and `startScheduler()` - -## Integration with Other Systems - -The SchedulerPlugin integrates with several other plugins and systems to provide a complete automation solution: - -### BreakHandler Integration - -The scheduler works closely with the BreakHandler plugin to: -- Respect global break settings -- Coordinate break times across plugins -- Share break statistics and patterns -- Manage login/logout during breaks - -Configuration options allow you to control: -- Whether to use BreakHandler for scheduled plugins -- Whether to log out during breaks -- Break duration settings - -### AutoLogin Integration - -The scheduler integrates with the AutoLogin plugin to: -- Automatically log in before starting plugins when needed -- Handle world selection based on configuration -- Manage disconnections and reconnections -- Support play schedule login/logout requirements - -### Antiban Features - -The scheduler implements various antiban measures: -- Randomized break patterns -- Natural play schedule enforcement -- Varied execution timing -- Random plugin selection within priority groups - -## Best Practices - -For optimal use of the Plugin Scheduler: - -1. **Set Clear Priorities**: - - High priority (8-10): Critical tasks - - Medium priority (4-7): Regular tasks - - Low priority (1-3): Background tasks - -2. **Use Appropriate Stop Conditions**: - - Always include a time-based stop condition as a fallback - - Use game-state conditions for more precise control - - Test conditions thoroughly before long-term use - -3. **Balance Random vs. Fixed Scheduling**: - - Use randomization for most plugins - - Reserve fixed schedules for time-sensitive tasks - - Mix default and non-default plugins for natural patterns - -4. **Configure Integration Features**: - - Enable BreakHandler integration for more natural patterns - - Use AutoLogin when running unattended - - Set up Play Schedules that match realistic play patterns diff --git a/docs/scheduler/tasks/README.md b/docs/scheduler/tasks/README.md deleted file mode 100644 index 5e106c4e045..00000000000 --- a/docs/scheduler/tasks/README.md +++ /dev/null @@ -1,123 +0,0 @@ -# Pre/Post Schedule Tasks System Documentation - -## Overview - -The Pre/Post Schedule Tasks system transforms plugin development by introducing automated preparation and cleanup capabilities into the Plugin Scheduler. This evolutionary enhancement shifts plugin architecture from manual resource management to declarative requirement specification, creating more reliable and user-friendly automation. - -### The Transformation - -Traditional plugin development required manual handling of equipment setup, inventory management, location positioning, and resource cleanup. The task system automates these operations through a declarative approach where plugins specify requirements rather than implementation details. - -### System Architecture - -The task system operates through three interconnected layers: - -**Infrastructure Layer**: The [`AbstractPrePostScheduleTasks`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/AbstractPrePostScheduleTasks.java) provides thread-safe execution services, lifecycle management, and error handling foundations. - -**Requirements Layer**: The [`PrePostScheduleRequirements`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/PrePostScheduleRequirements.java) framework enables declarative specification of plugin needs through a comprehensive requirement type system. - -**Integration Layer**: The enhanced [`SchedulablePlugin`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/api/SchedulablePlugin.java) interface connects plugins seamlessly with the scheduler ecosystem. - -## Evolution Through Three Commits - -The system development followed three strategic phases: - -### Phase 1: Enhanced Requirements Framework - -Introduced flexible requirement definitions with sophisticated logical operations, enabling complex requirement combinations through OR operation modes and intelligent planning algorithms. - -### Phase 2: Robust Infrastructure - -Enhanced task lifecycle management with comprehensive cancellation support, sophisticated error handling, and rich user interface components for real-time monitoring. - -### Phase 3: Production Integration - -Established core infrastructure with the complete requirement type ecosystem, demonstrating real-world implementation through the GOTR plugin integration. - -## Core Concepts - -### Declarative Requirements - -The system's power emerges from its declarative nature. Instead of writing procedural code to acquire items or navigate to locations, plugins declare requirements through specialized classes. The [`RequirementRegistry`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/registry/RequirementRegistry.java) manages these declarations and coordinates their fulfillment. - -### Intelligent Fulfillment - -The [`RequirementSolver`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSolver.java) analyzes requirements and determines optimal fulfillment strategies, considering factors like resource availability, player location, and requirement priorities. - -### Context-Aware Execution - -Requirements operate within specific contexts (PRE_SCHEDULE, POST_SCHEDULE, BOTH) allowing the system to coordinate when different preparations occur during the plugin lifecycle. - -## Documentation Structure - -### Foundation Documentation - -- **[Task Management System](task-management-system.md)** — Comprehensive guide to the infrastructure layer, covering execution services, lifecycle management, and error handling patterns -- **[Requirements System](requirements-system.md)** — Complete exploration of the requirement type ecosystem, fulfillment strategies, and advanced logical combinations -- **[Enhanced SchedulablePlugin API](enhanced-schedulable-plugin-api.md)** — Advanced integration patterns for scheduler connectivity and lifecycle management - -### Implementation Resources - -- **[Plugin Writer's Guide](plugin-writers-guide.md)** - Practical step-by-step implementation guidance with decision trees and best practices -- **[Requirements Integration](requirements-integration.md)** - Strategies for using requirements beyond scheduler context, enabling code reuse across different plugin architectures - -### Reference Implementations - -Study these production codebases for practical implementation insights: - -- **GOTR Integration**: Examine [GOTR Plugin Implementation](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/runecrafting/gotr/) demonstrating sophisticated minigame automation with comprehensive preparation requirements -- **Example Plugin**: Review [SchedulableExample Implementation](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/) showcasing complete system integration patterns - -## Implementation Approach - -### Understanding Before Implementation - -The task system's effectiveness depends on understanding its conceptual framework before diving into implementation specifics. Focus on grasping how declarative requirements translate into automated behaviors. - -### Three-Component Integration - -Every implementation involves three fundamental components working in coordination: - -**Requirements Definition**: Create a class extending [`PrePostScheduleRequirements`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/PrePostScheduleRequirements.java) that declares what your plugin needs to operate successfully. - -**Task Management**: Develop a class extending [`AbstractPrePostScheduleTasks`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/AbstractPrePostScheduleTasks.java) that handles both requirement fulfillment and custom plugin-specific preparation or cleanup operations. - -**Plugin Integration**: Modify your plugin class to implement the enhanced [`SchedulablePlugin`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/api/SchedulablePlugin.java) interface, establishing the connection with the scheduler system. - -### Development Philosophy - -Embrace the separation between "what" and "how" - specify what your plugin needs rather than how to obtain it. This approach creates more maintainable, reliable, and adaptable automation systems. - -## System Benefits - -### Transformation for Developers - -The system revolutionizes plugin development by eliminating repetitive resource management code, providing standardized error handling patterns, and enabling focus on core plugin functionality rather than setup procedures. - -### Enhanced User Experience - -Users benefit from consistent preparation procedures across all plugins, automatic resource acquisition, intelligent optimization of preparation paths, and comprehensive progress feedback through rich UI components. - -### Ecosystem Advantages - -The framework promotes code reuse through shared requirement implementations, ensures quality consistency across plugin behaviors, optimizes performance through centralized resource management, and simplifies maintenance through standardized patterns. - -## Technical Foundation - -### Key Components - -Explore the complete implementation through the [Task System Directory](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/) which contains all system components including requirement types, management infrastructure, UI components, and example implementations. - -### Extension Points - -The system design emphasizes extensibility through well-defined interfaces, allowing custom requirement types, specialized fulfillment strategies, and plugin-specific preparation procedures while maintaining integration with the broader framework. - -## Next Steps for Implementation - -1. **Conceptual Understanding**: Begin with the [Task Management System](task-management-system.md) documentation to understand infrastructure concepts -2. **Requirement Design**: Study the [Requirements System](requirements-system.md) to plan your plugin's requirement specification -3. **Practical Implementation**: Follow the [Plugin Writer's Guide](plugin-writers-guide.md) for step-by-step integration instructions -4. **Reference Study**: Examine the [GOTR implementation](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/runecrafting/gotr/) for production patterns -5. **Advanced Integration**: Explore [Requirements Integration](requirements-integration.md) for sophisticated usage patterns beyond basic scheduler integration - -The Pre/Post Schedule Tasks system represents a paradigm shift toward intelligent, declarative automation that handles the complexity of game state management while maintaining the flexibility needed for diverse plugin requirements. diff --git a/docs/scheduler/tasks/enhanced-schedulable-plugin-api.md b/docs/scheduler/tasks/enhanced-schedulable-plugin-api.md deleted file mode 100644 index acbf560ce07..00000000000 --- a/docs/scheduler/tasks/enhanced-schedulable-plugin-api.md +++ /dev/null @@ -1,237 +0,0 @@ -# Enhanced SchedulablePlugin API - -## Overview - -The Enhanced SchedulablePlugin API represents an evolution from the original SchedulablePlugin interface, introducing comprehensive task management capabilities that standardize plugin automation while maintaining flexibility for diverse implementation needs. This API transforms plugin development from manual resource coordination to declarative requirement specification. - -## Architectural Evolution - -### From Manual to Declarative - -The enhanced API shifts plugin development philosophy from imperative resource management to declarative requirement specification. Instead of implementing complex resource acquisition logic within your plugin, you describe what your plugin needs, and the task system handles the execution details. - -### Integration Philosophy - -The API is designed around the principle of seamless integration - plugins enhanced with task management capabilities should function identically whether used standalone or within the scheduler environment. This dual-mode operation ensures backward compatibility while enabling advanced automation features. - -### System Coordination - -Enhanced plugins participate in a broader ecosystem where resource coordination, conflict resolution, and user experience standardization are handled at the system level rather than requiring individual plugin implementations. - -## Core Interface Components - -### Enhanced SchedulablePlugin Interface - -The primary interface extends the basic SchedulablePlugin with task management capabilities: - -**Task Management Integration**: Methods for coordinating with the task execution system, enabling plugins to participate in comprehensive automation workflows. - -**Lifecycle Coordination**: Enhanced lifecycle methods that integrate with the scheduler's execution model while maintaining plugin autonomy. - -**Event Handling**: Standardized event handling for scheduler interactions, allowing plugins to respond appropriately to system coordination events. - -Reference the complete interface definition in [`SchedulablePlugin`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/api/SchedulablePlugin.java) for detailed method specifications. - -### Task Provider Integration - -Enhanced plugins can provide task management capabilities through the system's provider pattern: - -**Task Manager Provision**: Plugins supply task manager instances that handle resource preparation and cleanup operations. - -**Requirement Specification**: Declarative requirement definition that describes plugin resource needs without implementing acquisition logic. - -**Configuration Integration**: Integration with the scheduler's configuration system for coordinated user experience across plugin and task management. - -## Implementation Architecture - -### Dual-Mode Operation - -Enhanced plugins must support both standalone and scheduler-managed operation: - -**Standalone Functionality**: Complete plugin functionality when operating independently, maintaining existing user workflows and expectations. - -**Scheduler Integration**: Enhanced capabilities when operating within the scheduler environment, leveraging centralized resource management and coordination. - -**Detection Logic**: Intelligent detection of the operational context to enable appropriate behavior in each mode. - -### Resource Coordination - -The API facilitates sophisticated resource coordination: - -**Shared Resource Management**: Coordination with other system components for shared resources like banking, equipment management, and location positioning. - -**Conflict Resolution**: Participation in system-level conflict resolution when multiple plugins or operations compete for limited resources. - -**State Synchronization**: Coordination with the scheduler's state management to ensure consistent operation across complex automation workflows. - -### Configuration Architecture - -Enhanced plugins integrate with the scheduler's configuration system: - -**Unified Configuration**: Seamless integration between plugin-specific configuration and task management settings. - -**User Experience Consistency**: Standardized configuration patterns that provide consistent user experience across different plugin types. - -**Dynamic Configuration**: Support for runtime configuration changes that affect both plugin behavior and task management strategies. - -## Advanced Integration Patterns - -### Complex Workflow Coordination - -For sophisticated automation scenarios: - -**Multi-Phase Operations**: Support for complex operations that span multiple game activities or require sophisticated state transitions. - -**Conditional Execution**: Dynamic adaptation based on game state, resource availability, or user preferences. - -**Cross-Plugin Coordination**: Integration with other enhanced plugins for coordinated automation workflows. - -### Error Handling and Recovery - -Robust error handling strategies for enhanced plugins: - -**Graceful Degradation**: Intelligent fallback strategies when optimal conditions cannot be achieved. - -**State Recovery**: Restoration of proper state when operations are interrupted or fail. - -**User Communication**: Clear communication of status, issues, and recovery actions to maintain user awareness and control. - -### Performance and Responsiveness - -Design considerations for maintaining optimal performance: - -**Asynchronous Operations**: Integration with asynchronous execution patterns to maintain system responsiveness. - -**Resource Efficiency**: Efficient resource utilization that minimizes impact on game performance and user experience. - -**Scalability**: Design patterns that support multiple concurrent enhanced plugins without degrading system performance. - -## Implementation Guidelines - -### Interface Implementation Strategy - -Approach enhanced plugin implementation systematically: - -**Incremental Enhancement**: Add enhanced capabilities to existing plugins without disrupting core functionality. - -**Testing Strategy**: Comprehensive testing in both standalone and scheduler-managed modes to ensure reliable operation. - -**Compatibility Maintenance**: Preserve existing plugin behavior while adding enhanced capabilities. - -### Best Practices - -Follow established patterns for reliable enhanced plugin development: - -**Clear Separation**: Maintain clear separation between core plugin logic and enhanced task management capabilities. - -**Robust Integration**: Design integration points that handle edge cases gracefully and provide appropriate fallback behavior. - -**User Experience**: Prioritize user experience consistency across different operational modes. - -### Common Integration Challenges - -Understand and address typical implementation challenges: - -**State Management**: Coordinate plugin state with task execution state to prevent conflicts or inconsistencies. - -**Resource Competition**: Handle scenarios where plugin needs conflict with system resource management. - -**Timing Coordination**: Ensure proper timing between plugin operations and task execution phases. - -## Example Implementation Analysis - -### Production Examples - -Study these production implementations for guidance: - -**GOTR Integration**: Examine the [GOTR enhanced plugin](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/runecrafting/gotr/) implementation for sophisticated minigame automation with comprehensive task integration. - -**Example Implementation**: Review the [complete example plugin](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/) that demonstrates all aspects of enhanced plugin development. - -### Pattern Analysis - -Learn from established implementation patterns: - -**Interface Implementation**: Study how production plugins implement the enhanced interface while maintaining backward compatibility. - -**Resource Coordination**: Examine real-world approaches to resource coordination and conflict resolution. - -**Error Handling**: Understand proven strategies for robust error handling and recovery in enhanced plugins. - -## Testing and Validation - -### Comprehensive Testing Strategy - -Ensure reliable enhanced plugin operation through thorough testing: - -**Dual-Mode Testing**: Validate functionality in both standalone and scheduler-managed operation modes. - -**Integration Testing**: Test coordination with task management system and other system components. - -**Edge Case Testing**: Verify behavior under unusual conditions, resource constraints, and error scenarios. - -### Performance Validation - -Ensure enhanced plugins maintain acceptable performance: - -**Resource Usage**: Monitor resource consumption in enhanced mode to prevent performance degradation. - -**Responsiveness**: Validate that enhanced capabilities don't negatively impact user interface responsiveness. - -**Scalability**: Test behavior when multiple enhanced plugins operate concurrently. - -### User Experience Testing - -Validate user experience across operational modes: - -**Consistency**: Ensure user experience remains consistent between standalone and enhanced operation. - -**Feedback**: Verify that users receive appropriate feedback about enhanced plugin status and operations. - -**Control**: Ensure users maintain appropriate control over enhanced plugin behavior. - -## Migration and Upgrade Strategies - -### Existing Plugin Enhancement - -Transform existing plugins to support enhanced capabilities: - -**Gradual Enhancement**: Add enhanced capabilities incrementally without disrupting existing functionality. - -**Compatibility Preservation**: Maintain backward compatibility throughout the enhancement process. - -**Testing Integration**: Integrate enhanced testing strategies into existing plugin testing workflows. - -### Version Management - -Manage enhanced plugin versions effectively: - -**Feature Gating**: Use feature flags to control enhanced capability availability during development and deployment. - -**Configuration Migration**: Provide smooth migration paths for existing plugin configurations. - -**Documentation Updates**: Maintain current documentation that covers both basic and enhanced plugin capabilities. - -## Future Evolution - -### API Evolution Path - -Understand the planned evolution of enhanced plugin capabilities: - -**Feature Expansion**: Anticipated additions to enhanced plugin capabilities and coordination features. - -**Performance Improvements**: Ongoing optimization of enhanced plugin execution patterns and resource management. - -**Integration Enhancements**: Planned improvements to cross-plugin coordination and ecosystem integration. - -### Development Considerations - -Plan enhanced plugin development with future evolution in mind: - -**Extensible Architecture**: Design enhanced plugins to accommodate future API enhancements without requiring major refactoring. - -**Configuration Flexibility**: Implement configuration patterns that can evolve with API capabilities. - -**Testing Framework**: Establish testing frameworks that can adapt to API evolution while maintaining comprehensive coverage. - -The Enhanced SchedulablePlugin API represents a significant evolution in plugin automation capabilities. Success with this API requires understanding both the technical implementation details and the broader architectural philosophy that guides the system design. Focus on the declarative approach, emphasize resource coordination, and prioritize user experience consistency across all operational modes. diff --git a/docs/scheduler/tasks/plugin-writers-guide.md b/docs/scheduler/tasks/plugin-writers-guide.md deleted file mode 100644 index 87d45ce9d66..00000000000 --- a/docs/scheduler/tasks/plugin-writers-guide.md +++ /dev/null @@ -1,225 +0,0 @@ -# Plugin Writer's Guide - -## Overview - -This guide provides practical, step-by-step instructions for integrating the Pre/Post Schedule Tasks system into your plugin. Rather than learning abstract concepts, this guide focuses on the concrete implementation steps needed to transform your plugin from manual resource management to declarative requirement specification. - -## Understanding the Integration Process - -### The Transformation Journey - -Integrating the task system involves transforming your plugin from imperative resource management to declarative requirement specification. This transformation typically improves plugin reliability, reduces user setup burden, and standardizes behavior across the plugin ecosystem. - -### Integration Complexity Levels - -**Basic Integration**: Simple requirement definition with minimal custom logic, suitable for straightforward skilling or combat plugins. - -**Intermediate Integration**: Multiple requirement types with some custom preparation logic, typical for specialized activities or minigames. - -**Advanced Integration**: Complex requirement combinations, conditional logic, and sophisticated custom task implementations for high-end automation systems. - -## Prerequisites and Planning - -### Evaluation Phase - -Before beginning integration, evaluate your plugin's current resource management patterns: - -**Manual Operations**: Identify operations users currently perform manually before starting your plugin - these become candidates for requirement automation. - -**Resource Dependencies**: Catalog items, equipment, locations, and game state conditions your plugin needs to function effectively. - -**Failure Points**: Examine common failure modes in your current plugin and determine which could be prevented through proper requirement fulfillment. - -### Architecture Planning - -Plan your integration approach based on your plugin's complexity: - -**Simple Plugins**: Focus primarily on requirement definition with minimal custom task logic. - -**Complex Plugins**: Design custom task implementations for plugin-specific operations that cannot be expressed as standard requirements. - -**Ecosystem Plugins**: Consider integration with other scheduler-aware plugins and shared resource coordination. - -## Step-by-Step Implementation - -### Step 1: Requirements Analysis and Design - -Begin by analyzing your plugin's resource needs and designing appropriate requirements: - -**Equipment Analysis**: Determine optimal equipment for your plugin's activities, considering alternatives and upgrade paths. - -**Location Strategy**: Identify key locations your plugin operates in and determine positioning requirements. - -**Resource Planning**: Catalog consumables, tools, and other items needed for successful plugin execution. - -**State Dependencies**: Identify spellbook requirements, quest prerequisites, and other game state conditions. - -Study the [`ExamplePrePostScheduleRequirements`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleRequirements.java) implementation to understand requirement definition patterns. - -### Step 2: Requirements Implementation - -Create your requirements class extending [`PrePostScheduleRequirements`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/PrePostScheduleRequirements.java): - -**Class Structure**: Implement the abstract `initializeRequirements()` method to define all requirements your plugin needs. - -**Registry Usage**: Use the provided registry to register requirements with appropriate priorities and contexts. - -**Collection Integration**: Leverage [`ItemRequirementCollection`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/data/ItemRequirementCollection.java) for standard equipment sets rather than defining individual items. - -**Context Assignment**: Assign requirements to appropriate contexts (PRE_SCHEDULE, POST_SCHEDULE, BOTH) based on when they should be fulfilled. - -### Step 3: Task Manager Implementation - -Create your task manager extending [`AbstractPrePostScheduleTasks`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/AbstractPrePostScheduleTasks.java): - -**Core Methods**: Implement the required abstract methods, focusing on custom logic that cannot be expressed as requirements. - -**Error Handling**: Design robust error handling strategies for custom operations, considering both recoverable and non-recoverable failures. - -**Resource Management**: Ensure proper cleanup in custom task implementations, following the established patterns for resource lifecycle management. - -**Integration Hooks**: Implement scheduler detection logic to ensure task execution only occurs when appropriate. - -Reference the [`ExamplePrePostScheduleTasks`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleTasks.java) for implementation patterns and best practices. - -### Step 4: Plugin Integration - -Modify your main plugin class to integrate with the task system: - -**Interface Implementation**: Implement the enhanced [`SchedulablePlugin`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/api/SchedulablePlugin.java) interface to enable scheduler integration. - -**Lifecycle Modification**: Modify startup and shutdown procedures to execute tasks when appropriate while maintaining backward compatibility for non-scheduler usage. - -**Event Handling**: Implement scheduler event handlers to respond to stop requests and other scheduler coordination events. - -**State Management**: Ensure proper coordination between task execution and main plugin logic to prevent interference. - -### Step 5: Testing and Validation - -Develop comprehensive testing strategies to ensure reliable integration: - -**Requirement Testing**: Validate that all requirements can be fulfilled under various game conditions and resource availability scenarios. - -**Integration Testing**: Test the complete lifecycle from requirement fulfillment through main plugin execution to cleanup operations. - -**Edge Case Testing**: Verify behavior when requirements cannot be fulfilled, when operations time out, and when emergency cancellation is triggered. - -**Performance Testing**: Ensure task execution doesn't negatively impact main plugin performance or introduce unacceptable delays. - -## Advanced Implementation Patterns - -### Complex Requirement Combinations - -For activities requiring sophisticated resource coordination: - -**OR Requirements**: Use logical requirements to specify alternatives when multiple valid approaches exist. - -**Conditional Requirements**: Implement state-dependent requirements that adapt to current game conditions. - -**Hierarchical Planning**: Design requirement hierarchies for complex activities with multiple phases or optional enhancements. - -### Custom Task Implementation - -For plugin-specific operations beyond standard requirements: - -**Preparation Logic**: Implement custom pre-schedule tasks for plugin initialization, state validation, or specialized setup procedures. - -**Cleanup Operations**: Design custom post-schedule tasks for data persistence, state restoration, or plugin-specific resource management. - -**Integration Coordination**: Coordinate with other systems or plugins through custom task implementations when standard requirements are insufficient. - -### Performance Optimization - -Optimize task performance for smooth user experience: - -**Asynchronous Operations**: Design tasks to utilize asynchronous patterns where appropriate to maintain responsiveness. - -**Resource Efficiency**: Minimize resource usage during task execution to prevent interference with main plugin operations. - -**Caching Strategies**: Implement intelligent caching for expensive operations while maintaining accuracy through proper invalidation. - -## Common Implementation Challenges - -### Requirement Conflicts - -Address potential conflicts between requirements: - -**Resource Competition**: Handle scenarios where multiple requirements compete for limited resources. - -**Timing Conflicts**: Resolve conflicts between requirements that must be fulfilled in specific orders. - -**State Inconsistencies**: Design strategies for handling inconsistent game state during requirement fulfillment. - -### Error Recovery - -Implement robust error recovery strategies: - -**Graceful Degradation**: Design fallback strategies when optimal requirements cannot be fulfilled. - -**User Communication**: Provide clear feedback about requirement fulfillment status and any issues encountered. - -**Retry Logic**: Implement intelligent retry strategies for transient failures while avoiding infinite loops. - -### Integration Complexity - -Manage complexity in sophisticated integrations: - -**Code Organization**: Structure your implementation to maintain clear separation between requirement definition, task management, and main plugin logic. - -**Configuration Management**: Design configuration interfaces that expose appropriate task system options to users. - -**Debugging Support**: Implement comprehensive logging and debugging support to aid in troubleshooting integration issues. - -## Real-World Examples - -### Study Production Implementations - -Examine these production examples for implementation insights: - -**GOTR Integration**: Study the [GOTR plugin directory](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/runecrafting/gotr/) for a sophisticated minigame integration with comprehensive requirement management. - -**Example Plugin**: Review the [SchedulableExample implementation](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/) for a complete demonstration of all system features. - -### Implementation Patterns - -Learn from established patterns: - -**Requirement Organization**: Study how production implementations organize and structure their requirements for maintainability. - -**Error Handling**: Examine real-world error handling strategies and recovery mechanisms. - -**User Experience**: Understand how production plugins balance automation with user control and feedback. - -## Best Practices and Guidelines - -### Code Quality - -Maintain high code quality in your integration: - -**Clear Separation**: Keep requirement definition, task implementation, and main plugin logic clearly separated. - -**Comprehensive Testing**: Implement thorough testing for all aspects of your integration. - -**Documentation**: Document your requirements and custom task logic for future maintenance. - -### User Experience - -Design your integration with user experience in mind: - -**Predictable Behavior**: Ensure task execution is predictable and provides appropriate feedback. - -**Graceful Failures**: Handle failures gracefully with clear error messages and recovery suggestions. - -**Performance Impact**: Minimize the performance impact of task execution on overall plugin responsiveness. - -### Maintenance and Evolution - -Design for long-term maintenance: - -**Extensible Architecture**: Structure your implementation to accommodate future enhancements. - -**Version Compatibility**: Ensure your integration remains compatible with system updates. - -**Documentation Updates**: Keep documentation current as your implementation evolves. - -The integration process requires careful planning and attention to detail, but the result is more reliable, maintainable, and user-friendly plugin automation. Focus on understanding the concepts before implementing, and don't hesitate to study the example implementations for guidance on best practices and common patterns. diff --git a/docs/scheduler/tasks/requirements-integration.md b/docs/scheduler/tasks/requirements-integration.md deleted file mode 100644 index a29732437e1..00000000000 --- a/docs/scheduler/tasks/requirements-integration.md +++ /dev/null @@ -1,259 +0,0 @@ -# Requirements Integration - -## Overview - -Requirements integration represents the bridge between high-level plugin needs and concrete automation actions. This system transforms abstract requirements like "equipped for combat" or "positioned for mining" into specific, executable operations that prepare the game environment for plugin operation. - -## Integration Philosophy - -### Declarative Resource Management - -The requirements integration system embodies a declarative approach to resource management. Rather than implementing complex resource acquisition logic within each plugin, developers describe their needs using standardized requirement types, and the integration system handles the execution details. - -### Contextual Execution - -Requirements are fulfilled within specific contexts that determine when and how they should be executed. This contextual framework ensures requirements are fulfilled at appropriate times without interfering with plugin operation or creating user experience disruptions. - -### Extensible Foundation - -The integration architecture is designed for extensibility, allowing new requirement types to be added seamlessly while maintaining compatibility with existing implementations. This extensibility ensures the system can evolve to support new gameplay patterns and automation needs. - -## Integration Architecture - -### Registry-Based Management - -The integration system uses a registry-based architecture for requirement management: - -**Centralized Registration**: All requirements are registered through the [`RequirementRegistry`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/registry/RequirementRegistry.java), providing centralized management and coordination. - -**Type System**: Requirements are organized by type, allowing for specialized handling of different resource categories like equipment, inventory, location, and game state. - -**Priority Management**: The registry manages requirement priorities, ensuring critical requirements are fulfilled before optional enhancements. - -**Conflict Resolution**: Intelligent conflict resolution handles scenarios where requirements conflict or compete for limited resources. - -### Execution Framework - -Requirements are executed through a sophisticated framework that coordinates timing, resource management, and error handling: - -**Context-Aware Execution**: Requirements execute within appropriate contexts (PRE_SCHEDULE, POST_SCHEDULE, BOTH) based on their nature and timing requirements. - -**Resource Coordination**: The execution framework coordinates access to shared resources like banking, equipment management, and location positioning. - -**Error Handling**: Comprehensive error handling ensures failed requirements don't prevent other operations and provide appropriate user feedback. - -**Performance Optimization**: Intelligent execution patterns minimize overhead while maintaining reliability and user experience quality. - -## Core Integration Components - -### Requirement Type Ecosystem - -The system includes a comprehensive ecosystem of requirement types: - -**Equipment Requirements**: Managed through [`ItemRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/ItemRequirement.java) for automated equipment optimization and management. - -**Inventory Requirements**: Handled by [`ItemRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/ItemRequirement.java) for complex inventory preparation and item management. - -**Location Requirements**: Coordinated through [`LocationRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/LocationRequirement.java) for precise positioning and area preparation. - -**Game State Requirements**: Various specialized requirements for spellbooks, prayers, and other game state conditions. - -### Collection-Based Organization - -Related requirements are organized into collections for easier management: - -**Equipment Collections**: [`ItemRequirementCollection`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/data/ItemRequirementCollection.java) provides standardized equipment sets for common activities. - -**Activity-Specific Collections**: Specialized collections for specific activities like combat, skilling, or minigames that combine multiple requirement types. - -**User-Customizable Collections**: Support for user-defined requirement collections that can be shared across multiple plugins or activities. - -## Advanced Integration Patterns - -### Conditional Requirements - -The system supports sophisticated conditional requirement patterns: - -**State-Dependent Requirements**: Requirements that adapt based on current game state, player progress, or environmental conditions. - -**Logical Combinations**: OR requirements that specify alternatives when multiple valid approaches exist for achieving the same goal. - -**Hierarchical Dependencies**: Complex requirement hierarchies where fulfilling one requirement enables or modifies others. - -**Dynamic Adaptation**: Requirements that modify their behavior based on execution context or previous fulfillment attempts. - -### Resource Optimization - -Intelligent resource optimization ensures efficient requirement fulfillment: - -**Shared Resource Detection**: Automatic detection of shared resource needs across multiple requirements to optimize fulfillment order. - -**Cost-Benefit Analysis**: Intelligent analysis of fulfillment costs to choose optimal approaches when multiple options exist. - -**Resource Caching**: Strategic caching of expensive operations to improve performance across multiple requirement executions. - -**Batch Processing**: Optimization of related requirements to minimize repeated operations and resource access. - -### Error Recovery and Resilience - -Comprehensive error recovery ensures robust requirement integration: - -**Graceful Degradation**: Intelligent fallback strategies when optimal requirements cannot be fulfilled. - -**Partial Fulfillment**: Support for partial requirement fulfillment when complete fulfillment is not possible. - -**Retry Strategies**: Sophisticated retry logic for transient failures with appropriate backoff and timeout handling. - -**User Feedback**: Clear communication of requirement status, issues, and recovery actions to maintain user awareness. - -## Implementation Strategies - -### Basic Integration Approach - -For straightforward plugin integration: - -**Requirement Identification**: Systematically identify plugin resource needs and map them to appropriate requirement types. - -**Registry Configuration**: Configure the requirement registry with appropriate requirements, priorities, and contexts. - -**Testing Validation**: Comprehensive testing to ensure requirement fulfillment works reliably under various game conditions. - -Study the [`ExamplePrePostScheduleRequirements`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleRequirements.java) for basic integration patterns. - -### Advanced Integration Techniques - -For complex plugin requirements: - -**Custom Requirement Types**: Development of plugin-specific requirement types for specialized resource needs. - -**Conditional Logic**: Implementation of sophisticated conditional requirements that adapt to game state and context. - -**Cross-Plugin Coordination**: Integration with other plugins' requirements for coordinated resource management. - -**Performance Optimization**: Advanced optimization techniques for high-performance requirement fulfillment. - -### Integration Testing - -Comprehensive testing strategies for requirement integration: - -**Individual Requirement Testing**: Validation of each requirement type under various conditions and resource states. - -**Integration Testing**: Testing complete requirement fulfillment workflows from registration through execution. - -**Edge Case Validation**: Testing behavior under unusual conditions, resource constraints, and error scenarios. - -**Performance Testing**: Validation that requirement fulfillment doesn't negatively impact plugin or system performance. - -## Common Integration Challenges - -### Resource Competition - -Address scenarios where multiple requirements compete for limited resources: - -**Priority Resolution**: Use requirement priorities to resolve conflicts between competing requirements. - -**Resource Sharing**: Design requirements to share resources efficiently when possible. - -**Conflict Detection**: Implement detection of resource conflicts before they cause fulfillment failures. - -**Alternative Strategies**: Develop alternative fulfillment approaches when primary strategies conflict with other requirements. - -### Timing Coordination - -Manage complex timing relationships between requirements: - -**Execution Order**: Ensure requirements execute in appropriate order based on dependencies and priorities. - -**Context Coordination**: Coordinate requirement execution across different contexts to prevent interference. - -**Synchronization**: Synchronize requirement fulfillment with plugin lifecycle and game state changes. - -**Timeout Management**: Implement appropriate timeouts for requirement fulfillment to prevent indefinite delays. - -### State Management - -Handle complex game state interactions: - -**State Validation**: Ensure game state is appropriate for requirement fulfillment before execution. - -**State Preservation**: Preserve important game state during requirement fulfillment when necessary. - -**State Recovery**: Implement recovery strategies when requirement fulfillment disrupts expected game state. - -**State Monitoring**: Monitor game state changes that might affect requirement validity or fulfillment strategies. - -## Real-World Integration Examples - -### Production Implementations - -Study these production examples for integration insights: - -**GOTR Requirements**: Examine the [GOTR requirements implementation](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/runecrafting/gotr/) for sophisticated minigame requirement coordination. - -**Example Integration**: Review the [complete example](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/) for comprehensive requirement integration patterns. - -### Integration Patterns - -Learn from established integration approaches: - -**Requirement Organization**: Study how production implementations organize requirements for maintainability and clarity. - -**Error Handling**: Examine real-world error handling and recovery strategies in complex requirement scenarios. - -**Performance Optimization**: Understand proven techniques for optimizing requirement fulfillment performance. - -## Future Integration Evolution - -### System Enhancement - -Planned improvements to the requirements integration system: - -**New Requirement Types**: Development of additional requirement types to support emerging gameplay patterns and automation needs. - -**Performance Improvements**: Ongoing optimization of requirement fulfillment algorithms and resource management. - -**Integration Enhancements**: Improved coordination between requirements and enhanced plugin ecosystem integration. - -### Development Considerations - -Plan integration implementations with future evolution in mind: - -**Extensible Design**: Design requirement integrations to accommodate new requirement types and capabilities. - -**Backward Compatibility**: Ensure integration approaches remain compatible with system evolution. - -**Configuration Flexibility**: Implement configuration patterns that can evolve with system capabilities. - -## Best Practices for Integration - -### Design Principles - -Follow established principles for reliable requirement integration: - -**Clear Separation**: Maintain clear separation between requirement definition and fulfillment implementation. - -**Robust Error Handling**: Implement comprehensive error handling for all requirement fulfillment scenarios. - -**Performance Awareness**: Design integrations with performance impact awareness and optimization. - -### User Experience - -Prioritize user experience in requirement integration: - -**Predictable Behavior**: Ensure requirement fulfillment is predictable and provides appropriate feedback. - -**Graceful Failures**: Handle fulfillment failures gracefully with clear error messages and recovery suggestions. - -**Minimal Disruption**: Minimize disruption to user gameplay during requirement fulfillment. - -### Code Quality - -Maintain high code quality in integration implementations: - -**Comprehensive Testing**: Implement thorough testing for all aspects of requirement integration. - -**Clear Documentation**: Document requirement integration approaches for future maintenance and enhancement. - -**Consistent Patterns**: Follow established patterns and conventions for integration implementation. - -The requirements integration system represents a sophisticated approach to automated resource management that transforms plugin development from manual coordination to declarative specification. Success with this system requires understanding both the technical implementation details and the broader architectural philosophy that guides requirement fulfillment and resource coordination. diff --git a/docs/scheduler/tasks/requirements-system.md b/docs/scheduler/tasks/requirements-system.md deleted file mode 100644 index 872af97355b..00000000000 --- a/docs/scheduler/tasks/requirements-system.md +++ /dev/null @@ -1,197 +0,0 @@ -# Requirements System - -## Overview - -The Requirements System transforms plugin development by introducing a declarative approach to resource management and game state preparation. Built around the [`PrePostScheduleRequirements`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/PrePostScheduleRequirements.java) framework and powered by the [`RequirementRegistry`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/registry/RequirementRegistry.java), this system enables plugins to specify what they need rather than how to obtain it. - -## Declarative Philosophy - -### Paradigm Shift - -Traditional plugin development required imperative programming for resource acquisition - writing step-by-step procedures to obtain items, navigate to locations, and configure game state. The requirements system introduces a declarative paradigm where plugins specify desired outcomes and delegate implementation details to intelligent fulfillment algorithms. - -### Separation of Concerns - -The system achieves clean separation between requirement specification and fulfillment implementation. Plugin developers focus on defining requirements using specialized requirement types, while the fulfillment engine handles the complex logistics of actually meeting those requirements. - -### Intelligent Fulfillment - -The [`RequirementSolver`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSolver.java) analyzes requirement sets and determines optimal fulfillment strategies, considering factors like resource availability, player location, priority levels, and interdependencies between requirements. - -## Requirement Type Ecosystem - -### Item Requirements - -The [`ItemRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/ItemRequirement.java) system handles equipment and inventory management with sophisticated features: - -**Equipment Management**: Automatic detection of optimal equipment for activity requirements, with support for equipment slots, stat requirements, and compatibility checking. - -**Inventory Planning**: Intelligent inventory space management that considers item stacking, quantities needed, and space optimization for different activities. - -**Collection Integration**: The [`ItemRequirementCollection`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/data/ItemRequirementCollection.java) provides pre-configured equipment sets for common activities like combat, skilling, and specialized minigames. - -### Location Requirements - -The [`LocationRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/LocationRequirement.java) system manages player positioning and navigation: - -**Smart Navigation**: Integration with the walking system to automatically navigate to required locations using optimal pathfinding algorithms. - -**Proximity Handling**: Flexible proximity requirements that can specify exact positioning or acceptable ranges depending on activity needs. - -**Resource Location Integration**: The [`ResourceLocationOption`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/ResourceLocationOption.java) connects location requirements with resource availability, choosing optimal locations based on current game state. - -### Spellbook Requirements - -The [`SpellbookRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/SpellbookRequirement.java) manages magic system configuration: - -**Automatic Switching**: Seamless spellbook changes with restoration to original state during cleanup phases. - -**Context Awareness**: Different spellbook requirements for different phases of execution, enabling complex spell usage patterns. - -**State Preservation**: Careful tracking of original spellbook state to ensure proper restoration after task completion. - -### Shop Requirements - -The [`ShopRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/) system handles Grand Exchange and NPC shop interactions: - -**Multi-Item Support**: The [`MultiItemConfig`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/MultiItemConfig.java) enables complex purchasing strategies for items that come in sets or have alternatives. - -**World Hopping Integration**: The [`WorldHoppingConfig`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/WorldHoppingConfig.java) provides intelligent world selection for optimal shop availability. - -**Transaction Management**: Sophisticated handling of offer states, cancellations, and retry logic through the [`CancelledOfferState`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/CancelledOfferState.java) system. - -### Logical Requirements - -The [`LogicalRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/logical/) system enables complex requirement combinations: - -**OR Operations**: The [`OrRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/logical/OrRequirement.java) allows specification of alternative requirements, with intelligent selection based on the [`OrRequirementMode`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/OrRequirementMode.java) configuration. - -**Conditional Logic**: Advanced requirement combinations that adapt to current game state and player capabilities. - -**Hierarchical Planning**: Complex requirement trees that enable sophisticated preparation strategies for advanced activities. - -### Conditional Requirements - -The [`ConditionalRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/conditional/) system provides state-dependent requirement activation: - -**Dynamic Requirements**: Requirements that activate or deactivate based on current game conditions, player state, or plugin configuration. - -**Ordered Execution**: The [`OrderedRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/conditional/OrderedRequirement.java) enables sequential requirement fulfillment when order matters. - -**State-Driven Logic**: Requirements that adapt their behavior based on complex state evaluation using the [`ConditionalRequirementBuilder`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/ConditionalRequirementBuilder.java). - -## Registry Architecture - -### Centralized Management - -The [`RequirementRegistry`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/registry/RequirementRegistry.java) serves as the central coordination point for all requirements within a plugin, providing: - -**Uniqueness Enforcement**: Automatic prevention of duplicate requirements while allowing updates and refinements to existing requirements. - -**Type-Safe Access**: Efficient retrieval of requirements by type, context, and priority level with compile-time safety guarantees. - -**Consistency Guarantees**: Validation of requirement combinations to prevent conflicting or impossible requirement sets. - -### Context Management - -The [`TaskContext`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/TaskContext.java) system enables requirements to specify when they should be fulfilled: - -**PRE_SCHEDULE**: Requirements fulfilled before main plugin execution begins, ensuring all prerequisites are met. - -**POST_SCHEDULE**: Requirements for cleanup and restoration operations after plugin completion. - -**BOTH**: Requirements that must be fulfilled immediately when encountered, for urgent or time-sensitive operations. - -### Priority Framework - -The [`RequirementPriority`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementPriority.java) system enables intelligent requirement prioritization: - -**MANDATORY**: Requirements that must be fulfilled for plugin execution to proceed. - -**RECOMMENDED**: Requirements that significantly improve plugin performance but aren't essential. - -## Advanced Features - -### OR Requirement Modes - -The [`OrRequirementMode`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/OrRequirementMode.java) system provides sophisticated strategies for handling alternative requirements: - - **ANY_COMBINATION**: The total amount can be fulfilled by any combination of items in the OR requirement. For example, if 5 food items are needed, you could have 2 lobsters + 3 swordfish. - **SINGLE_TYPE**: Must fulfill the entire amount with exactly one type of item from the OR requirement. For example, if 5 food items are needed, you must have exactly 5 lobsters OR 5 swordfish, but not a combination. -### Requirement Selection - -The [`RequirementSelector`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSelector.java) provides sophisticated algorithms for choosing optimal requirement combinations when multiple options are available. - -### Collection Systems - -Pre-configured requirement collections eliminate repetitive specification of common requirement patterns: - -**Skill Outfits**: Complete equipment sets for various skills including woodcutting, mining, fishing, and combat activities. - -**Combat Configurations**: Comprehensive combat setups including weapons, armor, consumables, and utility items. - -**Utility Collections**: Common requirement patterns for activities like banking, transportation, and resource gathering. - -## Implementation Patterns - -### Basic Requirements Definition - -Plugin requirements are defined by extending [`PrePostScheduleRequirements`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/PrePostScheduleRequirements.java) and implementing the `initializeRequirements()` method. This method uses the registry to declare all requirements the plugin needs for successful operation. - -### Advanced Requirement Combinations - -Complex activities often require sophisticated requirement combinations that adapt to current game state. The logical requirement system enables these advanced patterns through OR operations, conditional requirements, and hierarchical planning structures. - -### Integration with Task Management - -Requirements integrate seamlessly with the task management system through automatic fulfillment during pre-schedule phases and cleanup during post-schedule phases. This integration ensures that plugins always start with their requirements satisfied and clean up properly when execution completes. - -## Performance and Optimization - -### Efficient Fulfillment - -The requirement fulfillment engine optimizes execution by: - -**Dependency Analysis**: Understanding relationships between requirements to optimize fulfillment order. - -**Resource Sharing**: Identifying opportunities to fulfill multiple requirements with single operations. - -**Path Optimization**: Coordinating location-based requirements to minimize unnecessary travel. - -### Caching and State Management - -The system employs intelligent caching strategies to avoid redundant validation and fulfillment operations, while maintaining accuracy through proper cache invalidation when game state changes. - -### Asynchronous Operations - -Where appropriate, the system utilizes asynchronous fulfillment patterns to improve responsiveness and allow for parallel requirement processing when operations don't interfere with each other. - -## Extension and Customization - -### Custom Requirement Types - -The system is designed for extensibility, enabling plugins to create custom requirement types for specialized needs while integrating with the existing fulfillment infrastructure. - -### Specialized Fulfillment Strategies - -Plugins can provide custom fulfillment strategies for complex or highly specialized requirements that cannot be handled by the standard fulfillment algorithms. - -### Integration Hooks - -Multiple integration points enable custom UI components, specialized validation logic, and plugin-specific optimization strategies while maintaining compatibility with the broader system. - -## Real-World Usage - -### Example Implementations - -Study the [`ExamplePrePostScheduleRequirements`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleRequirements.java) for comprehensive implementation patterns, or examine the GOTR integration in the [GOTR plugin directory](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/runecrafting/gotr/) for production usage. - -### Integration Patterns - -The requirements system integrates with various aspects of the plugin ecosystem, from simple equipment setups to complex multi-phase activities requiring sophisticated resource coordination. - -### Evolution and Enhancement - -The requirement system continues to evolve based on real-world usage patterns, with new requirement types and fulfillment strategies added to address emerging plugin needs while maintaining backward compatibility. - -The Requirements System represents a fundamental advancement in plugin automation, shifting from imperative resource management to declarative requirement specification. This transformation enables more reliable, maintainable, and user-friendly automation while providing the flexibility needed for diverse plugin requirements. diff --git a/docs/scheduler/tasks/task-management-system.md b/docs/scheduler/tasks/task-management-system.md deleted file mode 100644 index af262888588..00000000000 --- a/docs/scheduler/tasks/task-management-system.md +++ /dev/null @@ -1,218 +0,0 @@ -# Task Management System - -## Overview - -The Task Management System forms the infrastructure foundation of the Pre/Post Schedule Tasks architecture. Built around the [`AbstractPrePostScheduleTasks`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/AbstractPrePostScheduleTasks.java) base class, this system provides thread-safe execution services, lifecycle management, and comprehensive error handling for plugin preparation and cleanup operations. - -## Architectural Foundation - -### Design Principles - -The task management infrastructure operates on several core principles that ensure reliable and maintainable plugin automation: - -**Thread Safety**: All operations utilize dedicated executor services to prevent interference with main plugin execution and ensure consistent behavior across concurrent operations. - -**Lifecycle Management**: The system provides clear separation between preparation (pre-schedule) and cleanup (post-schedule) phases, with well-defined hooks for custom plugin-specific operations. - -**Resource Cleanup**: Implements AutoCloseable patterns with guaranteed resource cleanup, preventing memory leaks and ensuring proper shutdown procedures. - -**Error Resilience**: Comprehensive error handling with timeout support, cancellation capabilities, and graceful degradation when operations cannot complete successfully. - -### Infrastructure Components - -The task management system consists of several interconnected components: - -**Executor Services**: Separate thread pools for pre-schedule and post-schedule operations, ensuring isolated execution environments and preventing cross-contamination of operations. - -**Future Management**: CompletableFuture-based task coordination enabling asynchronous execution with proper timeout handling and cancellation support. - -**State Tracking**: The [`TaskExecutionState`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/state/TaskExecutionState.java) system provides comprehensive monitoring of task progress and status information for UI components. - -**Emergency Controls**: Built-in cancellation support through hotkey integration (Ctrl+C) allowing users to abort problematic operations safely. - -## Implementation Architecture - -### Base Class Structure - -The [`AbstractPrePostScheduleTasks`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/AbstractPrePostScheduleTasks.java) provides the foundation that all plugin task managers extend. This base class handles the complex infrastructure concerns while exposing simple extension points for plugin-specific logic. - -Key extension points include: - -- `getPrePostScheduleRequirements()`: Returns the requirements definition for the plugin -- `executeCustomPreScheduleTask()`: Plugin-specific preparation logic beyond requirement fulfillment -- `executeCustomPostScheduleTask()`: Plugin-specific cleanup logic beyond resource management -- `isScheduleMode()`: Detection logic for determining if the plugin is running under scheduler control - -### Execution Flow - -The task execution follows a predictable pattern that ensures consistent behavior across all plugins: - -**Pre-Schedule Phase**: The system first fulfills all requirements defined in the plugin's requirements specification, then executes any custom pre-schedule tasks. This ensures that all necessary resources, equipment, and game state conditions are met before the main plugin logic begins. - -**Main Execution**: After successful preparation, the system invokes the provided callback to start the main plugin execution. The plugin runs with the confidence that all prerequisites have been satisfied. - -**Post-Schedule Phase**: When the plugin completes or is stopped, the system executes custom cleanup tasks followed by requirement-based cleanup operations, ensuring proper resource management and state restoration. - -### Thread Management - -The system utilizes separate thread pools for different phases of execution to ensure proper isolation and prevent interference: - -**Pre-Schedule Executor**: Dedicated to requirement fulfillment and preparation tasks, with appropriate timeout handling to prevent hanging operations. - -**Post-Schedule Executor**: Focused on cleanup and resource management operations, designed to complete even when main execution has failed or been cancelled. - -**Main Thread Integration**: Careful coordination with the main plugin thread to ensure UI updates and game interactions occur safely. - -## Key Features - -### Cancellation Support - -The system provides multiple levels of cancellation support to handle different failure scenarios: - -**User-Initiated Cancellation**: Emergency hotkey support (Ctrl+C) allows users to abort operations that are taking too long or behaving unexpectedly. - -**Timeout-Based Cancellation**: Configurable timeouts prevent operations from hanging indefinitely, with graceful degradation when operations cannot complete within reasonable time limits. - -**Cascade Cancellation**: When one operation fails or is cancelled, the system intelligently determines whether to abort dependent operations or continue with available resources. - -### Error Handling Strategy - -The error handling approach balances robustness with user feedback: - -**Graceful Degradation**: Optional requirements that cannot be fulfilled don't prevent plugin execution, allowing partial preparation when full requirements cannot be met. - -**Detailed Logging**: Comprehensive logging provides developers with detailed information about failure causes while maintaining appropriate log levels for different scenarios. - -**User Communication**: Integration with the UI system ensures users receive clear feedback about preparation status, failures, and recovery options. - -### State Management - -The [`TaskExecutionState`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/state/TaskExecutionState.java) system tracks execution progress and provides information for UI components: - -**Progress Tracking**: Real-time monitoring of requirement fulfillment progress, including individual requirement status and overall completion percentage. - -**Status Information**: Current operation details, time elapsed, estimated completion time, and success/failure indicators. - -**UI Integration**: Seamless integration with overlay components and status panels for rich user feedback. - -**State Reset Capabilities**: The state tracking system provides intelligent reset functionality for handling interruptions and preparing for subsequent task executions, ensuring clean state transitions. - -### Game State Awareness and Initialization - -The task management system requires careful coordination with game state to ensure proper initialization timing: - -**Why Initialization Timing Matters**: Many requirements need access to live game information such as current player location for shop proximity calculations, world position data for location requirements, equipment state for validation, and banking locations for optimal routing strategies. - -**Proper Initialization Pattern**: The system should initialize when `GameState.LOGGED_IN` is reached to ensure all game information is available: - -```java -@Subscribe -public void onGameStateChanged(GameStateChanged event) { - if (event.getGameState() == GameState.LOGGED_IN) { - // Initialize when game information is available - getPrePostScheduleTasks(); - - // Auto-start if in scheduler mode - if (prePostScheduleTasks != null && - prePostScheduleTasks.isScheduleMode() && - !prePostScheduleTasks.isPreTaskComplete()) { - runPreScheduleTasks(); - } - } else if (event.getGameState() == GameState.LOGIN_SCREEN) { - // Reset on logout for fresh initialization - prePostScheduleRequirements = null; - prePostScheduleTasks = null; - } -} -``` - -**Initialization Validation**: The system validates successful initialization before allowing task execution: - -```java -@Override -public AbstractPrePostScheduleTasks getPrePostScheduleTasks() { - if (prePostScheduleRequirements == null || prePostScheduleTasks == null) { - if(Microbot.getClient().getGameState() != GameState.LOGGED_IN) { - log.debug("My Plugin - Cannot return pre/post schedule tasks - not logged in"); - return null; // Return null if not logged in - } - this.prePostScheduleRequirements = new MyPluginRequirements(config); - this.prePostScheduleTasks = new MyPluginTasks(this, keyManager, prePostScheduleRequirements); - if (prePostScheduleRequirements.isInitialized()){log.info("My Plugin PrePostScheduleRequirements initialized:\n{}", prePostScheduleRequirements.getDetailedDisplay());} - } - - // Critical: Validate initialization success - if (!prePostScheduleRequirements.isInitialized()) { - log.error("Failed to initialize requirements system!"); - this.prePostScheduleRequirements = null; - this.prePostScheduleTasks = null; - return null; // Prevent task execution with failed requirements - } - - return this.prePostScheduleTasks; -} -``` - -**Wait-for-Initialization Pattern**: Before executing tasks, plugins should verify that requirements are properly initialized, as some requirements need game data that's only available after login. - -## Integration Patterns - -### Plugin Implementation - -Plugins integrate with the task management system by extending the base class and implementing the required abstract methods. The implementation focuses on plugin-specific logic while delegating infrastructure concerns to the base class. - -Examine the [`ExamplePrePostScheduleTasks`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleTasks.java) for a comprehensive implementation example, or study the GOTR integration in the [GOTR plugin directory](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/runecrafting/gotr/) for production usage patterns. - -### Scheduler Integration - -The task management system integrates seamlessly with the broader scheduler architecture through the [`SchedulablePlugin`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/api/SchedulablePlugin.java) interface, providing automatic task execution when plugins are managed by the scheduler. - -### Resource Coordination - -The system coordinates with the requirements framework to ensure efficient resource utilization, preventing conflicts between concurrent operations and optimizing the order of requirement fulfillment. - -## Performance Considerations - -### Resource Efficiency - -The task management system is designed for efficient resource utilization: - -**Lazy Initialization**: Executor services and resources are created only when needed, reducing memory footprint for plugins that don't use task functionality. - -**Resource Pooling**: Thread pools are managed efficiently to prevent resource exhaustion while maintaining responsive behavior. - -**Cleanup Guarantees**: AutoCloseable implementation ensures resources are properly released even when exceptions occur. - -### Scalability - -The architecture supports multiple concurrent task managers without interference, enabling complex plugin ecosystems where multiple automation systems operate simultaneously. - -## Extension and Customization - -### Custom Task Types - -While the base system handles standard preparation and cleanup operations, plugins can extend the functionality through custom task implementations that integrate with the existing infrastructure. - -### Integration Hooks - -The system provides multiple integration points for advanced customization, including custom requirement types, specialized fulfillment strategies, and plugin-specific UI components. - -### Future Enhancements - -The architecture is designed for extensibility, with clear separation of concerns enabling future enhancements such as task prioritization, dependency management, and performance optimization without breaking existing implementations. - -## Implementation Guidelines - -### Development Approach - -When implementing task managers, focus on clear separation between requirement definition and custom logic. Use the requirements system for standard operations like equipment setup and inventory management, reserving custom tasks for plugin-specific operations that cannot be expressed as requirements. - -### Error Handling - -Implement robust error handling in custom task methods, providing clear error messages and appropriate recovery strategies. The base infrastructure handles many error scenarios, but plugin-specific operations require careful consideration of failure modes. - -### Testing and Validation - -The task management system provides multiple hooks for testing and validation, enabling comprehensive testing of both requirement fulfillment and custom task operations in isolation. - -The Task Management System provides the reliable foundation necessary for sophisticated plugin automation while maintaining the flexibility needed for diverse plugin requirements. By handling the complex infrastructure concerns, it enables plugin developers to focus on their core functionality while benefiting from standardized, robust preparation and cleanup procedures. diff --git a/docs/scheduler/user-guide.md b/docs/scheduler/user-guide.md deleted file mode 100644 index eceed835689..00000000000 --- a/docs/scheduler/user-guide.md +++ /dev/null @@ -1,469 +0,0 @@ -# Scheduler Plugin User Guide - -## Introduction - -The Plugin Scheduler is a sophisticated system that allows you to automatically schedule and manage plugins based on various conditions. This guide focuses on how to use the scheduler's user interface to set up plugin scheduling plans, including defining start and stop conditions, understanding plugin priorities, and configuring plugin behavior. - -## The Scheduler Interface - -The scheduler interface is divided into several main components: - -1. **Schedule Table**: Displays all scheduled plugins with their current status, configurations, and next run times -2. **Schedule Form**: Allows you to add new scheduled plugins -3. **Properties Panel**: Lets you modify settings for existing scheduled plugins -4. **Start Conditions Panel**: Configures when a plugin should start running -5. **Stop Conditions Panel**: Configures when a plugin should stop running - -![Scheduler Window Overview](../img/scheduler-overview.png) - -## Adding a New Plugin Schedule - -To create a new plugin schedule: - -1. Navigate to the "New Schedule" tab in the scheduler window -2. Select a plugin from the dropdown menu -3. Choose a scheduling method from the "Time Condition" dropdown: - - **Run Default**: Makes the plugin a default option (priority 0) - - **Run at Specific Time**: Runs the plugin once at a specific date/time - - **Run at Interval**: Runs the plugin at regular intervals - - **Run in Time Window**: Runs the plugin during specific hours of the day - - **Run on Day of Week**: Runs the plugin on specific days of the week -4. Configure the time condition options based on your selection -5. Set additional options: - - **Allow Random Scheduling**: Lets the scheduler randomly select this plugin when multiple are due - - **Requires time-based stop condition**: Requires you to add a time condition to stop the plugin - - **Allow continue**: Allows the plugin to resume automatically after being interrupted, maintaining progress - - **Priority**: Sets the plugin's priority level (0 = default plugin) -6. Click "Add Schedule" to create the schedule - -## Understanding Default vs. Non-Default Plugins - -### Default Plugins - -Default plugins serve as your "background" or "fallback" activities that run when nothing else is scheduled. Think of them as what your character does during downtime. - -**Characteristics:** -- Always have **priority 0** (automatically enforced by the scheduler) -- Run only when no other plugins are scheduled or eligible to run -- Typically use a very short interval check (often 1 second) to quickly start when needed -- Are displayed with a teal background color and ⭐ star icon in the schedule table - -**To mark a plugin as default:** -- Check the "Set as default plugin" checkbox in the schedule form, or -- Set its priority to 0 (the system will recognize it as a default plugin) - -**Example use cases:** -- AFK training activities (e.g., NMZ, splashing) -- Simple repetitive tasks that should run when nothing more important is active -- "Maintenance" plugins that handle routine tasks during downtime - -### Non-Default Plugins - -Non-default plugins are your primary scheduled activities that run according to specific conditions. - -**Characteristics:** -- Have a priority of 1 or higher (lower number = higher priority) -- Run according to their specific start conditions -- Will always take precedence over default plugins when their conditions are met -- Can interrupt default plugins (unless the default plugin has "Allow Continue" enabled) - -**Example use cases:** -- Your main skilling activities (fishing, mining, etc.) -- Time-specific activities (e.g., farm runs, daily challenges) -- Condition-based activities (e.g., "Bank items when inventory is full") - -## Defining Start and Stop Conditions - -The power of the Plugin Scheduler comes from its ability to create sophisticated start and stop conditions for your plugins. This allows for highly automated and intelligent plugin scheduling. - -For detailed instructions on creating and configuring various condition types (Time, Skill, Resource, Location), see the [Defining Conditions guide](defining-conditions.md). - -### Start Conditions - -Start conditions determine when a plugin is eligible to begin running. A plugin will start when all its start conditions are met (or any, depending on your logical configuration). - -**Basic Configuration:** - -1. Select your plugin in the schedule table -2. Navigate to the "Start Conditions" tab -3. Click "Add New Condition" -4. Select a condition category (Time, Skill, Resource, Location, etc.) -5. Choose a specific condition type within that category -6. Configure the parameters for your chosen condition -7. Click "Add" to add the condition to your logical structure - -**Example Start Conditions:** - -- **Time-based:** "Start at 8:00 PM every day" -- **Resource-based:** "Start when inventory is empty" -- **Location-based:** "Start when player is at the Grand Exchange" -- **Skill-based:** "Start when Mining reaches level 70" - -### Stop Conditions - -Stop conditions determine when a running plugin should stop. They're evaluated continuously while the plugin is running. - -**Basic Configuration:** - -1. Select your plugin in the schedule table -2. Navigate to the "Stop Conditions" tab -3. Click "Add New Condition" -4. Select a condition category -5. Choose a specific condition type -6. Configure the parameters -7. Click "Add" to add the condition - -**Example Stop Conditions:** - -- **Time-based:** "Stop after 2 hours of running" -- **Resource-based:** "Stop when inventory is full" or "Stop after collecting 1000 items" -- **Location-based:** "Stop when reaching Lumbridge" -- **Skill-based:** "Stop when gaining 50,000 XP in Fishing" - -### Building Complex Logical Structures - -You can combine multiple conditions using logical operators to create sophisticated rules: - -1. **AND Logic (ALL conditions must be met):** - - Click on the root condition node in the tree view - - Click "Convert to AND" - - All child conditions must be satisfied for the overall condition to be met - -2. **OR Logic (ANY condition can be met):** - - Click on the root condition node - - Click "Convert to OR" (default) - - Any child condition being satisfied will satisfy the overall condition - -3. **Nesting AND/OR Groups:** - - Select multiple conditions in the tree - - Click "Group AND" or "Group OR" - - This creates a sub-group with its own logical relationship - -4. **NOT Logic:** - - Select a condition - - Click "Negate" - - The condition's result will be inverted - -**Example Complex Condition:** -"Start the plugin when (it's between 8 PM and midnight) AND (player is in the Mining Guild OR at Al Kharid mines) AND (inventory is empty)" - -## The "Allow Continue" Setting Explained - -The "Allow Continue" option determines what happens when a plugin is interrupted by a higher-priority plugin and later has a chance to resume: - -- **When enabled**: The interrupted plugin will continue running immediately after the higher-priority plugin finishes, without resetting its start and stop conditions. This preserves all progress made toward stop conditions and doesn't require start conditions to be re-evaluated. -- **When disabled**: The interrupted plugin will not automatically resume after being interrupted. It will need to be triggered again from scratch, with all start and stop conditions reset. - -**How "Allow Continue" Works in Detail**: - -1. **State Preservation**: - - With "Allow Continue" enabled, a plugin that gets interrupted preserves its full state, including: - - Progress toward any stop conditions (e.g., items collected, time elapsed) - - Internal state variables and script position - - UI components and overlays - - This means the plugin can pick up exactly where it left off, which is ideal for complex, stateful plugins - -2. **Resumption Logic**: - - When a higher-priority plugin finishes: - - If the interrupted plugin had "Allow Continue" enabled, it immediately resumes - - If the interrupted plugin had "Allow Continue" disabled, it goes back into the pool of eligible plugins - - For default plugins with "Allow Continue" disabled, they must compete again with other default plugins based on run count - -3. **Default Plugin Considerations**: - - "Allow Continue" is especially important for default plugins (priority 0) that you want to resume predictably - - Without "Allow Continue", your default plugin might not be the one selected after an interruption - - This can lead to unpredictable switching between default plugins, which might not be desirable - -For a comprehensive explanation of this feature, see the [Allow Continue Feature Guide](allow-continue-feature.md). - -**Example Scenarios:** - -1. **Allow Continue = ON** - - You're running a Woodcutting plugin (priority 0) - - A Banking plugin (priority 2) interrupts it - - After banking is done, the Woodcutting plugin automatically resumes with all progress intact - - Perfect for plugins that should pick up where they left off after being interrupted - -2. **Allow Continue = OFF** - - You're running a Fishing plugin (priority 0) - - A Cooking plugin (priority 2) interrupts it - - After cooking is done, the Fishing plugin does not automatically resume - - The scheduler will select among eligible default plugins based on run count - - Best for tasks that should fully restart from the beginning when interrupted - -## What Happens When a Plugin Has No Stop Conditions - -When a schedulable plugin has no plugin-based stop conditions: - -1. The plugin will run indefinitely until: - - You manually stop it through the UI - - A higher-priority plugin interrupts it (after which it may resume if "Allow Continue" is enabled) - - The plugin's internal logic determines it should stop - -2. The scheduler UI will display a warning icon next to plugins without stop conditions, reminding you that they may run indefinitely. - -3. If "Requires time-based stop condition" is checked in the settings, the scheduler will display a prompt when you try to add a schedule without stop conditions. - -**Best Practices:** - -- **Always include at least one stop condition** for every plugin schedule -- **Add a time-based safety condition** (e.g., "stop after 2 hours") even if you have other stop conditions -- **Use LockCondition for critical operations** to prevent interruption during important tasks -- **Combine multiple stop conditions** for more sophisticated control, such as: - - "Stop after collecting 1000 items OR after 3 hours" (whichever comes first) - - "Stop when inventory is full AND we're in a safe area" - -**Safety Mechanism:** -The scheduler includes a built-in watchdog that monitors plugins for signs they might be stuck. If a plugin runs for an extended period without showing progress, the system can detect this and provide warnings. - -## Working with the Schedule Table - -The schedule table displays all configured plugins and their current status: - -- **Plugin**: The name of the plugin (⭐ indicates default plugins, â–ķ indicates currently running) -- **Schedule**: When the plugin is scheduled to run -- **Next Run**: When the plugin will run next -- **Start Conditions**: Summary of conditions that trigger the plugin to start -- **Stop Conditions**: Summary of conditions that will stop the plugin -- **Priority**: The plugin's priority level (0 = default) -- **Enabled**: Whether the plugin is enabled in the scheduler -- **Runs**: How many times the plugin has been executed - -### Row Colors and Indicators - -- **Purple background**: Currently running plugin -- **Amber background**: Next plugin scheduled to run -- **Teal background**: Default plugin -- **Green indicators**: Conditions are satisfied -- **Red indicators**: Conditions are not satisfied -- **Strikethrough text**: Plugin is disabled - -## Managing Existing Schedules - -To modify an existing schedule: - -1. Click on it in the schedule table -2. Use the "Plugin Properties" tab to adjust: - - Enabled status - - Default plugin setting - - Priority - - Random scheduling - - Time-based stop condition requirement - - Allow continue setting -3. Use the "Start Conditions" and "Stop Conditions" tabs to modify conditions -4. Click "Save Changes" to apply your modifications - -## Tips and Best Practices - -1. **Set appropriate priorities**: - - Higher priority plugins (higher numbers) run before lower priority ones - - Use priorities to establish a clear hierarchy of tasks - -2. **Use time windows effectively**: - - Consider using time windows to run different plugins at different times of day - - Time windows can help avoid detection by making your play patterns more natural - -3. **Combine condition types**: - - Mix time, resource, and location conditions for sophisticated automation - - Example: Run a mining plugin only when inventory is empty AND at certain times of day - -4. **Plan for interruptions**: - - Enable "Allow Continue" for tasks that should automatically resume after being interrupted - - Create robust stop conditions that handle unexpected situations - -5. **Monitor run statistics**: - - Use the statistics in the Properties panel to track plugin performance - - Look for unusually short runs that might indicate problems - -## Common Scenarios and Real-World Examples - -Here are some practical examples of how to set up the scheduler for common gameplay situations. Use these as templates for your own scheduling plans. - -### Scenario 1: AFK Training with Default Plugin - -**Goal**: Train a skill passively when you're not doing anything else - -**Solution**: -1. Add your AFK training plugin (e.g., NMZ, splashing) with "Run Default" time condition -2. Ensure "Set as default plugin" is checked (priority will be set to 0) -3. Add a safety stop condition like "Stop after 5 hours" to prevent running indefinitely -4. Consider adding "Stop when XP gained reaches 100,000" as an additional stop condition - -**Example Setup:** -- Plugin: AFKCombatPlugin -- Priority: 0 (Default) -- Start Conditions: None (runs as default) -- Stop Conditions: OR(TimeCondition(5 hours), SkillXPCondition(100,000 XP)) - -### Scenario 2: Resource Gathering and Processing Cycle - -**Goal**: Alternate between gathering resources and processing them (e.g., fishing and cooking) - -**Solution**: -1. Create a schedule for fishing plugin: - - Priority: 2 - - Start Condition: Inventory is empty OR time since last run > 15 minutes - - Stop Condition: Inventory is full (28 items) - -2. Create a schedule for cooking plugin: - - Priority: 1 (higher priority than fishing) - - Start Condition: Inventory has raw fish - - Stop Condition: No raw fish in inventory - -This creates a natural cycle: When inventory fills with fish, the cooking plugin (higher priority) takes over. When cooking is done, the fishing plugin starts again. - -### Scenario 3: Time-Limited Daily Activities - -**Goal**: Run specific plugins at certain times of day, like farm runs every few hours - -**Solution**: -1. Create a schedule with "Run in Time Window" or "Run at Interval" - - For farm runs: "Run at Interval" of 80 minutes - - For daily tasks: "Run in Time Window" (e.g., 6:00 PM to 7:00 PM) - -2. Add specific stop conditions: - - Time-based: "Stop after 15 minutes" (prevents running too long) - - Completion-based: "Stop when all farming patches are harvested" - -3. Enable "Allow Continue" if this activity should be able to resume after interruptions - -**Example Setup:** -- Plugin: FarmRunPlugin -- Priority: 1 -- Start Conditions: TimeCondition(Interval: 80 minutes) -- Stop Conditions: OR(TimeCondition(15 minutes), CompletionCondition(all patches harvested)) - -### Scenario 4: Location-Based Task Switching - -**Goal**: Run different plugins based on player location - -**Solution**: -1. Create a mining plugin: - - Priority: 2 - - Start Condition: In mining area AND inventory not full - - Stop Condition: Inventory full OR not in mining area - -2. Create a banking plugin: - - Priority: 1 (higher priority) - - Start Condition: Inventory full AND near bank - - Stop Condition: Inventory empty - -This creates a smart workflow where your character will mine until inventory is full, then prioritize banking when near a bank. - -### Scenario 5: Skill Goal Achievement - -**Goal**: Train a skill until reaching a specific level, then switch to another activity - -**Solution**: -1. Create a woodcutting plugin: - - Priority: 2 - - Start Condition: Woodcutting level < 70 - - Stop Condition: Woodcutting level reaches 70 - -2. Create a fishing plugin: - - Priority: 2 - - Start Condition: Woodcutting level >= 70 AND Fishing level < 70 - - Stop Condition: Fishing level reaches 70 - -This creates a progression plan where the scheduler automatically moves from one skill goal to the next. - -### Scenario 6: Setting Up a Cycle of Default Plugins - -**Goal**: Create a cycle where multiple default plugins (priority 0) take turns running, ensuring each runs once before cycling through again. - -**Solution**: - -1. Set up multiple plugins with priority 0 (default plugins): - - Plugin 1: Priority 0, with user-defined stop condition - - Plugin 2: Priority 0, with user-defined stop condition - - Plugin 3: Priority 0, with user-defined stop condition - -2. Ensure each plugin has a proper user stop condition: - - Time-based: "Run for 30-35 minutes" - - Skill XP-based: "Stop after gaining X experience" - - Resource-based: "Stop after collecting Y items" - -3. Enable "Allow Random Scheduling" for all plugins. - -When all plugins are set to priority 0 and multiple ones are due to run at the same time, the scheduler will automatically select the one with the **lowest run count**. This ensures each plugin gets a fair chance to run, and the system will cycle through all plugins before repeating. - -**Example Setup - Cycling through 3 Default Plugins:** - -- Plugin 1: Woodcutting - - Priority: 0 (Default) - - User Start Condition: default (interval condition 1sec) or if you want to delay any other time condition - - User Stop Condition: TimeCondition(30-35 minutes) - - Allow Random Scheduling: Enabled - -- Plugin 2: Fishing - - Priority: 0 (Default) - - User Start Condition: default (interval condition 1sec) or if you want to delay any other time condition - - User Stop Condition: TimeCondition(30-35 minutes) - - Allow Random Scheduling: Enabled - -- Plugin 3: Mining - - Priority: 0 (Default) - - User Start Condition: default (interval condition 1sec) or if you want to delay any other time condition - - User Stop Condition: TimeCondition(30-35 minutes) - - Allow Random Scheduling: Enabled - -**Higher Priority Tasks:** - -- Plugin 4: Birdhouse Run - - Priority: 1 - - User Start Condition: TimeCondition(Interval: 50 minutes) - - User Stop Condition: CompletionCondition(all birdhouses checked) - -- Plugin 5: Herb Run - - Priority: 1 - - Start Condition: TimeCondition(Interval: 80 minutes) - - Stop Condition: CompletionCondition(all herbs harvested) - -In this setup: - -- Plugins 4 and 5 will always interrupt and take precedence over default plugins when their conditions are met -- When no higher-priority plugin is running, the scheduler will cycle through Plugins 1, 2, and 3, always selecting the one with the lowest run count -- Each default plugin will run once before any of them runs a second time - -### Scenario 7: Advanced Priority-Based Task Chain with Herb Running - -**Goal**: Set up a hierarchy of tasks with herb running as a priority task that interrupts default activities. - -**Solution**: - -1. Create a Herb Runner plugin schedule: - - Priority: 10 (high priority) - - Start Condition: TimeCondition(Interval: 80 minutes) - - Stop Condition: None (uses reportFinished to signal completion) - - Allow Continue: Not needed (runs completely each time) - -2. Create a Tree Runner plugin schedule: - - Priority: 5 (middle priority) - - Start Condition: TimeCondition(Interval: 8 hours) - - Stop Condition: None (uses reportFinished to signal completion) - - Allow Continue: Not needed (runs completely each time) - -3. Create a default NMZ plugin schedule: - - Priority: 0 (default plugin) - - Start Condition: TimeWindowCondition(10PM-8AM or your preferred hours) - - Stop Condition: Optional TimeCondition(run for maximum 6 hours) - - Allow Continue: Enabled (important for preserving state when interrupted) - -This setup creates a priority-based chain where: - -- The Herb Runner has highest priority and will interrupt any other active plugin every 80 minutes -- The Tree Runner has middle priority and will interrupt only the NMZ plugin (but not Herb Runner) -- The NMZ plugin serves as the default activity that runs during the specified time window -- When the Herb Runner or Tree Runner completes its task, NMZ will automatically resume with its state preserved because "Allow Continue" is enabled - -## The "Allow Continue" Setting: Deep Dive - -The "Allow Continue" option is particularly important when using default plugins in a cycle or when higher-priority plugins might interrupt your default activities. Let's look more deeply at this feature: - -- **Behavior with Default Plugins:** - - Default plugins are meant to run in the background with no specific start conditions. - - If a default plugin is interrupted by a higher-priority plugin and "Allow Continue" is ON, it will resume immediately after the higher-priority plugin finishes. - - This is useful for preserving the state of long-running default activities. - -- **Behavior with Non-Default Plugins:** - - For non-default plugins, "Allow Continue" works similarly but consider diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java index bf15d0c83d8..25132fbb46a 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java @@ -32,13 +32,9 @@ import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginInstantiationException; import net.runelite.client.plugins.PluginManager; -import net.runelite.client.plugins.loottracker.LootTrackerItem; -import net.runelite.client.plugins.loottracker.LootTrackerPlugin; import net.runelite.client.plugins.loottracker.LootTrackerRecord; import net.runelite.client.plugins.microbot.configs.SpecialAttackConfigs; import net.runelite.client.plugins.microbot.pouch.PouchScript; -import net.runelite.client.plugins.microbot.util.cache.Rs2VarPlayerCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2VarbitCache; import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; import net.runelite.client.plugins.microbot.util.item.Rs2ItemManager; import net.runelite.client.plugins.microbot.util.menu.NewMenuEntry; @@ -192,10 +188,6 @@ public class Microbot { @Getter private static Rs2ItemManager rs2ItemManager = new Rs2ItemManager(); - @Setter - @Getter - public static boolean isRs2CacheEnabled = false; - @Inject @Getter private static Rs2PlayerCache rs2PlayerCache; @@ -223,16 +215,10 @@ public static boolean isDebug() { } public static int getVarbitValue(@Varbit int varbit) { - if (isRs2CacheEnabled()) { - return Rs2VarbitCache.getVarbitValue(varbit); - } return rs2PlayerCache.getVarbitValue(varbit); } public static int getVarbitPlayerValue(@Varp int varpId) { - if (isRs2CacheEnabled()) { - return Rs2VarPlayerCache.getVarPlayerValue(varpId); - } return rs2PlayerCache.getVarpValue(varpId); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotConfig.java index 28cdf25561a..fba8721ef0c 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotConfig.java @@ -124,18 +124,6 @@ default boolean enableMenuEntryLogging() { return false; } - String keyEnableCache = "enableRs2Cache"; - @ConfigItem( - keyName = keyEnableCache, - name = "Enable Microbot Cache", - description = "This will cache ingame entities (npcs, objects,...) to improve performance", - position = 0, - section = cacheSection - ) - default boolean isRs2CacheEnabled() { - return false; - } - @AllArgsConstructor enum GameChatLogLevel { ERROR("Error", Level.ERROR), diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotOverlay.java index a124c51c711..8a3be400409 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotOverlay.java @@ -5,22 +5,11 @@ import net.runelite.api.Point; import net.runelite.api.coords.LocalPoint; import net.runelite.api.coords.WorldPoint; -import net.runelite.api.gameval.InterfaceID; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.cache.Rs2CacheManager; -import net.runelite.client.plugins.microbot.util.cache.Rs2ObjectCache; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2CacheLoggingUtils; -import net.runelite.client.plugins.microbot.util.grandexchange.Rs2GrandExchange; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.tile.Rs2Tile; -import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; -import net.runelite.client.ui.FontManager; import net.runelite.client.ui.overlay.OverlayPanel; import net.runelite.client.ui.overlay.OverlayPosition; import net.runelite.client.ui.overlay.OverlayUtil; -import net.runelite.client.ui.overlay.components.ButtonComponent; -import net.runelite.client.ui.overlay.components.LineComponent; -import net.runelite.client.ui.overlay.components.TitleComponent; import javax.annotation.Nullable; import javax.inject.Inject; @@ -30,8 +19,6 @@ public class MicrobotOverlay extends OverlayPanel { MicrobotPlugin plugin; MicrobotConfig config; - public final ButtonComponent cacheButton; - public final ButtonComponent logCacheButton; @Inject MicrobotOverlay(MicrobotPlugin plugin, MicrobotConfig config) { @@ -39,42 +26,6 @@ public class MicrobotOverlay extends OverlayPanel { setPosition(OverlayPosition.TOP_RIGHT); this.plugin = plugin; this.config = config; - - // Initialize cache management button - cacheButton = new ButtonComponent("Clear Caches"); - cacheButton.setPreferredSize(new Dimension(120, 25)); - cacheButton.setParentOverlay(this); - cacheButton.setFont(FontManager.getRunescapeBoldFont()); - cacheButton.setOnClick(() -> { - try { - Rs2CacheManager.invalidateAllCaches(false); - Rs2CacheManager.triggerSceneScansForAllCaches(); // Repopulate caches immediately - //Microbot.getClientThread().runOnClientThreadOptional( - // ()-> {Microbot.openPopUp("Cache Manager", String.format("Cleared Cache:

%s", "All caches have been invalidated!")); - // return true;} - //); // causes Exception java.lang.IllegalStateException: component does not exist - } catch (Exception e) { - Microbot.log("Cache Manager: All caches have been invalidated and repopulated!"); - } - }); - - // Initialize cache logging button - logCacheButton = new ButtonComponent("Log to Files"); - logCacheButton.setPreferredSize(new Dimension(60, 25)); - logCacheButton.setParentOverlay(this); - logCacheButton.setFont(FontManager.getRunescapeBoldFont()); - logCacheButton.setOnClick(() -> { - // Run on separate thread to avoid blocking UI - new Thread(() -> { - try { - Rs2CacheLoggingUtils.logAllCachesToFiles(); - //Microbot.openPopUp("Cache Logger", String.format("Cache Logging:

%s", "All cache states dumped to files successfully!"));// causes Exception - } catch (Exception e) { - // Fallback to console if popup fails - Microbot.log(" \"All cache states dumped to files successfully!\"" + e.getMessage()); - } - }).start(); - }); } @Override @@ -84,247 +35,10 @@ public Dimension render(Graphics2D graphics) { for (Map.Entry dangerousTile : Rs2Tile.getDangerousGraphicsObjectTiles().entrySet()) { drawTile(graphics, dangerousTile.getKey(), Color.RED, dangerousTile.getValue().toString()); } - - // Show cache information if enabled in config and not hidden by overlapping widgets - boolean shouldShowCache = config.showCacheInfo(); - - // Always check for actual widget overlap with our render position - if (shouldShowCache) { - Rectangle estimatedCacheBounds = estimateCacheInfoBounds(); - if (estimatedCacheBounds != null) { - // Convert estimatedCacheBounds (panel/canvas) to canvas coordinates if needed - // Pass as canvas coordinates to plugin.hasWidgetOverlapWithBounds - shouldShowCache = !plugin.hasWidgetOverlapWithBounds(estimatedCacheBounds); - } - shouldShowCache = shouldShowCache && Microbot.isLoggedIn() && !Rs2Bank.isOpen() && !Rs2Widget.isWidgetVisible(InterfaceID.Worldmap.CLOSE) && !Rs2GrandExchange.isOpen(); - } - - if (shouldShowCache) { - panelComponent.getChildren().add(TitleComponent.builder() - .text("Cache Manager") - .color(Color.CYAN) - .build()); - - panelComponent.getChildren().add(LineComponent.builder().build()); - - panelComponent.getChildren().add(cacheButton); - panelComponent.getChildren().add(logCacheButton); - - // Only hook mouse listeners if visible - cacheButton.hookMouseListener(); - logCacheButton.hookMouseListener(); - // Render cache statistics tooltip when hovering over the clear cache button - if (cacheButton.isHovered()) { - renderCacheStatsTooltip(graphics); - } - - // Render logging info tooltip when hovering over the log cache button - if (logCacheButton.isHovered()) { - renderLogCacheTooltip(graphics); - } - } else { - // Unhook mouse listeners if not visible - cacheButton.unhookMouseListener(); - logCacheButton.unhookMouseListener(); - } - return super.render(graphics); } - /** - * Estimates the bounds where cache information would be rendered - * This helps determine potential overlap before actually rendering - */ - private Rectangle estimateCacheInfoBounds() { - try { - // Get the current overlay position and size - Rectangle currentBounds = this.getBounds(); - - // If we don't have bounds yet, use preferred location and calculate estimated size - if (currentBounds == null || (currentBounds.width == 0 && currentBounds.height == 0)) { - java.awt.Point preferredLocation = this.getPreferredLocation(); - if (preferredLocation == null) { - // Use default DYNAMIC position (top-left area) - preferredLocation = new java.awt.Point(10, 10); - } - - // Estimate cache info panel size based on typical components - // Title: ~150x20, Button: ~120x25, spacing: ~5-10px - int estimatedWidth = 200; // panelComponent preferred width - int estimatedHeight = 85; // Title + separator + 2 buttons + padding - - return new Rectangle(preferredLocation.x, preferredLocation.y, - estimatedWidth, estimatedHeight); - } - - // If we have existing bounds, estimate where cache info would appear within them - // Cache info appears after dangerous tiles, so add some offset - int cacheStartY = currentBounds.y + 10; // Some offset for dangerous tiles rendering - int cacheHeight = 60; // Estimated height for cache components - - return new Rectangle(currentBounds.x, cacheStartY, - currentBounds.width, cacheHeight); - - } catch (Exception e) { - // Fallback: return a small default area - return new Rectangle(10, 10, 200, 60); - } - } - - /** - * Renders cache statistics as a tooltip box when hovering over the button - */ - private void renderCacheStatsTooltip(Graphics2D graphics) { - try { - // Set smaller font for tooltip - Font originalFont = graphics.getFont(); - Font tooltipFont = new Font(Font.SANS_SERIF, Font.PLAIN, 10); - graphics.setFont(tooltipFont); - - // Get cache statistics - String cacheStats = Rs2CacheManager.getAllCacheStatisticsString(); - - // Get object type statistics - String objectTypeStats = ""; - try { - objectTypeStats = Rs2ObjectCache.getObjectTypeStatistics(); - } catch (Exception e) { - objectTypeStats = "Object stats unavailable"; - } - - // Combine cache stats with object type stats - String[] cacheLines = cacheStats.split("\n"); - String[] allLines = new String[cacheLines.length + 1]; - System.arraycopy(cacheLines, 0, allLines, 0, cacheLines.length); - allLines[cacheLines.length] = objectTypeStats; - - // Calculate tooltip position (next to the button) - Rectangle buttonBounds = cacheButton.getBounds(); - int tooltipX = buttonBounds.x + buttonBounds.width + 10; - int tooltipY = buttonBounds.y; - - // Calculate tooltip dimensions - FontMetrics metrics = graphics.getFontMetrics(); - int maxWidth = 0; - int totalHeight = 0; - - for (String line : allLines) { - if (!line.trim().isEmpty()) { - int lineWidth = metrics.stringWidth(line.trim()); - maxWidth = Math.max(maxWidth, lineWidth); - totalHeight += metrics.getHeight(); - } - } - - int padding = 6; - int backgroundWidth = maxWidth + (padding * 2); - int backgroundHeight = totalHeight + (padding * 2); - - // Draw semi-transparent background - Color backgroundColor = new Color(0, 0, 0, 180); - graphics.setColor(backgroundColor); - graphics.fillRect(tooltipX, tooltipY, backgroundWidth, backgroundHeight); - - // Draw border - graphics.setColor(Color.CYAN); - graphics.drawRect(tooltipX, tooltipY, backgroundWidth, backgroundHeight); - - // Render cache statistics text - graphics.setColor(Color.WHITE); - int lineY = tooltipY + metrics.getAscent() + padding; - - for (String line : allLines) { - if (!line.trim().isEmpty()) { - graphics.drawString(line.trim(), tooltipX + padding, lineY); - lineY += metrics.getHeight(); - } - } - - // Restore original font - graphics.setFont(originalFont); - - } catch (Exception e) { - // Silent fail for overlay rendering - } - } - - /** - * Renders logging information as a tooltip box when hovering over the log cache button - */ - private void renderLogCacheTooltip(Graphics2D graphics) { - try { - // Set smaller font for tooltip - Font originalFont = graphics.getFont(); - Font tooltipFont = new Font(Font.SANS_SERIF, Font.PLAIN, 10); - graphics.setFont(tooltipFont); - - // Define tooltip content - String[] lines = { - "Log Cache States to Files", - "", - "â€Ē Saves all cache data to log files", - "â€Ē Files saved in ~/.runelite/microbot-plugins/cache/", - "â€Ē Includes: NPCs, Objects, Ground Items,", - " Skills, Varbits, VarPlayers, Quests", - "â€Ē No console output (file only)", - "â€Ē Useful for analysis and debugging" - }; - - // Calculate tooltip position (next to the button) - Rectangle buttonBounds = logCacheButton.getBounds(); - int tooltipX = buttonBounds.x + buttonBounds.width + 10; - int tooltipY = buttonBounds.y; - - // Calculate tooltip dimensions - FontMetrics metrics = graphics.getFontMetrics(); - int maxWidth = 0; - int totalHeight = 0; - - for (String line : lines) { - if (!line.trim().isEmpty()) { - int lineWidth = metrics.stringWidth(line.trim()); - maxWidth = Math.max(maxWidth, lineWidth); - totalHeight += metrics.getHeight(); - } else { - totalHeight += metrics.getHeight() / 2; // Half height for empty lines - } - } - - int padding = 6; - int backgroundWidth = maxWidth + (padding * 2); - int backgroundHeight = totalHeight + (padding * 2); - - // Draw semi-transparent background - Color backgroundColor = new Color(0, 0, 0, 180); - graphics.setColor(backgroundColor); - graphics.fillRect(tooltipX, tooltipY, backgroundWidth, backgroundHeight); - - // Draw border - graphics.setColor(Color.GREEN); - graphics.drawRect(tooltipX, tooltipY, backgroundWidth, backgroundHeight); - - // Render tooltip text - graphics.setColor(Color.WHITE); - int lineY = tooltipY + metrics.getAscent() + padding; - - for (String line : lines) { - if (!line.trim().isEmpty()) { - graphics.drawString(line.trim(), tooltipX + padding, lineY); - lineY += metrics.getHeight(); - } else { - lineY += metrics.getHeight() / 2; // Half height for empty lines - } - } - - // Restore original font - graphics.setFont(originalFont); - - } catch (Exception e) { - // Silent fail for overlay rendering - } - } - private void drawTile(Graphics2D graphics, WorldPoint point, Color color, @Nullable String label) { WorldPoint playerLocation = Rs2Player.getWorldLocation(); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java index f3901214b88..06341e932b5 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java @@ -16,12 +16,12 @@ import net.runelite.client.events.RuneScapeProfileChanged; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.plugins.microbot.api.grounditem.Rs2GroundItemCache; import net.runelite.client.plugins.microbot.pouch.PouchOverlay; import net.runelite.client.plugins.microbot.ui.MicrobotPluginConfigurationDescriptor; import net.runelite.client.plugins.microbot.ui.MicrobotPluginListPanel; import net.runelite.client.plugins.microbot.ui.MicrobotTopLevelConfigPanel; import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.cache.*; import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; import net.runelite.client.plugins.microbot.util.inventory.Rs2Gembag; import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; @@ -147,8 +147,6 @@ protected void startUp() throws AWTException microbotConfig.onlyMicrobotLogging() ); - Microbot.setRs2CacheEnabled(microbotConfig.isRs2CacheEnabled()); - Microbot.pauseAllScripts.set(false); MicrobotPluginListPanel pluginListPanel = pluginListPanelProvider.get(); @@ -176,17 +174,13 @@ protected void startUp() throws AWTException Microbot.getPouchScript().startUp(); - // Initialize the cache system - if (microbotConfig.isRs2CacheEnabled()) { - initializeCacheSystem(); - } + Rs2GroundItemCache.registerEventBus(); if (overlayManager != null) { overlayManager.add(microbotOverlay); overlayManager.add(gembagOverlay); overlayManager.add(pouchOverlay); - microbotOverlay.cacheButton.hookMouseListener(); } } @@ -195,13 +189,9 @@ protected void shutDown() overlayManager.remove(microbotOverlay); overlayManager.remove(gembagOverlay); overlayManager.remove(pouchOverlay); - microbotOverlay.cacheButton.unhookMouseListener(); clientToolbar.removeNavigation(navButton); if (gameChatAppender.isStarted()) gameChatAppender.stop(); microbotVersionChecker.shutdown(); - - // Shutdown the cache system - shutdownCacheSystem(); } @@ -221,12 +211,6 @@ public void onRuneScapeProfileChanged(RuneScapeProfileChanged event) ) { log.info("\nReceived RuneScape profile change event from '{}' to '{}'", oldProfile, newProfile); - if (microbotConfig.isRs2CacheEnabled()) { - // Use async profile change to avoid blocking client thread - Rs2CacheManager.handleProfileChange(newProfile, oldProfile); - log.info("Initiated async profile change from '{}' to '{}'", oldProfile, newProfile); - } - return; } } @@ -235,11 +219,7 @@ public void onRuneScapeProfileChanged(RuneScapeProfileChanged event) public void onItemContainerChanged(ItemContainerChanged event) { Microbot.getPouchScript().onItemContainerChanged(event); - if (event.getContainerId() == InventoryID.BANK) - { - Rs2Bank.updateLocalBank(event); - } - else if (event.getContainerId() == InventoryID.INV) + if (event.getContainerId() == InventoryID.INV) { Rs2Inventory.storeInventoryItemsInMemory(event); } @@ -307,9 +287,6 @@ public void onGameStateChanged(GameStateChanged gameStateChanged) if (!wasLoggedIn) { LoginManager.markLoggedIn(); Rs2RunePouch.fullUpdate(); - if (microbotConfig.isRs2CacheEnabled()) { - Rs2CacheManager.registerEventHandlers(); - } } if (currentRegions != null) { Microbot.setLastKnownRegions(currentRegions.clone()); @@ -323,9 +300,6 @@ public void onGameStateChanged(GameStateChanged gameStateChanged) //Rs2CacheManager.emptyCacheState(); // should not be nessary here, handled in ClientShutdown event, // and we also handle correct cache loading in onRuneScapeProfileChanged event LoginManager.markLoggedOut(); - if (microbotConfig.isRs2CacheEnabled()) { - Rs2CacheManager.unregisterEventHandlers(); - } Microbot.setLastKnownRegions(null); } } @@ -434,10 +408,6 @@ public void onConfigChanged(ConfigChanged ev) gameChatAppender.stop(); } break; - case MicrobotConfig.keyEnableCache: - // Handle dynamic cache system initialization/shutdown - handleCacheConfigChange(ev.getNewValue()); - break; default: break; } @@ -536,115 +506,9 @@ public void onGameTick(GameTick event) @Subscribe(priority = 100) private void onClientShutdown(ClientShutdown e) { - // Save all caches through Rs2CacheManager using async operations - if (microbotConfig.isRs2CacheEnabled()) { - try { - // Use async save but wait for completion during shutdown - Rs2CacheManager.savePersistentCachesAsync().get(30, java.util.concurrent.TimeUnit.SECONDS); - log.info("Successfully saved all caches asynchronously during shutdown"); - } catch (Exception ex) { - log.error("Failed to save caches during shutdown: {}", ex.getMessage(), ex); - // Fallback to synchronous save if async fails - Rs2CacheManager.savePersistentCaches(); - } - Rs2CacheManager.getInstance().close(); - } - } - - /** - * Initializes the cache system and registers all caches with the EventBus. - * Cache loading from configuration will happen later during game events when the RS profile is available. - */ - private void initializeCacheSystem() { - try { - // Check if already initialized - if (Rs2CacheManager.isEventHandlersRegistered()) { - log.debug("Cache system already initialized, skipping"); - return; - } - - // Get the cache manager instance - Rs2CacheManager cacheManager = Rs2CacheManager.getInstance(); - - // Set the EventBus for cache event handling (without loading caches yet) - Rs2CacheManager.setEventBus(eventBus); - - // Register event handlers - Rs2CacheManager.registerEventHandlers(); - - // Keep deprecated EntityCache for backward compatibility (for now) - //Rs2EntityCache.getInstance(); - - log.info("Cache system initialized successfully with specialized caches"); - log.info("Cache persistence will be loaded when RS profile becomes available"); - log.debug("Cache statistics: {}", cacheManager.getCacheStatistics()); - - } catch (Exception e) { - log.error("Failed to initialize cache system: {}", e.getMessage(), e); - } - } - - /** - * Shuts down the cache system and cleans up resources. - */ - private void shutdownCacheSystem() { - try { - // Check if already shutdown - if (!Rs2CacheManager.isEventHandlersRegistered()) { - log.debug("Cache system already shutdown, skipping"); - return; - } - - Rs2CacheManager cacheManager = Rs2CacheManager.getInstance(); - - log.debug("Final cache statistics before shutdown: {}", cacheManager.getCacheStatistics()); - - // Unregister event handlers first - Rs2CacheManager.unregisterEventHandlers(); - - // Close the cache manager and all caches - cacheManager.close(); - - // Reset singleton instances for clean shutdown - Rs2CacheManager.resetInstance(); - Rs2VarbitCache.resetInstance(); - Rs2SkillCache.resetInstance(); - Rs2QuestCache.resetInstance(); - - // Reset specialized entity cache instances - Rs2NpcCache.resetInstance(); - Rs2GroundItemCache.resetInstance(); - Rs2ObjectCache.resetInstance(); - - // Reset deprecated EntityCache - //Rs2EntityCache.resetInstance(); - - log.info("Cache system shutdown completed"); - - } catch (Exception e) { - log.error("Error during cache system shutdown: {}", e.getMessage(), e); - } - } - - /** - * Handles cache configuration changes dynamically without requiring client restart. - * This method is called when the user changes the "Enable Microbot Cache" config option. - * - * @param newValue The new value of the cache enable config ("true" or "false") - */ - private void handleCacheConfigChange(String newValue) { - boolean enableCache = Objects.equals(newValue, "true"); - - if (enableCache) { - log.info("Cache system enabled via config change - initializing..."); - initializeCacheSystem(); - Microbot.showMessage("Cache system enabled successfully"); - } else { - log.info("Cache system disabled via config change - shutting down..."); - shutdownCacheSystem(); - Microbot.showMessage("Cache system disabled successfully"); - } + } + /** * Dynamically checks if any visible widget overlaps with the specified bounds * @param overlayBoundsCanvas The bounds to check for widget overlap diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Script.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Script.java index 243cf3fb144..443dccdbf63 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Script.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Script.java @@ -5,7 +5,6 @@ import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.shortestpath.ShortestPathPlugin; import net.runelite.client.plugins.microbot.util.Global; -import net.runelite.client.plugins.microbot.util.cache.Rs2CacheManager; import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; @@ -69,11 +68,6 @@ public boolean run() { return false; if (Thread.currentThread().isInterrupted()) return false; - //when we log in, we must wait for the cache to be loaded before doing anything - if (Microbot.isLoggedIn() && Microbot.isRs2CacheEnabled() && !Rs2CacheManager.isCacheDataValid()) { - log.debug("Cache data is not valid, waiting..."); - return false; - } if (Microbot.isLoggedIn()) { boolean hasRunEnergy = Microbot.getClient().getEnergy() > Microbot.runEnergyThreshold; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/GroundItemFilterPreset.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/GroundItemFilterPreset.java deleted file mode 100644 index 71c881d6c82..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/GroundItemFilterPreset.java +++ /dev/null @@ -1,141 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.rs2cachedebugger; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; - -/** - * Preset filters for Ground Items in the Game Information overlay system. - * Provides common filtering options for different item types and values. - */ -@Getter -@RequiredArgsConstructor -public enum GroundItemFilterPreset { - ALL("All Items", "Show all ground items"), - HIGH_VALUE("High Value (10k+)", "Show items worth 10,000+ coins"), - MEDIUM_VALUE("Medium Value (1k-10k)", "Show items worth 1,000-10,000 coins"), - LOW_VALUE("Low Value (<1k)", "Show items worth less than 1,000 coins"), - VALUABLE_ONLY("Valuable Only (50k+)", "Show items worth 50,000+ coins"), - RARE_ITEMS("Rare Items", "Show rare drops and unique items"), - STACKABLE("Stackable", "Show only stackable items"), - NON_STACKABLE("Non-Stackable", "Show only non-stackable items"), - TRADEABLE("Tradeable", "Show only tradeable items"), - UNTRADEABLE("Untradeable", "Show only untradeable items"), - EQUIPMENT("Equipment", "Show weapons, armor, and accessories"), - CONSUMABLES("Consumables", "Show food, potions, and consumable items"), - RESOURCES("Resources", "Show raw materials and resources"), - COINS("Coins", "Show coin drops only"), - RECENTLY_SPAWNED("Recently Spawned", "Show items spawned in last 10 ticks"), - OWNED_ITEMS("Owned Items", "Show items that belong to the player"), - PUBLIC_ITEMS("Public Items", "Show items visible to all players"), - WITHIN_5_TILES("Within 5 Tiles", "Show items within 5 tiles"), - WITHIN_10_TILES("Within 10 Tiles", "Show items within 10 tiles"), - CUSTOM("Custom", "Use custom filter criteria"); - - private final String displayName; - private final String description; - - @Override - public String toString() { - return displayName; - } - - /** - * Test if a ground item matches this filter preset. - * - * @param item The ground item to test - * @return true if the item matches the filter criteria - */ - public boolean test(Rs2GroundItemModel item) { - if (item == null) { - return false; - } - - switch (this) { - case ALL: - return true; - - case HIGH_VALUE: - return item.getValue() >= 10000; - - case MEDIUM_VALUE: - int value = item.getValue(); - return value >= 1000 && value < 10000; - - case LOW_VALUE: - return item.getValue() < 1000; - - case VALUABLE_ONLY: - return item.getValue() >= 50000; - - case RARE_ITEMS: - // Basic check - could be enhanced with specific rare item IDs - return item.getValue() >= 100000; - - case STACKABLE: - return item.isStackable(); - - case NON_STACKABLE: - return !item.isStackable(); - - case TRADEABLE: - return item.isTradeable(); - - case UNTRADEABLE: - return !item.isTradeable(); - - case EQUIPMENT: - // Basic check for equipment - could be enhanced with item categories - String name = item.getName(); - if (name == null) return false; - return name.toLowerCase().contains("sword") || - name.toLowerCase().contains("bow") || - name.toLowerCase().contains("armor") || - name.toLowerCase().contains("helm") || - name.toLowerCase().contains("boots") || - name.toLowerCase().contains("gloves"); - - case CONSUMABLES: - String consumableName = item.getName(); - if (consumableName == null) return false; - return consumableName.toLowerCase().contains("potion") || - consumableName.toLowerCase().contains("food") || - consumableName.toLowerCase().contains("cake") || - consumableName.toLowerCase().contains("brew"); - - case RESOURCES: - String resourceName = item.getName(); - if (resourceName == null) return false; - return resourceName.toLowerCase().contains("ore") || - resourceName.toLowerCase().contains("log") || - resourceName.toLowerCase().contains("fish") || - resourceName.toLowerCase().contains("bone"); - - case COINS: - return item.getId() == 995; // Coins item ID - - case RECENTLY_SPAWNED: - // For now, just return true - proper implementation would need cache timing - return true; - - case OWNED_ITEMS: - return item.isOwned(); - - case PUBLIC_ITEMS: - return !item.isOwned(); - - case WITHIN_5_TILES: - return item.getDistanceFromPlayer() <= 5; - - case WITHIN_10_TILES: - return item.getDistanceFromPlayer() <= 10; - - case CUSTOM: - // Custom filtering should be handled by the plugin logic - return true; - - default: - return true; - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/NpcFilterPreset.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/NpcFilterPreset.java deleted file mode 100644 index 21a5b3a310e..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/NpcFilterPreset.java +++ /dev/null @@ -1,111 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.rs2cachedebugger; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; - -/** - * Preset filters for NPCs in the Game Information overlay system. - * Provides common filtering options that can be selected from config. - */ -@Getter -@RequiredArgsConstructor -public enum NpcFilterPreset { - ALL("All NPCs", "Show all NPCs"), - ATTACKABLE("Attackable", "Show only attackable NPCs"), - NON_ATTACKABLE("Non-Attackable", "Show only non-attackable NPCs"), - COMBAT_LEVEL_HIGH("High Combat (100+)", "Show NPCs with combat level 100 or higher"), - COMBAT_LEVEL_MID("Mid Combat (50-99)", "Show NPCs with combat level 50-99"), - COMBAT_LEVEL_LOW("Low Combat (1-49)", "Show NPCs with combat level 1-49"), - INTERACTABLE("Interactable", "Show only interactable NPCs (shops, banks, etc.)"), - AGRESSIVE("Aggressive", "Show only aggressive NPCs"), - BOSSES("Bosses", "Show only boss NPCs"), - SLAYER_MONSTERS("Slayer Monsters", "Show only slayer task monsters"), - RECENTLY_SPAWNED("Recently Spawned", "Show NPCs spawned in last 10 ticks"), - NAMED_ONLY("Named Only", "Show only NPCs with custom names"), - WITHIN_5_TILES("Within 5 Tiles", "Show NPCs within 5 tiles"), - WITHIN_10_TILES("Within 10 Tiles", "Show NPCs within 10 tiles"), - CUSTOM("Custom", "Use custom filter criteria"); - - private final String displayName; - private final String description; - - @Override - public String toString() { - return displayName; - } - - /** - * Test if an NPC matches this filter preset. - * - * @param npc The NPC to test - * @return true if the NPC matches the filter criteria - */ - public boolean test(Rs2NpcModel npc) { - if (npc == null) { - return false; - } - - switch (this) { - case ALL: - return true; - - case ATTACKABLE: - return npc.getCombatLevel() > 0; - - case NON_ATTACKABLE: - return npc.getCombatLevel() <= 0; - - case COMBAT_LEVEL_HIGH: - return npc.getCombatLevel() >= 100; - - case COMBAT_LEVEL_MID: - return npc.getCombatLevel() >= 50 && npc.getCombatLevel() <= 99; - - case COMBAT_LEVEL_LOW: - return npc.getCombatLevel() >= 1 && npc.getCombatLevel() <= 49; - - case INTERACTABLE: - // Basic check for common interactable NPCs - String name = npc.getName(); - if (name == null) return false; - return name.toLowerCase().contains("banker") || - name.toLowerCase().contains("shop") || - name.toLowerCase().contains("clerk") || - name.toLowerCase().contains("trader"); - - case AGRESSIVE: - // This would require more complex logic to determine aggression - return npc.getCombatLevel() > 0; - - case BOSSES: - // Basic boss detection - could be enhanced with specific boss IDs - return npc.getCombatLevel() >= 200; - - case SLAYER_MONSTERS: - // This would require slayer task checking - placeholder for now - return npc.getCombatLevel() > 0; - - case RECENTLY_SPAWNED: - // For now, just return true - proper implementation would need cache timing - return true; - - case NAMED_ONLY: - String npcName = npc.getName(); - return npcName != null && !npcName.trim().isEmpty(); - - case WITHIN_5_TILES: - return npc.getDistanceFromPlayer() <= 5; - - case WITHIN_10_TILES: - return npc.getDistanceFromPlayer() <= 10; - - case CUSTOM: - // Custom filtering should be handled by the plugin logic - return true; - - default: - return true; - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/ObjectFilterPreset.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/ObjectFilterPreset.java deleted file mode 100644 index fb063fd2ccc..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/ObjectFilterPreset.java +++ /dev/null @@ -1,159 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.rs2cachedebugger; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel; - -/** - * Preset filters for Objects in the Game Information overlay system. - * Provides common filtering options for different object types. - */ -@Getter -@RequiredArgsConstructor -public enum ObjectFilterPreset { - ALL("All Objects", "Show all objects"), - INTERACTABLE("Interactable", "Show only interactable objects"), - BANKS("Banks", "Show bank booths and chests"), - DOORS("Doors", "Show doors and gates"), - STAIRS("Stairs", "Show stairs and ladders"), - TREES("Trees", "Show all trees"), - ROCKS("Rocks", "Show mining rocks"), - FISHING_SPOTS("Fishing Spots", "Show fishing spots"), - ALTARS("Altars", "Show prayer and magic altars"), - FURNACES("Furnaces", "Show furnaces and forges"), - ANVILS("Anvils", "Show anvils and smithing objects"), - COOKING("Cooking", "Show cooking ranges and fires"), - DECORATIVE("Decorative", "Show decorative objects only"), - WALLS("Walls", "Show wall objects"), - GROUND_OBJECTS("Ground Objects", "Show ground-level objects"), - RECENTLY_SPAWNED("Recently Spawned", "Show objects spawned in last 10 ticks"), - WITHIN_5_TILES("Within 5 Tiles", "Show objects within 5 tiles"), - WITHIN_10_TILES("Within 10 Tiles", "Show objects within 10 tiles"), - HIGH_VALUE("High Value", "Show valuable interaction objects"), - CUSTOM("Custom", "Use custom filter criteria"); - - private final String displayName; - private final String description; - - @Override - public String toString() { - return displayName; - } - - /** - * Test if an object matches this filter preset. - * - * @param object The object to test - * @return true if the object matches the filter criteria - */ - public boolean test(Rs2ObjectModel object) { - if (object == null) { - return false; - } - - switch (this) { - case ALL: - return true; - - case INTERACTABLE: - // Check if object has any actions - String[] actions = object.getActions(); - return actions != null && actions.length > 0; - - case BANKS: - String name = object.getName(); - if (name == null) return false; - return name.toLowerCase().contains("bank") || - name.toLowerCase().contains("chest") || - name.toLowerCase().contains("deposit"); - - case DOORS: - String doorName = object.getName(); - if (doorName == null) return false; - return doorName.toLowerCase().contains("door") || - doorName.toLowerCase().contains("gate"); - - case STAIRS: - String stairName = object.getName(); - if (stairName == null) return false; - return stairName.toLowerCase().contains("stair") || - stairName.toLowerCase().contains("ladder"); - - case TREES: - String treeName = object.getName(); - if (treeName == null) return false; - return treeName.toLowerCase().contains("tree") || - treeName.toLowerCase().contains("log"); - - case ROCKS: - String rockName = object.getName(); - if (rockName == null) return false; - return rockName.toLowerCase().contains("rock") || - rockName.toLowerCase().contains("ore") || - rockName.toLowerCase().contains("vein"); - - case FISHING_SPOTS: - String fishName = object.getName(); - if (fishName == null) return false; - return fishName.toLowerCase().contains("fishing") || - fishName.toLowerCase().contains("spot"); - - case ALTARS: - String altarName = object.getName(); - if (altarName == null) return false; - return altarName.toLowerCase().contains("altar"); - - case FURNACES: - String furnaceName = object.getName(); - if (furnaceName == null) return false; - return furnaceName.toLowerCase().contains("furnace") || - furnaceName.toLowerCase().contains("forge"); - - case ANVILS: - String anvilName = object.getName(); - if (anvilName == null) return false; - return anvilName.toLowerCase().contains("anvil"); - - case COOKING: - String cookName = object.getName(); - if (cookName == null) return false; - return cookName.toLowerCase().contains("range") || - cookName.toLowerCase().contains("fire") || - cookName.toLowerCase().contains("stove"); - - case DECORATIVE: - return object.getObjectType() == Rs2ObjectModel.ObjectType.DECORATIVE_OBJECT; - - case WALLS: - return object.getObjectType() == Rs2ObjectModel.ObjectType.WALL_OBJECT; - - case GROUND_OBJECTS: - return object.getObjectType() == Rs2ObjectModel.ObjectType.GROUND_OBJECT; - - case RECENTLY_SPAWNED: - // For now, just return true - proper implementation would need cache timing - return true; - - case WITHIN_5_TILES: - return object.getDistanceFromPlayer() <= 5; - - case WITHIN_10_TILES: - return object.getDistanceFromPlayer() <= 10; - - case HIGH_VALUE: - // Basic check for commonly valuable objects - String valueName = object.getName(); - if (valueName == null) return false; - return valueName.toLowerCase().contains("bank") || - valueName.toLowerCase().contains("shop") || - valueName.toLowerCase().contains("altar"); - - case CUSTOM: - // Custom filtering should be handled by the plugin logic - return true; - - default: - return true; - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/RenderStyle.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/RenderStyle.java deleted file mode 100644 index 6cd611e9f2e..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/RenderStyle.java +++ /dev/null @@ -1,32 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.rs2cachedebugger; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -/** - * Different rendering styles for entity overlays. - * Based on RuneLite's NPC and Object indicator patterns. - */ -@Getter -@RequiredArgsConstructor -public enum RenderStyle { - HULL("Hull", "Render convex hull outline"), - TILE("Tile", "Render tile area"), - TRUE_TILE("True Tile", "Render true tile (centered)"), - SW_TILE("SW Tile", "Render southwest tile"), - CLICKBOX("Clickbox", "Render clickbox area"), - OUTLINE("Outline", "Render model outline"), - NAME("Name", "Show entity name"), - MIXED("Mixed", "Combination of hull + name"), - MINIMAL("Minimal", "Just a small indicator"), - DETAILED("Detailed", "Full information display"), - BOTH("Both", "Render both hull and tile"); - - private final String displayName; - private final String description; - - @Override - public String toString() { - return displayName; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerConfig.java deleted file mode 100644 index ec63365ed34..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerConfig.java +++ /dev/null @@ -1,908 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.rs2cachedebugger; - -import net.runelite.client.config.*; - -import java.awt.Color; -import java.awt.event.KeyEvent; - -/** - * Configuration for the Rs2 Cache Debugger Plugin. - * Provides comprehensive cache debugging, overlay and filtering options for NPCs, Objects, and Ground Items. - */ -@ConfigGroup("rs2cachedebugger") -public interface Rs2CacheDebuggerConfig extends Config { - - // ===== CONFIG SECTIONS ===== - - @ConfigSection( - name = "General Settings", - description = "General plugin settings and hotkeys", - position = 0 - ) - String generalSection = "general"; - - @ConfigSection( - name = "NPC Settings", - description = "Configure NPC overlay appearance and filtering", - position = 1 - ) - String npcSection = "npc"; - - @ConfigSection( - name = "Object Settings", - description = "Configure object overlay appearance and filtering", - position = 2 - ) - String objectSection = "object"; - - @ConfigSection( - name = "Ground Item Settings", - description = "Configure ground item overlay appearance and filtering", - position = 3 - ) - String groundItemSection = "groundItem"; - - @ConfigSection( - name = "Info Panel Settings", - description = "Configure the information panel display", - position = 4 - ) - String infoPanelSection = "infoPanel"; - - - - // ===== GENERAL SETTINGS ===== - - @ConfigItem( - keyName = "enablePlugin", - name = "Enable Plugin", - description = "Master toggle for the entire plugin", - position = 0, - section = generalSection - ) - default boolean enablePlugin() { - return true; - } - - @ConfigItem( - keyName = "maxRenderDistance", - name = "Max Render Distance", - description = "Maximum distance to render entities (in tiles)", - position = 1, - section = generalSection - ) - @Range(min = 5, max = 50) - default int maxRenderDistance() { - return 15; - } - - @ConfigItem( - keyName = "verboseLogging", - name = "Verbose Logging", - description = "Enable detailed logging for debugging", - position = 2, - section = generalSection - ) - default boolean verboseLogging() { - return false; - } - - // Hotkeys - @ConfigItem( - keyName = "toggleNpcOverlayHotkey", - name = "Toggle NPC Overlay", - description = "Hotkey to toggle NPC overlay on/off", - position = 10, - section = generalSection - ) - default Keybind toggleNpcOverlayHotkey() { - return new Keybind(KeyEvent.VK_F1,0); - } - - @ConfigItem( - keyName = "toggleObjectOverlayHotkey", - name = "Toggle Object Overlay", - description = "Hotkey to toggle object overlay on/off", - position = 11, - section = generalSection - ) - default Keybind toggleObjectOverlayHotkey() { - return new Keybind(KeyEvent.VK_F2,0); - } - - @ConfigItem( - keyName = "toggleGroundItemOverlayHotkey", - name = "Toggle Ground Item Overlay", - description = "Hotkey to toggle ground item overlay on/off", - position = 12, - section = generalSection - ) - default Keybind toggleGroundItemOverlayHotkey() { - return new Keybind(KeyEvent.VK_F3,0); - } - - @ConfigItem( - keyName = "toggleInfoPanelHotkey", - name = "Toggle Info Panel", - description = "Hotkey to toggle information panel on/off", - position = 13, - section = generalSection - ) - default Keybind toggleInfoPanelHotkey() { - return new Keybind(KeyEvent.VK_F4,0); - } - - @ConfigItem( - keyName = "logCacheInfoHotkey", - name = "Log Cache Info", - description = "Hotkey to log cache information to console", - position = 14, - section = generalSection - ) - default Keybind logCacheInfoHotkey() { - return new Keybind(KeyEvent.VK_F5,0); - } - - @ConfigItem( - keyName = "showCachePerformanceMetrics", - name = "Show Cache Performance", - description = "Show cache hit/miss ratio and performance metrics", - position = 15, - section = generalSection - ) - default boolean showCachePerformanceMetrics() { - return false; - } - - @ConfigItem( - keyName = "logCacheStatsTicks", - name = "Cache Stats Log Interval", - description = "Log cache statistics every X ticks (0 = disabled)", - position = 16, - section = generalSection - ) - @Range(min = 0, max = 100) - default int logCacheStatsTicks() { - return 0; - } - - // ===== NPC SETTINGS ===== - - @ConfigItem( - keyName = "enableNpcOverlay", - name = "Enable NPC Overlay", - description = "Show NPC overlays", - position = 0, - section = npcSection - ) - default boolean enableNpcOverlay() { - return false; - } - - @ConfigItem( - keyName = "npcRenderStyle", - name = "NPC Render Style", - description = "How to render NPC overlays", - position = 1, - section = npcSection - ) - default RenderStyle npcRenderStyle() { - return RenderStyle.HULL; - } - - @ConfigItem( - keyName = "npcFilterPreset", - name = "NPC Filter Preset", - description = "Preset filter for NPCs to display", - position = 2, - section = npcSection - ) - default NpcFilterPreset npcFilterPreset() { - return NpcFilterPreset.ALL; - } - - @ConfigItem( - keyName = "npcBorderColor", - name = "NPC Border Color", - description = "Color for NPC overlay borders", - position = 3, - section = npcSection - ) - default Color npcBorderColor() { - return Color.ORANGE; - } - - @ConfigItem( - keyName = "npcFillColor", - name = "NPC Fill Color", - description = "Color for NPC overlay fill", - position = 4, - section = npcSection - ) - @Alpha - default Color npcFillColor() { - return new Color(255, 165, 0, 50); - } - - @ConfigItem( - keyName = "npcBorderWidth", - name = "NPC Border Width", - description = "Width of NPC overlay borders", - position = 5, - section = npcSection - ) - @Range(min = 1, max = 10) - default int npcBorderWidth() { - return 2; - } - - @ConfigItem( - keyName = "npcShowNames", - name = "Show NPC Names", - description = "Display NPC names above them", - position = 6, - section = npcSection - ) - default boolean npcShowNames() { - return false; - } - - @ConfigItem( - keyName = "npcShowCombatLevel", - name = "Show Combat Level", - description = "Display NPC combat levels", - position = 7, - section = npcSection - ) - default boolean npcShowCombatLevel() { - return false; - } - - @ConfigItem( - keyName = "npcShowId", - name = "Show NPC ID", - description = "Display NPC game IDs", - position = 8, - section = npcSection - ) - default boolean npcShowId() { - return false; - } - - @ConfigItem( - keyName = "npcShowCoordinates", - name = "Show World Coordinates", - description = "Display world coordinates for NPCs", - position = 9, - section = npcSection - ) - default boolean npcShowCoordinates() { - return false; - } - - @ConfigItem( - keyName = "npcInteractingColor", - name = "NPC Interacting Color", - description = "Color for NPCs interacting with player", - position = 10, - section = npcSection - ) - default Color npcInteractingColor() { - return Color.RED; - } - - @ConfigItem( - keyName = "npcShowDistance", - name = "Show Distance", - description = "Display distance to NPCs", - position = 9, - section = npcSection - ) - default boolean npcShowDistance() { - return false; - } - - @ConfigItem( - keyName = "npcCustomFilter", - name = "Custom NPC Filter", - description = "Custom text filter for NPC names (leave empty to disable)", - position = 10, - section = npcSection - ) - default String npcCustomFilter() { - return ""; - } - - @ConfigItem( - keyName = "npcMaxDistance", - name = "NPC Max Distance", - description = "Maximum distance to show NPCs (in tiles)", - position = 11, - section = npcSection - ) - @Range(min = 3, max = 30) - default int npcMaxDistance() { - return 15; - } - - // ===== OBJECT SETTINGS ===== - - @ConfigItem( - keyName = "enableObjectOverlay", - name = "Enable Object Overlay", - description = "Show object overlays", - position = 0, - section = objectSection - ) - default boolean enableObjectOverlay() { - return false; - } - - @ConfigItem( - keyName = "objectRenderStyle", - name = "Object Render Style", - description = "How to render object overlays", - position = 1, - section = objectSection - ) - default RenderStyle objectRenderStyle() { - return RenderStyle.HULL; - } - - @ConfigItem( - keyName = "objectFilterPreset", - name = "Object Filter Preset", - description = "Preset filter for objects to display", - position = 2, - section = objectSection - ) - default ObjectFilterPreset objectFilterPreset() { - return ObjectFilterPreset.INTERACTABLE; - } - - @ConfigItem( - keyName = "objectBorderColor", - name = "Default Object Border Color", - description = "Color for object overlay borders", - position = 3, - section = objectSection - ) - default Color objectBorderColor() { - return Color.BLUE; - } - - @ConfigItem( - keyName = "objectFillColor", - name = "Object Fill Color", - description = "Color for object overlay fill", - position = 4, - section = objectSection - ) - @Alpha - default Color objectFillColor() { - return new Color(0, 0, 255, 50); - } - - @ConfigItem( - keyName = "objectBorderWidth", - name = "Object Border Width", - description = "Width of object overlay borders", - position = 5, - section = objectSection - ) - @Range(min = 1, max = 10) - default int objectBorderWidth() { - return 2; - } - - @ConfigItem( - keyName = "objectShowNames", - name = "Show Object Names", - description = "Display object names", - position = 6, - section = objectSection - ) - default boolean objectShowNames() { - return false; - } - - @ConfigItem( - keyName = "objectShowId", - name = "Show Object ID", - description = "Display object game IDs and type", - position = 7, - section = objectSection - ) - default boolean objectShowId() { - return false; - } - - @ConfigItem( - keyName = "objectShowCoordinates", - name = "Show World Coordinates", - description = "Display world coordinates for objects", - position = 8, - section = objectSection - ) - default boolean objectShowCoordinates() { - return false; - } - - @ConfigItem( - keyName = "objectMaxDistance", - name = "Object Max Distance", - description = "Maximum distance to show objects (in tiles)", - position = 7, - section = objectSection - ) - @Range(min = 3, max = 30) - default int objectMaxDistance() { - return 15; - } - - @ConfigItem( - keyName = "objectCustomFilter", - name = "Custom Object Filter", - description = "Custom text filter for object names (leave empty to disable)", - position = 8, - section = objectSection - ) - default String objectCustomFilter() { - return ""; - } - - // Different object type colors - @ConfigItem( - keyName = "bankColor", - name = "Bank Color", - description = "Color for bank objects", - position = 10, - section = objectSection - ) - default Color bankColor() { - return Color.GREEN; - } - - @ConfigItem( - keyName = "altarColor", - name = "Altar Color", - description = "Color for altar objects", - position = 11, - section = objectSection - ) - default Color altarColor() { - return Color.CYAN; - } - - @ConfigItem( - keyName = "resourceColor", - name = "Resource Color", - description = "Color for resource objects (trees, rocks)", - position = 12, - section = objectSection - ) - default Color resourceColor() { - return Color.YELLOW; - } - - @ConfigItem( - keyName = "gameObjectColor", - name = "GameObject Color", - description = "Color for regular GameObjects", - position = 13, - section = objectSection - ) - default Color gameObjectColor() { - return Color.BLUE; - } - - @ConfigItem( - keyName = "wallObjectColor", - name = "WallObject Color", - description = "Color for wall objects", - position = 14, - section = objectSection - ) - default Color wallObjectColor() { - return new Color(0, 0, 139); // Dark blue - } - - @ConfigItem( - keyName = "decorativeObjectColor", - name = "DecorativeObject Color", - description = "Color for decorative objects", - position = 15, - section = objectSection - ) - default Color decorativeObjectColor() { - return new Color(173, 216, 230); // Light blue - } - - @ConfigItem( - keyName = "groundObjectColor", - name = "GroundObject Color", - description = "Color for ground objects", - position = 16, - section = objectSection - ) - default Color groundObjectColor() { - return new Color(0, 128, 0); // Dark green - } - - @ConfigItem( - keyName = "enableObjectTypeColoring", - name = "Enable Object Type Coloring", - description = "Use different colors for different object types", - position = 17, - section = objectSection - ) - default boolean enableObjectTypeColoring() { - return true; - } - - @ConfigItem( - keyName = "enableObjectCategoryColoring", - name = "Enable Object Category Coloring", - description = "Use different colors for object categories (bank, altar, resource)", - position = 18, - section = objectSection - ) - default boolean enableObjectCategoryColoring() { - return true; - } - - // Object type toggles - @ConfigItem( - keyName = "showGameObjects", - name = "Show GameObjects", - description = "Show regular game objects (type 10)", - position = 19, - section = objectSection - ) - default boolean showGameObjects() { - return true; - } - - @ConfigItem( - keyName = "showWallObjects", - name = "Show WallObjects", - description = "Show wall objects (type 1)", - position = 20, - section = objectSection - ) - default boolean showWallObjects() { - return true; - } - - @ConfigItem( - keyName = "showDecorativeObjects", - name = "Show DecorativeObjects", - description = "Show decorative objects (type 3)", - position = 21, - section = objectSection - ) - default boolean showDecorativeObjects() { - return true; - } - - @ConfigItem( - keyName = "showGroundObjects", - name = "Show GroundObjects", - description = "Show ground objects (type 2)", - position = 22, - section = objectSection - ) - default boolean showGroundObjects() { - return true; - } - - // ===== GROUND ITEM SETTINGS ===== - - @ConfigItem( - keyName = "enableGroundItemOverlay", - name = "Enable Ground Item Overlay", - description = "Show ground item overlays", - position = 0, - section = groundItemSection - ) - default boolean enableGroundItemOverlay() { - return false; - } - - @ConfigItem( - keyName = "groundItemRenderStyle", - name = "Ground Item Render Style", - description = "How to render ground item overlays", - position = 1, - section = groundItemSection - ) - default RenderStyle groundItemRenderStyle() { - return RenderStyle.TILE; - } - - @ConfigItem( - keyName = "groundItemFilterPreset", - name = "Ground Item Filter Preset", - description = "Preset filter for ground items to display", - position = 2, - section = groundItemSection - ) - default GroundItemFilterPreset groundItemFilterPreset() { - return GroundItemFilterPreset.HIGH_VALUE; - } - - @ConfigItem( - keyName = "groundItemBorderColor", - name = "Ground Item Border Color", - description = "Color for ground item overlay borders", - position = 3, - section = groundItemSection - ) - default Color groundItemBorderColor() { - return Color.GREEN; - } - - @ConfigItem( - keyName = "groundItemFillColor", - name = "Ground Item Fill Color", - description = "Color for ground item overlay fill", - position = 4, - section = groundItemSection - ) - @Alpha - default Color groundItemFillColor() { - return new Color(0, 255, 0, 50); - } - - @ConfigItem( - keyName = "groundItemBorderWidth", - name = "Ground Item Border Width", - description = "Width of ground item overlay borders", - position = 5, - section = groundItemSection - ) - @Range(min = 1, max = 10) - default int groundItemBorderWidth() { - return 2; - } - - @ConfigItem( - keyName = "groundItemShowNames", - name = "Show Item Names", - description = "Display ground item names", - position = 6, - section = groundItemSection - ) - default boolean groundItemShowNames() { - return true; - } - - @ConfigItem( - keyName = "groundItemShowValues", - name = "Show Item Values", - description = "Display ground item values", - position = 7, - section = groundItemSection - ) - default boolean groundItemShowValues() { - return true; - } - - @ConfigItem( - keyName = "groundItemShowId", - name = "Show Item ID", - description = "Display ground item game IDs", - position = 8, - section = groundItemSection - ) - default boolean groundItemShowId() { - return false; - } - - @ConfigItem( - keyName = "groundItemShowCoordinates", - name = "Show World Coordinates", - description = "Display world coordinates for ground items", - position = 9, - section = groundItemSection - ) - default boolean groundItemShowCoordinates() { - return false; - } - - @ConfigItem( - keyName = "groundItemMaxDistance", - name = "Ground Item Max Distance", - description = "Maximum distance to show ground items (in tiles)", - position = 8, - section = groundItemSection - ) - @Range(min = 3, max = 30) - default int groundItemMaxDistance() { - return 15; - } - - @ConfigItem( - keyName = "groundItemCustomFilter", - name = "Custom Ground Item Filter", - description = "Custom text filter for ground item names (leave empty to disable)", - position = 9, - section = groundItemSection - ) - default String groundItemCustomFilter() { - return ""; - } - - @ConfigItem( - keyName = "minimumItemValue", - name = "Minimum Item Value", - description = "Minimum value to show ground items (in coins)", - position = 9, - section = groundItemSection - ) - default int minimumItemValue() { - return 1000; - } - - @ConfigItem( - keyName = "groundItemShowQuantity", - name = "Show Item Quantity", - description = "Display quantity for stackable ground items", - position = 12, - section = groundItemSection - ) - default boolean groundItemShowQuantity() { - return true; - } - - @ConfigItem( - keyName = "groundItemShowDespawnTimer", - name = "Show Despawn Timer", - description = "Display countdown timer until item despawns", - position = 13, - section = groundItemSection - ) - default boolean groundItemShowDespawnTimer() { - return false; - } - - @ConfigItem( - keyName = "groundItemShowOwnership", - name = "Show Ownership Indicator", - description = "Display ownership status for ground items", - position = 14, - section = groundItemSection - ) - default boolean groundItemShowOwnership() { - return false; - } - - @ConfigItem( - keyName = "groundItemValueBasedColors", - name = "Value-Based Colors", - description = "Use different colors based on item value", - position = 15, - section = groundItemSection - ) - default boolean groundItemValueBasedColors() { - return false; - } - - @ConfigItem( - keyName = "groundItemLowValueThreshold", - name = "Low Value Threshold", - description = "Threshold for low value items (in GP)", - position = 16, - section = groundItemSection - ) - @Range(min = 1, max = 1000000) - default int groundItemLowValueThreshold() { - return 1000; - } - - @ConfigItem( - keyName = "groundItemMediumValueThreshold", - name = "Medium Value Threshold", - description = "Threshold for medium value items (in GP)", - position = 17, - section = groundItemSection - ) - @Range(min = 1000, max = 10000000) - default int groundItemMediumValueThreshold() { - return 10000; - } - - @ConfigItem( - keyName = "groundItemHighValueThreshold", - name = "High Value Threshold", - description = "Threshold for high value items (in GP)", - position = 18, - section = groundItemSection - ) - @Range(min = 10000, max = 100000000) - default int groundItemHighValueThreshold() { - return 100000; - } - - // ===== INFO PANEL SETTINGS ===== - - @ConfigItem( - keyName = "enableInfoPanel", - name = "Enable Info Panel", - description = "Enable the information panel", - position = 0, - section = infoPanelSection - ) - default boolean enableInfoPanel() { - return false; - } - - @ConfigItem( - keyName = "showInfoPanel", - name = "Show Info Panel", - description = "Display information panel with cache statistics", - position = 1, - section = infoPanelSection - ) - default boolean showInfoPanel() { - return false; - } - - @ConfigItem( - keyName = "infoPanelShowNpcs", - name = "Show NPC Info", - description = "Show NPC information in panel", - position = 1, - section = infoPanelSection - ) - default boolean infoPanelShowNpcs() { - return true; - } - - @ConfigItem( - keyName = "infoPanelShowObjects", - name = "Show Object Info", - description = "Show object information in panel", - position = 2, - section = infoPanelSection - ) - default boolean infoPanelShowObjects() { - return true; - } - - @ConfigItem( - keyName = "infoPanelShowGroundItems", - name = "Show Ground Item Info", - description = "Show ground item information in panel", - position = 3, - section = infoPanelSection - ) - default boolean infoPanelShowGroundItems() { - return true; - } - - @ConfigItem( - keyName = "infoPanelShowCacheStats", - name = "Show Cache Statistics", - description = "Show cache hit/miss statistics", - position = 4, - section = infoPanelSection - ) - default boolean infoPanelShowCacheStats() { - return true; - } - - @ConfigItem( - keyName = "infoPanelRefreshRate", - name = "Panel Refresh Rate", - description = "How often to refresh the info panel (in ticks)", - position = 5, - section = infoPanelSection - ) - @Range(min = 1, max = 10) - default int infoPanelRefreshRate() { - return 5; - } - - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerGroundItemOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerGroundItemOverlay.java deleted file mode 100644 index 915929e14d5..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerGroundItemOverlay.java +++ /dev/null @@ -1,157 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.rs2cachedebugger; - -import net.runelite.api.*; -import net.runelite.client.plugins.microbot.util.cache.overlay.Rs2GroundItemCacheOverlay; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; -import net.runelite.client.ui.overlay.outline.ModelOutlineRenderer; - -import java.awt.*; -import java.util.function.Predicate; - -import javax.inject.Inject; - -/** - * Rs2 Cache Debugger Ground Item Overlay with configurable rendering and filtering. - * Extends the base Ground Item cache overlay with config-driven customization. - * - * @author Vox - * @version 3.0 - Cache Debugging Focus - */ -public class Rs2CacheDebuggerGroundItemOverlay extends Rs2GroundItemCacheOverlay { - - private Rs2CacheDebuggerConfig config; - private Predicate renderFilter; - @Inject - public Rs2CacheDebuggerGroundItemOverlay(Client client, ModelOutlineRenderer modelOutlineRenderer) { - super(client, modelOutlineRenderer); - } - - /** - * Set the configuration for this overlay - */ - public void setConfig(Rs2CacheDebuggerConfig config) { - this.config = config; - updateRenderingOptions(); - } - - /** - * Set the render filter for Ground Items - */ - public Rs2CacheDebuggerGroundItemOverlay setRenderFilter(Predicate filter) { - this.renderFilter = filter; - return this; - } - - /** - * Update rendering options based on config - */ - private void updateRenderingOptions() { - if (config == null) return; - - // Update rendering options based on config - setRenderTile(config.groundItemRenderStyle() == RenderStyle.TILE || - config.groundItemRenderStyle() == RenderStyle.BOTH); - setRenderText(config.groundItemShowNames()); - - // New configuration options - setRenderItemInfo(config.groundItemShowId()); - setRenderWorldCoordinates(config.groundItemShowCoordinates()); - - // Advanced rendering features from Rs2GroundItemCacheOverlay - setRenderQuantity(config.groundItemShowQuantity()); - //setRenderValue(config.groundItemShowValues()); - setRenderDespawnTimer(config.groundItemShowDespawnTimer()); - setRenderOwnershipIndicator(config.groundItemShowOwnership()); - - // Set value thresholds for color coding - setValueThresholds( - config.groundItemLowValueThreshold(), - config.groundItemMediumValueThreshold(), - config.groundItemHighValueThreshold() - ); - } - - @Override - protected Color getDefaultBorderColor() { - if (config != null) { - return config.groundItemBorderColor(); - } - return super.getDefaultBorderColor(); - } - - @Override - protected Color getDefaultFillColor() { - if (config != null) { - // Create fill color with alpha from border color - Color borderColor = config.groundItemBorderColor(); - return new Color(borderColor.getRed(), borderColor.getGreen(), borderColor.getBlue(), 50); - } - return super.getDefaultFillColor(); - } - - /** - * Gets the border color for a specific ground item based on value and configuration. - * - * @param itemModel The ground item model - * @return The border color for this item - */ - @Override - protected Color getBorderColorForItem(Rs2GroundItemModel itemModel) { - if (config != null && config.groundItemShowValues()) { - return getItemValueColor(itemModel); - } - return getDefaultBorderColor(); - } - - /** - * Gets the fill color for a specific ground item based on value and configuration. - * - * @param itemModel The ground item model - * @return The fill color for this item - */ - @Override - protected Color getFillColorForItem(Rs2GroundItemModel itemModel) { - if (config != null && config.groundItemShowValues()) { - Color borderColor = getItemValueColor(itemModel); - return new Color(borderColor.getRed(), borderColor.getGreen(), borderColor.getBlue(), 50); - } - return getDefaultFillColor(); - } - - /** - * Gets the color for a ground item based on its value. - * - * @param itemModel The ground item model - * @return The value-based color - */ - private Color getItemValueColor(Rs2GroundItemModel itemModel) { - int totalValue = itemModel.getTotalValue(); - - if (totalValue >= config.groundItemHighValueThreshold()) { - return Color.RED; // High value items in red - } else if (totalValue >= config.groundItemMediumValueThreshold()) { - return Color.ORANGE; // Medium value items in orange - } else if (totalValue >= config.groundItemLowValueThreshold()) { - return Color.YELLOW; // Low value items in yellow - } else { - return getDefaultBorderColor(); // Default color for very low value items - } - } - - @Override - public Dimension render(Graphics2D graphics) { - if (config == null || !config.enableGroundItemOverlay()) { - return null; - } - - // Update configuration - updateRenderingOptions(); - - // Apply filter if set - if (renderFilter != null) { - setRenderFilter(renderFilter); - } - - return super.render(graphics); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerInfoPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerInfoPanel.java deleted file mode 100644 index dd4995230d4..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerInfoPanel.java +++ /dev/null @@ -1,326 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.rs2cachedebugger; - -import net.runelite.api.Client; -import net.runelite.client.plugins.microbot.util.cache.Rs2GroundItemCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2NpcCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2ObjectCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2Cache; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2NpcCacheUtils; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2ObjectCacheUtils; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2GroundItemCacheUtils; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel; -import net.runelite.client.ui.overlay.OverlayLayer; -import net.runelite.client.ui.overlay.OverlayPosition; -import net.runelite.client.ui.overlay.components.LineComponent; -import net.runelite.client.ui.overlay.components.PanelComponent; -import net.runelite.client.ui.overlay.components.TitleComponent; -import net.runelite.client.ui.overlay.Overlay; - -import java.awt.*; -import java.util.List; -import java.util.stream.Collectors; - -import javax.inject.Inject; - -/** - * Rs2 Cache Debugger Info Panel showing detailed cache information and viewport entities. - * Displays comprehensive information about NPCs, objects, and ground items in the current viewport - * with focus on cache performance and debugging statistics. - * - * @author Vox - * @version 3.0 - Cache Debugging Focus - */ -public class Rs2CacheDebuggerInfoPanel extends Overlay { - - private Rs2CacheDebuggerConfig config; - private final PanelComponent panelComponent = new PanelComponent(); - @Inject - public Rs2CacheDebuggerInfoPanel(Client client) { - setPosition(OverlayPosition.TOP_LEFT); - setLayer(OverlayLayer.ABOVE_WIDGETS); - } - - /** - * Set the configuration for this info panel - */ - public void setConfig(Rs2CacheDebuggerConfig config) { - this.config = config; - } - - @Override - public Dimension render(Graphics2D graphics) { - if (config == null || !config.enableInfoPanel() || !config.showInfoPanel()) { - return null; - } - - panelComponent.getChildren().clear(); - - // Panel title - panelComponent.getChildren().add(TitleComponent.builder() - .text("Rs2 Cache Debugger") - .color(Color.CYAN) - .build()); - - // Cache statistics - addCacheStatistics(); - - // Viewport entities - if (config.infoPanelShowNpcs() || config.infoPanelShowObjects() || config.infoPanelShowGroundItems()) { - panelComponent.getChildren().add(LineComponent.builder() - .left("").right("").build()); // Spacer - - panelComponent.getChildren().add(LineComponent.builder() - .left("Viewport Entities:") - .leftColor(Color.WHITE) - .build()); - } - - // NPCs in viewport - if (config.infoPanelShowNpcs()) { - addNpcInfo(); - } - - // Objects in viewport - if (config.infoPanelShowObjects()) { - addObjectInfo(); - } - - // Ground items in viewport - if (config.infoPanelShowGroundItems()) { - addGroundItemInfo(); - } - - return panelComponent.render(graphics); - } - - /** - * Add cache statistics to the panel - */ - private void addCacheStatistics() { - Rs2Cache npcCache = Rs2NpcCache.getInstance(); - Rs2Cache objectCache = Rs2ObjectCache.getInstance(); - Rs2Cache groundItemCache = Rs2GroundItemCache.getInstance(); - - panelComponent.getChildren().add(LineComponent.builder() - .left("Cache Statistics:") - .leftColor(Color.WHITE) - .build()); - - panelComponent.getChildren().add(LineComponent.builder() - .left("NPCs:") - .right(String.valueOf(npcCache.size())) - .leftColor(Color.ORANGE) - .rightColor(Color.WHITE) - .build()); - - panelComponent.getChildren().add(LineComponent.builder() - .left("Objects:") - .right(String.valueOf(objectCache.size())) - .leftColor(Color.BLUE) - .rightColor(Color.WHITE) - .build()); - - panelComponent.getChildren().add(LineComponent.builder() - .left("Ground Items:") - .right(String.valueOf(groundItemCache.size())) - .leftColor(Color.GREEN) - .rightColor(Color.WHITE) - .build()); - } - - /** - * Add NPC information to the panel - */ - private void addNpcInfo() { - List visibleNpcs = Rs2NpcCacheUtils.getAllInViewport() - .limit(5) // Limit to first 5 for display - .collect(Collectors.toList()); - - if (!visibleNpcs.isEmpty()) { - panelComponent.getChildren().add(LineComponent.builder() - .left("NPCs (" + visibleNpcs.size() + "):") - .leftColor(Color.ORANGE) - .build()); - - for (Rs2NpcModel npc : visibleNpcs) { - String npcInfo = buildNpcInfo(npc); - panelComponent.getChildren().add(LineComponent.builder() - .left(" " + npcInfo) - .leftColor(Color.LIGHT_GRAY) - .build()); - } - } - } - - /** - * Add object information to the panel - */ - private void addObjectInfo() { - List visibleObjects = Rs2ObjectCacheUtils.getAllInViewport() - .limit(5) // Limit to first 5 for display - .collect(Collectors.toList()); - - if (!visibleObjects.isEmpty()) { - panelComponent.getChildren().add(LineComponent.builder() - .left("Objects (" + visibleObjects.size() + "):") - .leftColor(Color.BLUE) - .build()); - - for (Rs2ObjectModel obj : visibleObjects) { - String objInfo = buildObjectInfo(obj); - panelComponent.getChildren().add(LineComponent.builder() - .left(" " + objInfo) - .leftColor(Color.LIGHT_GRAY) - .build()); - } - } - } - - /** - * Add ground item information to the panel - */ - private void addGroundItemInfo() { - List visibleItems = Rs2GroundItemCacheUtils.getAllInViewport() - .limit(5) // Limit to first 5 for display - .collect(Collectors.toList()); - - if (!visibleItems.isEmpty()) { - panelComponent.getChildren().add(LineComponent.builder() - .left("Ground Items (" + visibleItems.size() + "):") - .leftColor(Color.GREEN) - .build()); - - for (Rs2GroundItemModel item : visibleItems) { - String itemInfo = buildGroundItemInfo(item); - panelComponent.getChildren().add(LineComponent.builder() - .left(" " + itemInfo) - .leftColor(Color.LIGHT_GRAY) - .build()); - } - } - } - - /** - * Build info string for an NPC - */ - private String buildNpcInfo(Rs2NpcModel npc) { - StringBuilder info = new StringBuilder(); - - String name = npc.getName(); - if (name != null) { - info.append(name); - } else { - info.append("Unknown"); - } - - info.append(" [ID:").append(npc.getId()).append("]"); - - if (npc.getCombatLevel() > 0) { - info.append(" (CB: ").append(npc.getCombatLevel()).append(")"); - } - - info.append(" (").append(npc.getDistanceFromPlayer()).append("t)"); - - // Add interaction status - if (npc.getInteracting() != null) { - info.append(" [INTERACTING]"); - } - - // Add coordinates if config allows - if (config != null && config.npcShowCoordinates()) { - info.append(" @(").append(npc.getWorldLocation().getX()) - .append(",").append(npc.getWorldLocation().getY()).append(")"); - } - - return info.toString(); - } - - /** - * Build info string for an object - */ - private String buildObjectInfo(Rs2ObjectModel obj) { - StringBuilder info = new StringBuilder(); - - String name = obj.getName(); - if (name != null && !name.trim().isEmpty()) { - info.append(name); - } else { - info.append("Object"); - } - - info.append(" [ID:").append(obj.getId()).append("]"); - - // Add object type abbreviation - String typeAbbr = getObjectTypeAbbreviation(obj.getObjectType()); - info.append(" (").append(typeAbbr).append(")"); - - info.append(" (").append(obj.getDistanceFromPlayer()).append("t)"); - - // Add coordinates if config allows - if (config != null && config.objectShowCoordinates()) { - info.append(" @(").append(obj.getLocation().getX()) - .append(",").append(obj.getLocation().getY()).append(")"); - } - - return info.toString(); - } - - /** - * Get object type abbreviation for display - */ - private String getObjectTypeAbbreviation(Rs2ObjectModel.ObjectType objectType) { - switch (objectType) { - case GAME_OBJECT: - return "G"; - case WALL_OBJECT: - return "W"; - case DECORATIVE_OBJECT: - return "D"; - case GROUND_OBJECT: - return "Gnd"; - default: - return "?"; - } - } - - /** - * Build info string for a ground item - */ - private String buildGroundItemInfo(Rs2GroundItemModel item) { - StringBuilder info = new StringBuilder(); - - String name = item.getName(); - if (name != null) { - info.append(name); - } else { - info.append("Unknown Item"); - } - - info.append(" [ID:").append(item.getId()).append("]"); - - if (item.getQuantity() > 1) { - info.append(" x").append(item.getQuantity()); - } - - // Add value information - if (item.getValue() > 0) { - info.append(" (").append(item.getValue()).append("gp)"); - } - - info.append(" (").append(item.getDistanceFromPlayer()).append("t)"); - - if (item.isOwned()) { - info.append(" [OWNED]"); - } - - // Add coordinates if config allows - if (config != null && config.groundItemShowCoordinates()) { - info.append(" @(").append(item.getLocation().getX()) - .append(",").append(item.getLocation().getY()).append(")"); - } - - return info.toString(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerNpcOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerNpcOverlay.java deleted file mode 100644 index 03d5eee32b8..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerNpcOverlay.java +++ /dev/null @@ -1,168 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.rs2cachedebugger; - -import net.runelite.api.*; -import net.runelite.client.plugins.microbot.util.cache.overlay.Rs2NpcCacheOverlay; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; -import net.runelite.client.ui.overlay.outline.ModelOutlineRenderer; - -import java.awt.*; -import java.util.function.Predicate; - -import javax.inject.Inject; - -/** - * Rs2 Cache Debugger NPC Overlay with configurable rendering and filtering. - * Extends the base NPC cache overlay with config-driven customization. - * - * @author Vox - * @version 3.0 - Cache Debugging Focus - */ -public class Rs2CacheDebuggerNpcOverlay extends Rs2NpcCacheOverlay { - - private Rs2CacheDebuggerConfig config; - @Inject - public Rs2CacheDebuggerNpcOverlay(Client client, ModelOutlineRenderer modelOutlineRenderer) { - super(client, modelOutlineRenderer); - } - - /** - * Set the configuration for this overlay - */ - public void setConfig(Rs2CacheDebuggerConfig config) { - this.config = config; - updateRenderingOptions(); - } - - /** - * Set the render filter for NPCs - */ - public Rs2CacheDebuggerNpcOverlay setRenderFilter(Predicate filter) { - // Apply the filter to the parent class - super.setRenderFilter(filter); - return this; - } - - /** - * Update rendering options based on config - */ - private void updateRenderingOptions() { - if (config == null) return; - - // Update rendering options based on config - setRenderHull(config.npcRenderStyle() == RenderStyle.HULL || - config.npcRenderStyle() == RenderStyle.BOTH); - setRenderTile(config.npcRenderStyle() == RenderStyle.TILE || - config.npcRenderStyle() == RenderStyle.BOTH); - setRenderName(config.npcShowNames()); - setRenderOutline(config.npcRenderStyle() == RenderStyle.OUTLINE); - - // New configuration options - setRenderNpcInfo(config.npcShowId()); - setRenderWorldCoordinates(config.npcShowCoordinates()); - setRenderCombatLevel(config.npcShowCombatLevel()); - setRenderDistance(config.npcShowDistance()); - } - - @Override - protected Color getBorderColorForNpc(Rs2NpcModel npcModel) { - if (config == null) { - return super.getBorderColorForNpc(npcModel); - } - - // Check for specific NPC categories first - Color categoryColor = getNpcCategoryColor(npcModel); - if (categoryColor != null) { - return categoryColor; - } - - // Fallback to config default - return config.npcBorderColor(); - } - - @Override - protected Color getFillColorForNpc(Rs2NpcModel npcModel) { - Color borderColor = getBorderColorForNpc(npcModel); - return new Color(borderColor.getRed(), borderColor.getGreen(), borderColor.getBlue(), 50); - } - - /** - * Gets color based on NPC category (bank, shop, combat, etc.) - * - * @param npcModel The NPC model - * @return Category-specific color or null if no category match - */ - private Color getNpcCategoryColor(Rs2NpcModel npcModel) { - String name = npcModel.getName().toLowerCase(); - - // Get actions from the NPC composition - NPCComposition composition = npcModel.getTransformedComposition(); - String[] actions = null; - if (composition != null) { - actions = composition.getActions(); - } - - // Bank NPCs - if (hasAction(actions, "bank") || name.contains("banker")) { - return Color.GREEN; - } - - // Shop NPCs - if (hasAction(actions, "trade") || hasAction(actions, "shop") || name.contains("shop")) { - return Color.CYAN; - } - - // Combat NPCs (aggressive or high level) - if (npcModel.getCombatLevel() > 100) { - return Color.RED; - } - - // Training NPCs (low level combat) - if (npcModel.getCombatLevel() > 0 && npcModel.getCombatLevel() <= 100) { - return Color.ORANGE; - } - - return null; // No category match - } - - /** - * Checks if an action array contains a specific action - * - * @param actions The actions array - * @param action The action to look for - * @return true if the action exists - */ - private boolean hasAction(String[] actions, String action) { - if (actions == null || action == null) { - return false; - } - - for (String a : actions) { - if (a != null && a.toLowerCase().contains(action.toLowerCase())) { - return true; - } - } - return false; - } - - /** - * Get the interacting color for NPCs targeting the player - */ - public Color getInteractingColor() { - if (config != null) { - return config.npcInteractingColor(); - } - return Color.RED; // Default fallback - } - - @Override - public Dimension render(Graphics2D graphics) { - if (config == null || !config.enableNpcOverlay()) { - return null; - } - - // Update configuration - updateRenderingOptions(); - - return super.render(graphics); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerObjectOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerObjectOverlay.java deleted file mode 100644 index c7804609964..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerObjectOverlay.java +++ /dev/null @@ -1,231 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.rs2cachedebugger; - -import net.runelite.api.*; -import net.runelite.client.plugins.microbot.util.cache.overlay.Rs2ObjectCacheOverlay; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel; -import net.runelite.client.ui.overlay.outline.ModelOutlineRenderer; -import java.awt.*; -import java.util.function.Predicate; - -import javax.inject.Inject; - -import lombok.extern.slf4j.Slf4j; - -/** - * Rs2 Cache Debugger Object Overlay with configurable rendering and filtering. - * Extends the base Object cache overlay with config-driven customization. - * - * @author Vox - * @version 3.0 - Cache Debugging Focus - */ -@Slf4j -public class Rs2CacheDebuggerObjectOverlay extends Rs2ObjectCacheOverlay { - - private Rs2CacheDebuggerConfig config; - @Inject - public Rs2CacheDebuggerObjectOverlay(Client client, ModelOutlineRenderer modelOutlineRenderer) { - super(client, modelOutlineRenderer); - } - - /** - * Set the configuration for this overlay - */ - public void setConfig(Rs2CacheDebuggerConfig config) { - this.config = config; - updateRenderingOptions(); - } - - /** - * Set the render filter for Objects - */ - public Rs2CacheDebuggerObjectOverlay setRenderFilter(Predicate filter) { - // Apply the filter to the parent class - super.setRenderFilter(filter); - return this; - } - - /** - * Update rendering options based on config - */ - private void updateRenderingOptions() { - if (config == null) return; - - // Update rendering options based on config - setRenderHull(config.objectRenderStyle() == RenderStyle.HULL || - config.objectRenderStyle() == RenderStyle.BOTH); - setRenderTile(config.objectRenderStyle() == RenderStyle.TILE || - config.objectRenderStyle() == RenderStyle.BOTH); - setRenderClickbox(config.objectRenderStyle() == RenderStyle.CLICKBOX); - setRenderOutline(config.objectRenderStyle() == RenderStyle.OUTLINE); - - // New configuration options - setRenderObjectInfo(config.objectShowId()); - setRenderObjectName(config.objectShowNames()); - setRenderWorldCoordinates(config.objectShowCoordinates()); - setOnlyShowTextOnHover(true); // Always show text only on hover for better UX - - // Enable/disable object types based on config - setEnableGameObjects(config.showGameObjects()); - setEnableWallObjects(config.showWallObjects()); - setEnableDecorativeObjects(config.showDecorativeObjects()); - setEnableGroundObjects(config.showGroundObjects()); - - // Configure text rendering style for debugging - } - - @Override - protected Color getBorderColorForObject(Rs2ObjectModel objectModel) { - if (config == null) { - if (log.isDebugEnabled()) { - log.debug("Config is null, using super.getBorderColorForObject for object {}", objectModel.getId()); - } - return super.getBorderColorForObject(objectModel); - } - - - - // Check for object category-based coloring first (if enabled) - if (config.enableObjectCategoryColoring()) { - Color categoryColor = getObjectCategoryColor(objectModel); - if (categoryColor != null) { - - return categoryColor; - } - } - - // Check for object type-based coloring (if enabled) - if (config.enableObjectTypeColoring()) { - Color typeColor = getBorderColorForObjectType(objectModel.getObjectType()); - if (typeColor != null) { - return typeColor; - } - } - // Fallback to config default - Color defaultColor = config.objectBorderColor(); - return defaultColor; - } - - @Override - protected Color getBorderColorForObjectType(Rs2ObjectModel.ObjectType objectType) { - if (config == null || !config.enableObjectTypeColoring()) { - return super.getBorderColorForObjectType(objectType); - } - - // Use the new config options for different object types - switch (objectType) { - case GAME_OBJECT: - return config.gameObjectColor(); - case WALL_OBJECT: - return config.wallObjectColor(); - case DECORATIVE_OBJECT: - return config.decorativeObjectColor(); - case GROUND_OBJECT: - return config.groundObjectColor(); - case TILE_OBJECT: - return new Color(255, 165, 0); // Orange - same as TILE_OBJECT_COLOR - default: - return config.objectBorderColor(); - } - } - - @Override - protected Color getFillColorForObject(Rs2ObjectModel objectModel) { - Color borderColor = getBorderColorForObject(objectModel); - return new Color(borderColor.getRed(), borderColor.getGreen(), borderColor.getBlue(), 50); - } - - /** - * Gets color based on object category (bank, altar, resource, etc.) - * - * @param objectModel The object model - * @return Category-specific color or null if no category match - */ - private Color getObjectCategoryColor(Rs2ObjectModel objectModel) { - String name = objectModel.getName().toLowerCase(); - String[] actions = objectModel.getActions(); - - // Bank objects - if (name.contains("bank") || hasAction(actions, "bank") || hasAction(actions, "collect")) { - return config.bankColor(); - } - - // Altar objects - if (name.contains("altar") || hasAction(actions, "pray-at") || hasAction(actions, "pray")) { - return config.altarColor(); - } - - // Resource objects (trees, rocks, fishing spots, etc.) - if (isResourceObject(name, actions)) { - return config.resourceColor(); - } - - return null; // No category match - } - - /** - * Checks if an object is a resource object (trees, rocks, fishing spots, etc.) - * - * @param name The object name (lowercase) - * @param actions The object actions - * @return true if this is a resource object - */ - private boolean isResourceObject(String name, String[] actions) { - // Trees - if (name.contains("tree") || name.contains("log") || hasAction(actions, "chop")) { - return true; - } - - // Rocks and mining - if (name.contains("rock") || name.contains("ore") || hasAction(actions, "mine")) { - return true; - } - - // Fishing spots - if (name.contains("fishing") || name.contains("pool") || hasAction(actions, "fish")) { - return true; - } - - // Other resources - if (hasAction(actions, "pick") || hasAction(actions, "harvest") || hasAction(actions, "gather")) { - return true; - } - - return false; - } - - /** - * Checks if an action array contains a specific action - * - * @param actions The actions array - * @param action The action to look for - * @return true if the action exists - */ - private boolean hasAction(String[] actions, String action) { - if (actions == null || action == null) { - return false; - } - - for (String a : actions) { - if (a != null && a.toLowerCase().contains(action.toLowerCase())) { - return true; - } - } - return false; - } - - @Override - public Dimension render(Graphics2D graphics) { - if (config == null || !config.enableObjectOverlay()) { - return null; - } - try{ - // Update configuration - updateRenderingOptions(); - - return super.render(graphics); - } catch (Exception e) { - log.error("Error rendering Rs2CacheDebuggerObjectOverlay: {}", e.getMessage(), e); - return null; - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerPlugin.java deleted file mode 100644 index 34213f8da1a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerPlugin.java +++ /dev/null @@ -1,637 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.rs2cachedebugger; - -import com.google.inject.Provides; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.events.GameTick; -import net.runelite.client.config.ConfigManager; -import net.runelite.client.events.ConfigChanged; -import net.runelite.client.input.KeyManager; -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.util.cache.*; -import net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel; -import net.runelite.client.ui.overlay.OverlayManager; -import net.runelite.client.util.HotkeyListener; -import net.runelite.client.eventbus.Subscribe; - -import javax.inject.Inject; - -/** - * Rs2 Cache Debugger Plugin providing comprehensive cache debugging and entity information overlays. - * Features configurable overlays for NPCs, objects, and ground items with preset filters, - * custom colors, render styles, detailed cache statistics, and performance monitoring. - * - * This plugin focuses on debugging and visualizing the Rs2Cache system for NPCs, Objects, and Ground Items. - * - * @author Vox - * @version 3.0 - Cache Debugging Focus - */ -@PluginDescriptor( - name = "Rs2 Cache Debugger", - description = "Debug and visualize Rs2Cache system with configurable overlays, cache statistics, and performance monitoring", - tags = {"cache", "debug", "overlay", "npc", "object", "ground items", "performance", "vox"}, - enabledByDefault = false, - disableOnStartUp = true -) -@Slf4j -public class Rs2CacheDebuggerPlugin extends Plugin { - - @Inject - private Rs2CacheDebuggerConfig config; - - @Inject - private OverlayManager overlayManager; - - @Inject - private KeyManager keyManager; - - @Inject - private Rs2CacheDebuggerNpcOverlay npcOverlay; - - @Inject - private Rs2CacheDebuggerObjectOverlay objectOverlay; - - @Inject - private Rs2CacheDebuggerGroundItemOverlay groundItemOverlay; - - @Inject - private Rs2CacheDebuggerInfoPanel infoPanel; - - // State tracking - private boolean npcOverlayEnabled = false; - private boolean objectOverlayEnabled = false; - private boolean groundItemOverlayEnabled = false; - private boolean infoPanelEnabled = false; - - - - // Performance tracking - private int tickCounter = 0; - private long lastCacheStatsLogTime = 0; - - // Hotkey listeners - private final HotkeyListener toggleNpcOverlayListener = new HotkeyListener(() -> config.toggleNpcOverlayHotkey()) { - @Override - public void hotkeyPressed() { - toggleNpcOverlay(); - } - }; - - private final HotkeyListener toggleObjectOverlayListener = new HotkeyListener(() -> config.toggleObjectOverlayHotkey()) { - @Override - public void hotkeyPressed() { - toggleObjectOverlay(); - } - }; - - private final HotkeyListener toggleGroundItemOverlayListener = new HotkeyListener(() -> config.toggleGroundItemOverlayHotkey()) { - @Override - public void hotkeyPressed() { - toggleGroundItemOverlay(); - } - }; - - private final HotkeyListener logCacheInfoListener = new HotkeyListener(() -> config.logCacheInfoHotkey()) { - @Override - public void hotkeyPressed() { - Microbot.getClientThread().runOnSeperateThread(()-> {logDetailedCacheInfo(); return null;}); - } - }; - - private final HotkeyListener toggleInfoPanelListener = new HotkeyListener(() -> config.toggleInfoPanelHotkey()) { - @Override - public void hotkeyPressed() { - toggleInfoPanel(); - } - }; - - @Provides - Rs2CacheDebuggerConfig provideConfig(ConfigManager configManager) { - return configManager.getConfig(Rs2CacheDebuggerConfig.class); - } - - @Override - protected void startUp() throws Exception { - log.info("Starting Rs2 Cache Debugger Plugin"); - - - - // Configure overlays with config - configureOverlays(); - - // Register hotkey listeners - keyManager.registerKeyListener(toggleNpcOverlayListener); - keyManager.registerKeyListener(toggleObjectOverlayListener); - keyManager.registerKeyListener(toggleGroundItemOverlayListener); - keyManager.registerKeyListener(logCacheInfoListener); - keyManager.registerKeyListener(toggleInfoPanelListener); - - // Enable overlays based on config - if (config.enableNpcOverlay()) { - enableNpcOverlay(); - } - - if (config.enableObjectOverlay()) { - enableObjectOverlay(); - } - - if (config.enableGroundItemOverlay()) { - enableGroundItemOverlay(); - } - - if (config.enableInfoPanel()) { - enableInfoPanel(); - } - - // Reset performance counters - tickCounter = 0; - lastCacheStatsLogTime = System.currentTimeMillis(); - - log.info("Rs2 Cache Debugger Plugin started successfully"); - } - - @Override - protected void shutDown() throws Exception { - log.info("Shutting down Rs2 Cache Debugger Plugin"); - - // Unregister hotkey listeners - keyManager.unregisterKeyListener(toggleNpcOverlayListener); - keyManager.unregisterKeyListener(toggleObjectOverlayListener); - keyManager.unregisterKeyListener(toggleGroundItemOverlayListener); - keyManager.unregisterKeyListener(logCacheInfoListener); - keyManager.unregisterKeyListener(toggleInfoPanelListener); - - // Disable all overlays - disableNpcOverlay(); - disableObjectOverlay(); - disableGroundItemOverlay(); - disableInfoPanel(); - - log.info("Rs2 Cache Debugger Plugin shut down successfully"); - } - - /** - * Configure overlays with current config settings - */ - private void configureOverlays() { - // Configure NPC overlay - npcOverlay.setConfig(config); - npcOverlay.setRenderFilter(createNpcFilter()); - - // Configure Object overlay - objectOverlay.setConfig(config); - objectOverlay.setRenderFilter(createObjectFilter()); - - // Configure Ground Item overlay - groundItemOverlay.setConfig(config); - groundItemOverlay.setRenderFilter(createGroundItemFilter()); - - // Configure Info Panel - infoPanel.setConfig(config); - } - - /** - * Create NPC filter based on config - */ - private java.util.function.Predicate createNpcFilter() { - NpcFilterPreset preset = config.npcFilterPreset(); - String customFilter = config.npcCustomFilter(); - - return npcModel -> { - if (npcModel == null) { - return false; - } - - // Apply preset filter - if (!preset.test(npcModel)) { - return false; - } - - // Apply custom filter if specified - if (customFilter != null && !customFilter.trim().isEmpty()) { - String npcName = npcModel.getName(); - if (npcName == null) { - return false; - } - return npcName.toLowerCase().contains(customFilter.toLowerCase()); - } - - return true; - }; - } - - /** - * Create Object filter based on config - */ - private java.util.function.Predicate createObjectFilter() { - ObjectFilterPreset preset = config.objectFilterPreset(); - String customFilter = config.objectCustomFilter(); - - return objectModel -> { - if (objectModel == null || objectModel.getTileObject() == null) { - return false; - } - - // Check object type visibility settings - switch (objectModel.getObjectType()) { - case GAME_OBJECT: - if (!config.showGameObjects()) { - return false; - } - break; - case WALL_OBJECT: - if (!config.showWallObjects()) { - return false; - } - break; - case DECORATIVE_OBJECT: - if (!config.showDecorativeObjects()) { - return false; - } - break; - case GROUND_OBJECT: - if (!config.showGroundObjects()) { - return false; - } - break; - default: - return false; // Unknown object type - } - - // Apply distance filtering - int maxDistance = config.objectMaxDistance(); - if (objectModel.getDistanceFromPlayer() > maxDistance) { - return false; - } - - // Apply preset filter - if (!preset.test(objectModel)) { - return false; - } - - // Apply custom filter if specified - if (customFilter != null && !customFilter.trim().isEmpty()) { - // Check object name or ID - String objectName = objectModel.getName(); - if (objectName == null) { - return false; - } - return objectName.toLowerCase().contains(customFilter.toLowerCase()); - } - - return true; - }; - } - - /** - * Create Ground Item filter based on config - */ - private java.util.function.Predicate createGroundItemFilter() { - GroundItemFilterPreset preset = config.groundItemFilterPreset(); - String customFilter = config.groundItemCustomFilter(); - - return itemModel -> { - if (itemModel == null) { - return false; - } - - // Apply preset filter - if (!preset.test(itemModel)) { - return false; - } - - // Apply custom filter if specified - if (customFilter != null && !customFilter.trim().isEmpty()) { - String itemName = itemModel.getName(); - if (itemName == null) { - return false; - } - return itemName.toLowerCase().contains(customFilter.toLowerCase()); - } - - return true; - }; - } - - // Overlay control methods - public void toggleNpcOverlay() { - if (npcOverlayEnabled) { - disableNpcOverlay(); - } else { - enableNpcOverlay(); - } - } - - public void enableNpcOverlay() { - if (!npcOverlayEnabled) { - overlayManager.add(npcOverlay); - npcOverlayEnabled = true; - log.debug("NPC cache overlay enabled"); - } - } - - public void disableNpcOverlay() { - if (npcOverlayEnabled) { - overlayManager.remove(npcOverlay); - npcOverlayEnabled = false; - log.debug("NPC cache overlay disabled"); - } - } - - public void toggleObjectOverlay() { - if (objectOverlayEnabled) { - disableObjectOverlay(); - } else { - enableObjectOverlay(); - } - } - - public void enableObjectOverlay() { - if (!objectOverlayEnabled) { - overlayManager.add(objectOverlay); - objectOverlayEnabled = true; - log.debug("Object cache overlay enabled"); - } - } - - public void disableObjectOverlay() { - if (objectOverlayEnabled) { - overlayManager.remove(objectOverlay); - objectOverlayEnabled = false; - log.debug("Object cache overlay disabled"); - } - } - - public void toggleGroundItemOverlay() { - if (groundItemOverlayEnabled) { - disableGroundItemOverlay(); - } else { - enableGroundItemOverlay(); - } - } - - public void enableGroundItemOverlay() { - if (!groundItemOverlayEnabled) { - overlayManager.add(groundItemOverlay); - groundItemOverlayEnabled = true; - log.debug("Ground item cache overlay enabled"); - } - } - - public void disableGroundItemOverlay() { - if (groundItemOverlayEnabled) { - overlayManager.remove(groundItemOverlay); - groundItemOverlayEnabled = false; - log.debug("Ground item cache overlay disabled"); - } - } - - public void toggleInfoPanel() { - if (infoPanelEnabled) { - disableInfoPanel(); - } else { - enableInfoPanel(); - } - } - - public void enableInfoPanel() { - if (!infoPanelEnabled) { - overlayManager.add(infoPanel); - infoPanelEnabled = true; - log.debug("Cache info panel enabled"); - } - } - - public void disableInfoPanel() { - if (infoPanelEnabled) { - overlayManager.remove(infoPanel); - infoPanelEnabled = false; - log.debug("Cache info panel disabled"); - } - } - - /** - * Log detailed cache information using the new unified logging system - */ - public void logDetailedCacheInfo() { - log.info("=== Rs2 Cache Debugger - Detailed Cache Analysis ==="); - - boolean dumpToFile = config.showCachePerformanceMetrics(); // Use performance metrics config to determine file dumping - LogOutputMode outputMode = - dumpToFile ?LogOutputMode.BOTH - : LogOutputMode.CONSOLE_ONLY; - // Use new unified logging for all caches - log.info("Generating detailed cache state reports..."); - - // NPC Cache State - if (Rs2NpcCache.getInstance() != null) { - log.info("--- NPC Cache Analysis ---"); - // Use new LogOutputMode for better control - - Rs2NpcCache.logState(outputMode); - } - // Quest Cache State - if (Rs2QuestCache.getInstance() != null) { - log.info("--- Quest Cache Analysis ---"); - Rs2QuestCache.logState(outputMode); - } - - // Object Cache State - if (Rs2ObjectCache.getInstance() != null) { - log.info("--- Object Cache Analysis ---"); - Rs2ObjectCache.logState(outputMode); - } - - // Ground Item Cache State - if (Rs2GroundItemCache.getInstance() != null) { - log.info("--- Ground Item Cache Analysis ---"); - Rs2GroundItemCache.logState(outputMode); - } - - // Skill Cache State - if (Rs2SkillCache.getInstance() != null) { - log.info("--- Skill Cache Analysis ---"); - Rs2SkillCache.logState(outputMode); - } - - // Varbit Cache State - if (Rs2VarbitCache.getInstance() != null) { - log.info("--- Varbit Cache Analysis ---"); - Rs2VarbitCache.logState(outputMode); - } - - // VarPlayer Cache State - if (Rs2VarPlayerCache.getInstance() != null) { - log.info("--- VarPlayer Cache Analysis ---"); - Rs2VarPlayerCache.logState(outputMode); - } - - // Quest Cache State - if (Rs2QuestCache.getInstance() != null) { - log.info("--- Quest Cache Analysis ---"); - Rs2QuestCache.logState(outputMode); - } - - // Plugin State Summary - log.info("--- Plugin State Summary ---"); - log.info("Active Overlays - NPC: {}, Object: {}, Ground Items: {}, Info Panel: {}", - npcOverlayEnabled, objectOverlayEnabled, groundItemOverlayEnabled, infoPanelEnabled); - - if (dumpToFile) { - log.info("Cache state files written to: ~/.runelite/microbot-plugins/cache/"); - } - - log.info("=== End Cache Analysis ==="); - } - - - - @Subscribe - public void onGameTick(GameTick gameTick) { - tickCounter++; - - // Update overlay configurations if they changed - configureOverlays(); - - // Log cache statistics at configured interval - if (config.logCacheStatsTicks() > 0 && tickCounter % config.logCacheStatsTicks() == 0) { - logPeriodicCacheStats(); - } - } - - /** - * Log periodic cache statistics - */ - private void logPeriodicCacheStats() { - if (!config.verboseLogging()) { - return; - } - - long currentTime = System.currentTimeMillis(); - long timeSinceLastLog = currentTime - lastCacheStatsLogTime; - - log.debug("Periodic Cache Stats ({}ms interval):", timeSinceLastLog); - - if (Rs2NpcCache.getInstance() != null) { - var npcStats = Rs2NpcCache.getInstance().getStatistics(); - log.debug(" NPC Cache: {} entries, {} hits, {} misses", - npcStats.currentSize, npcStats.cacheHits, npcStats.cacheMisses); - } - - if (Rs2ObjectCache.getInstance() != null) { - var objectStats = Rs2ObjectCache.getInstance().getStatistics(); - log.debug(" Object Cache: {} entries, {} hits, {} misses", - objectStats.currentSize, objectStats.cacheHits, objectStats.cacheMisses); - } - - if (Rs2GroundItemCache.getInstance() != null) { - var groundItemStats = Rs2GroundItemCache.getInstance().getStatistics(); - log.debug(" Ground Item Cache: {} entries, {} hits, {} misses", - groundItemStats.currentSize, groundItemStats.cacheHits, groundItemStats.cacheMisses); - } - - lastCacheStatsLogTime = currentTime; - } - - @Subscribe - public void onConfigChanged(ConfigChanged configChanged) { - if (!configChanged.getGroup().equals("rs2cachedebugger")) { - return; - } - - String key = configChanged.getKey(); - - // Handle overlay enable/disable toggles - switch (key) { - case "enableNpcOverlay": - if (config.enableNpcOverlay()) { - enableNpcOverlay(); - } else { - disableNpcOverlay(); - } - break; - - case "enableObjectOverlay": - if (config.enableObjectOverlay()) { - enableObjectOverlay(); - } else { - disableObjectOverlay(); - } - break; - - case "enableGroundItemOverlay": - if (config.enableGroundItemOverlay()) { - enableGroundItemOverlay(); - } else { - disableGroundItemOverlay(); - } - break; - - case "enableInfoPanel": - if (config.enableInfoPanel()) { - enableInfoPanel(); - } else { - disableInfoPanel(); - } - break; - - // Handle filter changes that require reconfiguration - case "npcFilterPreset": - case "npcCustomFilter": - case "npcRenderStyle": - case "npcBorderColor": - case "objectFilterPreset": - case "objectCustomFilter": - case "objectRenderStyle": - case "objectBorderColor": - case "objectShowId": - case "objectShowCoordinates": - case "objectMaxDistance": - case "showGameObjects": - case "showWallObjects": - case "showDecorativeObjects": - case "showGroundObjects": - case "enableObjectTypeColoring": - case "enableObjectCategoryColoring": - case "gameObjectColor": - case "wallObjectColor": - case "decorativeObjectColor": - case "groundObjectColor": - case "bankColor": - case "altarColor": - case "resourceColor": - case "groundItemFilterPreset": - case "groundItemCustomFilter": - case "groundItemRenderStyle": - case "groundItemBorderColor": - configureOverlays(); - break; - } - } - - // Getters for overlay state - public boolean isNpcOverlayEnabled() { - return npcOverlayEnabled; - } - - public boolean isObjectOverlayEnabled() { - return objectOverlayEnabled; - } - - public boolean isGroundItemOverlayEnabled() { - return groundItemOverlayEnabled; - } - - public boolean isInfoPanelEnabled() { - return infoPanelEnabled; - } - - // Getters for performance monitoring - public int getTickCounter() { - return tickCounter; - } - - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/LocationStartNotificationOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/LocationStartNotificationOverlay.java deleted file mode 100644 index fc89dbd585d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/LocationStartNotificationOverlay.java +++ /dev/null @@ -1,145 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example; - -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -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.OverlayPriority; -import net.runelite.client.ui.overlay.components.LineComponent; -import net.runelite.client.ui.overlay.components.PanelComponent; -import net.runelite.client.ui.overlay.components.TitleComponent; - -import java.awt.*; - -/** - * Displays information about location-based start conditions - */ -public class LocationStartNotificationOverlay extends Overlay { - private final SchedulableExamplePlugin plugin; - private final SchedulableExampleConfig config; - private final PanelComponent panelComponent = new PanelComponent(); - - public LocationStartNotificationOverlay(SchedulableExamplePlugin plugin, SchedulableExampleConfig config) { - super(plugin); - setPosition(OverlayPosition.TOP_LEFT); - setLayer(OverlayLayer.ABOVE_SCENE); - setPriority(OverlayPriority.MED); - this.plugin = plugin; - this.config = config; - } - - @Override - public Dimension render(Graphics2D graphics) { - if (!Microbot.isLoggedIn() || !config.enableLocationStartCondition()) { - return null; - } - - panelComponent.getChildren().clear(); - - // Show title - panelComponent.getChildren().add(TitleComponent.builder() - .text("Location Conditions") - .color(Color.WHITE) - .build()); - - if (config.locationStartType() == SchedulableExampleConfig.LocationStartType.BANK) { - // Bank location information - panelComponent.getChildren().add(LineComponent.builder() - .left("Type:") - .right("Bank Location") - .build()); - - panelComponent.getChildren().add(LineComponent.builder() - .left("Target:") - .right(config.bankStartLocation().name()) - .build()); - - panelComponent.getChildren().add(LineComponent.builder() - .left("Distance:") - .right(config.bankDistance() + " tiles") - .build()); - - // Check and show if condition is met - boolean inRange = isNearBank(); - Color statusColor = inRange ? Color.GREEN : Color.RED; - panelComponent.getChildren().add(LineComponent.builder() - .left("Status:") - .right(inRange ? "In Range" : "Out of Range") - .rightColor(statusColor) - .build()); - - } else if (config.locationStartType() == SchedulableExampleConfig.LocationStartType.CUSTOM_AREA) { - // Custom area information - panelComponent.getChildren().add(LineComponent.builder() - .left("Type:") - .right("Custom Area") - .build()); - - if (config.customAreaActive() && config.customAreaCenter() != null) { - WorldPoint center = config.customAreaCenter(); - panelComponent.getChildren().add(LineComponent.builder() - .left("Center:") - .right(center.getX() + ", " + center.getY() + ", " + center.getPlane()) - .build()); - - panelComponent.getChildren().add(LineComponent.builder() - .left("Radius:") - .right(config.customAreaRadius() + " tiles") - .build()); - - // Check and show if condition is met - boolean inArea = plugin.isPlayerInCustomArea(); - Color statusColor = inArea ? Color.GREEN : Color.RED; - panelComponent.getChildren().add(LineComponent.builder() - .left("Status:") - .right(inArea ? "In Area" : "Out of Area") - .rightColor(statusColor) - .build()); - - // Show distance to center if not in area - if (!inArea) { - - WorldPoint playerPos = Rs2Player.getWorldLocation(); - if (playerPos != null) { - int distance = playerPos.distanceTo(center); - panelComponent.getChildren().add(LineComponent.builder() - .left("Distance:") - .right(distance + " tiles away") - .build()); - } - } - } else { - panelComponent.getChildren().add(LineComponent.builder() - .left("Status:") - .right("No Area Defined") - .rightColor(Color.YELLOW) - .build()); - - panelComponent.getChildren().add(LineComponent.builder() - .left("Help:") - .right("Press hotkey to mark area") - .build()); - } - } - - return panelComponent.render(graphics); - } - - /** - * Checks if the player is near the configured bank - */ - private boolean isNearBank() { - WorldPoint playerPos = Rs2Player.getWorldLocation(); - if (playerPos == null) { - return false; - } - - WorldPoint bankPos = config.bankStartLocation().getWorldPoint(); - int maxDistance = config.bankDistance(); - - return (playerPos.getPlane() == bankPos.getPlane() && - playerPos.distanceTo(bankPos) <= maxDistance); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/README.md b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/README.md deleted file mode 100644 index a367c3a8b72..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/README.md +++ /dev/null @@ -1,499 +0,0 @@ -# SchedulableExamplePlugin Documentation - -## Overview -The `SchedulableExamplePlugin` demonstrates how to create a plugin compatible with Microbot's scheduler system. It implements the `ConditionProvider` interface to define configurable conditions for when the plugin should automatically start and stop based on various in-game criteria. This plugin serves as a comprehensive example for developers wanting to create scripts that can be managed by the scheduler framework, providing a template for implementing different types of conditions and state management approaches. - -The plugin provides a practical implementation of conditional logic that can be used to automate tasks in a controlled manner. By integrating with the scheduler, it enables users to create complex automation workflows where multiple plugins can work together in a sequenced manner, each starting and stopping based on specific game conditions. - -## Key Features -- **Seamless integration** with the Microbot scheduler framework -- **Highly configurable** start and stop conditions -- **Location-based start conditions**: - - Bank locations with configurable distance - - Custom areas with adjustable radius -- **Multiple stop condition types**: - - **Time-based**: Run for specified duration - - **Resource collection**: Stop after gathering specific items - - **Loot collection**: Stop after collecting specific drops - - **Item processing**: Stop after crafting/converting items - - **NPC kill counting**: Stop after killing a specific number of NPCs - -## Architecture -The plugin consists of four primary components that work together to provide a complete implementation of a scheduler-compatible plugin: - -1. **SchedulableExamplePlugin** (`SchedulableExamplePlugin.java`) - - Main plugin class implementing `ConditionProvider` and `KeyListener` - - Manages the plugin lifecycle and condition creation - - Handles hotkey inputs for custom area marking - - Serves as the central orchestrator that connects configuration, script execution, and condition evaluation - - Implements the scheduler integration points through the `ConditionProvider` methods - - Maintains state between sessions by saving and loading world locations - -2. **SchedulableExampleConfig** (`SchedulableExampleConfig.java`) - - Configuration interface with `@ConfigGroup` and `@ConfigItem` annotations - - Defines all configurable parameters for the plugin - - Organizes settings into logical sections using `@ConfigSection` annotations - - Provides default values for configuration options - - Includes setter methods for mutable state (like custom area coordinates) - - Uses enums to define valid option sets (like `LocationStartType` and `ProcessTrackingMode`) - - Implements hidden configuration items for internal state persistence - - Creates a hierarchical organization of settings with sections that can be collapsed by default - -3. **LocationStartNotificationOverlay** (`LocationStartNotificationOverlay.java`) - - Visual overlay displaying location-based start condition information - - Provides real-time feedback on condition status - - Uses RuneLite's overlay system to render information on the game screen - - Dynamically updates to show relevant details based on the current configuration - - Shows information about bank locations or custom areas depending on the active configuration - - Implements color-coded status indicators (green for met conditions, red for unmet) - - Displays distance measurements to target locations when relevant - - Updates in real-time as the player moves around the game world - - Provides helpful guidance messages for setting up custom areas - -## Condition Provider Implementation -The `ConditionProvider` interface is the key integration point between the plugin and the scheduler system. By implementing this interface, the plugin can define under what circumstances it should be automatically started or stopped by the scheduler. This provides a powerful abstraction that allows the scheduler to manage multiple plugins without needing to understand their specific functionality. - -### Stop Conditions -The `getStopCondition()` method returns a logical condition determining when the plugin should automatically stop. This method is called by the scheduler to evaluate whether the plugin should be terminated. The implementation combines multiple condition types into a single logical expression that can be evaluated to determine if stopping criteria have been met: - -```java -@Override -public LogicalCondition getStopCondition() { - // Create an OR condition - we'll stop when ANY of the enabled conditions are met - OrCondition orCondition = new OrCondition(); - - // Add enabled conditions based on configuration - if (config.enableTimeCondition()) { - orCondition.addCondition(createTimeCondition()); - } - - if (config.enableLootItemCondition()) { - orCondition.addCondition(createLootItemCondition()); - } - - // Add more conditions... - - // If no conditions were added, add a fallback time condition - if (orCondition.getConditions().isEmpty()) { - orCondition.addCondition(IntervalCondition.createRandomized(Duration.ofMinutes(5), Duration.ofMinutes(5))); - } - - return orCondition; -} -``` - -### Start Conditions -The `getStartCondition()` method returns a logical condition determining when the plugin is allowed to start. The scheduler uses this to determine if the plugin should be automatically started when it's scheduled to run. Unlike stop conditions which should always return a value, start conditions can return `null` to indicate that the plugin can start without any preconditions: - -```java -@Override -public LogicalCondition getStartCondition() { - // Default to no start conditions (always allowed to start) - if (!config.enableLocationStartCondition()) { - return null; - } - - // Create a logical condition for start conditions - LogicalCondition startCondition = null; - - // Create location-based condition based on selected type - if (config.locationStartType() == SchedulableExampleConfig.LocationStartType.BANK) { - // Bank-based start condition - // ... - } else if (config.locationStartType() == SchedulableExampleConfig.LocationStartType.CUSTOM_AREA) { - // Custom area start condition - // ... - } - - return startCondition; -} -``` - -## Detailed Condition Types -The plugin implements several types of conditions that can be used to control when it should start or stop. Each condition type is implemented as a separate method that creates and configures a condition object based on the current plugin configuration. - -### Time Condition -The time condition is the simplest form of stop condition, which will trigger after a specified duration has elapsed. This is useful for limiting the runtime of a plugin to prevent excessive resource usage or to simulate human-like play patterns with regular breaks: -```java -private Condition createTimeCondition() { - int minMinutes = config.minRuntime(); - int maxMinutes = config.maxRuntime(); - - return IntervalCondition.createRandomized( - Duration.ofMinutes(minMinutes), - Duration.ofMinutes(maxMinutes) - ); -} -``` - -### Loot Item Condition -The loot item condition is used to stop the plugin after collecting a specific number of items. This is particularly useful for gathering activities where you want to collect a certain amount of a resource before stopping. The condition supports both AND and OR logical operations, allowing for complex item collection goals: - -```java -private LogicalCondition createLootItemCondition() { - // Parse the comma-separated list of items - List lootItemsList = parseItemList(config.lootItems()); - - boolean andLogical = config.itemsToLootLogical(); - int minLootItems = config.minItems(); - int maxLootItems = config.maxItems(); - - // Create randomized targets for each item - List minLootItemPerPattern = new ArrayList<>(); - List maxLootItemPerPattern = new ArrayList<>(); - - // Generate target counts... - - // Create the appropriate logical condition based on config - if (andLogical) { - return LootItemCondition.createAndCondition( - lootItemsList, - minLootItemPerPattern, - maxLootItemPerPattern, - includeNoted, - allowNoneOwner - ); - } else { - return LootItemCondition.createOrCondition( - lootItemsList, - minLootItemPerPattern, - maxLootItemPerPattern, - includeNoted, - allowNoneOwner - ); - } -} -``` - -### Gathered Resource Condition -The gathered resource condition is similar to the loot item condition but is specifically designed for tracking resources gathered through skilling activities (mining, fishing, woodcutting, etc.). This allows for more precise tracking of gathering activities and can differentiate between items obtained through different methods: - -```java -private LogicalCondition createGatheredResourceCondition() { - // Parse the comma-separated list of resources - List resourcesList = parseItemList(config.gatheredResources()); - - boolean andLogical = config.resourcesLogical(); - int minResources = config.minResources(); - int maxResources = config.maxResources(); - boolean includeNoted = config.includeResourceNoted(); - - // Create target lists with randomized counts for each resource - List minResourcesPerItem = new ArrayList<>(); - List maxResourcesPerItem = new ArrayList<>(); - - for (String resource : resourcesList) { - int minCount = Rs2Random.between(minResources, maxResources); - int maxCount = Rs2Random.between(minCount, maxResources); - - minResourcesPerItem.add(minCount); - maxResourcesPerItem.add(maxCount); - } - - // Create the appropriate logical condition based on configuration - if (andLogical) { - return GatheredResourceCondition.createAndCondition( - resourcesList, - minResourcesPerItem, - maxResourcesPerItem, - includeNoted - ); - } else { - return GatheredResourceCondition.createOrCondition( - resourcesList, - minResourcesPerItem, - maxResourcesPerItem, - includeNoted - ); - } -} - -### Process Item Condition -The process item condition is designed to track item transformation operations such as crafting, smithing, cooking, and other production skills. It can monitor the consumption of source items, the production of target items, or both, making it versatile for various crafting and production tasks: - -```java -private Condition createProcessItemCondition() { - ProcessItemCondition.TrackingMode trackingMode; - - // Map config enum to condition enum - switch (config.trackingMode()) { - case SOURCE_CONSUMPTION: - trackingMode = ProcessItemCondition.TrackingMode.SOURCE_CONSUMPTION; - break; - // Other modes... - } - - // Create the appropriate process item condition based on tracking mode - if (trackingMode == ProcessItemCondition.TrackingMode.SOURCE_CONSUMPTION) { - // If tracking source consumption - // ... - } - // Other tracking modes... -} -``` - -### NPC Kill Count Condition -The NPC kill count condition monitors the number of NPCs killed during the plugin's execution. It supports pattern matching for NPC names and can be configured to track kills per NPC type or the total kill count across all specified NPCs. This is particularly useful for combat training and slayer task automation: - -```java -private LogicalCondition createNpcKillCountCondition() { - // Parse the comma-separated list of NPC names - List npcNamesList = parseItemList(config.npcNames()); - - boolean andLogical = config.npcLogical(); - int minKills = config.minKills(); - int maxKills = config.maxKills(); - boolean killsPerType = config.killsPerType(); - - // If we're counting per NPC type vs. total kills... -} -``` - -## Custom Area Management -The custom area feature allows users to define a specific area in the game world where the plugin should operate. This is implemented through a combination of configuration settings, hotkey handling, and visual overlay feedback. The custom area is defined as a circle with a configurable radius centered on the player's position when the area is created: - -```java -private void toggleCustomArea() { - if (!Microbot.isLoggedIn()) { - log.info("Cannot toggle custom area: Not logged in"); - return; - } - - boolean isActive = config.customAreaActive(); - - if (isActive) { - // Clear the custom area - config.setCustomAreaActive(false); - config.setCustomAreaCenter(null); - log.info("Custom area removed"); - } else { - // Create new custom area at current position - WorldPoint currentPos = null; - if (Microbot.isLoggedIn()){ - currentPos = Rs2Player.getWorldLocation(); - } - if (currentPos != null) { - config.setCustomAreaCenter(currentPos); - config.setCustomAreaActive(true); - log.info("Custom area created at: " + currentPos.toString() + " with radius: " + config.customAreaRadius()); - } - } -} -``` - -## Integration with Scheduler Events -The plugin integrates with the scheduler system by responding to events dispatched by the scheduler. The most important of these is the `PluginScheduleEntry`, which is triggered when the scheduler determines that a plugin should be stopped based on its stop conditions. The plugin handles this event by performing cleanup operations and then requesting that it be disabled: - -```java -@Override -@Subscribe -public void onPluginScheduleEntry(PluginScheduleEntry event) { - // Save location before stopping - if (event.getPlugin() == this) { - config.setLastLocation(Rs2Player.getWorldLocation()); - log.info("Scheduling stop for plugin: {}", event.getPlugin().getClass().getSimpleName()); - - // Schedule the stop operation on the client thread - Microbot.getClientThread().invokeLater(() -> { - try { - Microbot.getPluginManager().setPluginEnabled(this, false); - Microbot.getPluginManager().stopPlugin(this); - } catch (Exception e) { - log.error("Error stopping plugin", e); - } - }); - } -} -``` - -## Helper Methods -The plugin includes several helper methods that provide utility functionality for various aspects of its operation. These methods encapsulate common operations and logic to improve code readability and maintainability: - -```java -private List parseItemList(String itemsString) { - List itemsList = new ArrayList<>(); - if (itemsString != null && !itemsString.isEmpty()) { - String[] itemsArray = itemsString.split(","); - for (String item : itemsArray) { - String trimmedItem = item.trim(); - try { - // Validate regex pattern - java.util.regex.Pattern.compile(trimmedItem); - itemsList.add(trimmedItem); - log.debug("Valid item pattern found: {}", trimmedItem); - } catch (java.util.regex.PatternSyntaxException e) { - log.warn("Invalid regex pattern: '{}' - {}", trimmedItem, e.getMessage()); - } - } - } - return itemsList; -} -``` - -## Usage Guide - -### Setting Up the Plugin - -1. **Enable the plugin** through RuneLite's plugin manager - - Navigate to the plugin list and locate "Schedulable Example" - - Check the checkbox to enable it - - Note that the plugin can also be enabled by the scheduler when appropriate - -2. **Configure desired start/stop conditions** in the plugin's configuration panel: - - Click the configuration icon next to the plugin name - - Expand the various sections to access different types of conditions - - Configure at least one stop condition to ensure the plugin doesn't run indefinitely - - Common configurations include: - - Set time limits (minimum and maximum runtime) - - Define item collection targets (specific items and quantities) - - Configure NPC kill counts for combat activities - - Set up resource gathering goals for skilling activities - - Define item processing targets for crafting and production - -3. **Set up location-based start conditions** if desired: - - Enable the location start condition option - - Choose between bank location or custom area: - - **Bank Location**: Select a predefined bank location and set the maximum distance - - **Custom Area**: Position your character in the desired location and press the configured area marking hotkey - - The location overlay will show you when you're in a valid start position - - For custom areas, you can adjust the radius to control the size of the valid area - -4. **Start the plugin** in one of two ways: - - Manually start it through the plugin manager - - Let the scheduler start it automatically when scheduled and when start conditions are met - -5. **Monitor the plugin's operation**: - - Watch the status messages in the Microbot status area - - Check the overlay for location-based information - - The plugin will update its progress tracking as it runs - -6. **The plugin will automatically stop** when any of the following occurs: - - Any of the enabled stop conditions are satisfied - - The scheduler sends a stop event - - The plugin is manually disabled through the plugin manager - -## Example Configuration - -This configuration would make the plugin: -- Only start when the player is at the Grand Exchange -- Stop after running for 30-45 minutes OR after collecting 100-200 oak logs (whichever happens first) - -``` -enableLocationStartCondition: true -locationStartType: BANK -bankStartLocation: GRAND_EXCHANGE -bankDistance: 5 - -enableTimeCondition: true -minRuntime: 30 -maxRuntime: 45 - -enableLootItemCondition: true -lootItems: "Oak logs" -minItems: 100 -maxItems: 200 -``` - -## Technical Implementation Notes - -### Core Design Patterns and Principles - -1. **Thread Safety** - - The plugin uses `Microbot.getClientThread().invokeLater()` to ensure operations run on the client thread - - This is critical for preventing race conditions and ensuring proper interaction with the game client - - All UI updates and game state modifications should be performed on the client thread - -2. **State Persistence** - - Configuration state is saved between sessions using RuneLite's ConfigManager - - The plugin maintains state across sessions by saving: - - Last known player location - - Custom area definitions - - Configuration parameters - - This allows seamless continuation of tasks even after client restarts - -3. **Random Variance** - - Stop conditions use randomized ranges to add human-like variability - - The `Rs2Random.between()` utility is used to generate random values within configured ranges - - This prevents predictable patterns that might appear bot-like - - Different randomization approaches are used for different types of conditions - -4. **Pattern Matching** - - Item and NPC name matching supports regular expressions for flexibility - - This allows for powerful pattern matching capabilities like: - - Wildcards (e.g., ".*bones.*" to match any item containing "bones") - - Character classes (e.g., "[A-Za-z]+ logs" to match any type of logs) - - Alternations (e.g., "goblin|rat|spider" to match multiple NPC types) - - Regular expression patterns are validated before use to prevent runtime errors - -5. **Logical Composition** - - Conditions can be combined with AND/OR logic for complex triggering - - The `LogicalCondition` interface and its implementations (`AndCondition`, `OrCondition`) provide a composable framework - - This allows for arbitrarily complex condition trees to be constructed - - Each logical condition can contain any mix of primitive conditions or nested logical conditions - -6. **State Machine Pattern** - - The `SchedulableExampleScript` uses a state machine to manage its operation - - Different states handle different aspects of the script's functionality - - Transitions between states occur based on in-game conditions - - This provides a clear, maintainable structure for complex bot logic - -7. **Event-Driven Architecture** - - The plugin responds to events from the scheduler and game client - - Events trigger state changes and condition evaluations - - This decouples the plugin's logic from the specific timing of game updates - -## Extending the Plugin - -### Adding New Condition Types - -To extend the plugin with new types of conditions: - -1. **Create a new condition class** implementing the `Condition` interface - - Define the logic for when the condition is satisfied - - Implement the `reset()` method to reinitialize the condition's state - - Consider extending existing base classes like `ResourceCondition` if appropriate - -2. **Add configuration options** to `SchedulableExampleConfig` - - Create a new configuration section with `@ConfigSection` if needed - - Add configuration items with `@ConfigItem` annotations - - Define appropriate default values and descriptions - - Consider using enums for options with a fixed set of valid values - -3. **Implement a creation method** in `SchedulableExamplePlugin` - - Create a method that constructs and configures your new condition - - Add appropriate logic to handle configuration options - - Include randomization if appropriate for human-like behavior - - Handle edge cases and provide fallback values - -4. **Add the condition** to the appropriate logical group in `getStopCondition()` - - Check if the condition is enabled in the configuration - - Add it to the existing logical condition structure (typically an `OrCondition`) - - Consider how it interacts with other existing conditions - -### Implementing New Features - -To add entirely new functionality to the plugin: - -1. **Extend the script class** with new methods and state management - - Add new states to the state machine if needed - - Implement the logic for the new functionality - - Update the main loop to handle the new states and operations - -2. **Update the configuration interface** with options for the new features - - Group related settings into logical sections - - Provide clear descriptions and default values - - Add validation where appropriate - -3. **Enhance the overlay** if visual feedback is needed - - Add new information to the overlay rendering - - Consider color coding or other visual cues for status - - Ensure the overlay remains uncluttered and informative - -4. **Add new condition types** if needed for the new functionality - - Follow the steps outlined above for adding conditions - - Ensure the conditions properly integrate with the new features - -5. **Update documentation** to reflect the new capabilities - - Document configuration options - - Explain new condition types - - Provide usage examples \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleConfig.java deleted file mode 100644 index ed6b4c9bb49..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleConfig.java +++ /dev/null @@ -1,889 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example; - -import net.runelite.api.coords.WorldPoint; -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.Keybind; -import net.runelite.client.config.Range; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; -import net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example.enums.UnifiedLocation; -import net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example.enums.SpellbookOption; - -import java.awt.event.KeyEvent; - -@ConfigGroup("SchedulableExample") -public interface SchedulableExampleConfig extends Config { - @ConfigSection( - name = "Start Conditions", - description = "Conditions for when the plugin is allowed to start", - position = 0 - ) - String startConditionSection = "startConditions"; - - @ConfigSection( - name = "Location Start Conditions", - description = "Location-based conditions for when the plugin is allowed to start", - position = 1, - closedByDefault = false - ) - String locationStartConditionSection = "locationStartConditions"; - - // Location Start Condition Settings - @ConfigItem( - keyName = "enableLocationStartCondition", - name = "Enable Location Start Condition", - description = "Enable location-based start condition", - position = 0, - section = locationStartConditionSection - ) - default boolean enableLocationStartCondition() { - return false; - } - - @ConfigItem( - keyName = "locationStartType", - name = "Location Type", - description = "Type of location condition to use for starting the plugin", - position = 1, - section = locationStartConditionSection - ) - default LocationStartType locationStartType() { - return LocationStartType.BANK; - } - - @ConfigItem( - keyName = "bankStartLocation", - name = "Bank Location", - description = "Bank location where the plugin should start", - position = 2, - section = locationStartConditionSection - ) - default BankLocation bankStartLocation() { - return BankLocation.GRAND_EXCHANGE; - } - @Range( - min = 10, - max = 100 - ) - @ConfigItem( - keyName = "bankDistance", - name = "Bank Distance (tiles)", - description = "Maximum distance from bank to start the plugin", - position = 3, - section = locationStartConditionSection - ) - default int bankDistance() { - return 20; - } - - @ConfigItem( - keyName = "customAreaActive", - name = "Custom Area Active", - description = "Whether a custom area has been defined using the hotkey", - position = 4, - section = locationStartConditionSection, - hidden = true - ) - default boolean customAreaActive() { - return false; - } - - void setCustomAreaActive(boolean active); - - @ConfigItem( - keyName = "areaMarkHotkey", - name = "Area Mark Hotkey", - description = "Hotkey to mark current position as center of custom area (press again to clear)", - position = 5, - section = locationStartConditionSection - ) - default Keybind areaMarkHotkey() { - return Keybind.NOT_SET; - } - - @ConfigItem( - keyName = "customAreaRadius", - name = "Custom Area Radius (tiles)", - description = "Radius of the custom area around the marked position", - position = 6, - section = locationStartConditionSection - ) - default int customAreaRadius() { - return 10; - } - - @ConfigItem( - keyName = "customAreaCenter", - name = "Custom Area Center", - description = "Center point of the custom area", - position = 7, - section = locationStartConditionSection, - hidden = true - ) - default WorldPoint customAreaCenter() { - return null; - } - - void setCustomAreaCenter(WorldPoint center); - - // Enum for location start types - enum LocationStartType { - BANK("Bank Location"), - CUSTOM_AREA("Custom Area"); - - private final String name; - - LocationStartType(String name) { - this.name = name; - } - - @Override - public String toString() { - return name; - } - } - - enum ProcessTrackingMode { - SOURCE_CONSUMPTION("Source Consumption"), - TARGET_PRODUCTION("Target Production"), - EITHER("Either"), - BOTH("Both"); - - private final String name; - - ProcessTrackingMode(String name) { - this.name = name; - } - - @Override - public String toString() { - return name; - } - } - - - @ConfigSection( - name = "Stop Conditions", - description = "Conditions for when the plugin should stop", - position = 101 - ) - String stopSection = "stopConditions"; - - @ConfigSection( - name = "Time Conditions", - description = "Time-based conditions for stopping the plugin", - position = 102, - closedByDefault = false - ) - String timeConditionSection = "timeConditions"; - - @ConfigSection( - name = "Loot Item Conditions", - description = "Conditions related to looted items", - position = 103, - closedByDefault = false - ) - String lootItemConditionSection = "lootItemConditions"; - - @ConfigSection( - name = "Gathered Resource Conditions", - description = "Conditions related to gathered resources (mining, fishing, etc.)", - position = 104, - closedByDefault = false - ) - String gatheredResourceConditionSection = "gatheredResourceConditions"; - - @ConfigSection( - name = "Process Item Conditions", - description = "Conditions related to processed items (crafting, smithing, etc.)", - position = 105, - closedByDefault = false - ) - String processItemConditionSection = "processItemConditions"; - @ConfigSection( - name = "NPC Conditions", - description = "Conditions related to NPCs", - position = 106, - closedByDefault = false - ) - String npcConditionSection = "npcConditions"; - - @ConfigSection( - name = "Pre/Post Schedule Requirements", - description = "Configure requirements for pre and post schedule tasks", - position = 107, - closedByDefault = false - ) - String prePostScheduleRequirementsSection = "prePostScheduleRequirements"; - - // Time Condition Settings - @ConfigItem( - keyName = "enableTimeCondition", - name = "Enable Time Condition", - description = "Enable time-based stop condition", - position = 0, - section = timeConditionSection - ) - default boolean enableTimeCondition() { - return true; - } - - @ConfigItem( - keyName = "minRuntime", - name = "Minimum Runtime (minutes)", - description = "Minimum time to run before stopping", - position = 1, - section = timeConditionSection - ) - default int minRuntime() { - return 1; - } - - @ConfigItem( - keyName = "maxRuntime", - name = "Maximum Runtime (minutes)", - description = "Maximum time to run before stopping", - position = 2, - section = timeConditionSection - ) - default int maxRuntime() { - return 2; - } - - // Loot Item Condition Settings - @ConfigItem( - keyName = "enableLootItemCondition", - name = "Enable Loot Item Condition", - description = "Enable condition to stop based on looted items", - position = 0, - section = lootItemConditionSection - ) - default boolean enableLootItemCondition() { - return true; - } - - @ConfigItem( - keyName = "lootItems", - name = "Loot Items to Track", - description = "Comma separated list of items. Supports regex patterns (.*bones.*)", - position = 1, - section = lootItemConditionSection - ) - default String lootItems() { - return "Logs"; - } - - @ConfigItem( - keyName = "itemsToLootLogical", - name = "Or(False)/And(True)", - description = "Logical operator for items to loot: False=OR, True=AND", - position = 2, - section = lootItemConditionSection - ) - default boolean itemsToLootLogical() { - return false; - } - - @ConfigItem( - keyName = "minItems", - name = "Minimum Items", - description = "Minimum number of items to loot before stopping", - position = 3, - section = lootItemConditionSection - ) - default int minItems() { - return 5; - } - - @ConfigItem( - keyName = "maxItems", - name = "Maximum Items", - description = "Maximum number of items to loot before stopping", - position = 4, - section = lootItemConditionSection - ) - default int maxItems() { - return 10; - } - - @ConfigItem( - keyName = "includeNoted", - name = "Include Noted Items", - description = "Include noted items in loot tracking", - position = 5, - section = lootItemConditionSection - ) - default boolean includeNoted() { - return false; - } - - @ConfigItem( - keyName = "allowNoneOwner", - name = "Allow None Owner", - description = "Allow items not owned by the player (e.g. items which are spawned)", - position = 6, - section = lootItemConditionSection - ) - default boolean allowNoneOwner() { - return false; - } - - // Gathered Resource Condition Settings - @ConfigItem( - keyName = "enableGatheredResourceCondition", - name = "Enable Gathered Resource Condition", - description = "Enable condition to stop based on gathered resources", - position = 0, - section = gatheredResourceConditionSection - ) - default boolean enableGatheredResourceCondition() { - return false; - } - - @ConfigItem( - keyName = "gatheredResources", - name = "Resources to Track", - description = "Comma separated list of resources to track (e.g. logs,ore,fish)", - position = 1, - section = gatheredResourceConditionSection - ) - default String gatheredResources() { - return "logs"; - } - - @ConfigItem( - keyName = "resourcesLogical", - name = "Or(False)/And(True)", - description = "Logical operator for resources: False=OR, True=AND", - position = 2, - section = gatheredResourceConditionSection - ) - default boolean resourcesLogical() { - return false; - } - - @ConfigItem( - keyName = "minResources", - name = "Minimum Resources", - description = "Minimum number of resources to gather before stopping", - position = 3, - section = gatheredResourceConditionSection - ) - default int minResources() { - return 10; - } - - @ConfigItem( - keyName = "maxResources", - name = "Maximum Resources", - description = "Maximum number of resources to gather before stopping", - position = 4, - section = gatheredResourceConditionSection - ) - default int maxResources() { - return 15; - } - - @ConfigItem( - keyName = "includeResourceNoted", - name = "Include Noted Resources", - description = "Include noted resources in tracking", - position = 5, - section = gatheredResourceConditionSection - ) - default boolean includeResourceNoted() { - return false; - } - - // Process Item Condition Settings - @ConfigItem( - keyName = "enableProcessItemCondition", - name = "Enable Process Item Condition", - description = "Enable condition to stop based on processed items", - position = 0, - section = processItemConditionSection - ) - default boolean enableProcessItemCondition() { - return false; - } - - @ConfigItem( - keyName = "trackingMode", - name = "Tracking Mode", - description = "How to track item processing (source items consumed or target items produced)", - position = 1, - section = processItemConditionSection - ) - default ProcessTrackingMode trackingMode() { - return ProcessTrackingMode.SOURCE_CONSUMPTION; - } - - @ConfigItem( - keyName = "sourceItems", - name = "Source Items", - description = "Comma separated list of source items (e.g. logs,ore)", - position = 2, - section = processItemConditionSection - ) - default String sourceItems() { - return "logs"; - } - - @ConfigItem( - keyName = "targetItems", - name = "Target Items", - description = "Comma separated list of target items (e.g. bow,shield)", - position = 3, - section = processItemConditionSection - ) - default String targetItems() { - return "bow"; - } - - @ConfigItem( - keyName = "minProcessedItems", - name = "Minimum Processed Items", - description = "Minimum number of items to process before stopping", - position = 4, - section = processItemConditionSection - ) - default int minProcessedItems() { - return 5; - } - - @ConfigItem( - keyName = "maxProcessedItems", - name = "Maximum Processed Items", - description = "Maximum number of items to process before stopping", - position = 5, - section = processItemConditionSection - ) - default int maxProcessedItems() { - return 10; - } - // NPC Kill Count Condition Settings - @ConfigItem( - keyName = "enableNpcKillCountCondition", - name = "Enable NPC Kill Count Condition", - description = "Enable condition to stop based on NPC kill count", - position = 0, - section = npcConditionSection - ) - default boolean enableNpcKillCountCondition() { - return false; - } - @ConfigItem( - keyName = "npcNames", - name = "NPCs to Track", - description = "Comma separated list of NPC names to track kills for. Supports regex patterns.", - position = 1, - section = npcConditionSection - ) - default String npcNames() { - return "goblin"; - } - - @ConfigItem( - keyName = "npcLogical", - name = "Or(False)/And(True)", - description = "Logical operator for NPCs: False=OR (any NPC satisfies), True=AND (all NPCs must be killed)", - position = 2, - section = npcConditionSection - ) - default boolean npcLogical() { - return false; - } - - @ConfigItem( - keyName = "minKills", - name = "Minimum Kills", - description = "Minimum number of NPCs to kill before stopping", - position = 3, - section = npcConditionSection - ) - default int minKills() { - return 5; - } - - @ConfigItem( - keyName = "maxKills", - name = "Maximum Kills", - description = "Maximum number of NPCs to kill before stopping", - position = 4, - section = npcConditionSection - ) - default int maxKills() { - return 10; - } - - @ConfigItem( - keyName = "killsPerType", - name = "Count Per NPC Type", - description = "If true, need to kill the specified count of EACH NPC type. If false, count total kills across all types.", - position = 5, - section = npcConditionSection - ) - default boolean killsPerType() { - return true; - } - - - // Location tracking - @ConfigItem( - keyName = "lastLocation", - name = "Last Location", - description = "Last tracked location", - hidden = true - ) - default WorldPoint lastLocation() { - return null; - } - default void setLastLocation(WorldPoint location){ - if (location != null) { - if (Microbot.getConfigManager() != null) { - Microbot.getConfigManager().setConfiguration("SchedulableExample", "lastLocation", location); - } - } - - } - - // Pre/Post Schedule Requirements Configuration - @ConfigItem( - keyName = "enablePrePostRequirements", - name = "Enable Pre/Post Requirements", - description = "Enable pre and post schedule requirements and tasks", - position = 0, - section = prePostScheduleRequirementsSection - ) - default boolean enablePrePostRequirements() { - return false; - } - - @ConfigItem( - keyName = "preScheduleSpellbook", - name = "Pre-Schedule Spellbook", - description = "Spellbook required before starting the plugin (None = no switching)", - position = 1, - section = prePostScheduleRequirementsSection - ) - default SpellbookOption preScheduleSpellbook() { - return SpellbookOption.NONE; - } - - @ConfigItem( - keyName = "postScheduleSpellbook", - name = "Post-Schedule Spellbook", - description = "Spellbook to switch to after plugin completion (None = no switching)", - position = 2, - section = prePostScheduleRequirementsSection - ) - default SpellbookOption postScheduleSpellbook() { - return SpellbookOption.NONE; - } - - @ConfigItem( - keyName = "preScheduleLocation", - name = "Pre-Schedule Location", - description = "Location required before starting the plugin (None = no location requirement)", - position = 3, - section = prePostScheduleRequirementsSection - ) - default UnifiedLocation preScheduleLocation() { - return UnifiedLocation.NONE; - } - - @ConfigItem( - keyName = "postScheduleLocation", - name = "Post-Schedule Location", - description = "Location to move to after plugin completion (None = no location requirement)", - position = 4, - section = prePostScheduleRequirementsSection - ) - default UnifiedLocation postScheduleLocation() { - return UnifiedLocation.NONE; - } - - - - @ConfigItem( - keyName = "enableConditionalItemRequirement", - name = "Enable Alch Conditional Requirement based on Fire Staff/Rune", - description = "Enable the fire staff/fire rune conditional requirement for alching in pre-schedule tasks.", - position = 5, - section = prePostScheduleRequirementsSection - ) - default boolean enableConditionalItemRequirement() { - return false; - } - - - @ConfigItem( - keyName = "enableEquipmentRequirement", - name = "Enable Equipment Requirement", - description = "Enable equipment requirement", - position = 6, - section = prePostScheduleRequirementsSection - ) - default boolean enableEquipmentRequirement() { - return false; - } - - @ConfigItem( - keyName = "enableInventoryRequirement", - name = "Enable Inventory Requirement", - description = "Enable inventory requirement", - position = 7, - section = prePostScheduleRequirementsSection - ) - default boolean enableInventoryRequirement() { - return false; - } - @ConfigItem( - keyName = "enableLootRequirement", - name = "Enable Loot Requirement", - description = "Enable loot requirement for coins near Lumbridge", - position = 8, - section = prePostScheduleRequirementsSection - ) - default boolean enableLootRequirement() { - return false; - } - @ConfigItem( - keyName = "enableShopRequirement", - name = "Enable Shop Requirement", - description = "Enable shop maple longbow, buy from grand exchange as pre-schedule and sell at store on post-schedule", - position = 9, - section = prePostScheduleRequirementsSection - ) - default boolean enableShopRequirement() { - return false; - } - - @ConfigItem( - keyName = "externalRequirements", - name = "Enable External Requirements", - description = "Enable external requirements test for pre and post schedule tasks", - position = 10, - section = prePostScheduleRequirementsSection - ) - default boolean externalRequirements() { - return false; - } - - - - @ConfigSection( - name = "Antiban Testing", - description = "Antiban system testing and configuration", - position = 199, - closedByDefault = true - ) - String antibanTestSection = "antibanTestSection"; - - @ConfigItem( - keyName = "enableAntibanTesting", - name = "Enable Antiban Testing", - description = "Enable antiban features testing including micro breaks", - position = 0, - section = antibanTestSection - ) - default boolean enableAntibanTesting() { - return false; - } - - @ConfigItem( - keyName = "enableMicroBreaks", - name = "Enable Micro Breaks", - description = "Enable micro breaks during plugin execution", - position = 1, - section = antibanTestSection - ) - default boolean enableMicroBreaks() { - return false; - } - - @ConfigItem( - keyName = "microBreakChance", - name = "Micro Break Chance", - description = "Chance (0.0-1.0) of taking a micro break per check", - position = 2, - section = antibanTestSection - ) - @Range(min = 0, max = 100) - default int microBreakChancePercent() { - return 10; // 10% default - } - - @ConfigItem( - keyName = "microBreakDurationMin", - name = "Micro Break Min Duration (minutes)", - description = "Minimum duration for micro breaks in minutes", - position = 3, - section = antibanTestSection - ) - @Range(min = 1, max = 30) - default int microBreakDurationMin() { - return 3; - } - - @ConfigItem( - keyName = "microBreakDurationMax", - name = "Micro Break Max Duration (minutes)", - description = "Maximum duration for micro breaks in minutes", - position = 4, - section = antibanTestSection - ) - @Range(min = 1, max = 60) - default int microBreakDurationMax() { - return 15; - } - - @ConfigItem( - keyName = "statusReportInterval", - name = "Status Report Interval (seconds)", - description = "How often to report break status (0 = disable reporting)", - position = 5, - section = antibanTestSection - ) - @Range(min = 0, max = 300) - default int statusReportInterval() { - return 30; // Report every 30 seconds - } - - @ConfigItem( - keyName = "enableActionCooldowns", - name = "Enable Action Cooldowns", - description = "Enable action cooldown testing", - position = 6, - section = antibanTestSection - ) - default boolean enableActionCooldowns() { - return false; - } - - @ConfigItem( - keyName = "moveMouseOffScreen", - name = "Move Mouse Off-Screen", - description = "Move mouse off-screen during breaks", - position = 7, - section = antibanTestSection - ) - default boolean moveMouseOffScreen() { - return false; - } - - @ConfigSection( - name = "Debug Options", - description = "Options for testing and debugging", - position = 200, - closedByDefault = true - ) - String debugSection = "debugSection"; - - @ConfigItem( - keyName = "aliveReportTimeout", - name = "Alive Report Timeout (sec)", - description = "Time in seconds before script reports it's alive", - position = 0, - section = debugSection - ) - @Range( - min = 10, - max = 100 - ) - default int aliveReportTimeout() { - return 10; - } - - @ConfigItem( - keyName = "finishPluginNotSuccessfulHotkey", - name = "Finish Plugin Not-Successful Hotkey", - description = "Press this hotkey to manually trigger the PluginScheduleEntryMainTaskFinishedEvent for testing not successful completion", - position = 1, - section = debugSection - ) - default Keybind finishPluginNotSuccessfulHotkey() { - return new Keybind(KeyEvent.VK_F2, 0); - } - - @ConfigItem( - keyName = "finishPluginSuccessfulHotkey", - name = "Finish Plugin Hotkey", - description = "Press this hotkey to manually trigger the PluginScheduleEntryMainTaskFinishedEvent for testing successful completion", - position = 2, - section = debugSection - ) - default Keybind finishPluginSuccessfulHotkey() { - return new Keybind(KeyEvent.VK_F3, 0); - } - - - @ConfigItem( - keyName = "finishReason", - name = "Finish Reason", - description = "The reason to report when finishing the plugin", - position = 3, - section = debugSection - ) - default String finishReason() { - return "Task completed successfully"; - } - - @ConfigItem( - keyName = "lockConditionHotkey", - name = "Lock Condition Hotkey", - description = "Press this hotkey to toggle the lock condition (prevents plugin from being stopped)", - position = 4, - section = debugSection - ) - default Keybind lockConditionHotkey() { - return Keybind.NOT_SET; - } - - @ConfigItem( - keyName = "lockDescription", - name = "Lock Reason", - description = "Description of why the plugin is locked", - position = 5, - section = debugSection - ) - default String lockDescription() { - return "Plugin in critical state - do not stop"; - } - - @ConfigItem( - keyName = "testPreScheduleTasksHotkey", - name = "Test Pre-Schedule Tasks Hotkey", - description = "Press this hotkey to test pre-schedule tasks functionality (equipment, spellbook, location setup)", - position = 6, - section = debugSection - ) - default Keybind testPreScheduleTasksHotkey() { - return Keybind.NOT_SET; - } - - @ConfigItem( - keyName = "testPostScheduleTasksHotkey", - name = "Test Post-Schedule Tasks Hotkey", - description = "Press this hotkey to test post-schedule tasks functionality (cleanup, banking, spellbook restoration)", - position = 7, - section = debugSection - ) - default Keybind testPostScheduleTasksHotkey() { - return Keybind.NOT_SET; - } - - @ConfigItem( - keyName = "cancelTasksHotkey", - name = "Cancel & Reset Tasks Hotkey", - description = "Press this hotkey to cancel any running pre/post schedule tasks and reset execution state", - position = 8, - section = debugSection - ) - default Keybind cancelTasksHotkey() { - return Keybind.NOT_SET; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleOverlay.java deleted file mode 100644 index cd5ad772a10..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleOverlay.java +++ /dev/null @@ -1,139 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example; - -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.state.TaskExecutionState; -import net.runelite.client.ui.overlay.OverlayLayer; -import net.runelite.client.ui.overlay.OverlayPanel; -import net.runelite.client.ui.overlay.OverlayPosition; -import net.runelite.client.ui.overlay.components.ComponentConstants; - -import javax.inject.Inject; -import java.awt.*; - -/** - * Overlay for the SchedulableExample plugin that displays the current state of - * pre/post schedule requirements and task execution. - * - * This overlay demonstrates how to integrate the RequirementOverlayComponentFactory - * to provide real-time feedback about requirement fulfillment progress. - */ -public class SchedulableExampleOverlay extends OverlayPanel { - - private final SchedulableExamplePlugin plugin; - - @Inject - public SchedulableExampleOverlay(SchedulableExamplePlugin plugin) { - super(plugin); - this.plugin = plugin; - setPosition(OverlayPosition.TOP_LEFT); - setPreferredSize(new Dimension(ComponentConstants.STANDARD_WIDTH, 200)); - setNaughty(); - setDragTargetable(true); - setLayer(OverlayLayer.UNDER_WIDGETS); - } - - @Override - public Dimension render(Graphics2D graphics) { - // Clear previous components - panelComponent.getChildren().clear(); - - // Get the task manager and requirements - SchedulableExamplePrePostScheduleTasks tasks = (SchedulableExamplePrePostScheduleTasks)plugin.getPrePostScheduleTasks(); - - // Only show overlay if pre/post requirements are enabled or tasks are running - if (!plugin.getConfig().enablePrePostRequirements() && - (tasks == null || !tasks.isExecuting())) { - return null; // Don't show overlay when not needed - } - - try { - // Show concise information only - boolean isExecuting = tasks != null && tasks.isExecuting(); - boolean hasPrePostRequirements = plugin.getConfig().enablePrePostRequirements(); - - // Main title with status indication - String titleText = "SchedulableExample"; - Color titleColor = Color.CYAN; - - if (isExecuting) { - TaskExecutionState executionState = tasks.getExecutionState(); - if (executionState.isInErrorState()) { - titleText += " (ERROR)"; - titleColor = Color.RED; - } else { - titleText += " (ACTIVE)"; - titleColor = Color.YELLOW; - } - } else if (hasPrePostRequirements) { - titleText += " (READY)"; - titleColor = Color.CYAN; - } - - panelComponent.getChildren().add(net.runelite.client.ui.overlay.components.TitleComponent.builder() - .text(titleText) - .color(titleColor) - .build()); - - // Show current status - if (isExecuting) { - TaskExecutionState executionState = tasks.getExecutionState(); - String phase = executionState.getCurrentPhase() != null ? - executionState.getCurrentPhase().toString() : "EXECUTING"; - int progress = executionState.getProgressPercentage(); - String statusText = progress > 0 ? phase + " (" + progress + "%)" : phase; - - panelComponent.getChildren().add(net.runelite.client.ui.overlay.components.LineComponent.builder() - .left("Phase:") - .right(statusText) - .leftColor(Color.WHITE) - .rightColor(Color.YELLOW) - .build()); - - // Show detailed status if available and short enough - String detailedStatus = executionState.getDetailedStatus(); - if (detailedStatus != null && !detailedStatus.isEmpty() && detailedStatus.length() <= 25) { - panelComponent.getChildren().add(net.runelite.client.ui.overlay.components.LineComponent.builder() - .left("Status:") - .right(detailedStatus) - .leftColor(Color.WHITE) - .rightColor(Color.CYAN) - .build()); - } - } else { - // Show requirements status when not executing - String requirementsText = hasPrePostRequirements ? "ENABLED" : "DISABLED"; - Color requirementsColor = hasPrePostRequirements ? Color.GREEN : Color.GRAY; - - panelComponent.getChildren().add(net.runelite.client.ui.overlay.components.LineComponent.builder() - .left("Pre/Post:") - .right(requirementsText) - .leftColor(Color.WHITE) - .rightColor(requirementsColor) - .build()); - } - - // Show essential controls hint - panelComponent.getChildren().add(net.runelite.client.ui.overlay.components.LineComponent.builder() - .left("Hotkeys:") - .right("See config") - .leftColor(Color.WHITE) - .rightColor(Color.LIGHT_GRAY) - .build()); - - } catch (Exception e) { - // Show error in overlay - panelComponent.getChildren().add(net.runelite.client.ui.overlay.components.TitleComponent.builder() - .text("SchedulableExample - ERROR") - .color(Color.RED) - .build()); - - panelComponent.getChildren().add(net.runelite.client.ui.overlay.components.LineComponent.builder() - .left("Error:") - .right(e.getMessage() != null ? e.getMessage() : "Unknown error") - .leftColor(Color.WHITE) - .rightColor(Color.RED) - .build()); - } - - return super.render(graphics); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePlugin.java deleted file mode 100644 index 7fa73314123..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePlugin.java +++ /dev/null @@ -1,978 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example; - - -import java.awt.event.KeyEvent; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; - -import com.google.inject.Provides; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Client; -import net.runelite.api.GameState; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.GameStateChanged; -import net.runelite.client.config.ConfigDescriptor; -import net.runelite.client.config.ConfigManager; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.events.ConfigChanged; -import net.runelite.client.input.KeyListener; -import net.runelite.client.input.KeyManager; -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.pluginscheduler.api.SchedulablePlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.AreaCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.LocationCondition; -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.condition.logical.OrCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.npc.NpcKillCountCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.GatheredResourceCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.LootItemCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ProcessItemCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.event.PluginScheduleEntryPostScheduleTaskEvent; -import net.runelite.client.plugins.microbot.pluginscheduler.event.PluginScheduleEntryPreScheduleTaskEvent; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.AbstractPrePostScheduleTasks; -import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -import net.runelite.client.ui.overlay.OverlayManager; -import net.runelite.client.util.HotkeyListener; - -@PluginDescriptor( - name = "Schedulable Example", - description = "Designed for use with the scheduler and testing its features", - tags = {"microbot", "woodcutting", "combat", "scheduler", "condition"}, - enabledByDefault = false -) -@Slf4j -public class SchedulableExamplePlugin extends Plugin implements SchedulablePlugin, KeyListener { - - - - @Inject - private SchedulableExampleConfig config; - - @Inject - private Client client; - - @Inject - private KeyManager keyManager; - - @Inject - private OverlayManager overlayManager; - - @Inject - private SchedulableExampleOverlay overlay; - - @Provides - SchedulableExampleConfig provideConfig(ConfigManager configManager) { - if (configManager == null) { - log.warn("ConfigManager is null, cannot provide SchedulableExampleConfig"); - return null; - } - return configManager.getConfig(SchedulableExampleConfig.class); - } - - /** - * Gets the plugin configuration. - * - * @return The SchedulableExampleConfig instance - */ - public SchedulableExampleConfig getConfig() { - return config; - } - - private SchedulableExampleScript script; - private WorldPoint lastLocation = null; - private int itemsCollected = 0; - - private LockCondition lockCondition; - private LogicalCondition startCondition = null; - private LogicalCondition stopCondition = null; - - - // Pre/Post Schedule Tasks and Requirements - private SchedulableExamplePrePostScheduleRequirements prePostScheduleRequirements = null; - private SchedulableExamplePrePostScheduleTasks prePostScheduleTasks = null; - - // HotkeyListener for the area marking - private final HotkeyListener areaHotkeyListener = new HotkeyListener(() -> config.areaMarkHotkey()) { - @Override - public void hotkeyPressed() { - toggleCustomArea(); - } - }; - - // HotkeyListener for testing PluginScheduleEntryMainTaskFinishedEvent - private final HotkeyListener finishPluginSuccessHotkeyListener = new HotkeyListener(() -> config.finishPluginSuccessfulHotkey()) { - @Override - public void hotkeyPressed() { - String reason = config.finishReason() + " (success)"; - boolean success = true; - log.info("\nManually triggering plugin finish: reason='{}', success={}", reason, success); - Microbot.getClientThread().invokeLater( () -> {reportFinished(reason, success); return true;}); - } - }; - // HotkeyListener for testing PluginScheduleEntryMainTaskFinishedEvent - private final HotkeyListener finishPluginNotSuccessHotkeyListener = new HotkeyListener(() -> config.finishPluginNotSuccessfulHotkey()) { - @Override - public void hotkeyPressed() { - String reason = config.finishReason()+ " (not success)"; - boolean success = false; - log.info("\nManually triggering plugin finish: reason='{}', success={}", reason, success); - Microbot.getClientThread().invokeLater( () -> {reportFinished(reason, success); return true;}); - } - }; - - // HotkeyListener for toggling the lock condition - private final HotkeyListener lockConditionHotkeyListener = new HotkeyListener(() -> config.lockConditionHotkey()) { - @Override - public void hotkeyPressed() { - log.info("Toggling lock condition for plugin: {}", getName()); - if (stopCondition == null || stopCondition.getConditions().isEmpty()) { - log.warn("Stop condition is not initialized. Cannot toggle lock condition."); - return; - } - boolean newState = toggleLock((Condition)(stopCondition)); - log.info("\n\tLock condition toggled: {}", newState ? "LOCKED - " + config.lockDescription() : "UNLOCKED"); - } - }; - - // HotkeyListener for testing pre-schedule tasks - private final HotkeyListener testPreScheduleTasksHotkeyListener = new HotkeyListener(() -> config.testPreScheduleTasksHotkey()) { - @Override - public void hotkeyPressed() { - // Initialize Pre/Post Schedule Requirements and Tasks if needed - if (config.enablePrePostRequirements()) { - if (getPrePostScheduleTasks() == null) { - log.info("Initializing Pre/Post failed "); - return; - } - // Test only pre-schedule tasks - SchedulableExamplePlugin.this.runPreScheduleTasks(); - - - } else { - log.info("Pre/Post Schedule Requirements are disabled in configuration"); - } - } - }; - - // HotkeyListener for testing post-schedule tasks - private final HotkeyListener testPostScheduleTasksHotkeyListener = new HotkeyListener(() -> config.testPostScheduleTasksHotkey()) { - @Override - public void hotkeyPressed() { - // Initialize Pre/Post Schedule Requirements and Tasks if needed - if (config.enablePrePostRequirements()) { - if (getPrePostScheduleTasks() == null) { - log.info("Initializing Pre/Post failed "); - return; - } - - // Test only post-schedule tasks - runPostScheduleTasks(); - } else { - log.info("Pre/Post Schedule Requirements are disabled in configuration"); - } - } - }; - - // HotkeyListener for cancelling tasks - private final HotkeyListener cancelTasksHotkeyListener = new HotkeyListener(() -> config.cancelTasksHotkey()) { - @Override - public void hotkeyPressed() { - log.info("Cancel tasks hotkey pressed for plugin: {}", getName()); - - if (prePostScheduleTasks != null) { - if (prePostScheduleTasks.isPreScheduleRunning()) { - prePostScheduleTasks.cancelPreScheduleTasks(); - log.info("Cancelled pre-schedule tasks"); - } else if (prePostScheduleTasks.isPostScheduleRunning()) { - prePostScheduleTasks.cancelPostScheduleTasks(); - log.info("Cancelled post-schedule tasks"); - } else { - log.info("No pre/post schedule tasks are currently running"); - } - - // Reset the execution state to allow fresh start - prePostScheduleTasks.reset(); - log.info("Reset pre/post schedule tasks execution state"); - } else { - log.info("No pre/post schedule tasks manager initialized"); - } - } - }; - - @Override - protected void startUp() { - loadLastLocation(); - this.script = new SchedulableExampleScript(); - - - - keyManager.registerKeyListener(this); - - // Register the hotkey listeners - keyManager.registerKeyListener(areaHotkeyListener); - keyManager.registerKeyListener(finishPluginSuccessHotkeyListener); - keyManager.registerKeyListener(finishPluginNotSuccessHotkeyListener); - keyManager.registerKeyListener(lockConditionHotkeyListener); - keyManager.registerKeyListener(testPreScheduleTasksHotkeyListener); - keyManager.registerKeyListener(testPostScheduleTasksHotkeyListener); - keyManager.registerKeyListener(cancelTasksHotkeyListener); - - // Add the overlay - overlayManager.add(overlay); - boolean scheduleMode = Microbot.getConfigManager().getConfiguration( - "SchedulableExample", - "scheduleMode", - Boolean.class - ); - log.info("\n\tSchedulable Example plugin started\n\t -In SchedulerMode:{}\n\t -Press {} to test the PluginScheduleEntryMainTaskFinishedEvent successfully\n\t -Press {} to test the PluginScheduleEntryMainTaskFinishedEvent unsuccessfully\n\t -Use {} to toggle the lock condition (prevents the plugin from being stopped)\n\t -Use {} to test Pre-Schedule Tasks functionality\n\t -Use {} to test Post-Schedule Tasks functionality\n\t -Use {} to cancel running pre/post schedule tasks", - scheduleMode, - config.finishPluginSuccessfulHotkey(), - config.finishPluginNotSuccessfulHotkey(), - config.lockConditionHotkey(), - config.testPreScheduleTasksHotkey(), - config.testPostScheduleTasksHotkey(), - config.cancelTasksHotkey()); - - } - - /** - * Override the default event handler to start the script properly after pre-schedule tasks. - * This follows the same pattern as runPreScheduleTasks() but integrates with the scheduler. - */ - @Subscribe - public void onPluginScheduleEntryPreScheduleTaskEvent(PluginScheduleEntryPreScheduleTaskEvent event) { - - if (event.getPlugin() != this) { - return; // Not for this plugin - } - - log.info("Received PluginScheduleEntryPreScheduleTaskEvent for SchedulableExample plugin"); - - if (prePostScheduleTasks != null && event.isSchedulerControlled() && !prePostScheduleTasks.isPreTaskComplete()) { - // Plugin has pre/post tasks and is under scheduler control - log.info("SchedulableExample starting with pre-schedule tasks from scheduler"); - try { - // Execute pre-schedule tasks with callback to start the script - runPreScheduleTasks(); - } catch (Exception e) { - log.error("Error during Pre-Schedule Tasks for SchedulableExample", e); - } - } - } - - - @Override - protected void shutDown() { - // Clean up PrePostScheduleTasks if initialized - if (prePostScheduleTasks != null) { - try { - prePostScheduleTasks.close(); - log.info("PrePostScheduleTasks cleaned up successfully"); - } catch (Exception e) { - log.error("Error cleaning up PrePostScheduleTasks", e); - } finally { - prePostScheduleTasks = null; - prePostScheduleRequirements = null; - } - } - - if (script != null && script.isRunning()) { - saveCurrentLocation(); - script.shutdown(); - } - unlock((Condition)(stopCondition)); - keyManager.unregisterKeyListener(this); - keyManager.unregisterKeyListener(areaHotkeyListener); - keyManager.unregisterKeyListener(finishPluginSuccessHotkeyListener); - keyManager.unregisterKeyListener(finishPluginNotSuccessHotkeyListener); - keyManager.unregisterKeyListener(lockConditionHotkeyListener); - keyManager.unregisterKeyListener(testPreScheduleTasksHotkeyListener); - keyManager.unregisterKeyListener(testPostScheduleTasksHotkeyListener); - keyManager.unregisterKeyListener(cancelTasksHotkeyListener); - - // Remove the overlay - overlayManager.remove(overlay); - } - - /** - * Toggles the custom area state and updates configuration - */ - private void toggleCustomArea() { - if (!Microbot.isLoggedIn()) { - log.info("Cannot toggle custom area: Not logged in"); - return; - } - - boolean isActive = config.customAreaActive(); - - if (isActive) { - // Clear the custom area - config.setCustomAreaActive(false); - config.setCustomAreaCenter(null); - log.info("Custom area removed"); - } else { - // Create new custom area at current position - WorldPoint currentPos = null; - if (Microbot.isLoggedIn()){ - currentPos = Rs2Player.getWorldLocation(); - } - if (currentPos != null) { - config.setCustomAreaCenter(currentPos); - config.setCustomAreaActive(true); - log.info("Custom area created at: " + currentPos.toString() + " with radius: " + config.customAreaRadius()); - } - } - } - - /** - * Checks if the player is in the custom area - */ - public boolean isPlayerInCustomArea() { - if (!config.customAreaActive() || config.customAreaCenter() == null) { - return false; - } - if (!Microbot.isLoggedIn()) { - return false; - } - WorldPoint currentPos = Rs2Player.getWorldLocation(); - if (currentPos == null) { - return false; - } - - WorldPoint center = config.customAreaCenter(); - int radius = config.customAreaRadius(); - - // Check if player is within radius of the center point and on the same plane - return (currentPos.getPlane() == center.getPlane() && - currentPos.distanceTo(center) <= radius); - } - - private void loadLastLocation() { - WorldPoint savedLocation = config.lastLocation(); - if (savedLocation == null) { - log.warn("No saved location found in config."); - if (Microbot.isLoggedIn()){ - this.lastLocation = Rs2Player.getWorldLocation(); - } - return; - } - this.lastLocation = savedLocation; - } - - private void saveCurrentLocation() { - if (client.getLocalPlayer() != null) { - WorldPoint currentLoc = client.getLocalPlayer().getWorldLocation(); - config.setLastLocation(currentLoc); - } - } - - private LogicalCondition createStopCondition() { - // Create an OR condition - we'll stop when ANY of the enabled conditions are met - OrCondition orCondition = new OrCondition(); - if (this.lockCondition == null) { - this.lockCondition = new LockCondition("Locked because the Plugin "+getName()+" is in a critical operation", false,true); //ensure unlock on shutdown of the plugin ! - } - - // Add enabled conditions based on configuration - if (config.enableTimeCondition()) { - orCondition.addCondition(createTimeCondition()); - } - - if (config.enableLootItemCondition()) { - orCondition.addCondition(createLootItemCondition()); - } - - if (config.enableGatheredResourceCondition()) { - orCondition.addCondition(createGatheredResourceCondition()); - } - - if (config.enableProcessItemCondition()) { - orCondition.addCondition(createProcessItemCondition()); - } - - if (config.enableNpcKillCountCondition()) { - orCondition.addCondition(createNpcKillCountCondition()); - } - - // If no conditions were added, add a fallback time condition - if (orCondition.getConditions().isEmpty()) { - log.warn("No stop conditions were enabled. Adding default time condition of 5 minutes."); - orCondition.addCondition(IntervalCondition.createRandomized(Duration.ofMinutes(5), Duration.ofMinutes(5))); - } - - // Add a lock condition that can be toggled manually - // NOTE: This condition uses AND logic with the other conditions since it's in an AND condition - AndCondition andCondition = new AndCondition(); - //andCondition.addCondition(orCondition); - andCondition.addCondition(lockCondition); - - List all = andCondition.findAllLockConditions(); - log.info("\nCreated stop condition: \n{}"+"\nFound {} lock conditions in stop condition: {}", andCondition.getDescription(), all.size(), all); - - return andCondition; - - } - - - - /** - * Tests only the pre-schedule tasks functionality. - * This method demonstrates how pre-schedule tasks work and logs the results. - */ - private void runPreScheduleTasks() { - if (prePostScheduleTasks != null && !prePostScheduleTasks.isPreTaskRunning() && !prePostScheduleTasks.isPreTaskComplete()) { - executePreScheduleTasks(() -> { - log.info("Pre-Schedule Tasks completed successfully for SchedulableExample"); - // Ensure script is initialized - if (this.script == null) { - this.script = new SchedulableExampleScript(); - } - if (this.script.isRunning()) { - this.script.shutdown(); - } - // Start the actual script after pre-schedule tasks are done - this.script.run(config, lastLocation); - }); - } - - } - private void runPostScheduleTasks( ){ - if (prePostScheduleTasks != null && !prePostScheduleTasks.isPostScheduleRunning() && !prePostScheduleTasks.isPostTaskComplete()) { - executePostScheduleTasks(()->{ - if( this.script != null && this.script.isRunning()) { - this.script.shutdown(); - } - }); - }else { - log.info("Post-Schedule Tasks already completed or running for SchedulableExample"); - } - - - } - - - private LogicalCondition createStartCondition() { - try { - // Default to no start conditions (always allowed to start) - if (!config.enableLocationStartCondition()) { - return null; - } - - // Create a logical condition for start conditions - LogicalCondition startCondition = null; - - // Create location-based condition based on selected type - if (config.locationStartType() == SchedulableExampleConfig.LocationStartType.BANK) { - // Bank-based start condition - BankLocation selectedBank = config.bankStartLocation(); - int distance = config.bankDistance(); - - // Create condition using bank location - startCondition = new OrCondition(); // Use OR to allow multiple possible conditions - Condition bankCondition = LocationCondition.atBank(selectedBank, distance); - ((OrCondition) startCondition).addCondition(bankCondition); - - log.debug("Created bank start condition: " + selectedBank.name() + " within " + distance + " tiles"); - } else if (config.locationStartType() == SchedulableExampleConfig.LocationStartType.CUSTOM_AREA) { - // Custom area start condition - if (config.customAreaActive() && config.customAreaCenter() != null) { - WorldPoint center = config.customAreaCenter(); - int radius = config.customAreaRadius(); - - // Create area condition centered on the saved point - startCondition = new OrCondition(); - AreaCondition areaCondition = LocationCondition.createArea( - "Custom Start Area", - center, - radius * 2, // Width (diameter) - radius * 2 // Height (diameter) - ); - ((OrCondition) startCondition).addCondition(areaCondition); - - log.debug("Created custom area start condition at: " + center + " with radius: " + radius); - } else { - log.warn("Custom area start condition selected but no area is defined"); - // Return null to indicate no start condition (always allowed to start) - return null; - } - } - - return startCondition; - } catch (Exception e) { - log.error("Error creating start condition", e); - e.printStackTrace(); - return new OrCondition(); // Fallback to no conditions - } - } - /** - * Returns a logical condition that determines when the plugin is allowed to start - */ - @Override - public LogicalCondition getStartCondition() { - if (this.startCondition == null) { - this.startCondition = createStartCondition(); - } - return this.startCondition; - - } - @Override - public LogicalCondition getStopCondition() { - // Create a new stop condition - if (this.stopCondition == null) { - this.stopCondition = createStopCondition(); - } - return this.stopCondition; - } - @Override - public AbstractPrePostScheduleTasks getPrePostScheduleTasks() { - SchedulableExampleConfig config = provideConfig(Microbot.getConfigManager()); - if (prePostScheduleRequirements == null || prePostScheduleTasks == null) { - if(Microbot.getClient().getGameState() != GameState.LOGGED_IN) { - log.debug("Schedulable Example - Cannot provide pre/post schedule tasks - not logged in"); - return null; // Return null if not logged in - } - log.info("Initializing Pre/Post Schedule Requirements and Tasks..."); - this.prePostScheduleRequirements = new SchedulableExamplePrePostScheduleRequirements(config); - this.prePostScheduleTasks = new SchedulableExamplePrePostScheduleTasks(this, keyManager,prePostScheduleRequirements); - // Log the requirements status - if (prePostScheduleRequirements.isInitialized()) log.info("\nPrePostScheduleRequirements initialized:\n{}", prePostScheduleRequirements.getDetailedDisplay()); - } - // Return the pre/post schedule tasks instance - return this.prePostScheduleTasks; - } - - /** - * Creates a time-based condition based on config settings - */ - private Condition createTimeCondition() { - // Existing implementation - int minMinutes = config.minRuntime(); - int maxMinutes = config.maxRuntime(); - - return IntervalCondition.createRandomized( - Duration.ofMinutes(minMinutes), - Duration.ofMinutes(maxMinutes) - ); - - } - - /** - * Creates a loot item condition based on config settings - */ - private LogicalCondition createLootItemCondition() { - // Parse the comma-separated list of items - List lootItemsList = parseItemList(config.lootItems()); - if (lootItemsList.isEmpty()) { - log.warn("No valid loot items specified, defaulting to 'Logs'"); - lootItemsList.add("Logs"); - } - - boolean andLogical = config.itemsToLootLogical(); - int minLootItems = config.minItems(); - int maxLootItems = config.maxItems(); - - // Create randomized targets for each item - List minLootItemPerPattern = new ArrayList<>(); - List maxLootItemPerPattern = new ArrayList<>(); - - for (int i = 0; i < lootItemsList.size(); i++) { - int minLoot = Rs2Random.between(minLootItems, maxLootItems); - int maxLoot = Rs2Random.between(minLoot, maxLootItems); - - // Ensure max is not less than min - if (maxLoot < minLoot) { - maxLoot = maxLootItems; - } - - minLootItemPerPattern.add(minLoot); - maxLootItemPerPattern.add(maxLoot); - } - - boolean includeNoted = config.includeNoted(); - boolean allowNoneOwner = config.allowNoneOwner(); - - // Create the appropriate logical condition based on config - if (andLogical) { - return LootItemCondition.createAndCondition( - lootItemsList, - minLootItemPerPattern, - maxLootItemPerPattern, - includeNoted, - allowNoneOwner - ); - } else { - return LootItemCondition.createOrCondition( - lootItemsList, - minLootItemPerPattern, - maxLootItemPerPattern, - includeNoted, - allowNoneOwner - ); - } - } - - /** - * Creates a gathered resource condition based on config settings - */ - private LogicalCondition createGatheredResourceCondition() { - // Parse the comma-separated list of resources - List resourcesList = parseItemList(config.gatheredResources()); - if (resourcesList.isEmpty()) { - log.warn("No valid resources specified, defaulting to 'logs'"); - resourcesList.add("logs"); - } - - boolean andLogical = config.resourcesLogical(); - int minResources = config.minResources(); - int maxResources = config.maxResources(); - boolean includeNoted = config.includeResourceNoted(); - - // Create target lists - List minResourcesPerItem = new ArrayList<>(); - List maxResourcesPerItem = new ArrayList<>(); - - for (int i = 0; i < resourcesList.size(); i++) { - int minCount = Rs2Random.between(minResources, maxResources); - int maxCount = Rs2Random.between(minCount, maxResources); - - // Ensure max is not less than min - if (maxCount < minCount) { - maxCount = maxResources; - } - - minResourcesPerItem.add(minCount); - maxResourcesPerItem.add(maxCount); - } - - // Create the appropriate logical condition - if (andLogical) { - return GatheredResourceCondition.createAndCondition( - resourcesList, - minResourcesPerItem, - maxResourcesPerItem, - includeNoted - ); - } else { - return GatheredResourceCondition.createOrCondition( - resourcesList, - minResourcesPerItem, - maxResourcesPerItem, - includeNoted - ); - } - } - - /** - * Creates a process item condition based on config settings - */ - private Condition createProcessItemCondition() { - ProcessItemCondition.TrackingMode trackingMode; - - // Map config enum to condition enum - switch (config.trackingMode()) { - case SOURCE_CONSUMPTION: - trackingMode = ProcessItemCondition.TrackingMode.SOURCE_CONSUMPTION; - break; - case TARGET_PRODUCTION: - trackingMode = ProcessItemCondition.TrackingMode.TARGET_PRODUCTION; - break; - case EITHER: - trackingMode = ProcessItemCondition.TrackingMode.EITHER; - break; - case BOTH: - trackingMode = ProcessItemCondition.TrackingMode.BOTH; - break; - default: - trackingMode = ProcessItemCondition.TrackingMode.SOURCE_CONSUMPTION; - } - - List sourceItemsList = parseItemList(config.sourceItems()); - List targetItemsList = parseItemList(config.targetItems()); - - int minProcessed = config.minProcessedItems(); - int maxProcessed = config.maxProcessedItems(); - - // Create the appropriate process item condition based on tracking mode - if (trackingMode == ProcessItemCondition.TrackingMode.SOURCE_CONSUMPTION) { - // If tracking source consumption - if (!sourceItemsList.isEmpty()) { - return ProcessItemCondition.forConsumption(sourceItemsList.get(0), - Rs2Random.between(minProcessed, maxProcessed)); - } - } else if (trackingMode == ProcessItemCondition.TrackingMode.TARGET_PRODUCTION) { - // If tracking target production - if (!targetItemsList.isEmpty()) { - return ProcessItemCondition.forProduction(targetItemsList.get(0), - Rs2Random.between(minProcessed, maxProcessed)); - } - } else if (trackingMode == ProcessItemCondition.TrackingMode.BOTH) { - // If tracking both source and target - if (!sourceItemsList.isEmpty() && !targetItemsList.isEmpty()) { - return ProcessItemCondition.forRecipe( - sourceItemsList.get(0), 1, - targetItemsList.get(0), 1, - Rs2Random.between(minProcessed, maxProcessed) - ); - } - } - - // Default fallback - log.warn("Invalid process item configuration, using default"); - return ProcessItemCondition.forConsumption("logs", 10); - } - /** - * Creates an NPC kill count condition based on config settings - */ - private LogicalCondition createNpcKillCountCondition() { - // Parse the comma-separated list of NPC names - List npcNamesList = parseItemList(config.npcNames()); - if (npcNamesList.isEmpty()) { - log.warn("No valid NPC names specified, defaulting to 'goblin'"); - npcNamesList.add("goblin"); - } - - boolean andLogical = config.npcLogical(); - int minKills = config.minKills(); - int maxKills = config.maxKills(); - boolean killsPerType = config.killsPerType(); - - // If we're counting per NPC type, create target lists for each NPC - if (killsPerType) { - List minKillsPerNpc = new ArrayList<>(); - List maxKillsPerNpc = new ArrayList<>(); - - for (int i = 0; i < npcNamesList.size(); i++) { - int minKillCount = Rs2Random.between(minKills, maxKills); - int maxKillCount = Rs2Random.between(minKillCount, maxKills); - - // Ensure max is not less than min - if (maxKillCount < minKillCount) { - maxKillCount = maxKills; - } - - minKillsPerNpc.add(minKillCount); - maxKillsPerNpc.add(maxKillCount); - } - - // Create the appropriate logical condition based on config - if (andLogical) { - return NpcKillCountCondition.createAndCondition( - npcNamesList, - minKillsPerNpc, - maxKillsPerNpc - ); - } else { - return NpcKillCountCondition.createOrCondition( - npcNamesList, - minKillsPerNpc, - maxKillsPerNpc - ); - } - } - // If we're counting total kills across all NPC types - else { - // Generate a single randomized kill count target - int targetMin = minKills; - int targetMax = maxKills; - - // Create multiple individual conditions with same ranges - if (andLogical) { - return NpcKillCountCondition.createAndCondition( - npcNamesList, - targetMin, - targetMax - ); - } else { - return NpcKillCountCondition.createOrCondition( - npcNamesList, - targetMin, - targetMax - ); - } - } - } - - /** - * Helper method to parse a comma-separated list of items - */ - private List parseItemList(String itemsString) { - List itemsList = new ArrayList<>(); - if (itemsString != null && !itemsString.isEmpty()) { - String[] itemsArray = itemsString.split(","); - for (String item : itemsArray) { - String trimmedItem = item.trim(); - try { - // Validate regex pattern - java.util.regex.Pattern.compile(trimmedItem); - itemsList.add(trimmedItem); - log.debug("Valid item pattern found: {}", trimmedItem); - } catch (java.util.regex.PatternSyntaxException e) { - log.warn("Invalid regex pattern: '{}' - {}", trimmedItem, e.getMessage()); - } - } - } - return itemsList; - } - @Override - public ConfigDescriptor getConfigDescriptor() { - if (Microbot.getConfigManager() == null) { - return null; - } - SchedulableExampleConfig conf = Microbot.getConfigManager().getConfig(SchedulableExampleConfig.class); - return Microbot.getConfigManager().getConfigDescriptor(conf); - } - @Override - public void onStopConditionCheck() { - // Update item count when condition is checked - if (script != null) { - itemsCollected = script.getLogsCollected(); - } - } - - // Method for the scheduler to check progress - public int getItemsCollected() { - return itemsCollected; - } - @Subscribe - public void onConfigChanged(ConfigChanged event) - { - final ConfigDescriptor desc = getConfigDescriptor(); - if (desc != null && desc.getGroup() != null && event.getGroup().equals(desc.getGroup().value())) { - - this.startCondition = null; - this.stopCondition = null; - log.info( - "Config change detected for {}: {}={}, config group {}", - getName(), - event.getGroup(), - event.getKey(), - desc.getGroup().value() - ); - if (config.enablePrePostRequirements()) { - if (prePostScheduleTasks != null && !prePostScheduleTasks.isExecuting()) { - if (prePostScheduleRequirements != null) { - prePostScheduleRequirements.setConfig(config); - prePostScheduleRequirements.reset(); - } - // prePostScheduleTasks.reset(); when we allow reexecution of pre/post-schedule tasks on config change - log.info("PrePostScheduleRequirements initialized:\n{}", prePostScheduleRequirements.getDetailedDisplay()); - } - } else { - log.info("Pre/Post Schedule Requirements are disabled in configuration"); - } - } - } - - @Subscribe - public void onGameStateChanged(GameStateChanged event) { - if (event.getGameState() == GameState.LOGGED_IN) { - log.info("GameState changed to LOGGED_IN"); - getPrePostScheduleTasks(); - if( prePostScheduleTasks != null - && prePostScheduleTasks.isScheduleMode() && - !prePostScheduleTasks.isPreTaskComplete() && - !prePostScheduleTasks.isPreScheduleRunning()) { - log.info("Plugin is running in Scheduler Mode - waiting for scheduler to start pre-schedule tasks"); - } else { - log.info("Plugin is running in normal mode"); - } - }else if (event.getGameState() == GameState.LOGIN_SCREEN) { - - } - } - @Override - @Subscribe - public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - // Save location before stopping - if (event.getPlugin() == this) { - WorldPoint currentLocation = null; - if (Microbot.isLoggedIn()) { - currentLocation = Rs2Player.getWorldLocation(); - } - if ( Microbot.getConfigManager() == null) { - log.warn("Cannot save last location - ConfigManager or current location is null"); - return; - } - Microbot.getConfigManager().setConfiguration("SchedulableExample", "lastLocation", currentLocation); - log.info("Scheduling stop for plugin: {}", event.getPlugin().getClass().getSimpleName()); - - runPostScheduleTasks(); - /*try { - Microbot.log("Successfully exited SchedulerExamplePlugin - stopping plugin"); - Microbot.getClientThread().invokeLater(() -> { - Microbot.stopPlugin(this); - return true; - }); - } catch (Exception ex) { - Microbot.log("Error during safe exit: " + ex.getMessage()); - Microbot.getClientThread().invokeLater(() -> { - Microbot.stopPlugin(this); - return true; - }); - }*/ - - - - - // Schedule the stop operation on the client thread - //Microbot.getClientThread().invokeLater(() -> { - // try { - // Microbot.getPluginManager().setPluginEnabled(this, false); - // Microbot.getPluginManager().stopPlugin(this); - // } catch (Exception e) { - // log.error("Error stopping plugin", e); - // } - // }); - } - } - - - - - - - @Override - public void keyTyped(KeyEvent e) { - // Not used - } - - @Override - public void keyPressed(KeyEvent e) { - // Movement handling has been moved to VoxQoL plugin - // This plugin now only handles its core scheduling functionality - } - - - - - - - - - - - - @Override - public void keyReleased(KeyEvent e) { - // Not used but required by the KeyListener interface - } - - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePrePostScheduleRequirements.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePrePostScheduleRequirements.java deleted file mode 100644 index 58183a098a6..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePrePostScheduleRequirements.java +++ /dev/null @@ -1,640 +0,0 @@ - -package net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example; - -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.api.coords.WorldArea; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.gameval.ItemID; -import net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example.enums.UnifiedLocation; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.PrePostScheduleRequirements; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.data.ItemRequirementCollection; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.SpellbookRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.collection.LootRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.ConditionalRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location.LocationRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.OrRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.ShopItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.ShopRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.models.ShopOperation; -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.util.equipment.Rs2Equipment; -import net.runelite.client.plugins.microbot.util.grandexchange.models.TimeSeriesInterval; -import net.runelite.client.plugins.microbot.util.grounditem.models.Rs2SpawnLocation; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.magic.Rs2Spellbook; -import net.runelite.client.plugins.microbot.util.magic.Rs2Staff; -import net.runelite.client.plugins.microbot.util.magic.Runes; -import net.runelite.client.plugins.microbot.util.shop.StoreLocations; -import net.runelite.client.plugins.microbot.util.shop.models.Rs2ShopItem; -import net.runelite.client.plugins.microbot.util.shop.models.Rs2ShopType; - -import java.time.Duration; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.function.BooleanSupplier; - -/** - * Example implementation of PrePostScheduleRequirements for the SchedulableExample plugin. - * This demonstrates configurable requirements that can be enabled/disabled via plugin configuration. - * - * Features demonstrated: - * - Spellbook requirement (optional Lunar spellbook) - * - Location requirements (pre: Varrock West, post: Grand Exchange) - * - Loot requirement (coins near Lumbridge) - * - Equipment requirement (Staff of Air) - * - Inventory requirement (10k coins) - */ -@Slf4j -public class SchedulableExamplePrePostScheduleRequirements extends PrePostScheduleRequirements { - @Setter - private SchedulableExampleConfig config; - - public SchedulableExamplePrePostScheduleRequirements(SchedulableExampleConfig config) { - super("SchedulableExample", "Testing", false); - this.config = config; - } - - /** - * Initialize requirements based on configuration settings. - */ - private boolean initializeConfigurableRequirements() { - if (config == null) { - return false; // Ensure config is initialized before proceeding - } - if (!config.enablePrePostRequirements()) { - return true; // Skip requirements if disabled - } - boolean success = true; - this.getRegistry().clear(); - // Configure spellbook requirements based on dropdown selection - if (!config.preScheduleSpellbook().isNone()) { - SpellbookRequirement preSpellbookRequirement = new SpellbookRequirement( - config.preScheduleSpellbook().getSpellbook(), - TaskContext.PRE_SCHEDULE, - RequirementPriority.MANDATORY, - 7, - "Pre-schedule spellbook: " + config.preScheduleSpellbook().getDisplayName() - ); - this.register(preSpellbookRequirement); - } - - if (!config.postScheduleSpellbook().isNone()) { - SpellbookRequirement postSpellbookRequirement = new SpellbookRequirement( - config.postScheduleSpellbook().getSpellbook(), - TaskContext.POST_SCHEDULE, - RequirementPriority.MANDATORY, - 7, - "Post-schedule spellbook: " + config.postScheduleSpellbook().getDisplayName() - ); - this.register(postSpellbookRequirement); - } - - - - // Configure location requirements based on dropdown selection - if (!config.preScheduleLocation().equals(UnifiedLocation.NONE)) { - LocationRequirement preLocationRequirement; - - // Handle different location types appropriately - switch (config.preScheduleLocation().getType()) { - case BANK: - preLocationRequirement = new LocationRequirement( - (BankLocation) config.preScheduleLocation().getOriginalLocationData(), - true, // use transportation - -1, // no specific world required - TaskContext.PRE_SCHEDULE, - RequirementPriority.MANDATORY - ); - break; - - case DEPOSIT_BOX: - case SLAYER_MASTER: - case FARMING: - case HUNTING: - default: - // For non-bank locations, use WorldPoint - preLocationRequirement = new LocationRequirement( - config.preScheduleLocation().getWorldPoint(), - config.preScheduleLocation().getDisplayName(), - true, // use members - 9, - true, // use transportation - -1, // no specific world required - TaskContext.PRE_SCHEDULE, - RequirementPriority.MANDATORY, - 1, - "Must be at " + config.preScheduleLocation().getDisplayName() + " to begin the schedule" - ); - break; - } - - this.register(preLocationRequirement); - } - - - if (!config.postScheduleLocation().equals(UnifiedLocation.NONE)) { - LocationRequirement postLocationRequirement; - - // Handle different location types appropriately - switch (config.postScheduleLocation().getType()) { - case BANK: - postLocationRequirement = new LocationRequirement( - (BankLocation) config.postScheduleLocation().getOriginalLocationData(), - true, // use transportation - -1, // no specific world required - TaskContext.POST_SCHEDULE, - RequirementPriority.MANDATORY - ); - break; - - case DEPOSIT_BOX: - case SLAYER_MASTER: - case FARMING: - case HUNTING: - default: - // For non-bank locations, use WorldPoint - postLocationRequirement = new LocationRequirement( - config.postScheduleLocation().getWorldPoint(), - config.postScheduleLocation().getDisplayName(), - true, - 10, // acceptable distance - true, // use transportation - -1, // no specific world required - TaskContext.POST_SCHEDULE, - RequirementPriority.MANDATORY - ); - break; - } - - this.register(postLocationRequirement); - } - - - // Loot requirement - Coins near Lumbridge Castle - if (config.enableLootRequirement()) { - List coinSpawns = Arrays.asList( - new WorldPoint(3205, 3229, 0), // Lumbridge Castle ground floor coin spawns - new WorldPoint(3207, 3229, 0), - new WorldPoint(3209, 3229, 0) - ); - - Rs2SpawnLocation coinsSpawnLocation = new Rs2SpawnLocation( - ItemID.COINS, - "Lumbridge Castle", - "Ground Floor - East Wing", - coinSpawns, - false, // Not members only - 0, // Ground floor - Duration.ofSeconds(30) // Respawn time - ); - - LootRequirement coinsLootRequirement = new LootRequirement( - ItemID.COINS, - 5, // Amount to collect - "Test coins collection from Lumbridge Castle spawns", - coinsSpawnLocation - ); - - register(coinsLootRequirement); - } - // Equipment requirement - Staff of Air - if (config.enableEquipmentRequirement()) { - register(new ItemRequirement( - ItemID.STAFF_OF_AIR, - 1, - EquipmentInventorySlot.WEAPON, - -2, // must be equipped (equipment slot enforced) - RequirementPriority.MANDATORY, - 6, - "Staff of Air for basic magic", - TaskContext.PRE_SCHEDULE - )); - ItemRequirementCollection.registerAmuletOfGlory(this, - RequirementPriority.MANDATORY, 4, - TaskContext.PRE_SCHEDULE, - true); - ItemRequirementCollection.registerRingOfDueling(this, - RequirementPriority.MANDATORY, 4, - TaskContext.PRE_SCHEDULE, - true); - ItemRequirementCollection.registerWoodcuttingAxes(this,RequirementPriority.MANDATORY, - TaskContext.PRE_SCHEDULE,-1); // -1 for no inventory slot means the axe can be placed in a any inventory slot, and also be equipped, -2 would mean it can only be equipped - ItemRequirementCollection.registerPickAxes(this, RequirementPriority.MANDATORY, - TaskContext.POST_SCHEDULE); - } - - // Inventory requirement - 10k coins - if (config.enableInventoryRequirement()) { - register(new ItemRequirement( - ItemID.COINS, - 10000, - -1, // inventory slot - RequirementPriority.RECOMMENDED, - 8, - "10,000 coins for general purposes", - TaskContext.PRE_SCHEDULE - )); - - - // Add some basic optional requirements that are always available - register( new ItemRequirement( - ItemID.AIRRUNE, - 50, - null, // no equipment slot - -1, // inventory slot - RequirementPriority.MANDATORY, - 5, - "Basic runes for magic", - TaskContext.PRE_SCHEDULE - )); - register( new ItemRequirement( - ItemID.WATERRUNE, - 50, - null, // no equipment slot - -1, // inventory slot - RequirementPriority.RECOMMENDED, - 5, - "Basic runes for magic", - TaskContext.PRE_SCHEDULE - )); - register( new ItemRequirement( - ItemID.EARTHRUNE, - 50, - null, // no equipment slot - -1, // inventory slot - RequirementPriority.RECOMMENDED, - 5, - "Basic runes for magic", - TaskContext.PRE_SCHEDULE - )); - - // Basic runes for magic - register(new ItemRequirement( - ItemID.LAWRUNE, - 10, - -1, // inventory slot - RequirementPriority.MANDATORY, - 5, - "Law runes for magic", - TaskContext.PRE_SCHEDULE - )); - - // ==================================================================== - // OR REQUIREMENT MODES DEMONSTRATION - // ==================================================================== - // This example demonstrates both OR requirement modes: - // - // 1. ANY_COMBINATION (default): Can fulfill with any combination of food items - // Example: 2 lobsters + 3 swordfish = 5 total food items ✓ - // - // 2. SINGLE_TYPE: Must fulfill with exactly one type of item - // Example: Exactly 5 lobsters OR 5 swordfish OR 5 monkfish ✓ - // But NOT 2 lobsters + 3 swordfish ✗ - // - // You can set the mode using: setOrRequirementMode(OrRequirementMode.SINGLE_TYPE) - // Default mode is ANY_COMBINATION for backward compatibility - // ==================================================================== - - // Basic food for emergencies (demonstrates OR requirement) - register(ItemRequirement.createOrRequirement( - Arrays.asList( - ItemID.LOBSTER, - ItemID.SWORDFISH, - ItemID.MONKFISH, - ItemID.BREAD - ), - 5, - null, // no equipment slot - -1, // inventory slot - RequirementPriority.MANDATORY, - 4, - "Basic food for health restoration (OR requirement - any combination or single type based on mode)", - TaskContext.PRE_SCHEDULE - )); - } - - // Shop requirement - Multi-Item Maple Bow Trading Example - // This demonstrates the unified stock management system with multi-item operations: - // - Single BUY requirement for both maple bow types from Grand Exchange (pre-schedule) - // - Single SELL requirement for both maple bow types to Brian's Archery Supplies (post-schedule) - - // Create shop items for both maple bow types - Rs2ShopItem mapleLongbowGEItem = Rs2ShopItem.createGEItem( - ItemID.MAPLE_LONGBOW, // Maple longbow ID (851) - 0.99, // sell at 0.99% of the GE price fast selling - 1.01 // buy at 101% of the GE price fast buying - ); - - Rs2ShopItem mapleShortbowGEItem = Rs2ShopItem.createGEItem( - ItemID.MAPLE_SHORTBOW, // Maple shortbow ID (853) - 0.99, // sell at 99% of the GE price lasted price - 1.01 // buy at 101% of the GE price lasted price - ); - - // Brian's Archery Supplies shop setup (Rimmington) - WorldPoint brianShopLocation = new WorldPoint(2957, 3204, 0); // Brian's Archery Supplies in Rimmington - WorldArea brianShopArea = new WorldArea(brianShopLocation.getX() - 4, brianShopLocation.getY() - 4, 8, 8, brianShopLocation.getPlane()); - - Rs2ShopItem mapleLongbowShopItem = new Rs2ShopItem( - ItemID.MAPLE_LONGBOW, // Maple longbow ID (851) - "Brian", // Shop NPC name - brianShopArea, // Shop area - Rs2ShopType.ARCHERY_SHOP, - 1.0, // 100% sell rate (from OSRS wiki) - 0.65, // 65% buy rate (from OSRS wiki - Brian buys at 65% value) - 2.0, // Change percent - Map.of(), // No quest requirements - false, // Not members only - "Brian's Archery Supplies in Rimmington", // Notes - Duration.ofMinutes(2), // Restock time: 2 minutes (from OSRS wiki) - 2 // Base stock: 2 maple longbows (from OSRS wiki) - ); - - Rs2ShopItem mapleShortbowShopItem = new Rs2ShopItem( - ItemID.MAPLE_SHORTBOW, // Maple shortbow ID (853) - "Brian", // Shop NPC name - brianShopArea, // Same shop area - Rs2ShopType.ARCHERY_SHOP, - 1.0, // 100% sell rate - 0.65, // 65% buy rate - 2.0, // Change percent - Map.of(), // No quest requirements - false, // Not members only - "Brian's Archery Supplies in Rimmington", // Notes - Duration.ofMinutes(2), // Restock time - 2 // Base stock: 2 maple shortbows - ); - - // Create multi-item shop requirements using the current Map-based system - Map geBuyItems = Map.of( - mapleLongbowGEItem, new ShopItemRequirement(mapleLongbowGEItem, - 2, - 1, - TimeSeriesInterval.FIVE_MINUTES, - true), // 2 longbows, flexible buying - mapleShortbowGEItem, new ShopItemRequirement(mapleShortbowGEItem, - 2, - 1, - TimeSeriesInterval.FIVE_MINUTES, - true) // 2 shortbows, flexible buying - ); - - // Brian's shop: base stock=2, can sell up to 10 per world (max stock=12) - Map brianSellItems = Map.of( - mapleLongbowShopItem, new ShopItemRequirement(mapleLongbowShopItem, 20, 10), // Sell up to 10 longbows per world - mapleShortbowShopItem, new ShopItemRequirement(mapleShortbowShopItem, 20, 10) // Sell up to 10 shortbows per world - ); - - // Single multi-item BUY requirement for Grand Exchange - ShopRequirement buyMapleBowsRequirement = new ShopRequirement( - geBuyItems, - ShopOperation.BUY, - RequirementType.SHOP, - RequirementPriority.MANDATORY, - 8, - "Buy maple bows (longbow x20, shortbow x20) from Grand Exchange (pre-schedule)", - TaskContext.PRE_SCHEDULE - ); - - // Single multi-item SELL requirement for Brian's shop - ShopRequirement sellMapleBowsRequirement = new ShopRequirement( - brianSellItems, - ShopOperation.SELL, - RequirementType.SHOP, - RequirementPriority.MANDATORY, - 8, - "Sell up to 10 maple bows per type to Brian's Archery Supplies (post-schedule)", - TaskContext.POST_SCHEDULE - ); - - if (config.enableShopRequirement()) { - // Register the unified multi-item shop requirements - this.register(buyMapleBowsRequirement); - this.register(sellMapleBowsRequirement); - } - - // Custom Shop Requirement - Hammer and Bucket from nearest General Store - if (config.externalRequirements()) { - // Find the nearest general store that has both hammer and bucket - StoreLocations nearestStore = StoreLocations.getNearestStoreWithAllItems(ItemID.HAMMER, ItemID.BUCKET_EMPTY); - log.info("Nearest general store with hammer and bucket: {}", nearestStore != null ? nearestStore.getName() : "None found"); - if (nearestStore != null) { - // Create shop items for hammer and bucket from the nearest general store - WorldArea storeArea = new WorldArea( - nearestStore.getLocation().getX() - 3, - nearestStore.getLocation().getY() - 3, - 6, 6, - nearestStore.getLocation().getPlane() - ); - - Rs2ShopItem hammerShopItem = new Rs2ShopItem( - ItemID.HAMMER, - nearestStore.getNpcName(), - storeArea, - nearestStore.getShopType(), - nearestStore.getSellRate(), // Standard sell rate for general stores - nearestStore.getBuyRate(), - nearestStore.getChangePercent(), - nearestStore.getQuestRequirements(), - nearestStore.isMembers(), - "Hammer from " + nearestStore.getName(), - Duration.ofMinutes(5), - 5 // Base stock for hammers - ); - - Rs2ShopItem bucketShopItem = new Rs2ShopItem( - ItemID.BUCKET_EMPTY, - nearestStore.getNpcName(), - storeArea, - nearestStore.getShopType(), - nearestStore.getSellRate(), // Standard sell rate for general stores - nearestStore.getBuyRate(), - nearestStore.getChangePercent(), - nearestStore.getQuestRequirements(), - nearestStore.isMembers(), - "Empty bucket from " + nearestStore.getName(), - Duration.ofMinutes(5), - 3 // Base stock for buckets - ); - - // Create shop item requirements - ShopItemRequirement hammerRequirement = new ShopItemRequirement(hammerShopItem, 1, 0); - ShopItemRequirement bucketRequirement = new ShopItemRequirement(bucketShopItem, 1, 0); - - // Create the unified shop requirement for buying both items - Map shopItems = new LinkedHashMap<>(); - shopItems.put(hammerShopItem, hammerRequirement); - shopItems.put(bucketShopItem, bucketRequirement); - - ShopRequirement buyToolsRequirement = new ShopRequirement( - shopItems, - ShopOperation.BUY, - RequirementType.SHOP, - RequirementPriority.MANDATORY, - 7, - "Buy hammer and bucket from nearest general store (" + nearestStore.getName() + ")", - TaskContext.PRE_SCHEDULE - ); - - // Add as custom requirement to test external requirement fulfillment (step 7) - this.addCustomRequirement(buyToolsRequirement, TaskContext.PRE_SCHEDULE); - - log.info("Added custom shop requirement for hammer and bucket from: {}", nearestStore.getName()); - } else { - log.warn("No general store found with both hammer and bucket items"); - success = false; // Mark as failure if no store found - } - } - - // === Alch Conditional Requirement Example === - if (config.enableConditionalItemRequirement()) { - - // Build fire staff requirements (all staves that provide fire runes) - List staffWithFireRunesRequirements = Arrays.stream(Rs2Staff.values()) - .filter(staff -> staff.getRunes().contains(Runes.FIRE) && staff != Rs2Staff.NONE) - .map(staff -> new ItemRequirement( - staff.getItemID(), - 1, - EquipmentInventorySlot.WEAPON, - -2, - RequirementPriority.MANDATORY, - 10, - staff.name() + " equipped", - TaskContext.PRE_SCHEDULE, - null, null, null, null, false)) - .collect(java.util.stream.Collectors.toList()); - // Helper to check if any fire staff is available in inventory or bank - BooleanSupplier hasFireStaffCondition = () -> hasFireStaffAvailable(staffWithFireRunesRequirements); - OrRequirement fireStaffOrRequirement = new OrRequirement( - RequirementPriority.MANDATORY, - "Any fire staff equipped", - TaskContext.PRE_SCHEDULE, - staffWithFireRunesRequirements.toArray(new ItemRequirement[0]) - ); - - ItemRequirement fireRuneRequirement = new ItemRequirement( - ItemID.FIRERUNE, - 5, - -1, - RequirementPriority.MANDATORY, - 10, - "Fire runes in inventory", - TaskContext.PRE_SCHEDULE - ); - - ConditionalRequirement alchConditionalRequirement = new ConditionalRequirement( - RequirementPriority.MANDATORY, - 10, - "Alching: Fire staff or fire runes", - TaskContext.PRE_SCHEDULE, - false - ); - alchConditionalRequirement - .addStep( - () -> { - try { - return !hasFireStaffCondition.getAsBoolean(); - } catch (Throwable t) { - return false; - } - }, - fireRuneRequirement, - "Fire runes in inventory (no fire staff available)" - ) - .addStep( - () -> { - try { - return hasFireStaffCondition.getAsBoolean(); - } catch (Throwable t) { - return false; - } - }, - fireStaffOrRequirement, - "Any fire staff equipped (fire staff available)" - ); - - SpellbookRequirement normalSpellbookRequirement = new SpellbookRequirement( - Rs2Spellbook.MODERN, - TaskContext.PRE_SCHEDULE, - RequirementPriority.MANDATORY, - 10, - "Normal spellbook required for High Alchemy" - ); - - ItemRequirement natureRuneRequirement = new ItemRequirement( - ItemID.NATURERUNE, - 1, - -2, - RequirementPriority.MANDATORY, - 10, - "Nature rune for alching", - TaskContext.PRE_SCHEDULE - ); - - this.register(alchConditionalRequirement); - this.register(normalSpellbookRequirement); - this.register(natureRuneRequirement); - } - - return success; // Return true if all requirements initialized successfully - } - - /** - * Checks if any fire staff from the requirements is available in inventory or bank. - */ - private static boolean hasFireStaffAvailable(List staffReqs) { - int[] staffIds = staffReqs.stream().mapToInt(ItemRequirement::getId).toArray(); - return Rs2Inventory.contains(staffIds) || Rs2Bank.hasItem(staffIds)|| Rs2Equipment.isWearing(staffIds); - } - - - - /** - * Initialize the base item requirements collection. - * This demonstrates basic equipment and inventory requirements. - */ - @Override - protected boolean initializeRequirements() { - if (config == null){ - return false; // Ensure config is initialized before proceeding - } - return initializeConfigurableRequirements(); - } - - /** - * Gets a display string showing which requirements are currently enabled. - * Useful for debugging and logging. - */ - public String getDetailedDisplay() { - StringBuilder sb = new StringBuilder(); - sb.append("SchedulableExample Requirements Status:\n"); - sb.append(" Pre/Post Requirements: ").append(config.enablePrePostRequirements() ? "ENABLED" : "DISABLED").append("\n"); - - if (config.enablePrePostRequirements()) { - // Show new dropdown configurations - sb.append(" - Pre-Schedule Spellbook: ").append(config.preScheduleSpellbook().getDisplayName()).append("\n"); - sb.append(" - Post-Schedule Spellbook: ").append(config.postScheduleSpellbook().getDisplayName()).append("\n"); - sb.append(" - Pre-Schedule Location: ").append(config.preScheduleLocation().getDisplayName()).append("\n"); - sb.append(" - Post-Schedule Location: ").append(config.postScheduleLocation().getDisplayName()).append("\n"); - - // Show legacy configurations - sb.append(" - Loot Requirement: ").append(config.enableLootRequirement() ? "ENABLED (Coins at Lumbridge)" : "DISABLED").append("\n"); - sb.append(" - Equipment Requirement: ").append(config.enableEquipmentRequirement() ? "ENABLED (Staff of Air)" : "DISABLED").append("\n"); - sb.append(" - Inventory Requirement: ").append(config.enableInventoryRequirement() ? "ENABLED (10k Coins)" : "DISABLED").append("\n"); - sb.append(" - Shop Requirement: ").append(config.enableShopRequirement() ? "ENABLED (Hammer & Bucket from nearest general store)" : "DISABLED").append("\n"); - } - sb.append(super.getDetailedDisplay()); - - return sb.toString(); - } - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePrePostScheduleTasks.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePrePostScheduleTasks.java deleted file mode 100644 index 2dfef194ca7..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePrePostScheduleTasks.java +++ /dev/null @@ -1,186 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example; - -import java.util.concurrent.CompletableFuture; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.input.KeyManager; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LockCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.AbstractPrePostScheduleTasks; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.PrePostScheduleRequirements; - -/** - * Implementation of AbstractPrePostScheduleTasks for the SchedulableExample plugin. - * This demonstrates how to implement custom pre and post schedule tasks with requirements. - * - * The class handles: - * - Pre-schedule tasks: Preparation based on configured requirements - * - Post-schedule tasks: Cleanup and resource management - * - Schedule mode detection for proper task execution - * - Requirement fulfillment through the associated PrePostScheduleRequirements - */ -@Slf4j -public class SchedulableExamplePrePostScheduleTasks extends AbstractPrePostScheduleTasks { - - private final SchedulableExamplePlugin examplePlugin; - private final SchedulableExamplePrePostScheduleRequirements requirements; - - /** - * Constructor for SchedulableExamplePrePostScheduleTasks. - * - * @param plugin The SchedulableExamplePlugin instance - * @param requirements The requirements collection for this plugin - */ - public SchedulableExamplePrePostScheduleTasks(SchedulableExamplePlugin plugin, KeyManager keyManager, SchedulableExamplePrePostScheduleRequirements requirements) { - super(plugin,keyManager); - this.examplePlugin = plugin; - this.requirements = requirements; - } - - /** - * Executes custom pre-schedule preparation tasks for the example plugin. - * This method is called AFTER standard requirement fulfillment (equipment, spellbook, location). - * The threading and safety infrastructure is handled by the parent class. - * - * @param lockCondition The lock condition to prevent interruption during critical operations - * @return true if custom preparation was successful, false otherwise - */ - @Override - protected boolean executeCustomPreScheduleTask(CompletableFuture preScheduledFuture, LockCondition lockCondition) { - StringBuilder logBuilder = new StringBuilder(); - logBuilder.append("SchedulableExample: Executing custom pre-schedule tasks...\n"); - - // Check if pre/post requirements are enabled - if (!examplePlugin.getConfig().enablePrePostRequirements()) { - logBuilder.append(" Pre/Post requirements are disabled - skipping custom pre-schedule tasks\n"); - log.info(logBuilder.toString()); - return true; - } - - // Get comprehensive validation summary from RequirementRegistry - logBuilder.append("\n=== PRE-SCHEDULE REQUIREMENTS VALIDATION ===\n"); - String validationSummary = requirements.getRegistry().getValidationSummary(net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext.PRE_SCHEDULE); - logBuilder.append(validationSummary).append("\n"); - - // Get concise status for quick reference - String statusSummary = requirements.getRegistry().getValidationStatusSummary(net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext.PRE_SCHEDULE); - logBuilder.append("Status Summary: ").append(statusSummary).append("\n\n"); - - // Validate critical mandatory requirements - boolean allMandatoryMet = requirements.getRegistry().getAllRequirements().stream() - .filter(req -> req.getPriority() == net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority.MANDATORY) - .filter(req -> req.isPreSchedule()) - .allMatch(net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement::isFulfilled); - - if (!allMandatoryMet) { - logBuilder.append("⚠ïļ WARNING: Some mandatory pre-schedule requirements are not fulfilled\n"); - logBuilder.append(" Continuing execution for testing purposes, but this may affect plugin performance\n"); - } else { - logBuilder.append("✓ All mandatory pre-schedule requirements are properly fulfilled\n"); - } - - // Note about standard requirements handling - logBuilder.append("\n--- Infrastructure Notes ---\n"); - logBuilder.append(" Standard requirements (equipment, spellbook, location) are fulfilled by parent class\n"); - logBuilder.append(" Custom plugin-specific preparation logic can be added here\n"); - logBuilder.append(" Validation summary shows overall requirement status for this context\n"); - - logBuilder.append("\nCustom pre-schedule tasks completed successfully"); - log.info(logBuilder.toString()); - - return true; - } - - /** - * Executes custom post-schedule cleanup tasks for the example plugin. - * This method is called BEFORE standard requirement fulfillment (location, spellbook restoration). - * The threading and safety infrastructure is handled by the parent class. - * - * @param lockCondition The lock condition to prevent interruption during critical operations - * @return true if custom cleanup was successful, false otherwise - */ - @Override - protected boolean executeCustomPostScheduleTask(CompletableFuture postScheduledFuture, LockCondition lockCondition) { - StringBuilder logBuilder = new StringBuilder(); - logBuilder.append("SchedulableExample: Executing custom post-schedule tasks...\n"); - - // Check if pre/post requirements are enabled - if (!examplePlugin.getConfig().enablePrePostRequirements()) { - logBuilder.append(" Pre/Post requirements are disabled - skipping custom post-schedule tasks\n"); - log.info(logBuilder.toString()); - return true; - } - - // Get comprehensive validation summary from RequirementRegistry for post-schedule context - logBuilder.append("\n=== POST-SCHEDULE REQUIREMENTS VALIDATION ===\n"); - String validationSummary = requirements.getRegistry().getValidationSummary(net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext.POST_SCHEDULE); - logBuilder.append(validationSummary).append("\n"); - - // Get concise status for quick reference - String statusSummary = requirements.getRegistry().getValidationStatusSummary(net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext.POST_SCHEDULE); - logBuilder.append("Status Summary: ").append(statusSummary).append("\n\n"); - - // Session completion summary - logBuilder.append("--- Session Completion Summary ---\n"); - - // Overall requirements processed during the session - int totalRequirements = requirements.getRegistry().getAllRequirements().size(); - int externalRequirements = requirements.getRegistry().getExternalRequirements(net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext.BOTH).size(); - - logBuilder.append(" Total requirements processed: ").append(totalRequirements).append("\n"); - if (externalRequirements > 0) { - logBuilder.append(" External requirements: ").append(externalRequirements).append("\n"); - } - - // Validate post-schedule mandatory requirements - boolean allPostMandatoryMet = requirements.getRegistry().getAllRequirements().stream() - .filter(req -> req.getPriority() == net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority.MANDATORY) - .filter(req -> req.isPostSchedule()) - .allMatch(net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement::isFulfilled); - - if (!allPostMandatoryMet) { - logBuilder.append("⚠ïļ WARNING: Some mandatory post-schedule requirements are not fulfilled\n"); - } else if (requirements.getRegistry().getAllRequirements().stream().anyMatch(req -> req.getPriority() == net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority.MANDATORY && req.isPostSchedule())) { - logBuilder.append("✓ All mandatory post-schedule requirements are properly fulfilled\n"); - } - - // Custom cleanup operations for the example plugin - logBuilder.append("\n--- Custom Plugin Cleanup ---\n"); - logBuilder.append(" ✓ Example-specific inventory cleanup completed\n"); - logBuilder.append(" ✓ Example-specific session data saved\n"); - logBuilder.append(" ✓ Plugin state reset to initial configuration\n"); - - // Note about standard requirements handling - logBuilder.append("\n--- Infrastructure Notes ---\n"); - logBuilder.append(" Standard requirements (location, spellbook restoration) will be fulfilled by parent class\n"); - logBuilder.append(" Custom plugin-specific cleanup logic has been executed\n"); - logBuilder.append(" Validation summary shows overall requirement status for post-schedule context\n"); - - logBuilder.append("\nCustom post-schedule tasks completed successfully"); - log.info(logBuilder.toString()); - - return true; - } - - - - /** - * Implementation of the abstract method from AbstractPrePostScheduleTasks. - * Returns the PrePostScheduleRequirements instance for this plugin. - * - * @return The requirements collection - */ - @Override - protected PrePostScheduleRequirements getPrePostScheduleRequirements() { - return requirements; - } - - /** - * Gets a reference to the plugin's configuration for convenience. - * - * @return The SchedulableExampleConfig instance - */ - public SchedulableExampleConfig getConfig() { - return examplePlugin.getConfig(); - } - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleScript.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleScript.java deleted file mode 100644 index d1dbe17cbb3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleScript.java +++ /dev/null @@ -1,495 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example; - -import net.runelite.api.Constants; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.gameval.ItemID; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.Script; -import net.runelite.client.plugins.microbot.breakhandler.BreakHandlerScript; -import net.runelite.client.plugins.microbot.util.antiban.Rs2Antiban; -import net.runelite.client.plugins.microbot.util.antiban.Rs2AntibanSettings; -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.equipment.Rs2Equipment; -import net.runelite.client.plugins.microbot.util.events.PluginPauseEvent; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -// import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.regex.Pattern; -import lombok.extern.slf4j.Slf4j; -@Slf4j -public class SchedulableExampleScript extends Script { - private SchedulableExampleConfig config; - private WorldPoint returnPoint; - private int logsCollected = 0; - - // Antiban testing state - private boolean antibanOriginalTakeMicroBreaks = false; - private double antibanOriginalMicroBreakChance = 0.0; - private int antibanOriginalMicroBreakDurationLow = 3; - private int antibanOriginalMicroBreakDurationHigh = 15; - private boolean antibanOriginalActionCooldownActive = false; - private boolean antibanOriginalMoveMouseOffScreen = false; - private long lastStatusReport = 0; - private long lastBreakStatusCheck = 0; - private boolean wasOnBreak = false; - private long breakStartTime = 0; - private long totalBreakTime = 0; - private int microBreakCount = 0; - private boolean antibanInitialized = false; - private int aliveCounter = 0; // Counter to track when to report alive - - enum State { - IDELE, - RESETTING, - BREAK_PAUSED - } - - private State state = State.IDELE; - - - - public boolean main(SchedulableExampleConfig config) { - if (!Microbot.isLoggedIn()) return false; - if (!super.run()) return false; - - // Initialize antiban settings if enabled - if (config.enableAntibanTesting() && !antibanInitialized) { - setupAntibanTesting(); - } - - // Check break status and handle state changes - handleBreakStatusChecks(); - - // Set initial location if none was saved - if (initialPlayerLocation == null) { - initialPlayerLocation = Rs2Player.getWorldLocation(); - } - - if (this.returnPoint == null) { - this.returnPoint = initialPlayerLocation; - } - - // Check if we have an axe - if (!hasAxe()) { - // Microbot.status = "No axe found! Stopping..."; - // return false; - } - - // Handle break pause state - don't do anything while on break - if (state == State.BREAK_PAUSED) { - return true; - } - - // Skip if player is moving or animating, unless resetting - if (state != State.RESETTING && (Rs2Player.isMoving() || Rs2Player.isAnimating())) { - return true; - } - - // Trigger antiban behaviors if enabled - if (config.enableAntibanTesting()) { - handleAntibanBehaviors(); - } - - switch (state) { - case IDELE: - if (Rs2Inventory.isFull()) { - state = State.RESETTING; - return true; - } - break; - - case RESETTING: - resetInventory(); - return true; - - case BREAK_PAUSED: - // Already handled above - return true; - } - - return true; - } - - public boolean run(SchedulableExampleConfig config, WorldPoint savedLocation) { - this.returnPoint = savedLocation; - this.config = config; - this.aliveCounter = 0; - this.mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { - try { - if (!Microbot.isLoggedIn()) return; - if (!super.run()) return; - log.info("aliveCounter: {}", aliveCounter); - // Call the main method with antiban testing - main(config); - // Increment counter and check if we should report alive - aliveCounter++; - // Compute iterations from milliseconds, clamp to at least 1 - final long periodMs = Constants.GAME_TICK_LENGTH * 2L; - final long timeoutMs = Math.max(0L, (long) config.aliveReportTimeout()*1000L); - final int reportThreshold = (int) Math.max(1L, (long) Math.ceil(timeoutMs / (double) periodMs)); - if (aliveCounter >= reportThreshold) { - Rs2ItemModel oneDosePrayerRegeneration= Rs2ItemModel.createFromCache(ItemID._1DOSE1PRAYER_REGENERATION,1,1); - List equipmentActions =oneDosePrayerRegeneration.getEquipmentActions(); - boolean isTradeable = oneDosePrayerRegeneration.isTradeable(); - log.info("{}",oneDosePrayerRegeneration.toString() ); - Rs2ItemModel graceFullHelm= Rs2ItemModel.createFromCache(ItemID.GRACEFUL_HOOD,1,1); - List graceFullHelmActions = graceFullHelm.getEquipmentActions(); - boolean isGraceFullHelmTradeable = graceFullHelm.isTradeable(); - log.info("{}",graceFullHelm.toString() ); - log.info("SchedulableExampleScript is alive! \n- PauseEvent {} (valid),pauseAllScripts: {}, BreakHanlderLook: {}", PluginPauseEvent.isPaused(), Microbot.pauseAllScripts.get(), BreakHandlerScript.lockState.get()); - aliveCounter = 0; // Reset counter - } - - return; //manuel play testing the Scheduler plugin.. doing nothing for now - } catch (Exception ex) { - Microbot.log("SchedulableExampleScript error: " + ex.getMessage()); - } - }, 0, Constants.GAME_TICK_LENGTH*2, TimeUnit.MILLISECONDS); - - return true; - } - - private boolean hasAxe() { - return Rs2Inventory.hasItem("axe") || Rs2Equipment.isWearing("axe"); - } - - private void resetInventory() { - // Update count before moving to next state - updateItemCount(); - state = State.IDELE; - } - - /* - private void bankItems() { - Microbot.status = "Banking "; - - // Find and use nearest bank - if (!Rs2Bank.isOpen()) { - if (!Rs2Bank.useBank()) { - return; - } - } - - // Deposit logs but keep axe - Rs2Bank.depositAllExcept("axe"); - Rs2Bank.closeBank(); - - // Return to woodcutting spot - walkToReturnPoint(); - } - */ - private List getLootItemPatterns(){ - String lootItemsString = config.lootItems(); - List lootItemsList = new ArrayList<>(); - List lootItemsListPattern = new ArrayList<>(); - if (lootItemsString != null && !lootItemsString.isEmpty()) { - String[] lootItemsArray = lootItemsString.split(","); - for (String item : lootItemsArray) { - String trimmedItem = item.trim(); - try { - // Validate regex pattern - lootItemsListPattern.add(java.util.regex.Pattern.compile(trimmedItem)); - lootItemsList.add(trimmedItem); - } catch (java.util.regex.PatternSyntaxException e) { - //log.warn("Invalid regex pattern: '{}' - {}", trimmedItem, e.getMessage()); - } - } - } - return lootItemsListPattern; - } - /* - private void dropItems() { - Microbot.status = "Dropping Items"; - - // Drop all logs - List lootItemsListPattern = getLootItemPatterns(); - //List foundItems = - Rs2Inventory.all().forEach(item -> { - if (lootItemsListPattern.stream().anyMatch(pattern -> pattern.matcher(item.getName()).find())) { - - }else{ - // Drop all logs - Rs2Inventory.dropAll(item.getName()); - } - - }); - // Drop all logs - - } - - - - private void walkToReturnPoint() { - if (Rs2Player.getWorldLocation().distanceTo(returnPoint) > 3) { - Rs2Walker.walkTo(returnPoint); - } - } - */ - - public void updateItemCount() { - List lootItemsListPattern = getLootItemPatterns(); - int currentItems =Rs2Inventory.all().stream().filter(item -> lootItemsListPattern.stream().anyMatch(pattern -> pattern.matcher(item.getName()).find())).mapToInt(Rs2ItemModel::getQuantity).sum(); - - - if (currentItems > 0) { - logsCollected += currentItems; - Microbot.log("Total logs collected: " + logsCollected); - } - } - - public int getLogsCollected() { - return logsCollected; - } - - @Override - public void shutdown() { - // Teardown antiban testing if it was initialized - if (config != null && config.enableAntibanTesting()) { - teardownAntibanTesting(); - } - - Microbot.log("Shutting down SchedulableExampleScript"); - super.shutdown(); - returnPoint = null; - - // Log final antiban stats if testing was enabled - if (config != null && config.enableAntibanTesting()) { - Microbot.log("Final " + getAntibanStats()); - } - } - - /** - * Sets up antiban testing configuration based on plugin config - */ - private void setupAntibanTesting() { - if (antibanInitialized) { - return; - } - - Microbot.log("Setting up antiban testing..."); - - // Store original antiban settings - antibanOriginalTakeMicroBreaks = Rs2AntibanSettings.takeMicroBreaks; - antibanOriginalMicroBreakChance = Rs2AntibanSettings.microBreakChance; - antibanOriginalMicroBreakDurationLow = Rs2AntibanSettings.microBreakDurationLow; - antibanOriginalMicroBreakDurationHigh = Rs2AntibanSettings.microBreakDurationHigh; - antibanOriginalActionCooldownActive = Rs2AntibanSettings.actionCooldownActive; - antibanOriginalMoveMouseOffScreen = Rs2AntibanSettings.moveMouseOffScreen; - - // Apply test configuration - if (config.enableMicroBreaks()) { - Rs2AntibanSettings.takeMicroBreaks = true; - Rs2AntibanSettings.microBreakChance = config.microBreakChancePercent() / 100.0; - Rs2AntibanSettings.microBreakDurationLow = config.microBreakDurationMin(); - Rs2AntibanSettings.microBreakDurationHigh = config.microBreakDurationMax(); - - Microbot.log("Micro breaks enabled - Chance: " + (config.microBreakChancePercent()) + - "%, Duration: " + config.microBreakDurationMin() + "-" + config.microBreakDurationMax() + " minutes"); - } - - if (config.enableActionCooldowns()) { - Rs2AntibanSettings.actionCooldownActive = true; - Microbot.log("Action cooldowns enabled"); - } - - if (config.moveMouseOffScreen()) { - Rs2AntibanSettings.moveMouseOffScreen = true; - Microbot.log("Mouse off-screen movement enabled"); - } - - // Set antiban activity - Rs2Antiban.setActivity(Activity.GENERAL_WOODCUTTING); - Rs2Antiban.setActivityIntensity(ActivityIntensity.MODERATE); - - antibanInitialized = true; - Microbot.log("Antiban testing setup complete"); - } - - /** - * Restores original antiban settings - */ - private void teardownAntibanTesting() { - if (!antibanInitialized) { - return; - } - - Microbot.log("Restoring original antiban settings..."); - - // Restore original settings - Rs2AntibanSettings.takeMicroBreaks = antibanOriginalTakeMicroBreaks; - Rs2AntibanSettings.microBreakChance = antibanOriginalMicroBreakChance; - Rs2AntibanSettings.microBreakDurationLow = antibanOriginalMicroBreakDurationLow; - Rs2AntibanSettings.microBreakDurationHigh = antibanOriginalMicroBreakDurationHigh; - Rs2AntibanSettings.actionCooldownActive = antibanOriginalActionCooldownActive; - Rs2AntibanSettings.moveMouseOffScreen = antibanOriginalMoveMouseOffScreen; - - // Reset antiban activity - Rs2Antiban.resetAntibanSettings(); - - antibanInitialized = false; - Microbot.log("Antiban settings restored"); - } - - /** - * Handles break status monitoring and state transitions - */ - private void handleBreakStatusChecks() { - long currentTime = System.currentTimeMillis(); - - // Check break status every second - if (currentTime - lastBreakStatusCheck < 1000) { - return; - } - lastBreakStatusCheck = currentTime; - - boolean isCurrentlyOnBreak = BreakHandlerScript.isBreakActive() || - Rs2AntibanSettings.microBreakActive || - Rs2AntibanSettings.actionCooldownActive; - - // Detect break start - if (isCurrentlyOnBreak && !wasOnBreak) { - handleBreakStart(); - } - // Detect break end - else if (!isCurrentlyOnBreak && wasOnBreak) { - handleBreakEnd(); - } - - // Report status periodically if on break - if (isCurrentlyOnBreak && config.statusReportInterval() > 0) { - if (currentTime - lastStatusReport >= config.statusReportInterval() * 1000) { - reportBreakStatus(); - lastStatusReport = currentTime; - } - } - - wasOnBreak = isCurrentlyOnBreak; - } - - /** - * Handles the start of a break - */ - private void handleBreakStart() { - breakStartTime = System.currentTimeMillis(); - state = State.BREAK_PAUSED; - - String breakType = getBreakType(); - Microbot.log("Break started - Type: " + breakType + ", Script state: PAUSED"); - - if (Rs2AntibanSettings.microBreakActive) { - microBreakCount++; - } - } - - /** - * Handles the end of a break - */ - private void handleBreakEnd() { - if (breakStartTime > 0) { - long breakDuration = System.currentTimeMillis() - breakStartTime; - totalBreakTime += breakDuration; - - String breakType = getBreakType(); - Microbot.log("Break ended - Type: " + breakType + - ", Duration: " + formatDuration(breakDuration) + - ", Script state: RESUMED"); - - breakStartTime = 0; - } - - // Resume normal operation - if (state == State.BREAK_PAUSED) { - state = State.IDELE; - } - } - - /** - * Reports current break status - */ - private void reportBreakStatus() { - String breakType = getBreakType(); - long currentBreakDuration = breakStartTime > 0 ? - System.currentTimeMillis() - breakStartTime : 0; - - Microbot.log("Break Status - Type: " + breakType + - ", Current Duration: " + formatDuration(currentBreakDuration) + - ", Total Break Time: " + formatDuration(totalBreakTime) + - ", Micro Breaks: " + microBreakCount); - } - - /** - * Determines the current break type - */ - private String getBreakType() { - if (BreakHandlerScript.isBreakActive()) { - return "Regular Break"; - } else if (Rs2AntibanSettings.microBreakActive) { - return "Micro Break"; - } else if (Rs2AntibanSettings.actionCooldownActive) { - return "Action Cooldown"; - } else { - return "Unknown"; - } - } - - /** - * Handles antiban behaviors during normal operation - */ - private void handleAntibanBehaviors() { - // Trigger action cooldown occasionally - if (config.enableActionCooldowns() && Math.random() < 0.01) { // 1% chance per tick - Rs2Antiban.actionCooldown(); - } - - // Take micro breaks by chance - if (config.enableMicroBreaks() && Math.random() < (config.microBreakChancePercent() / 100.0 )) { - Rs2Antiban.takeMicroBreakByChance(); - if (Rs2AntibanSettings.microBreakActive) { - Microbot.log("Taking a new micro break - Count: " + microBreakCount); - } - } - - // Move mouse randomly - if (config.moveMouseOffScreen() && Math.random() < 0.005) { // 0.5% chance per tick - Rs2Antiban.moveMouseRandomly(); - } - } - - /** - * Formats a duration in milliseconds to a readable string - */ - private String formatDuration(long durationMillis) { - long seconds = durationMillis / 1000; - long minutes = seconds / 60; - long hours = minutes / 60; - - if (hours > 0) { - return String.format("%dh %dm %ds", hours, minutes % 60, seconds % 60); - } else if (minutes > 0) { - return String.format("%dm %ds", minutes, seconds % 60); - } else { - return String.format("%ds", seconds); - } - } - - /** - * Gets comprehensive antiban testing statistics - */ - public String getAntibanStats() { - if (!config.enableAntibanTesting()) { - return "Antiban testing disabled"; - } - - return String.format("Antiban Stats - Total Break Time: %s, Micro Breaks: %d, " + - "Current State: %s, Break Active: %s", - formatDuration(totalBreakTime), - microBreakCount, - state.name(), - wasOnBreak ? getBreakType() : "None"); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/enums/SpellbookOption.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/enums/SpellbookOption.java deleted file mode 100644 index c23bd3de976..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/enums/SpellbookOption.java +++ /dev/null @@ -1,49 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example.enums; - -import net.runelite.client.plugins.microbot.util.magic.Rs2Spellbook; -import lombok.Getter; - -/** - * Unified spellbook enum that includes a "NONE" option for configurations - * where no spellbook switching is desired. - */ -@Getter -public enum SpellbookOption { - - NONE("None (No Switching)", null), - MODERN("Standard/Modern Spellbook", Rs2Spellbook.MODERN), - ANCIENT("Ancient Magicks", Rs2Spellbook.ANCIENT), - LUNAR("Lunar Spellbook", Rs2Spellbook.LUNAR), - ARCEUUS("Arceuus Spellbook", Rs2Spellbook.ARCEUUS); - - private final String displayName; - private final Rs2Spellbook spellbook; - - SpellbookOption(String displayName, Rs2Spellbook spellbook) { - this.displayName = displayName; - this.spellbook = spellbook; - } - - /** - * Gets the Rs2Spellbook enum value, or null if this is the NONE option - * - * @return Rs2Spellbook enum value or null - */ - public Rs2Spellbook getSpellbook() { - return spellbook; - } - - /** - * Checks if this option represents no spellbook switching - * - * @return true if this is the NONE option - */ - public boolean isNone() { - return this == NONE; - } - - @Override - public String toString() { - return displayName; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/enums/UnifiedLocation.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/enums/UnifiedLocation.java deleted file mode 100644 index af6bfd6197d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/enums/UnifiedLocation.java +++ /dev/null @@ -1,190 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example.enums; - -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; -import net.runelite.client.plugins.microbot.util.depositbox.DepositBoxLocation; -import net.runelite.client.plugins.microbot.util.walker.enums.*; -import lombok.Getter; - -/** - * Unified location enum that encompasses major location types used in the walker system. - * This provides a single interface for selecting locations from various categories - * including banks, deposit boxes, farming locations, slayer masters, and hunting areas. - * - * This is a simplified version containing only the most commonly used locations - * to avoid compatibility issues with varying enum implementations. - */ -@Getter -public enum UnifiedLocation { - - // None option - NONE("None", LocationType.NONE, null), - - // Major Bank Locations - BANK_GRAND_EXCHANGE("Grand Exchange Bank", LocationType.BANK, BankLocation.GRAND_EXCHANGE), - BANK_VARROCK_WEST("Varrock West Bank", LocationType.BANK, BankLocation.VARROCK_WEST), - BANK_VARROCK_EAST("Varrock East Bank", LocationType.BANK, BankLocation.VARROCK_EAST), - BANK_LUMBRIDGE_FRONT("Lumbridge Bank", LocationType.BANK, BankLocation.LUMBRIDGE_FRONT), - BANK_FALADOR_WEST("Falador West Bank", LocationType.BANK, BankLocation.FALADOR_WEST), - BANK_FALADOR_EAST("Falador East Bank", LocationType.BANK, BankLocation.FALADOR_EAST), - BANK_EDGEVILLE("Edgeville Bank", LocationType.BANK, BankLocation.EDGEVILLE), - BANK_DRAYNOR_VILLAGE("Draynor Village Bank", LocationType.BANK, BankLocation.DRAYNOR_VILLAGE), - BANK_AL_KHARID("Al Kharid Bank", LocationType.BANK, BankLocation.AL_KHARID), - BANK_CATHERBY("Catherby Bank", LocationType.BANK, BankLocation.CATHERBY), - BANK_CAMELOT("Camelot Bank", LocationType.BANK, BankLocation.CAMELOT), - BANK_ARDOUGNE_NORTH("Ardougne North Bank", LocationType.BANK, BankLocation.ARDOUGNE_NORTH), - BANK_ARDOUGNE_SOUTH("Ardougne South Bank", LocationType.BANK, BankLocation.ARDOUGNE_SOUTH), - BANK_CANIFIS("Canifis Bank", LocationType.BANK, BankLocation.CANIFIS), - BANK_FISHING_GUILD("Fishing Guild Bank", LocationType.BANK, BankLocation.FISHING_GUILD), - BANK_FOSSIL_ISLAND("Fossil Island Bank", LocationType.BANK, BankLocation.FOSSIL_ISLAND), - BANK_ARCEUUS("Arceuus Bank", LocationType.BANK, BankLocation.ARCEUUS), - BANK_HOSIDIUS("Hosidius Bank", LocationType.BANK, BankLocation.HOSIDIUS), - BANK_LOVAKENGJ("Lovakengj Bank", LocationType.BANK, BankLocation.LOVAKENGJ), - BANK_PISCARILIUS("Piscarilius Bank", LocationType.BANK, BankLocation.PISCARILIUS), - BANK_SHAYZIEN_BANK("Shayzien Bank", LocationType.BANK, BankLocation.SHAYZIEN_BANK), - BANK_FARMING_GUILD("Farming Guild Bank", LocationType.BANK, BankLocation.FARMING_GUILD), - - // Major Deposit Box Locations - DEPOSIT_BOX_GRAND_EXCHANGE("Grand Exchange Deposit Box", LocationType.DEPOSIT_BOX, DepositBoxLocation.GRAND_EXCHANGE), - DEPOSIT_BOX_EDGEVILLE("Edgeville Deposit Box", LocationType.DEPOSIT_BOX, DepositBoxLocation.EDGEVILLE), - DEPOSIT_BOX_BARBARIAN_ASSAULT("Barbarian Assault Deposit Box", LocationType.DEPOSIT_BOX, DepositBoxLocation.BARBARIAN_ASSAULT), - DEPOSIT_BOX_FALADOR("Falador Deposit Box", LocationType.DEPOSIT_BOX, DepositBoxLocation.FALADOR), - DEPOSIT_BOX_VARROCK("Varrock Deposit Box", LocationType.DEPOSIT_BOX, DepositBoxLocation.VARROCK), - DEPOSIT_BOX_LUMBRIDGE("Lumbridge Deposit Box", LocationType.DEPOSIT_BOX, DepositBoxLocation.LUMBRIDGE), - DEPOSIT_BOX_FARMING_GUILD("Farming Guild Deposit Box", LocationType.DEPOSIT_BOX, DepositBoxLocation.FARMING_GUILD), - - // Major Slayer Masters - SLAYER_MASTER_TURAEL("Turael (Burthorpe)", LocationType.SLAYER_MASTER, SlayerMasters.TURAEL), - SLAYER_MASTER_SPRIA("Spria (Draynor Village)", LocationType.SLAYER_MASTER, SlayerMasters.SPRIA), - SLAYER_MASTER_MAZCHNA("Mazchna (Canifis)", LocationType.SLAYER_MASTER, SlayerMasters.MAZCHNA), - SLAYER_MASTER_VANNAKA("Vannaka (Edgeville Dungeon)", LocationType.SLAYER_MASTER, SlayerMasters.VANNAKA), - SLAYER_MASTER_CHAELDAR("Chaeldar (Zanaris)", LocationType.SLAYER_MASTER, SlayerMasters.CHAELDAR), - SLAYER_MASTER_KONAR("Konar quo Maten (Mount Karuulm)", LocationType.SLAYER_MASTER, SlayerMasters.KONAR), - SLAYER_MASTER_NIEVE("Nieve (Gnome Stronghold)", LocationType.SLAYER_MASTER, SlayerMasters.NIEVE), - SLAYER_MASTER_STEVE("Steve (Gnome Stronghold)", LocationType.SLAYER_MASTER, SlayerMasters.STEVE), - SLAYER_MASTER_DURADEL("Duradel (Shilo Village)", LocationType.SLAYER_MASTER, SlayerMasters.DURADEL), - SLAYER_MASTER_KRYSTILIA("Krystilia (Edgeville)", LocationType.SLAYER_MASTER, SlayerMasters.KRYSTILIA), - - // Major Farming Locations - Allotments - FARMING_FALADOR_ALLOTMENT("Falador Allotment", LocationType.FARMING, Allotments.FALADOR), - FARMING_CATHERBY_ALLOTMENT("Catherby Allotment", LocationType.FARMING, Allotments.CATHERBY), - FARMING_ARDOUGNE_ALLOTMENT("Ardougne Allotment", LocationType.FARMING, Allotments.ARDOUGNE), - FARMING_MORYTANIA_ALLOTMENT("Morytania Allotment", LocationType.FARMING, Allotments.MORYTANIA), - FARMING_KOUREND_ALLOTMENT("Kourend Allotment", LocationType.FARMING, Allotments.KOUREND), - FARMING_GUILD_ALLOTMENT("Farming Guild Allotment", LocationType.FARMING, Allotments.FARMING_GUILD), - - // Major Farming Locations - Trees - FARMING_TREE_FALADOR("Falador Tree", LocationType.FARMING, Trees.FALADOR), - FARMING_TREE_FARMING_GUILD("Farming Guild Tree", LocationType.FARMING, Trees.FARMING_GUILD), - FARMING_TREE_GNOME_STRONGHOLD("Gnome Stronghold Tree", LocationType.FARMING, Trees.GNOME_STRONGHOLD), - FARMING_TREE_LUMBRIDGE("Lumbridge Tree", LocationType.FARMING, Trees.LUMBRIDGE), - FARMING_TREE_TAVERLEY("Taverley Tree", LocationType.FARMING, Trees.TAVERLEY), - FARMING_TREE_VARROCK("Varrock Tree", LocationType.FARMING, Trees.VARROCK), - - // Major Farming Locations - Fruit Trees - FARMING_FRUIT_TREE_BRIMHAVEN("Brimhaven Fruit Tree", LocationType.FARMING, FruitTrees.BRIMHAVEN), - FARMING_FRUIT_TREE_CATHERBY("Catherby Fruit Tree", LocationType.FARMING, FruitTrees.CATHERBY), - FARMING_FRUIT_TREE_FARMING_GUILD("Farming Guild Fruit Tree", LocationType.FARMING, FruitTrees.FARMING_GUILD), - FARMING_FRUIT_TREE_GNOME_STRONGHOLD("Gnome Stronghold Fruit Tree", LocationType.FARMING, FruitTrees.GNOME_STRONGHOLD), - FARMING_FRUIT_TREE_TREE_GNOME_VILLAGE("Tree Gnome Village Fruit Tree", LocationType.FARMING, FruitTrees.TREE_GNOME_VILLAGE), - FARMING_FRUIT_TREE_TAI_BWO_WANNAI("Tai Bwo Wannai Fruit Tree", LocationType.FARMING, FruitTrees.TAI_BWO_WANNAI), - FARMING_FRUIT_TREE_PRIFDDINAS("Prifddinas Fruit Tree", LocationType.FARMING, FruitTrees.PRIFDDINAS); - - private final String displayName; - private final LocationType type; - private final Object locationData; - - UnifiedLocation(String displayName, LocationType type, Object locationData) { - this.displayName = displayName; - this.type = type; - this.locationData = locationData; - } - - /** - * Gets the WorldPoint for this location. - * - * @return WorldPoint if available, null otherwise - */ - public WorldPoint getWorldPoint() { - if (locationData == null) { - return null; - } - - switch (type) { - case BANK: - return ((BankLocation) locationData).getWorldPoint(); - - case DEPOSIT_BOX: - return ((DepositBoxLocation) locationData).getWorldPoint(); - - case SLAYER_MASTER: - return ((SlayerMasters) locationData).getWorldPoint(); - - case FARMING: - if (locationData instanceof Allotments) { - return ((Allotments) locationData).getWorldPoint(); - } else if (locationData instanceof Herbs) { - return ((Herbs) locationData).getWorldPoint(); - } else if (locationData instanceof Trees) { - return ((Trees) locationData).getWorldPoint(); - } else if (locationData instanceof FruitTrees) { - return ((FruitTrees) locationData).getWorldPoint(); - } else if (locationData instanceof Bushes) { - return ((Bushes) locationData).getWorldPoint(); - } else if (locationData instanceof Hops) { - return ((Hops) locationData).getWorldPoint(); - } else if (locationData instanceof CompostBins) { - return ((CompostBins) locationData).getWorldPoint(); - } - break; - - case HUNTING: - if (locationData instanceof Birds) { - return ((Birds) locationData).getWorldPoint(); - } else if (locationData instanceof Chinchompas) { - return ((Chinchompas) locationData).getWorldPoint(); - } else if (locationData instanceof Insects) { - return ((Insects) locationData).getWorldPoint(); - } else if (locationData instanceof Kebbits) { - return ((Kebbits) locationData).getWorldPoint(); - } else if (locationData instanceof Salamanders) { - return ((Salamanders) locationData).getWorldPoint(); - } else if (locationData instanceof SpecialHuntingAreas) { - return ((SpecialHuntingAreas) locationData).getWorldPoint(); - } - break; - - case NONE: - default: - return null; - } - - return null; - } - - /** - * Gets the original location data object (BankLocation, DepositBoxLocation, etc.) - * - * @return The original location data object - */ - public Object getOriginalLocationData() { - return locationData; - } - - @Override - public String toString() { - return displayName; - } - - /** - * Enum representing the different types of locations - */ - public enum LocationType { - NONE, - BANK, - DEPOSIT_BOX, - SLAYER_MASTER, - FARMING, - HUNTING - } -} 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/externalplugins/MicrobotPluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java index a3d695cbb74..1ba73828533 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.graph.Graph; import com.google.common.graph.GraphBuilder; @@ -348,7 +349,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()); } @@ -514,9 +519,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(); } @@ -553,9 +572,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"); } 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) { - List allLockCondtions = ((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 getUserBlockingConditions() { - List blockingConditions = new ArrayList<>(); - - if (userLogicalCondition != null && !userLogicalCondition.isSatisfied() && userLogicalCondition instanceof LogicalCondition) { - blockingConditions.addAll(((LogicalCondition) userLogicalCondition).getBlockingConditions()); - } - - return blockingConditions; - } - - /** - * Gets a list of all conditions that are currently blocking the plugin from running. - * This is useful for diagnosing why a plugin is not activating. - * - * @return List of all conditions preventing plugin activation - */ - public List getBlockingConditions() { - List blockingConditions = new ArrayList<>(); - - blockingConditions.addAll(getPluginBlockingConditions()); - blockingConditions.addAll(getUserBlockingConditions()); - - return blockingConditions; - } - - /** - * Gets a list of all plugin-defined "leaf" conditions that are preventing the plugin from running. - * Leaf conditions are the non-logical conditions that represent the actual root causes - * for why the plugin can't run. - * - * @return List of all plugin-defined leaf conditions preventing activation - */ - public List getPluginLeafBlockingConditions() { - List leafBlockingConditions = new ArrayList<>(); - - if (pluginCondition != null && !pluginCondition.isSatisfied()) { - if (pluginCondition instanceof LogicalCondition) { - leafBlockingConditions.addAll(((LogicalCondition) pluginCondition).getLeafBlockingConditions()); - } else { - // If condition is not a LogicalCondition but is not satisfied, add it directly - leafBlockingConditions.add(pluginCondition); - } - } - - return leafBlockingConditions; - } - - /** - * Gets a list of all user-defined "leaf" conditions that are preventing the plugin from running. - * Leaf conditions are the non-logical conditions that represent the actual root causes - * for why the plugin can't run. - * - * @return List of all user-defined leaf conditions preventing activation - */ - public List getUserLeafBlockingConditions() { - List leafBlockingConditions = new ArrayList<>(); - - if (userLogicalCondition != null && !userLogicalCondition.isSatisfied()) { - if (userLogicalCondition instanceof LogicalCondition) { - leafBlockingConditions.addAll(((LogicalCondition) userLogicalCondition).getLeafBlockingConditions()); - } else { - // If condition is not a LogicalCondition but is not satisfied, add it directly - leafBlockingConditions.add(userLogicalCondition); - } - } - - return leafBlockingConditions; - } - - /** - * Gets a list of all "leaf" conditions that are preventing the plugin from running. - * Leaf conditions are the non-logical conditions that represent the actual root causes - * for why the plugin can't run. - * - * @return List of all leaf conditions preventing plugin activation - */ - public List getLeafBlockingConditions() { - List leafBlockingConditions = new ArrayList<>(); - - leafBlockingConditions.addAll(getPluginLeafBlockingConditions()); - leafBlockingConditions.addAll(getUserLeafBlockingConditions()); - - return leafBlockingConditions; - } - - /** - * Gets a human-readable explanation of why the plugin-defined conditions are not satisfied, - * detailing the specific blocking conditions. - * - * @return A string explaining why the plugin-defined conditions are not satisfied - */ - public String getPluginBlockingExplanation() { - if (pluginCondition == null || pluginCondition.isSatisfied()) { - return "Plugin conditions are all satisfied."; - } - - StringBuilder explanation = new StringBuilder(); - explanation.append("Plugin conditions not satisfied because:\n"); - - if (pluginCondition instanceof LogicalCondition) { - explanation.append(((LogicalCondition) pluginCondition).getBlockingExplanation() - .replace("\n", "\n ")).append("\n"); - } else { - explanation.append(" ").append(pluginCondition.getDescription()) - .append(" (").append(pluginCondition.getClass().getSimpleName()).append(")\n"); - } - - // Add root causes summary - explanation.append("\nPlugin Root Causes:\n"); - List leafBlockingConditions = getPluginLeafBlockingConditions(); - for (int i = 0; i < leafBlockingConditions.size(); i++) { - Condition condition = leafBlockingConditions.get(i); - explanation.append(i + 1).append(") ").append(condition.getDescription()) - .append(" (").append(condition.getClass().getSimpleName()).append(")\n"); - } - - return explanation.toString(); - } - - /** - * Gets a human-readable explanation of why the user-defined conditions are not satisfied, - * detailing the specific blocking conditions. - * - * @return A string explaining why the user-defined conditions are not satisfied - */ - public String getUserBlockingExplanation() { - if (userLogicalCondition == null || userLogicalCondition.isSatisfied()) { - return "User conditions are all satisfied."; - } - - StringBuilder explanation = new StringBuilder(); - explanation.append("User conditions not satisfied because:\n"); - - if (userLogicalCondition instanceof LogicalCondition) { - explanation.append(((LogicalCondition) userLogicalCondition).getBlockingExplanation() - .replace("\n", "\n ")).append("\n"); - } else { - explanation.append(" ").append(userLogicalCondition.getDescription()) - .append(" (").append(userLogicalCondition.getClass().getSimpleName()).append(")\n"); - } - - // Add root causes summary - explanation.append("\nUser Root Causes:\n"); - List leafBlockingConditions = getUserLeafBlockingConditions(); - for (int i = 0; i < leafBlockingConditions.size(); i++) { - Condition condition = leafBlockingConditions.get(i); - explanation.append(i + 1).append(") ").append(condition.getDescription()) - .append(" (").append(condition.getClass().getSimpleName()).append(")\n"); - } - - return explanation.toString(); - } - - /** - * Gets a human-readable explanation of why the plugin cannot run, - * detailing the specific blocking conditions from both plugin and user sources. - * - * @return A string explaining why the plugin cannot run - */ - public String getBlockingExplanation() { - if (areAllConditionsMet()) { - return "All conditions are satisfied. The plugin should be running."; - } - - StringBuilder explanation = new StringBuilder(); - explanation.append("Plugin cannot run because:\n\n"); - - // Check plugin conditions - if (pluginCondition != null && !pluginCondition.isSatisfied()) { - explanation.append("Plugin Conditions:\n"); - if (pluginCondition instanceof LogicalCondition) { - explanation.append(((LogicalCondition) pluginCondition).getBlockingExplanation() - .replace("\n", "\n ")).append("\n"); - } else { - explanation.append(" ").append(pluginCondition.getDescription()) - .append(" (").append(pluginCondition.getClass().getSimpleName()).append(")\n"); - } - } - - // Check user conditions - if (userLogicalCondition != null && !userLogicalCondition.isSatisfied()) { - explanation.append("User Conditions:\n"); - if (userLogicalCondition instanceof LogicalCondition) { - explanation.append(((LogicalCondition) userLogicalCondition).getBlockingExplanation() - .replace("\n", "\n ")).append("\n"); - } else { - explanation.append(" ").append(userLogicalCondition.getDescription()) - .append(" (").append(userLogicalCondition.getClass().getSimpleName()).append(")\n"); - } - } - - // Add root causes summary - explanation.append("\nRoot Causes Summary:\n"); - List leafBlockingConditions = getLeafBlockingConditions(); - for (int i = 0; i < leafBlockingConditions.size(); i++) { - Condition condition = leafBlockingConditions.get(i); - explanation.append(i + 1).append(") ").append(condition.getDescription()) - .append(" (").append(condition.getClass().getSimpleName()).append(")\n"); - } - - return explanation.toString(); - } - - /** - * Gets a concise summary of the plugin-defined root causes why the plugin cannot run. - * - * @return A string summarizing the plugin-defined root causes - */ - public String getPluginRootCausesSummary() { - if (pluginCondition == null || pluginCondition.isSatisfied()) { - return "All plugin conditions are satisfied"; - } - - List leafBlockingConditions = getPluginLeafBlockingConditions(); - - if (leafBlockingConditions.isEmpty()) { - return "No specific plugin blocking conditions found"; - } - - StringBuilder summary = new StringBuilder(); - summary.append("Plugin root causes preventing activation (").append(leafBlockingConditions.size()).append("):\n"); - - for (int i = 0; i < leafBlockingConditions.size(); i++) { - Condition condition = leafBlockingConditions.get(i); - summary.append(i + 1).append(") ").append(condition.getDescription()); - - // Add progress information if available - double progress = condition.getProgressPercentage(); - if (progress > 0 && progress < 100) { - summary.append(" - ").append(String.format("%.1f%%", progress)).append(" complete"); - } - - if (i < leafBlockingConditions.size() - 1) { - summary.append("\n"); - } - } - - return summary.toString(); - } - - /** - * Gets a concise summary of the user-defined root causes why the plugin cannot run. - * - * @return A string summarizing the user-defined root causes - */ - public String getUserRootCausesSummary() { - if (userLogicalCondition == null || userLogicalCondition.isSatisfied()) { - return "All user conditions are satisfied"; - } - - List leafBlockingConditions = getUserLeafBlockingConditions(); - - if (leafBlockingConditions.isEmpty()) { - return "No specific user blocking conditions found"; - } - - StringBuilder summary = new StringBuilder(); - summary.append("User root causes preventing activation (").append(leafBlockingConditions.size()).append("):\n"); - - for (int i = 0; i < leafBlockingConditions.size(); i++) { - Condition condition = leafBlockingConditions.get(i); - summary.append(i + 1).append(") ").append(condition.getDescription()); - - // Add progress information if available - double progress = condition.getProgressPercentage(); - if (progress > 0 && progress < 100) { - summary.append(" - ").append(String.format("%.1f%%", progress)).append(" complete"); - } - - if (i < leafBlockingConditions.size() - 1) { - summary.append("\n"); - } - } - - return summary.toString(); - } - - /** - * Gets a concise summary of all root causes why the plugin cannot run, - * combining both plugin-defined and user-defined causes. - * - * @return A string summarizing the root causes preventing plugin activation - */ - public String getRootCausesSummary() { - if (areAllConditionsMet()) { - return "All conditions are satisfied"; - } - - List leafBlockingConditions = getLeafBlockingConditions(); - - if (leafBlockingConditions.isEmpty()) { - return "No specific blocking conditions found"; - } - - StringBuilder summary = new StringBuilder(); - summary.append("Root causes preventing plugin activation (").append(leafBlockingConditions.size()).append("):\n"); - - for (int i = 0; i < leafBlockingConditions.size(); i++) { - Condition condition = leafBlockingConditions.get(i); - summary.append(i + 1).append(") ").append(condition.getDescription()); - - // Add progress information if available - double progress = condition.getProgressPercentage(); - if (progress > 0 && progress < 100) { - summary.append(" - ").append(String.format("%.1f%%", progress)).append(" complete"); - } - - if (i < leafBlockingConditions.size() - 1) { - summary.append("\n"); - } - } - - return summary.toString(); - } - - /** - * Pauses all time-based conditions in both user and plugin logical structures. - * When paused, time conditions cannot be satisfied and their trigger times will be - * shifted by the duration of the pause when resumed. - */ - public void pause(){ - pauseAllConditions(); - // Unregister from events while paused - if (eventsRegistered) { - unregisterEvents(); - } - } - public void pauseUserConditions() { - // Pause all time conditions and unregister events - List timeConditions = getUserConditions(); - for (Condition condition : timeConditions) { - condition.pause(); - } - - } - public void pausePluginConditions() { - // Pause all time conditions and unregister events - List timeConditions = getPluginConditions(); - for (Condition condition : timeConditions) { - condition.pause(); - } - - } - public void pauseAllConditions() { - // Pause all time conditions and unregister events - List timeConditions = getConditions(); - for (Condition condition : timeConditions) { - condition.pause(); - } - - } - - /** - * resumes all time-based conditions in both user and plugin logical structures. - * When resumed, time conditions will resume normal operation with their trigger - * times shifted by the duration of the pause. - */ - public void resume(){ - resumeAllConditions(); - // Re-register for events when resumed - if (!eventsRegistered) { - registerEvents(); - } - - } - - public void resumeAllConditions() { - // resume all time conditions - List timeConditions = getConditions(); - for (Condition condition : timeConditions) { - condition.resume(); - } - - } - public void resumeUserConditions() { - // resume all time conditions - List timeConditions = getUserConditions(); - for (Condition condition : timeConditions) { - condition.resume(); - } - - } - public void resumePluginTimeConditions() { - // resume all time conditions - List timeConditions = getPluginConditions(); - for (Condition condition : timeConditions) { - condition.resume(); - } - - } - - /** - * Checks if any time-based conditions are currently paused. - * - * @return true if at least one time condition is paused, false otherwise - */ - public boolean hasAnyPausedConditions() { - List timeConditions = getAllTimeConditions(); - for (TimeCondition condition : timeConditions) { - if (condition.isPaused()) { - return true; - } - } - return false; - } - /** - * Checks if any time-based conditions are currently paused. - * - * @return true if at least one time condition is paused, false otherwise - */ - public boolean isPaused() { - return hasAnyPausedConditions(); - } - - /** - * Gets the estimated time until the next condition trigger. - * This method uses the new estimation system that provides more accurate - * predictions for when conditions will be satisfied. - * - * @return Optional containing the estimated duration until next trigger, or empty if not determinable - */ - public Optional getEstimatedDurationUntilSatisfied() { - // If conditions are already met, return zero duration - if (areAllConditionsMet()) { - return Optional.of(Duration.ZERO); - } - - return getEstimatedTimeWhenIsSatisfiedForLogical(getFullLogicalCondition()); - } - - /** - * Gets the estimated time until user conditions will be satisfied. - * This method focuses only on user-defined conditions. - * - * @return Optional containing the estimated duration until user conditions are satisfied - */ - public Optional getEstimatedDurationUntilUserConditionsSatisfied() { - if (areUserConditionsMet()) { - return Optional.of(Duration.ZERO); - } - - return getEstimatedTimeWhenIsSatisfiedForLogical(getFullLogicalUserCondition()); - } - - /** - * Gets the estimated time until plugin conditions will be satisfied. - * This method focuses only on plugin-defined conditions. - * - * @return Optional containing the estimated duration until plugin conditions are satisfied - */ - public Optional getEstimatedDurationUntilPluginConditionsSatisfied() { - if (arePluginConditionsMet()) { - return Optional.of(Duration.ZERO); - } - - return getEstimatedTimeWhenIsSatisfiedForLogical(getFullLogicalPluginCondition()); - } - - /** - * Helper method to get estimated satisfaction time for a logical condition. - * This recursively evaluates the condition hierarchy using the new estimation system. - * - * @param logicalCondition The logical condition to evaluate - * @return Optional containing the estimated duration, or empty if not determinable - */ - private Optional getEstimatedTimeWhenIsSatisfiedForLogical(LogicalCondition logicalCondition) { - if (logicalCondition == null) { - return Optional.empty(); - } - - return logicalCondition.getEstimatedTimeWhenIsSatisfied(); - } - - /** - * Formats the estimated trigger time as a human-readable string. - * - * @return A string representing the estimated time until conditions will be satisfied - */ - public String getEstimatedTriggerTimeString() { - Optional estimate = getEstimatedDurationUntilSatisfied(); - if (estimate.isPresent()) { - return formatDurationEstimate(estimate.get()); - } - return "Cannot estimate trigger time"; - } - - /** - * Formats an estimated duration into a human-readable string. - * - * @param duration The duration to format - * @return A formatted string representation of the duration - */ - private String formatDurationEstimate(Duration duration) { - long seconds = duration.getSeconds(); - - if (seconds < 0) { - return "Already satisfied"; - } else if (seconds == 0) { - return "Satisfied now"; - } else if (seconds < 60) { - return String.format("Estimated in ~%d seconds", seconds); - } else if (seconds < 3600) { - return String.format("Estimated in ~%d minutes %d seconds", - seconds / 60, seconds % 60); - } else if (seconds < 86400) { // Less than a day - return String.format("Estimated in ~%d hours %d minutes", - seconds / 3600, (seconds % 3600) / 60); - } else { - // More than a day away - long days = seconds / 86400; - long hours = (seconds % 86400) / 3600; - return String.format("Estimated in ~%d days %d hours", days, hours); - } - } - - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ConditionType.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ConditionType.java deleted file mode 100644 index b41e285792e..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ConditionType.java +++ /dev/null @@ -1,44 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition; - -import net.runelite.api.NPC; - -/** - * Defines the types of conditions available for script execution. - */ -public enum ConditionType { - TIME("TIME"), - SKILL("SKILL"), - RESOURCE("RESOURCE"), - LOCATION("LOCATION"), - LOGICAL("LOGICAL"), - NPC("NPC"), - VARBIT("VARBIT"); - - - private final String identifier; - - ConditionType(String identifier) { - this.identifier = identifier; - } - - public String getIdentifier() { - return identifier; - } - - /** - * Finds a condition type by its identifier string. - */ - public static ConditionType fromIdentifier(String identifier) { - for (ConditionType type : values()) { - if (type.identifier.equals(identifier)) { - return type; - } - } - return null; - } - - @Override - public String toString() { - return identifier; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/AreaCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/AreaCondition.java deleted file mode 100644 index 98ca69fb63c..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/AreaCondition.java +++ /dev/null @@ -1,153 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.location; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.coords.WorldArea; -import net.runelite.api.coords.WorldPoint; - -/** - * Condition that is met when the player is inside a rectangular area - */ -@Slf4j -@EqualsAndHashCode(callSuper = false) - -public class AreaCondition extends LocationCondition { - public static String getVersion() { - return "0.0.1"; - } - @Getter - private final WorldArea area; - - /** - * Create a condition that is met when the player is inside the specified area - * - * @param x1 Southwest corner x - * @param y1 Southwest corner y - * @param x2 Northeast corner x - * @param y2 Northeast corner y - * @param plane The plane the area is on - */ - public AreaCondition(String name, int x1, int y1, int x2, int y2, int plane) { - super(name); - int width = Math.abs(x2 - x1) + 1; - int height = Math.abs(y2 - y1) + 1; - int startX = Math.min(x1, x2); - int startY = Math.min(y1, y2); - this.area = new WorldArea(startX, startY, width, height, plane); - } - - /** - * Create a condition that is met when the player is inside the specified area - * - * @param area The area to check - */ - public AreaCondition(String name, WorldArea area) { - super(name); - if (area == null) { - throw new IllegalArgumentException("Area cannot be null"); - } - this.area = area; - } - - @Override - protected void updateLocationStatus() { - if (!canCheckLocation()) { - return; - } - - try { - WorldPoint location = getCurrentLocation(); - if (location != null) { - satisfied = area.contains(location); - - if (satisfied) { - log.debug("Player entered target area"); - } - } - } catch (Exception e) { - log.error("Error checking if player is in area", e); - } - } - - @Override - public String getDescription() { - WorldPoint location = getCurrentLocation(); - String statusInfo = ""; - - if (location != null) { - boolean inArea = area.contains(location); - statusInfo = String.format(" (currently %s)", inArea ? "inside area" : "outside area"); - } - - return String.format("Player in area: %d,%d to %d,%d (plane %d)%s", - area.getX(), area.getY(), - area.getX() + area.getWidth() - 1, - area.getY() + area.getHeight() - 1, - area.getPlane(), - statusInfo); - } - - @Override - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - // Basic description - sb.append("Area Condition: Player must be within a specific area\n"); - - // Status information - WorldPoint location = getCurrentLocation(); - boolean inArea = location != null && area.contains(location); - sb.append("Status: ").append(inArea ? "Satisfied" : "Not satisfied").append("\n"); - - // Area details - sb.append("Area Coordinates: (").append(area.getX()).append(", ").append(area.getY()).append(") to (") - .append(area.getX() + area.getWidth() - 1).append(", ") - .append(area.getY() + area.getHeight() - 1).append(")\n"); - sb.append("Area Size: ").append(area.getWidth()).append(" x ").append(area.getHeight()).append(" tiles\n"); - sb.append("Plane: ").append(area.getPlane()).append("\n"); - - // Current player position - if (location != null) { - sb.append("Player Position: (").append(location.getX()).append(", ") - .append(location.getY()).append(", ").append(location.getPlane()).append(")\n"); - sb.append("Player In Area: ").append(inArea ? "Yes" : "No"); - } else { - sb.append("Player Position: Unknown"); - } - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - // Basic information - sb.append("AreaCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Type: Area (Player must be within area)\n"); - sb.append(" │ Coordinates: (").append(area.getX()).append(", ").append(area.getY()).append(") to (") - .append(area.getX() + area.getWidth() - 1).append(", ") - .append(area.getY() + area.getHeight() - 1).append(")\n"); - sb.append(" │ Size: ").append(area.getWidth()).append(" x ").append(area.getHeight()).append(" tiles\n"); - sb.append(" │ Plane: ").append(area.getPlane()).append("\n"); - - // Status information - sb.append(" └─ Status ──────────────────────────────────\n"); - WorldPoint location = getCurrentLocation(); - boolean inArea = location != null && area.contains(location); - sb.append(" Satisfied: ").append(inArea).append("\n"); - - // Player location - if (location != null) { - sb.append(" Player Position: (").append(location.getX()).append(", ") - .append(location.getY()).append(", ").append(location.getPlane()).append(")\n"); - sb.append(" In Target Area: ").append(inArea ? "Yes" : "No"); - } else { - sb.append(" Player Position: Unknown"); - } - - return sb.toString(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/LocationCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/LocationCondition.java deleted file mode 100644 index 05c98ba0c7a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/LocationCondition.java +++ /dev/null @@ -1,278 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.location; - -import com.formdev.flatlaf.json.Location; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Client; -import net.runelite.api.GameState; -import net.runelite.api.coords.WorldArea; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.GameTick; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.OrCondition; -import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -/** - * Base class for all location-based conditions. - * Provides common functionality for conditions that depend on player location. - */ -@Slf4j -@EqualsAndHashCode(callSuper = false) -public abstract class LocationCondition implements Condition { - protected transient volatile boolean satisfied = false; - /** - * Indicates whether the condition is satisfied based on the player's location. - */ - @Getter - protected final String name; - public LocationCondition(String name) { - this.name = name; - } - @Override - public boolean isSatisfied() { - return satisfied; //update in the child class, via updateLocationStatus - } - - @Override - public void reset() { - - } - - @Override - public void reset(boolean randomize) { - - } - - @Override - public ConditionType getType() { - return ConditionType.LOCATION; - } - - @Override - public void onGameTick(GameTick event) { - updateLocationStatus(); - } - - /** - * Updates the satisfaction status based on the player's current location. - * Each subclass will implement its own location check logic. - */ - protected abstract void updateLocationStatus(); - - /** - * Gets the player's current location if available. - * - * @return The player's current WorldPoint or null if unavailable - */ - protected WorldPoint getCurrentLocation() { - if (!canCheckLocation()) { - return null; - } - return Rs2Player.getWorldLocation(); - } - - /** - * Checks if the game client is in a state where location checks can be performed. - * - * @return True if location checks can be performed, false otherwise - */ - protected boolean canCheckLocation() { - Client client = Microbot.getClient(); - return client != null && client.getGameState() == GameState.LOGGED_IN; - } - - /** - * Returns a detailed description of the location condition with additional status information. - */ - public abstract String getDetailedDescription(); - - /** - * Creates a condition that is satisfied when the player is at the location for the given bank - * - * @param bank The bank location - * @param distance The maximum distance from the bank point - * @return A condition that is satisfied when the player is within distance of the bank - */ - public static Condition atBank(BankLocation bank, int distance) { - - WorldPoint baPoint = bank.getWorldPoint(); - log .info("Bank location: " + bank.name() + " - " + baPoint); - return new PositionCondition("At " + bank.name(), baPoint, distance); - - - - } - - - // Probelem for now is the enums are protect in worldmap plugin.... - // import net.runelite.client.plugins.worldmap.RareTreeLocation; - // /** - // * Creates a condition that is satisfied when the player is at any of the locations for the given farming patch - // * - // * @param patch The farming patch location - // * @param distance The maximum distance from any patch point - // * @return A condition that is satisfied when the player is at the farming patch - // */ - // public static Condition atFarmingPatch(FarmingPatchLocation patch, int distance) { - // WorldPoint[] locations = patch.getLocations(); - // if (locations.length == 1) { - // return new PositionCondition("At " + patch.name(), locations[0], distance); - // } else { - // OrCondition orCondition = new OrCondition(); - // for (WorldPoint point : locations) { - // orCondition.addCondition( - // new PositionCondition("At " + patch.name() + " location", point, distance) - // ); - // } - // return orCondition; - // } - // } - - // /** - // * Creates a condition that is satisfied when the player is at any of the locations for the given tree type - // * - // * @param treeLocation The rare tree location - // * @param distance The maximum distance from any tree location - // * @return A condition that is satisfied when the player is at the tree - // */ - // public static Condition atRareTree(RareTreeLocation treeLocation, int distance) { - // WorldPoint[] locations = treeLocation.getLocations(); - // if (locations.length == 1) { - // return new PositionCondition("At " + treeLocation.name(), locations[0], distance); - // } else { - // OrCondition orCondition = new OrCondition(); - // for (WorldPoint point : locations) { - // orCondition.addCondition( - // new PositionCondition("At " + treeLocation.name() + " location", point, distance) - // ); - // } - // return orCondition; - // } - // } - /** - * Creates a condition that is satisfied when the player is at any of the given points - * - * @param name Descriptive name for the condition - * @param points Array of points to check - * @param distance The maximum distance from any point - * @return A condition that is satisfied when the player is at any of the points - */ - public static Condition atAnyPoint(String name, WorldPoint[] points, int distance) { - if (points == null || points.length == 0) { - throw new IllegalArgumentException("At least one point must be provided"); - } - if (distance < 0) { - throw new IllegalArgumentException("Distance must be >= 0"); - } - for (int i = 0; i < points.length; i++) { - if (points[i] == null) { - throw new IllegalArgumentException("points[" + i + "] must not be null"); - } - } - if (points.length == 1) { - return new PositionCondition(name, points[0], distance); - } else { - OrCondition orCondition = new OrCondition(); - for (int i = 0; i < points.length; i++) { - orCondition.addCondition(new PositionCondition(name + " (point " + (i + 1) + ")", points[i], distance)); - } - return orCondition; - } - } - - - /** - * Creates a rectangle area condition centered on the given point - * - * @param name Descriptive name for the condition - * @param center The center point of the rectangle - * @param width Width of the area (in tiles) - * @param height Height of the area (in tiles) - * @return A condition that is satisfied when the player is within the area - */ - public static AreaCondition createArea(String name, WorldPoint center, int width, int height) { - int halfWidth = width / 2; - int halfHeight = height / 2; - int x1 = center.getX() - halfWidth; - int y1 = center.getY() - halfHeight; - int x2 = center.getX() + halfWidth; - int y2 = center.getY() + halfHeight; - return new AreaCondition(name, x1, y1, x2, y2, center.getPlane()); - } - - /** - * Creates a condition that is satisfied when the player is within any of the given areas - * - * @param name Descriptive name for the condition - * @param areas Array of WorldAreas to check - * @return A condition that is satisfied when the player is in any of the areas - */ - public static Condition inAnyArea(String name, WorldArea[] areas) { - if (areas.length == 0) { - throw new IllegalArgumentException("At least one area must be provided"); - } - - if (areas.length == 1) { - return new AreaCondition(name, areas[0]); - } else { - OrCondition orCondition = new OrCondition(); - for (int i = 0; i < areas.length; i++) { - orCondition.addCondition( - new AreaCondition(name + " (area " + (i+1) + ")", areas[i]) - ); - } - return orCondition; - } - } - - /** - * Creates a condition that is satisfied when the player is within any of the given rectangular areas - * - * @param name Descriptive name for the condition - * @param areaDefinitions Array of area definitions, each containing [x1, y1, x2, y2, plane] - * @return A condition that is satisfied when the player is in any of the areas - */ - public static Condition inAnyArea(String name, int[][] areaDefinitions) { - if (areaDefinitions.length == 0) { - throw new IllegalArgumentException("At least one area must be provided"); - } - - if (areaDefinitions.length == 1) { - int[] def = areaDefinitions[0]; - if (def.length != 5) { - throw new IllegalArgumentException("Each area definition must contain [x1, y1, x2, y2, plane]"); - } - return new AreaCondition(name, def[0], def[1], def[2], def[3], def[4]); - } else { - OrCondition orCondition = new OrCondition(); - for (int i = 0; i < areaDefinitions.length; i++) { - int[] def = areaDefinitions[i]; - if (def.length != 5) { - throw new IllegalArgumentException("Each area definition must contain [x1, y1, x2, y2, plane]"); - } - orCondition.addCondition( - new AreaCondition(name + " (area " + (i+1) + ")", def[0], def[1], def[2], def[3], def[4]) - ); - } - return orCondition; - } - } - @Override - public void pause() { - - - - } - - @Override - public void resume() { - - } - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/PositionCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/PositionCondition.java deleted file mode 100644 index 3dc99948ab9..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/PositionCondition.java +++ /dev/null @@ -1,248 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.location; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.Microbot; -/** - * Condition that is met when the player is within a certain distance of a specific position - */ -@Slf4j -@EqualsAndHashCode(callSuper = false) -public class PositionCondition extends LocationCondition { - public static String getVersion() { - return "0.0.1"; - } - @Getter - private final WorldPoint targetPosition; - @Getter - private final int maxDistance; - - /** - * Create a condition that is met when the player is within the specified distance of the target position - * - * @param x The target x coordinate - * @param y The target y coordinate - * @param plane The target plane - * @param maxDistance The maximum distance from the target position (in tiles) - */ - public PositionCondition(String name, int x, int y, int plane, int maxDistance) { - super(name); - if (maxDistance < 0) { - throw new IllegalArgumentException("Max distance cannot be negative"); - } - if (x < 0 || y < 0) { - throw new IllegalArgumentException("Coordinates and plane must be non-negative"); - } - if (maxDistance > 0 && maxDistance > 104) { - throw new IllegalArgumentException("Max distance must be within the valid range (0-104)"); - } - this.targetPosition = new WorldPoint(x, y, plane); - this.maxDistance = maxDistance; - } - - /** - * Create a condition that is met when the player is at the exact position - * - * @param x The target x coordinate - * @param y The target y coordinate - * @param plane The target plane - */ - public PositionCondition(String name, int x, int y, int plane) { - this(name, x, y, plane, 0); - } - - /** - * Create a condition that is met when the player is within the specified distance of the target position - * - * @param position The target position - * @param maxDistance The maximum distance from the target position (in tiles) - */ - public PositionCondition(String name, WorldPoint position, int maxDistance) { - super(name); - if (maxDistance < 0) { - throw new IllegalArgumentException("Max distance cannot be negative"); - } - if (position == null) { - throw new IllegalArgumentException("Position cannot be null"); - } - - this.targetPosition = position; - this.maxDistance = maxDistance; - } - - @Override - protected void updateLocationStatus() { - if (Microbot.isDebug()){ - log.info("Checking player position against target position: {}", targetPosition); - } - - if (!canCheckLocation()) { - return; - } - try { - WorldPoint currentPosition = getCurrentLocation(); - if (Microbot.isDebug()){ - log.info("Current position: {}", currentPosition); - log.info("Target position: {}", targetPosition); - log.info("Max distance: {}", maxDistance); - log.info("Current plane: {}", currentPosition != null ? currentPosition.getPlane() : "null"); - } - if (currentPosition != null && currentPosition.getPlane() == targetPosition.getPlane()) { - int distance = currentPosition.distanceTo(targetPosition); - if (Microbot.isDebug()){ - log.info("Distance to target position: {}", distance); - log.info("Max distance: {}", maxDistance); - } - this.satisfied = distance <= maxDistance; - - if (this.satisfied) { - log.debug("Player reached target position, distance: {}", distance); - } - } - } catch (Exception e) { - log.error("Error checking player position", e); - } - } - - @Override - public String getDescription() { - WorldPoint currentLocation = getCurrentLocation(); - String distanceInfo = ""; - String playerPositionInfo = ""; - - if (currentLocation != null) { - int distance = -1; - boolean onSamePlane = currentLocation.getPlane() == targetPosition.getPlane(); - - if (onSamePlane) { - distance = currentLocation.distanceTo(targetPosition); - distanceInfo = String.format(" (current distance: %d tiles)", distance); - } else { - distanceInfo = " (not on same plane)"; - } - - playerPositionInfo = String.format(" | Player at: %d, %d, %d", - currentLocation.getX(), currentLocation.getY(), currentLocation.getPlane()); - } else { - playerPositionInfo = " | Player position unknown"; - } - - if (maxDistance == 0) { - return String.format("Player at position: %d, %d, %d%s%s", - targetPosition.getX(), targetPosition.getY(), targetPosition.getPlane(), distanceInfo, playerPositionInfo); - } else { - return String.format("Player within %d tiles of: %d, %d, %d%s%s", - maxDistance, targetPosition.getX(), targetPosition.getY(), targetPosition.getPlane(), distanceInfo, playerPositionInfo); - } - } - - @Override - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - // Basic description - if (maxDistance == 0) { - sb.append("Position Condition: Player must be at exact position\n"); - } else { - sb.append("Position Condition: Player must be within ").append(maxDistance) - .append(" tiles of target position\n"); - } - - // Status information - WorldPoint currentPosition = getCurrentLocation(); - int distance = -1; - boolean onSamePlane = false; - - if (currentPosition != null) { - onSamePlane = currentPosition.getPlane() == targetPosition.getPlane(); - if (onSamePlane) { - distance = currentPosition.distanceTo(targetPosition); - } - } - - boolean isSatisfied = onSamePlane && distance <= maxDistance && distance != -1; - sb.append("Status: ").append(isSatisfied ? "Satisfied" : "Not satisfied").append("\n"); - - // Target details - sb.append("Target Position: (").append(targetPosition.getX()).append(", ") - .append(targetPosition.getY()).append(", ").append(targetPosition.getPlane()).append(")\n"); - - if (maxDistance > 0) { - sb.append("Max Distance: ").append(maxDistance).append(" tiles\n"); - } - - // Current player position and distance - if (currentPosition != null) { - sb.append("Player Position: (").append(currentPosition.getX()).append(", ") - .append(currentPosition.getY()).append(", ").append(currentPosition.getPlane()).append(")\n"); - - if (onSamePlane) { - sb.append("Current Distance: ").append(distance).append(" tiles"); - if (distance <= maxDistance) { - sb.append(" (within range)"); - } else { - sb.append(" (outside range)"); - } - } else { - sb.append("Current Distance: Not on same plane"); - } - } else { - sb.append("Player Position: Unknown"); - } - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - // Basic information - sb.append("PositionCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - if (maxDistance == 0) { - sb.append(" │ Type: Position (Player must be at exact position)\n"); - } else { - sb.append(" │ Type: Position (Player must be within ").append(maxDistance) - .append(" tiles of target)\n"); - } - sb.append(" │ Target: (").append(targetPosition.getX()).append(", ") - .append(targetPosition.getY()).append(", ").append(targetPosition.getPlane()).append(")\n"); - - if (maxDistance > 0) { - sb.append(" │ Max Distance: ").append(maxDistance).append(" tiles\n"); - } - - // Status information - sb.append(" └─ Status ──────────────────────────────────\n"); - WorldPoint currentPosition = getCurrentLocation(); - int distance = -1; - boolean onSamePlane = false; - - if (currentPosition != null) { - onSamePlane = currentPosition.getPlane() == targetPosition.getPlane(); - if (onSamePlane) { - distance = currentPosition.distanceTo(targetPosition); - } - - sb.append(" Player Position: (").append(currentPosition.getX()).append(", ") - .append(currentPosition.getY()).append(", ").append(currentPosition.getPlane()).append(")\n"); - - if (onSamePlane) { - sb.append(" Current Distance: ").append(distance).append(" tiles\n"); - } else { - sb.append(" Current Distance: Not on same plane\n"); - } - - boolean isSatisfied = onSamePlane && distance <= maxDistance; - sb.append(" Satisfied: ").append(isSatisfied); - } else { - sb.append(" Player Position: Unknown\n"); - sb.append(" Satisfied: false"); - } - - return sb.toString(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/RegionCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/RegionCondition.java deleted file mode 100644 index 22ee8051b8a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/RegionCondition.java +++ /dev/null @@ -1,149 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.location; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.coords.WorldPoint; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -/** - * Condition that is met when the player enters a specific region - */ -@Slf4j -public class RegionCondition extends LocationCondition { - public static String getVersion() { - return "0.0.1"; - } - @Getter - private final Set targetRegions; - - /** - * Create a condition that is met when the player enters any of the specified regions - * - * @param regionIds The region IDs to check for - */ - public RegionCondition(String name,int... regionIds) { - super(name); - if (regionIds == null || regionIds.length == 0) { - throw new IllegalArgumentException("Region IDs cannot be null or empty"); - } - this.targetRegions = new HashSet<>(); - for (int id : regionIds) { - targetRegions.add(id); - } - } - - @Override - protected void updateLocationStatus() { - if (!canCheckLocation()) { - return; - } - - try { - WorldPoint location = getCurrentLocation(); - if (location != null) { - int currentRegion = location.getRegionID(); - satisfied = targetRegions.contains(currentRegion); - - if (satisfied) { - log.debug("Player entered target region: {}", currentRegion); - } - } - } catch (Exception e) { - log.error("Error checking player region", e); - } - } - - @Override - public String getDescription() { - WorldPoint location = getCurrentLocation(); - String currentRegionInfo = ""; - - if (location != null) { - int currentRegion = location.getRegionID(); - boolean inTargetRegion = targetRegions.contains(currentRegion); - currentRegionInfo = String.format(" (current region: %d, %s)", - currentRegion, inTargetRegion ? "matched" : "not matched"); - } - - return "Player in regions: " + Arrays.toString(targetRegions.toArray()) + currentRegionInfo; - } - - @Override - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - // Basic description - int regionCount = targetRegions.size(); - if (regionCount == 1) { - sb.append("Region Condition: Player must be in a specific region\n"); - } else { - sb.append("Region Condition: Player must be in one of ").append(regionCount) - .append(" specified regions\n"); - } - - // Status information - WorldPoint location = getCurrentLocation(); - int currentRegion = -1; - boolean inTargetRegion = false; - - if (location != null) { - currentRegion = location.getRegionID(); - inTargetRegion = targetRegions.contains(currentRegion); - } - - sb.append("Status: ").append(inTargetRegion ? "Satisfied" : "Not satisfied").append("\n"); - - // Target region details - sb.append("Target Regions: ").append(Arrays.toString(targetRegions.toArray())).append("\n"); - - // Current player region - if (location != null) { - sb.append("Current Region: ").append(currentRegion); - sb.append(inTargetRegion ? " (matched)" : " (not matched)"); - } else { - sb.append("Current Region: Unknown"); - } - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - // Basic information - sb.append("RegionCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - - int regionCount = targetRegions.size(); - if (regionCount == 1) { - sb.append(" │ Type: Region (Player must be in specific region)\n"); - } else { - sb.append(" │ Type: Region (Player must be in one of ") - .append(regionCount).append(" regions)\n"); - } - - sb.append(" │ Target Regions: ").append(Arrays.toString(targetRegions.toArray())).append("\n"); - - // Status information - sb.append(" └─ Status ──────────────────────────────────\n"); - WorldPoint location = getCurrentLocation(); - - if (location != null) { - int currentRegion = location.getRegionID(); - boolean inTargetRegion = targetRegions.contains(currentRegion); - - sb.append(" Current Region: ").append(currentRegion).append("\n"); - sb.append(" Matched: ").append(inTargetRegion).append("\n"); - sb.append(" Satisfied: ").append(inTargetRegion); - } else { - sb.append(" Current Region: Unknown\n"); - sb.append(" Satisfied: false"); - } - - return sb.toString(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/readme.md b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/readme.md deleted file mode 100644 index 2e5eb3c59de..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/readme.md +++ /dev/null @@ -1,28 +0,0 @@ - -``` Java -// Check if player is at a bank -Condition atGrandExchange = LocationCondition.atBank(BankLocation.GRAND_EXCHANGE, 5); - -// Check if player is at any farming patch -Condition atAnyAllotmentPatch = LocationCondition.atFarmingPatch(FarmingPatchLocation.ALLOTMENT, 3); - -// Check if player is at a specific yew tree (all yew tree locations) -Condition atYewTree = LocationCondition.atRareTree(RareTreeLocation.YEW, 2); - -// Check if player is in an area around a point -Condition inMiningArea = LocationCondition.createArea("Mining Area", new WorldPoint(3230, 3145, 0), 10, 10); -``` - -``` Java -// Check if player is in any of several areas -WorldArea area1 = new WorldArea(3200, 3200, 10, 10, 0); -WorldArea area2 = new WorldArea(3300, 3300, 5, 5, 0); -Condition inEitherArea = LocationCondition.inAnyArea("Training areas", new WorldArea[]{area1, area2}); - -// Using raw coordinates -int[][] miningAreas = { - {3220, 3145, 3235, 3155, 0}, // Mining area 1 - {3270, 3160, 3278, 3168, 0} // Mining area 2 -}; -Condition inAnyMiningArea = LocationCondition.inAnyArea("Mining spots", miningAreas); -``` \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/AreaConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/AreaConditionAdapter.java deleted file mode 100644 index d12b22637d8..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/AreaConditionAdapter.java +++ /dev/null @@ -1,81 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.location.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.coords.WorldArea; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.AreaCondition; - -import java.lang.reflect.Type; - -/** - * Serializes and deserializes AreaCondition objects - */ -@Slf4j -public class AreaConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(AreaCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add type information - json.addProperty("type", AreaCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Store area information - data.addProperty("name", src.getName()); - WorldArea area = src.getArea(); - data.addProperty("x", area.getX()); - data.addProperty("y", area.getY()); - data.addProperty("width", area.getWidth()); - data.addProperty("height", area.getHeight()); - data.addProperty("plane", area.getPlane()); - data.addProperty("version", AreaCondition.getVersion()); - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public AreaCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(AreaCondition.getVersion())) { - - throw new JsonParseException("Version mismatch in AreaCondition: expected " + - AreaCondition.getVersion() + ", got " + version); - } - } - - // Get area information - String name = dataObj.get("name").getAsString(); - int x = dataObj.get("x").getAsInt(); - int y = dataObj.get("y").getAsInt(); - int width = dataObj.get("width").getAsInt(); - int height = dataObj.get("height").getAsInt(); - int plane = dataObj.get("plane").getAsInt(); - - // Create area and condition - WorldArea area = new WorldArea(x, y, width, height, plane); - return new AreaCondition(name, area); - - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/PositionConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/PositionConditionAdapter.java deleted file mode 100644 index 59087f849f3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/PositionConditionAdapter.java +++ /dev/null @@ -1,76 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.location.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.PositionCondition; - -import java.lang.reflect.Type; - -/** - * Serializes and deserializes PositionCondition objects - */ -@Slf4j -public class PositionConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(PositionCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add type information - json.addProperty("type", PositionCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Store position information - data.addProperty("name", src.getName()); - WorldPoint point = src.getTargetPosition(); - data.addProperty("x", point.getX()); - data.addProperty("y", point.getY()); - data.addProperty("plane", point.getPlane()); - data.addProperty("maxDistance", src.getMaxDistance()); - data.addProperty("version", PositionCondition.getVersion()); - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public PositionCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(PositionCondition.getVersion())) { - throw new JsonParseException("Version mismatch in PositionCondition: expected " + - PositionCondition.getVersion() + ", got " + version); - } - } - - // Get position information - String name = dataObj.get("name").getAsString(); - int x = dataObj.get("x").getAsInt(); - int y = dataObj.get("y").getAsInt(); - int plane = dataObj.get("plane").getAsInt(); - int maxDistance = dataObj.get("maxDistance").getAsInt(); - - // Create condition - return new PositionCondition(name, x, y, plane, maxDistance); - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/RegionConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/RegionConditionAdapter.java deleted file mode 100644 index e46db8c9dde..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/RegionConditionAdapter.java +++ /dev/null @@ -1,79 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.location.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.RegionCondition; - -import java.lang.reflect.Type; -import java.util.Set; - -/** - * Serializes and deserializes RegionCondition objects - */ -@Slf4j -public class RegionConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(RegionCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add type information - json.addProperty("type", RegionCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Store region information - data.addProperty("name", src.getName()); - JsonArray regionIds = new JsonArray(); - for (Integer regionId : src.getTargetRegions()) { - regionIds.add(regionId); - } - data.add("regionIds", regionIds); - data.addProperty("version", RegionCondition.getVersion()); - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public RegionCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(RegionCondition.getVersion())) { - throw new JsonParseException("Version mismatch in RegionCondition: expected " + - RegionCondition.getVersion() + ", got " + version); - } - } - - // Get region information - String name = dataObj.get("name").getAsString(); - JsonArray regionIdsArray = dataObj.getAsJsonArray("regionIds"); - int[] regionIds = new int[regionIdsArray.size()]; - - for (int i = 0; i < regionIdsArray.size(); i++) { - regionIds[i] = regionIdsArray.get(i).getAsInt(); - } - - // Create condition - return new RegionCondition(name, regionIds); - - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/ui/LocationConditionUtil.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/ui/LocationConditionUtil.java deleted file mode 100644 index 157a65e1cca..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/ui/LocationConditionUtil.java +++ /dev/null @@ -1,831 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.location.ui; - -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.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.AreaCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.PositionCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.RegionCondition; -import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import java.awt.*; -import java.util.Set; -import java.util.function.Consumer; - - -/** - * Utility class for creating panels for location-based conditions - */ -public class LocationConditionUtil { - public static final Color BRAND_BLUE = new Color(25, 130, 196); - /** - * Creates a unified location condition panel with tab selection for different location condition types - */ - public static void createLocationConditionPanel(JPanel panel, GridBagConstraints gbc) { - // Main label - JLabel titleLabel = new JLabel("Location Condition:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // Create tabbed pane for different location condition types - gbc.gridy++; - JTabbedPane tabbedPane = new JTabbedPane(); - tabbedPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - tabbedPane.setForeground(Color.WHITE); - - // Create panels for each condition type - JPanel positionPanel = new JPanel(new GridBagLayout()); - positionPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JPanel areaPanel = new JPanel(new GridBagLayout()); - areaPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JPanel regionPanel = new JPanel(new GridBagLayout()); - regionPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Add tabs - tabbedPane.addTab("Position", positionPanel); - tabbedPane.addTab("Area", areaPanel); - tabbedPane.addTab("Region", regionPanel); - - // Create condition panels in each tab - GridBagConstraints tabGbc = new GridBagConstraints(); - tabGbc.gridx = 0; - tabGbc.gridy = 0; - tabGbc.weightx = 1; - tabGbc.fill = GridBagConstraints.HORIZONTAL; - tabGbc.anchor = GridBagConstraints.NORTHWEST; - - createPositionConditionPanel(positionPanel, tabGbc); - createAreaConditionPanel(areaPanel, tabGbc); - createRegionConditionPanel(regionPanel, tabGbc); - - // Add the tabbed pane to the main panel - panel.add(tabbedPane, gbc); - - // Store the tabbed pane for later access - panel.putClientProperty("locationTabbedPane", tabbedPane); - } - - /** - * Sets up panel with values from an existing location condition - * - * @param panel The panel containing the UI components - * @param condition The location condition to read values from - */ - public static void setupLocationCondition(JPanel panel, Condition condition) { - if (condition == null) { - return; - } - - if (condition instanceof PositionCondition) { - setupPositionCondition(panel, (PositionCondition) condition); - } else if (condition instanceof AreaCondition) { - setupAreaCondition(panel, (AreaCondition) condition); - } else if (condition instanceof RegionCondition) { - setupRegionCondition(panel, (RegionCondition) condition); - } - } - - /** - * Sets up a position condition panel with values from an existing condition - */ - private static void setupPositionCondition(JPanel panel, PositionCondition condition) { - JSpinner xSpinner = (JSpinner) panel.getClientProperty("posXSpinner"); - JSpinner ySpinner = (JSpinner) panel.getClientProperty("posYSpinner"); - JSpinner zSpinner = (JSpinner) panel.getClientProperty("posZSpinner"); - JSpinner radiusSpinner = (JSpinner) panel.getClientProperty("posRadiusSpinner"); - - if (xSpinner != null && ySpinner != null && zSpinner != null && radiusSpinner != null) { - WorldPoint position = condition.getTargetPosition(); - if (position != null) { - xSpinner.setValue(position.getX()); - ySpinner.setValue(position.getY()); - zSpinner.setValue(position.getPlane()); - } - radiusSpinner.setValue(condition.getMaxDistance()); - } - } - - /** - * Sets up an area condition panel with values from an existing condition - */ - private static void setupAreaCondition(JPanel panel, AreaCondition condition) { - JSpinner x1Spinner = (JSpinner) panel.getClientProperty("areaX1Spinner"); - JSpinner y1Spinner = (JSpinner) panel.getClientProperty("areaY1Spinner"); - JSpinner z1Spinner = (JSpinner) panel.getClientProperty("areaZ1Spinner"); - JSpinner x2Spinner = (JSpinner) panel.getClientProperty("areaX2Spinner"); - JSpinner y2Spinner = (JSpinner) panel.getClientProperty("areaY2Spinner"); - JSpinner z2Spinner = (JSpinner) panel.getClientProperty("areaZ2Spinner"); - - if (x1Spinner != null && y1Spinner != null && z1Spinner != null && - x2Spinner != null && y2Spinner != null && z2Spinner != null) { - - WorldArea area = condition.getArea(); - if (area != null) { - x1Spinner.setValue(area.getX()); - y1Spinner.setValue(area.getY()); - z1Spinner.setValue(area.getPlane()); - x2Spinner.setValue(area.getX() + area.getWidth() - 1); - y2Spinner.setValue(area.getY() + area.getHeight() - 1); - z2Spinner.setValue(area.getPlane()); - } - } - } - - /** - * Sets up a region condition panel with values from an existing condition - */ - private static void setupRegionCondition(JPanel panel, RegionCondition condition) { - JTextField regionIdsField = (JTextField) panel.getClientProperty("regionIdsField"); - JTextField nameField = (JTextField) panel.getClientProperty("regionNameField"); - - if (regionIdsField != null && condition != null) { - // Format the region IDs as a comma-separated string - Set regionIds = condition.getTargetRegions(); - if (regionIds != null && !regionIds.isEmpty()) { - StringBuilder sb = new StringBuilder(); - for (Integer regionId : regionIds) { - if (sb.length() > 0) { - sb.append(","); - } - sb.append(regionId); - } - regionIdsField.setText(sb.toString()); - } - } - - if (nameField != null) { - nameField.setText(condition.getName()); - } - } - - /** - * Creates a location condition based on the selected tab and configuration - */ - public static Condition createLocationCondition(JPanel configPanel) { - JTabbedPane tabbedPane = (JTabbedPane) configPanel.getClientProperty("locationTabbedPane"); - - if (tabbedPane == null) { - throw new IllegalStateException("Location condition panel not properly configured - locationTabbedPane not found"); - } - - int selectedIndex = tabbedPane.getSelectedIndex(); - - // Get the specific tab panel that contains the components for the selected condition type - JPanel activeTabPanel = (JPanel) tabbedPane.getComponentAt(selectedIndex); - - switch (selectedIndex) { - case 0: // Position - return createPositionCondition(activeTabPanel); - case 1: // Area - return createAreaCondition(activeTabPanel); - case 2: // Region - return createRegionCondition(activeTabPanel); - default: - throw new IllegalStateException("Unknown location condition type"); - } - } - - /** - * Creates a panel for configuring PositionCondition - */ - private static void createPositionConditionPanel(JPanel panel, GridBagConstraints gbc) { - // Section title - JLabel titleLabel = new JLabel("Position Condition (Specific Location):"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // Position coordinates - gbc.gridy++; - JPanel positionPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - positionPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel positionLabel = new JLabel("Target Position:"); - positionLabel.setForeground(Color.WHITE); - positionPanel.add(positionLabel); - - // X input - SpinnerNumberModel xModel = new SpinnerNumberModel(3000, 0, 20000, 1); - JSpinner xSpinner = new JSpinner(xModel); - xSpinner.setPreferredSize(new Dimension(70, xSpinner.getPreferredSize().height)); - positionPanel.add(xSpinner); - - JLabel xLabel = new JLabel("X"); - xLabel.setForeground(Color.WHITE); - positionPanel.add(xLabel); - - // Y input - SpinnerNumberModel yModel = new SpinnerNumberModel(3000, 0, 20000, 1); - JSpinner ySpinner = new JSpinner(yModel); - ySpinner.setPreferredSize(new Dimension(70, ySpinner.getPreferredSize().height)); - positionPanel.add(ySpinner); - - JLabel yLabel = new JLabel("Y"); - yLabel.setForeground(Color.WHITE); - positionPanel.add(yLabel); - - panel.add(positionPanel, gbc); - - // Plane selection - gbc.gridy++; - JPanel planePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - planePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel planeLabel = new JLabel("Plane:"); - planeLabel.setForeground(Color.WHITE); - planePanel.add(planeLabel); - - SpinnerNumberModel planeModel = new SpinnerNumberModel(0, 0, 3, 1); - JSpinner planeSpinner = new JSpinner(planeModel); - planePanel.add(planeSpinner); - - panel.add(planePanel, gbc); - - // Distance range - gbc.gridy++; - JPanel distancePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - distancePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel distanceLabel = new JLabel("Max Distance (tiles):"); - distanceLabel.setForeground(Color.WHITE); - distancePanel.add(distanceLabel); - - SpinnerNumberModel distanceModel = new SpinnerNumberModel(5, 0, 104, 1); - JSpinner distanceSpinner = new JSpinner(distanceModel); - distancePanel.add(distanceSpinner); - - // Add info about distance=0 meaning exact location - JLabel exactLabel = new JLabel("(0 = exact position)"); - exactLabel.setForeground(Color.LIGHT_GRAY); - distancePanel.add(exactLabel); - - panel.add(distancePanel, gbc); - - // Current location getter - gbc.gridy++; - JPanel currentLocPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - currentLocPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JButton useCurrentLocationButton = new JButton("Use Current Location"); - useCurrentLocationButton.setBackground(ColorScheme.BRAND_ORANGE); - useCurrentLocationButton.setForeground(Color.WHITE); - useCurrentLocationButton.setFocusPainted(false); - useCurrentLocationButton.setToolTipText("Use your character's current position"); - useCurrentLocationButton.addActionListener(e -> { - // Get the current player location - if (!Microbot.isLoggedIn() || Microbot.getClient() == null || Microbot.getClient().getLocalPlayer() == null) { - return; - } - WorldPoint currentPoint = Rs2Player.getWorldLocation(); - if (currentPoint != null) { - xSpinner.setValue(currentPoint.getX()); - ySpinner.setValue(currentPoint.getY()); - planeSpinner.setValue(currentPoint.getPlane()); - } - }); - currentLocPanel.add(useCurrentLocationButton); - - JTextField nameField = new JTextField(20); - // Bank location selector - JButton selectBankButton = new JButton("Select Bank Location"); - selectBankButton.setBackground(BRAND_BLUE); // Using consistent color scheme - selectBankButton.setForeground(Color.WHITE); - selectBankButton.setToolTipText("Choose from common bank locations in the game"); - selectBankButton.setFocusPainted(false); // More consistent with other UI elements - selectBankButton.addActionListener(e -> { - // Show bank location selector - showBankLocationSelector(panel, (location) -> { - if (location != null) { - WorldPoint point = location.getWorldPoint(); - xSpinner.setValue(point.getX()); - ySpinner.setValue(point.getY()); - planeSpinner.setValue(point.getPlane()); - - // Update name field to include bank name - nameField.setText("At " + location.name() + " Bank"); - } - }); - }); - currentLocPanel.add(selectBankButton); - - panel.add(currentLocPanel, gbc); - - // Add a helpful description - gbc.gridy++; - JLabel descriptionLabel = new JLabel("Condition is met when player is within specified distance of target"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Name field for the condition - gbc.gridy++; - JPanel namePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - namePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel nameLabel = new JLabel("Condition Name:"); - nameLabel.setForeground(Color.WHITE); - namePanel.add(nameLabel); - - - nameField.setText("Position Condition"); - namePanel.add(nameField); - - panel.add(namePanel, gbc); - - // Store components for later access - panel.putClientProperty("positionXSpinner", xSpinner); - panel.putClientProperty("positionYSpinner", ySpinner); - panel.putClientProperty("positionPlaneSpinner", planeSpinner); - panel.putClientProperty("positionDistanceSpinner", distanceSpinner); - panel.putClientProperty("positionNameField", nameField); - } - /** - * Creates a panel for configuring AreaCondition - */ - private static void createAreaConditionPanel(JPanel panel, GridBagConstraints gbc) { - // Section title - JLabel titleLabel = new JLabel("Area Condition (Rectangular Area):"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // First corner coordinates - gbc.gridy++; - JPanel corner1Panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - corner1Panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel corner1Label = new JLabel("Southwest Corner:"); - corner1Label.setForeground(Color.WHITE); - corner1Panel.add(corner1Label); - - // X1 input - SpinnerNumberModel x1Model = new SpinnerNumberModel(3000, 0, 20000, 1); - JSpinner x1Spinner = new JSpinner(x1Model); - x1Spinner.setPreferredSize(new Dimension(70, x1Spinner.getPreferredSize().height)); - corner1Panel.add(x1Spinner); - - JLabel x1Label = new JLabel("X"); - x1Label.setForeground(Color.WHITE); - corner1Panel.add(x1Label); - - // Y1 input - SpinnerNumberModel y1Model = new SpinnerNumberModel(3000, 0, 20000, 1); - JSpinner y1Spinner = new JSpinner(y1Model); - y1Spinner.setPreferredSize(new Dimension(70, y1Spinner.getPreferredSize().height)); - corner1Panel.add(y1Spinner); - - JLabel y1Label = new JLabel("Y"); - y1Label.setForeground(Color.WHITE); - corner1Panel.add(y1Label); - - panel.add(corner1Panel, gbc); - - // Second corner coordinates - gbc.gridy++; - JPanel corner2Panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - corner2Panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel corner2Label = new JLabel("Northeast Corner:"); - corner2Label.setForeground(Color.WHITE); - corner2Panel.add(corner2Label); - - // X2 input - SpinnerNumberModel x2Model = new SpinnerNumberModel(3010, 0, 20000, 1); - JSpinner x2Spinner = new JSpinner(x2Model); - x2Spinner.setPreferredSize(new Dimension(70, x2Spinner.getPreferredSize().height)); - corner2Panel.add(x2Spinner); - - JLabel x2Label = new JLabel("X"); - x2Label.setForeground(Color.WHITE); - corner2Panel.add(x2Label); - - // Y2 input - SpinnerNumberModel y2Model = new SpinnerNumberModel(3010, 0, 20000, 1); - JSpinner y2Spinner = new JSpinner(y2Model); - y2Spinner.setPreferredSize(new Dimension(70, y2Spinner.getPreferredSize().height)); - corner2Panel.add(y2Spinner); - - JLabel y2Label = new JLabel("Y"); - y2Label.setForeground(Color.WHITE); - corner2Panel.add(y2Label); - - panel.add(corner2Panel, gbc); - - // Plane selection - gbc.gridy++; - JPanel planePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - planePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel planeLabel = new JLabel("Plane:"); - planeLabel.setForeground(Color.WHITE); - planePanel.add(planeLabel); - - SpinnerNumberModel planeModel = new SpinnerNumberModel(0, 0, 3, 1); - JSpinner planeSpinner = new JSpinner(planeModel); - planePanel.add(planeSpinner); - - panel.add(planePanel, gbc); - - // Current location getter - gbc.gridy++; - JPanel currentLocPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - currentLocPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JButton useCurrentLocationButton = new JButton("Create Area Around Current Location"); - useCurrentLocationButton.setBackground(ColorScheme.BRAND_ORANGE); - useCurrentLocationButton.setForeground(Color.WHITE); - useCurrentLocationButton.addActionListener(e -> { - // Get current player location and create area around it - WorldPoint currentPoint = Rs2Player.getWorldLocation(); - if (currentPoint != null) { - // Set corner1 to 5 tiles southwest - x1Spinner.setValue(currentPoint.getX() - 5); - y1Spinner.setValue(currentPoint.getY() - 5); - - // Set corner2 to 5 tiles northeast - x2Spinner.setValue(currentPoint.getX() + 5); - y2Spinner.setValue(currentPoint.getY() + 5); - - // Set plane - planeSpinner.setValue(currentPoint.getPlane()); - } - }); - currentLocPanel.add(useCurrentLocationButton); - - panel.add(currentLocPanel, gbc); - - // Add a helpful description - gbc.gridy++; - JLabel descriptionLabel = new JLabel("Condition is met when player is inside the rectangular area"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Name field for the condition - gbc.gridy++; - JPanel namePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - namePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel nameLabel = new JLabel("Condition Name:"); - nameLabel.setForeground(Color.WHITE); - namePanel.add(nameLabel); - - JTextField nameField = new JTextField(20); - nameField.setText("Area Condition"); - namePanel.add(nameField); - - panel.add(namePanel, gbc); - - // Store components for later access - panel.putClientProperty("areaX1Spinner", x1Spinner); - panel.putClientProperty("areaY1Spinner", y1Spinner); - panel.putClientProperty("areaX2Spinner", x2Spinner); - panel.putClientProperty("areaY2Spinner", y2Spinner); - panel.putClientProperty("areaPlaneSpinner", planeSpinner); - panel.putClientProperty("areaNameField", nameField); - } - /** - * Creates a panel for configuring RegionCondition - */ - private static void createRegionConditionPanel(JPanel panel, GridBagConstraints gbc) { - // Section title - JLabel titleLabel = new JLabel("Region Condition (Game Region):"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // Region IDs input - gbc.gridy++; - JPanel regionPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - regionPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel regionsLabel = new JLabel("Region IDs (comma-separated):"); - regionsLabel.setForeground(Color.WHITE); - regionPanel.add(regionsLabel); - - JTextField regionIdsField = new JTextField(20); - regionIdsField.setToolTipText("Enter region IDs separated by commas (e.g., 12850,12851)"); - regionPanel.add(regionIdsField); - - panel.add(regionPanel, gbc); - - // Current region getter - gbc.gridy++; - JPanel currentRegionPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - currentRegionPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JButton useCurrentRegionButton = new JButton("Use Current Region"); - useCurrentRegionButton.setBackground(ColorScheme.BRAND_ORANGE); - useCurrentRegionButton.setForeground(Color.WHITE); - useCurrentRegionButton.addActionListener(e -> { - // Get the current player region - WorldPoint currentPoint = Rs2Player.getWorldLocation(); - if (currentPoint != null) { - int regionId = currentPoint.getRegionID(); - regionIdsField.setText(String.valueOf(regionId)); - } - }); - currentRegionPanel.add(useCurrentRegionButton); - - panel.add(currentRegionPanel, gbc); - - // Region presets panel - gbc.gridy++; - JPanel presetsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - presetsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel presetsLabel = new JLabel("Common Regions:"); - presetsLabel.setForeground(Color.WHITE); - presetsPanel.add(presetsLabel); - - // Add example regions as presets - String[][] regionPresets = { - {"Lumbridge", "12850"}, - {"Varrock", "12853"}, - {"Grand Exchange", "12598"}, - {"Falador", "12084"}, - {"Edgeville", "12342"} - }; - - JComboBox regionPresetsCombo = new JComboBox<>(); - regionPresetsCombo.addItem("Select a region..."); - for (String[] preset : regionPresets) { - regionPresetsCombo.addItem(preset[0]); - } - - regionPresetsCombo.addActionListener(e -> { - int selectedIndex = regionPresetsCombo.getSelectedIndex(); - if (selectedIndex > 0) { - regionIdsField.setText(regionPresets[selectedIndex - 1][1]); - } - }); - - presetsPanel.add(regionPresetsCombo); - - panel.add(presetsPanel, gbc); - - // Add a helpful description - gbc.gridy++; - JLabel descriptionLabel = new JLabel("Condition is met when player is in any of the specified regions"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Name field for the condition - gbc.gridy++; - JPanel namePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - namePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel nameLabel = new JLabel("Condition Name:"); - nameLabel.setForeground(Color.WHITE); - namePanel.add(nameLabel); - - JTextField nameField = new JTextField(20); - nameField.setText("Region Condition"); - namePanel.add(nameField); - - panel.add(namePanel, gbc); - - // Store components for later access - panel.putClientProperty("regionIdsField", regionIdsField); - panel.putClientProperty("regionNameField", nameField); - } - /** - * Creates a PositionCondition from the panel configuration - */ - public static PositionCondition createPositionCondition(JPanel configPanel) { - JSpinner xSpinner = (JSpinner) configPanel.getClientProperty("positionXSpinner"); - JSpinner ySpinner = (JSpinner) configPanel.getClientProperty("positionYSpinner"); - JSpinner planeSpinner = (JSpinner) configPanel.getClientProperty("positionPlaneSpinner"); - JSpinner distanceSpinner = (JSpinner) configPanel.getClientProperty("positionDistanceSpinner"); - JTextField nameField = (JTextField) configPanel.getClientProperty("positionNameField"); - - if (xSpinner == null || ySpinner == null || planeSpinner == null || distanceSpinner == null) { - throw new IllegalStateException("Position condition panel not properly configured - missing spinner components"); - } - - int x = (Integer) xSpinner.getValue(); - int y = (Integer) ySpinner.getValue(); - int plane = (Integer) planeSpinner.getValue(); - int distance = (Integer) distanceSpinner.getValue(); - - String name = nameField != null ? nameField.getText() : "Position Condition"; - if (name.isEmpty()) { - name = "Position Condition"; - } - - return new PositionCondition(name, x, y, plane, distance); - } - - - /** - * Creates an AreaCondition from the panel configuration - */ - public static AreaCondition createAreaCondition(JPanel configPanel) { - JSpinner x1Spinner = (JSpinner) configPanel.getClientProperty("areaX1Spinner"); - JSpinner y1Spinner = (JSpinner) configPanel.getClientProperty("areaY1Spinner"); - JSpinner x2Spinner = (JSpinner) configPanel.getClientProperty("areaX2Spinner"); - JSpinner y2Spinner = (JSpinner) configPanel.getClientProperty("areaY2Spinner"); - JSpinner planeSpinner = (JSpinner) configPanel.getClientProperty("areaPlaneSpinner"); - JTextField nameField = (JTextField) configPanel.getClientProperty("areaNameField"); - - if (x1Spinner == null || y1Spinner == null || x2Spinner == null || y2Spinner == null || planeSpinner == null) { - throw new IllegalStateException("Area condition panel not properly configured - missing spinner components"); - } - - int x1 = (Integer) x1Spinner.getValue(); - int y1 = (Integer) y1Spinner.getValue(); - int x2 = (Integer) x2Spinner.getValue(); - int y2 = (Integer) y2Spinner.getValue(); - int plane = (Integer) planeSpinner.getValue(); - - String name = nameField != null ? nameField.getText() : "Area Condition"; - if (name.isEmpty()) { - name = "Area Condition"; - } - - return new AreaCondition(name, x1, y1, x2, y2, plane); - } - /** - * Creates a RegionCondition from the panel configuration - */ - public static RegionCondition createRegionCondition(JPanel configPanel) { - JTextField regionIdsField = (JTextField) configPanel.getClientProperty("regionIdsField"); - JTextField nameField = (JTextField) configPanel.getClientProperty("regionNameField"); - - if (regionIdsField == null) { - throw new IllegalStateException("Region condition panel not properly configured - missing regionIdsField"); - } - - String regionIdsText = regionIdsField.getText().trim(); - if (regionIdsText.isEmpty()) { - throw new IllegalArgumentException("Region IDs cannot be empty"); - } - - // Parse comma-separated region IDs - String[] regionIdStrings = regionIdsText.split(","); - int[] regionIds = new int[regionIdStrings.length]; - - try { - for (int i = 0; i < regionIdStrings.length; i++) { - regionIds[i] = Integer.parseInt(regionIdStrings[i].trim()); - } - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid region ID format. Must be comma-separated integers."); - } - - String name = nameField != null ? nameField.getText() : "Region Condition"; - if (name.isEmpty()) { - name = "Region Condition"; - } - - return new RegionCondition(name, regionIds); - } - /** - * Shows a dialog to select a bank location - * - * @param parentComponent The parent component for the dialog - * @param callback Callback function to handle the selected bank location - */ - private static void showBankLocationSelector(Component parentComponent, Consumer callback) { - // Create a dialog for bank location selection - JDialog dialog = new JDialog(SwingUtilities.getWindowAncestor(parentComponent), "Select Bank Location", Dialog.ModalityType.APPLICATION_MODAL); - dialog.setLayout(new BorderLayout()); - dialog.setSize(400, 500); - dialog.setLocationRelativeTo(parentComponent); - - // Create a panel for the dialog content - JPanel contentPanel = new JPanel(new BorderLayout()); - contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - contentPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Search field at the top - JTextField searchField = new JTextField(); - searchField.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - searchField.setForeground(Color.WHITE); - searchField.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - BorderFactory.createEmptyBorder(5, 5, 5, 5))); - - JLabel searchLabel = new JLabel("Search:"); - searchLabel.setForeground(Color.WHITE); - searchLabel.setFont(FontManager.getRunescapeSmallFont()); - - JPanel searchPanel = new JPanel(new BorderLayout()); - searchPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - searchPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0)); - searchPanel.add(searchLabel, BorderLayout.WEST); - searchPanel.add(searchField, BorderLayout.CENTER); - - // List of bank locations - DefaultListModel bankListModel = new DefaultListModel<>(); - for (BankLocation location : BankLocation.values()) { - bankListModel.addElement(location); - } - - JList bankList = new JList<>(bankListModel); - bankList.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - bankList.setForeground(Color.WHITE); - bankList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - bankList.setCellRenderer(new DefaultListCellRenderer() { - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, - boolean isSelected, boolean cellHasFocus) { - super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - if (value instanceof BankLocation) { - setText(((BankLocation) value).name()); - } - return this; - } - }); - - JScrollPane scrollPane = new JScrollPane(bankList); - scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); - scrollPane.getVerticalScrollBar().setUnitIncrement(16); - - // Filter the list when search text changes - searchField.getDocument().addDocumentListener(new DocumentListener() { - @Override - public void insertUpdate(DocumentEvent e) { - filterList(); - } - - @Override - public void removeUpdate(DocumentEvent e) { - filterList(); - } - - @Override - public void changedUpdate(DocumentEvent e) { - filterList(); - } - - private void filterList() { - String searchText = searchField.getText().toLowerCase(); - DefaultListModel filteredModel = new DefaultListModel<>(); - - for (BankLocation location : BankLocation.values()) { - if (location.name().toLowerCase().contains(searchText)) { - filteredModel.addElement(location); - } - } - - bankList.setModel(filteredModel); - } - }); - - // Buttons at the bottom - JButton selectButton = new JButton("Select"); - selectButton.setBackground(ColorScheme.BRAND_ORANGE); - selectButton.setForeground(Color.WHITE); - selectButton.setFocusPainted(false); - selectButton.addActionListener(e -> { - BankLocation selectedLocation = bankList.getSelectedValue(); - callback.accept(selectedLocation); - dialog.dispose(); - }); - - JButton cancelButton = new JButton("Cancel"); - cancelButton.setBackground(ColorScheme.LIGHT_GRAY_COLOR); - cancelButton.setForeground(Color.BLACK); - cancelButton.setFocusPainted(false); - cancelButton.addActionListener(e -> dialog.dispose()); - - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - buttonPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - buttonPanel.add(cancelButton); - buttonPanel.add(selectButton); - - // Add everything to the content panel - contentPanel.add(searchPanel, BorderLayout.NORTH); - contentPanel.add(scrollPane, BorderLayout.CENTER); - contentPanel.add(buttonPanel, BorderLayout.SOUTH); - - // Add content panel to dialog and show it - dialog.add(contentPanel); - dialog.setVisible(true); - } - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/AndCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/AndCondition.java deleted file mode 100644 index 5f1db7465eb..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/AndCondition.java +++ /dev/null @@ -1,245 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.logical; - -import java.time.Duration; -import java.time.ZonedDateTime; -import java.util.Optional; -import java.util.List; -import java.util.ArrayList; -import lombok.EqualsAndHashCode; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; - -/** - * Logical AND combination of conditions - all must be met. - */ -@EqualsAndHashCode(callSuper = true) -public class AndCondition extends LogicalCondition { - @Override - public boolean isSatisfied() { - if (conditions.isEmpty()) return true; - return conditions.stream().allMatch(Condition::isSatisfied); - } - - /** - * Returns a detailed description of the AND condition with additional status information - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - // Basic description - sb.append("AND Logical Condition: All conditions must be satisfied\n"); - - // Status information - boolean satisfied = isSatisfied(); - sb.append("Status: ").append(satisfied ? "Satisfied" : "Not satisfied").append("\n"); - sb.append("Child Conditions: ").append(conditions.size()).append("\n"); - - // Progress information - double progress = getProgressPercentage(); - sb.append(String.format("Overall Progress: %.1f%%\n", progress)); - - // Count satisfied conditions - int satisfiedCount = 0; - for (Condition condition : conditions) { - if (condition.isSatisfied()) { - satisfiedCount++; - } - } - sb.append("Satisfied Conditions: ").append(satisfiedCount).append("/").append(conditions.size()).append("\n\n"); - - // List all child conditions - sb.append("Child Conditions:\n"); - for (int i = 0; i < conditions.size(); i++) { - Condition condition = conditions.get(i); - sb.append(String.format("%d. %s [%s]\n", - i + 1, - condition.getDescription(), - condition.isSatisfied() ? "SATISFIED" : "NOT SATISFIED")); - } - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - // Basic information - sb.append("AndCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Type: AND (All conditions must be satisfied)\n"); - sb.append(" │ Child Conditions: ").append(conditions.size()).append("\n"); - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - boolean allSatisfied = isSatisfied(); - sb.append(" │ Satisfied: ").append(allSatisfied).append("\n"); - - // Count satisfied conditions - int satisfiedCount = 0; - for (Condition condition : conditions) { - if (condition.isSatisfied()) { - satisfiedCount++; - } - } - sb.append(" │ Satisfied Conditions: ").append(satisfiedCount).append("/").append(conditions.size()).append("\n"); - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Child conditions - if (!conditions.isEmpty()) { - sb.append(" ├─ Child Conditions ────────────────────────\n"); - - for (int i = 0; i < conditions.size(); i++) { - Condition condition = conditions.get(i); - String prefix = (i == conditions.size() - 1) ? " └─ " : " ├─ "; - - sb.append(prefix).append(String.format("Condition %d: %s [%s]\n", - i + 1, - condition.getClass().getSimpleName(), - condition.isSatisfied() ? "SATISFIED" : "NOT SATISFIED")); - } - } else { - sb.append(" └─ No Child Conditions ───────────────────────\n"); - } - - return sb.toString(); - } - - /** - * For an AND condition, any unsatisfied child condition blocks the entire AND. - * This method returns all child conditions that are currently not satisfied. - * - * @return List of all unsatisfied child conditions - */ - @Override - public List getBlockingConditions() { - List blockingConditions = new ArrayList<>(); - - // In an AND condition, any unsatisfied condition blocks the entire AND - for (Condition condition : conditions) { - if (!condition.isSatisfied()) { - blockingConditions.add(condition); - } - } - - return blockingConditions; - } - - /** - * Gets the next time this AND condition will be satisfied. - * If all conditions are satisfied, returns the minimum trigger time among all conditions. - * If any condition is not satisfied, returns the maximum trigger time among unsatisfied TimeConditions. - * - * @return Optional containing the next trigger time, or empty if none available - */ - @Override - public Optional getCurrentTriggerTime() { - if (conditions.isEmpty()) { - return Optional.empty(); - } - - boolean allSatisfied = true; - ZonedDateTime minTriggerTime = null; - ZonedDateTime maxUnsatisfiedTimeConditionTriggerTime = null; - - // Check if all conditions are satisfied and track min trigger time - for (Condition condition : conditions) { - // Check if this condition is satisfied - if (!condition.isSatisfied()) { - allSatisfied = false; - - // For unsatisfied TimeConditions, track the maximum trigger time - if (condition instanceof TimeCondition) { - Optional nextTrigger = condition.getCurrentTriggerTime(); - if (nextTrigger.isPresent()) { - ZonedDateTime triggerTime = nextTrigger.get(); - if (maxUnsatisfiedTimeConditionTriggerTime == null || - triggerTime.isAfter(maxUnsatisfiedTimeConditionTriggerTime)) { - maxUnsatisfiedTimeConditionTriggerTime = triggerTime; - } - } - } - } - // If satisfied, track the minimum trigger time - else { - Optional triggerTime = condition.getCurrentTriggerTime(); - if (triggerTime.isPresent()) { - if (minTriggerTime == null || triggerTime.get().isBefore(minTriggerTime)) { - minTriggerTime = triggerTime.get(); - } - } - } - } - - // If all conditions are satisfied, return the minimum trigger time - if (allSatisfied) { - return minTriggerTime != null ? Optional.of(minTriggerTime) : Optional.empty(); - } - - // If at least one condition is not satisfied, return the maximum trigger time - // of unsatisfied TimeConditions - return maxUnsatisfiedTimeConditionTriggerTime != null ? - Optional.of(maxUnsatisfiedTimeConditionTriggerTime) : Optional.empty(); - } - public void pause() { - // Pause all child conditions - for (Condition condition : conditions) { - condition.pause(); - } - - - } - - - public void resume() { - // Resume all child conditions - for (Condition condition : conditions) { - condition.resume(); - } - - } - - /** - * Gets the estimated time until this AND condition will be satisfied. - * For an AND condition, this returns the maximum (latest) estimated time - * among all child conditions, since all conditions must be satisfied - * for the entire AND condition to be satisfied. - * - * @return Optional containing the estimated duration until satisfaction, or empty if not determinable - */ - @Override - public Optional getEstimatedTimeWhenIsSatisfied() { - if (conditions.isEmpty()) { - return Optional.of(Duration.ZERO); - } - - // If all conditions are already satisfied, return zero - if (isSatisfied()) { - return Optional.of(Duration.ZERO); - } - - Duration longestTime = Duration.ZERO; - boolean hasEstimate = false; - boolean allHaveEstimates = true; - - for (Condition condition : conditions) { - Optional estimate = condition.getEstimatedTimeWhenIsSatisfied(); - - if (estimate.isPresent()) { - hasEstimate = true; - Duration currentEstimate = estimate.get(); - - if (currentEstimate.compareTo(longestTime) > 0) { - longestTime = currentEstimate; - } - } else { - // If any condition can't provide an estimate, we can't provide a reliable estimate - // for the entire AND condition - allHaveEstimates = false; - } - } - - // Only return an estimate if all conditions can provide estimates - return (hasEstimate && allHaveEstimates) ? Optional.of(longestTime) : Optional.empty(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/LockCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/LockCondition.java deleted file mode 100644 index e19f76e7ed6..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/LockCondition.java +++ /dev/null @@ -1,167 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.logical; - -import java.time.ZonedDateTime; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.breakhandler.BreakHandlerScript; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; - -/** - * A condition that can be manually locked/unlocked by a plugin. - * When locked, the condition is always unsatisfied regardless of other conditions. - * This can be used to prevent a plugin from being stopped during critical operations. - */ -@Slf4j -@EqualsAndHashCode(callSuper = false) -public class LockCondition implements Condition { - - private final AtomicBoolean locked = new AtomicBoolean(false); - @Getter - private final String reason; - @Getter - private final boolean withBreakHandlerLock; - - @Deprecated - public LockCondition(String reason) { - this(reason, false, true); - } - /** - * Creates a new LockCondition with the specified reason and withBreakHandlerLock flag. - * The lock will be initially unlocked (defaultLock = false). - * - * @param reason The reason or description for this lock condition - * @param withBreakHandlerLock Whether to also lock the BreakHandlerScript when this lock is active - */ - public LockCondition(String reason, boolean withBreakHandlerLock) { - this(reason, false, withBreakHandlerLock); - } - - /** - * Creates a new LockCondition with a default reason and specified initial lock state. - * - * @param defaultLock The initial state of the lock (true for locked, false for unlocked) - * @param withBreakHandlerLock Whether to also lock the BreakHandlerScript when this lock is active - */ - public LockCondition(boolean defaultLock, boolean withBreakHandlerLock) { - this("Plugin is in a critical operation", defaultLock, withBreakHandlerLock); - } - /** - * Creates a new LockCondition with the specified reason and initial lock state. - * - * @param reason The reason or description for this lock condition - * @param defaultLock The initial state of the lock (true for locked, false for unlocked) - */ - public LockCondition(String reason, boolean defaultLock, boolean withBreakHandlerLock) { - this.reason = reason; - this.locked.set(defaultLock); - this.withBreakHandlerLock = withBreakHandlerLock; - } - - /** - * Locks the condition, preventing the plugin from being stopped. - */ - public void lock() { - if (locked == null) { - log.warn("LockCondition is null, cannot lock"); - return; - } - boolean wasLocked = locked.getAndSet(true); - if(withBreakHandlerLock){ - BreakHandlerScript.setLockState(true); - } - if (!wasLocked) { - log.debug("LockCondition locked: {}", reason); - } - } - - /** - * Unlocks the condition, allowing the plugin to be stopped. - */ - public void unlock() { - if (locked == null) { - log.warn("LockCondition is null, cannot unlock"); - return; - } - boolean wasLocked = locked.getAndSet(false); - if (withBreakHandlerLock){ - BreakHandlerScript.setLockState(false); - } - if (wasLocked) { - log.debug("LockCondition unlocked: {}", reason); - } - } - - /** - * Toggles the lock state. - * - * @return The new lock state (true if locked, false if unlocked) - */ - public boolean toggleLock() { - if (locked == null) { - log.warn("LockCondition is null, cannot toggle lock state"); - return false; - } - boolean newState = !locked.get(); - BreakHandlerScript.setLockState(newState); - locked.set(newState); - return newState; - } - - /** - * Checks if the condition is currently locked. - * - * @return true if locked, false otherwise - */ - public boolean isLocked() { - return locked.get(); - } - - @Override - public boolean isSatisfied() { - // If locked, the condition is NOT satisfied, which prevents stopping - return !isLocked(); - } - - @Override - public String getDescription() { - return "Lock Condition: " + (isLocked() ? "\"LOCKED\" - " + reason : "UNLOCKED"); - } - - @Override - public String getDetailedDescription() { - return getDescription(); - } - - @Override - public ConditionType getType() { - return ConditionType.LOGICAL; - } - - @Override - public void reset(boolean randomize) { - // Reset does nothing by default - lock state is controlled manually - } - - @Override - public Optional getCurrentTriggerTime() { - // Lock conditions don't have a specific trigger time - return Optional.empty(); - } - - public void pause() { - - - - } - - - public void resume() { - - } - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/LogicalCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/LogicalCondition.java deleted file mode 100644 index 80be25c99d0..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/LogicalCondition.java +++ /dev/null @@ -1,1504 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.logical; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.events.*; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.enums.UpdateOption; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; - -/** - * Base class for logical combinations of conditions. - * - * IMPORTANT: When adding new event types to the Condition interface, you MUST also: - * 1. Override the event method in this class - * 2. Use the propagateEvent() helper to forward the event to all child conditions - * - * This ensures proper event propagation through the condition hierarchy. - */ -@Slf4j -@EqualsAndHashCode(callSuper = false) -public abstract class LogicalCondition implements Condition { - - public LogicalCondition(Condition... conditions) { - for (Condition condition : conditions) { - addCondition(condition); - } - } - @Getter - protected List conditions = new ArrayList<>(); - - public LogicalCondition addCondition(Condition condition) { - //check if the condition is already in the list, with .equals() - // this prevents duplicates and unnecessary processing, - for (Condition conditionInList : conditions) { - if (conditionInList.equals(condition)) { - return this; - } - } - conditions.add(condition); - return this; - } - - @Override - public ConditionType getType() { - return ConditionType.LOGICAL; - } - /** - * Helper method to propagate any event to all child conditions. - * This centralizes the propagation logic to avoid code duplication. - * - * @param The event type - * @param event The event object to propagate - * @param eventHandler The method reference to the appropriate event handler - */ - protected void propagateEvent(T event, PropagationHandler eventHandler) { - for (Condition condition : conditions) { - try { - eventHandler.handle(condition, event); - } catch (Exception e) { - // Optional: Add logging - log.error("Error propagating event to condition: " + condition.getClass().getSimpleName(), e); - //log stack trace if needed - e.printStackTrace(); - } - } - } - - /** - * Functional interface for event propagation handling - */ - @FunctionalInterface - protected interface PropagationHandler { - void handle(Condition condition, T event); - } - - - @Override - public void onStatChanged(StatChanged event) { - propagateEvent(event, (condition, e) -> condition.onStatChanged(e)); - } - - @Override - public void onItemContainerChanged(ItemContainerChanged event) { - propagateEvent(event, (condition, e) -> condition.onItemContainerChanged(e)); - } - - @Override - public void onGameTick(GameTick event) { - propagateEvent(event, (condition, e) -> condition.onGameTick(e)); - } - - @Override - public void onNpcChanged(NpcChanged event) { - propagateEvent(event, (condition, e) -> condition.onNpcChanged(e)); - } - - @Override - public void onNpcSpawned(NpcSpawned event) { - propagateEvent(event, (condition, e) -> condition.onNpcSpawned(e)); - } - - @Override - public void onNpcDespawned(NpcDespawned event) { - propagateEvent(event, (condition, e) -> condition.onNpcDespawned(e)); - } - - @Override - public void onGroundObjectSpawned(GroundObjectSpawned event) { - propagateEvent(event, (condition, e) -> condition.onGroundObjectSpawned(e)); - } - - @Override - public void onGroundObjectDespawned(GroundObjectDespawned event) { - propagateEvent(event, (condition, e) -> condition.onGroundObjectDespawned(e)); - } - - @Override - public void onItemSpawned(ItemSpawned event) { - propagateEvent(event, (condition, e) -> condition.onItemSpawned(e)); - } - - @Override - public void onItemDespawned(ItemDespawned event) { - propagateEvent(event, (condition, e) -> condition.onItemDespawned(e)); - } - - @Override - public void onMenuOptionClicked(MenuOptionClicked event) { - propagateEvent(event, (condition, e) -> condition.onMenuOptionClicked(e)); - } - - @Override - public void onChatMessage(ChatMessage event) { - propagateEvent(event, (condition, e) -> condition.onChatMessage(e)); - } - - @Override - public void onHitsplatApplied(HitsplatApplied event) { - propagateEvent(event, (condition, e) -> condition.onHitsplatApplied(e)); - } - - @Override - public void onVarbitChanged(VarbitChanged event) { - propagateEvent(event, (condition, e) -> condition.onVarbitChanged(e)); - } - - @Override - public void onInteractingChanged(InteractingChanged event) { - propagateEvent(event, (condition, e) -> condition.onInteractingChanged(e)); - } - - @Override - public void onAnimationChanged(AnimationChanged event) { - propagateEvent(event, (condition, e) -> condition.onAnimationChanged(e)); - } - @Override - public void onGameStateChanged(GameStateChanged gameStateChanged) { - propagateEvent(gameStateChanged, (condition, e) -> condition.onGameStateChanged(e)); - } - - - - /** - * Checks if this logical condition contains the specified condition, - * either directly or within any nested logical conditions. - * - * @param targetCondition The condition to search for - * @return true if the condition exists within this logical structure, false otherwise - */ - public boolean contains(Condition targetCondition) { - // Recursively search in nested logical conditions - for (Condition condition : conditions) { - - // If this is a logical condition, search within it - if (condition instanceof LogicalCondition) { - if (((LogicalCondition) condition).contains(targetCondition)) { - return true; - } - } - // Special case for NotCondition which wraps a single condition - else if (condition instanceof NotCondition) { - if (((NotCondition) condition).getCondition().equals(targetCondition)) { - return true; - } - - // If the wrapped condition is itself a logical condition, search within it - Condition wrappedCondition = ((NotCondition) condition).getCondition(); - if (wrappedCondition instanceof LogicalCondition) { - if (((LogicalCondition) wrappedCondition).contains(targetCondition)) { - return true; - } - } - } - if (condition.equals(targetCondition)) { - //log.info("Found condition: {} equals\nthe condition {}\nin the logical{}",targetCondition.getDescription(), - //condition.getDescription(), this.getDescription()); - return true; - } - } - - // Not found - return false; - } - - @Override - public double getProgressPercentage() { - if (conditions.isEmpty()) { - return isSatisfied() ? 100.0 : 0.0; - } - - // For AND conditions, use the average progress (average over links) - if (this instanceof AndCondition) { - return conditions.stream() - .mapToDouble(Condition::getProgressPercentage) - .average() - .orElse(0.0); - } - // For OR conditions, use the maximum progress (strongest link) - else if (this instanceof OrCondition) { - return conditions.stream() - .mapToDouble(Condition::getProgressPercentage) - .max() - .orElse(0.0); - } - - // Default fallback to average progress - return conditions.stream() - .mapToDouble(Condition::getProgressPercentage) - .average() - .orElse(0.0); - } - - @Override - public int getTotalConditionCount() { - if (conditions.isEmpty()) { - return 0; - } - - // Sum up all nested condition counts - return conditions.stream() - .mapToInt(Condition::getTotalConditionCount) - .sum(); - } - - @Override - public int getMetConditionCount() { - if (conditions.isEmpty()) { - return 0; - } - - // Sum up all nested met condition counts - return conditions.stream() - .mapToInt(Condition::getMetConditionCount) - .sum(); - } - - @Override - public 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(")"); - } - - // Add count of met conditions - int total = getTotalConditionCount(); - int met = getMetConditionCount(); - if (total > 0) { - sb.append(" - ").append(met).append("/").append(total).append(" conditions SATISFIED"); - } - } - - sb.append("\n"); - - // Add nested conditions with additional indent - for (Condition condition : conditions) { - sb.append(condition.getStatusInfo(indent + 2, showProgress)).append("\n"); - } - - return sb.toString(); - } - - /** - * Removes a condition from this logical condition or its nested structure. - * Returns true if the condition was found and removed. - */ - public boolean removeCondition(Condition targetCondition) { - // Direct removal from this logical condition's immediate children - if (conditions.remove(targetCondition)) { - return true; - } - - // Search nested logical conditions - for (int i = 0; i < conditions.size(); i++) { - Condition condition = conditions.get(i); - - // Handle nested logical conditions - if (condition instanceof LogicalCondition) { - LogicalCondition nestedLogical = (LogicalCondition) condition; - if (nestedLogical.removeCondition(targetCondition)) { - // If this left the nested logical empty, remove it too - if (nestedLogical.getConditions().isEmpty()) { - conditions.remove(i); - } - return true; - } - } - // Handle NOT condition as a special case - else if (condition instanceof NotCondition) { - NotCondition notCondition = (NotCondition) condition; - - // If NOT directly wraps our target condition - if (notCondition.getCondition() == targetCondition) { - conditions.remove(i); - return true; - } - - // If NOT wraps a logical condition, try removing from there - if (notCondition.getCondition() instanceof LogicalCondition) { - LogicalCondition nestedLogical = (LogicalCondition) notCondition.getCondition(); - if (nestedLogical.removeCondition(targetCondition)) { - // If this left the nested logical empty, remove the NOT condition too - if (nestedLogical.getConditions().isEmpty()) { - conditions.remove(i); - } - return true; - } - } - } - } - - return false; - } - - /** - * Finds a logical condition that contains the given condition. - * Useful for determining which logical group a condition belongs to. - * - * @param targetCondition The condition to find - * @return The logical condition containing the target, or null if not found - */ - public LogicalCondition findContainingLogical(Condition targetCondition) { - // Check if it's directly in this logical's conditions - if (conditions.contains(targetCondition)) { - return this; - } - - // Search in nested logical conditions - for (Condition condition : conditions) { - if (condition instanceof LogicalCondition) { - LogicalCondition result = ((LogicalCondition) condition).findContainingLogical(targetCondition); - if (result != null) { - return result; - } - } else if (condition instanceof NotCondition) { - NotCondition notCondition = (NotCondition) condition; - - // If NOT directly wraps our target - if (notCondition.getCondition() == targetCondition) { - return this; - } - - // If NOT wraps a logical, search in there - if (notCondition.getCondition() instanceof LogicalCondition) { - LogicalCondition result = - ((LogicalCondition) notCondition.getCondition()).findContainingLogical(targetCondition); - if (result != null) { - return result; - } - } - } - } - - return null; - } - - public void softReset() { - if (isSatisfied()){ - for (Condition condition : conditions) { - condition.reset(); - } - } - } - - public void softReset(boolean randomize) { - if (isSatisfied()){ - for (Condition condition : conditions) { - condition.reset(randomize); - } - } - } - public void reset() { - - for (Condition condition : conditions) { - condition.reset(); - } - - } - - public void reset(boolean randomize) { - - for (Condition condition : conditions) { - condition.reset(randomize); - } - - } - - public void hardReset(){ - for (Condition condition : conditions) { - condition.hardReset(); - } - } - /** - * Adds a condition to a specific position in the condition tree - * Useful for preserving ordering when reconstructing the tree. - */ - public LogicalCondition addConditionAt(int index, Condition condition) { - if (index >= 0 && index <= conditions.size()) { - conditions.add(index, condition); - } else { - conditions.add(condition); - } - return this; - } - - /** - * Gets a human-readable description of this logical condition. - * This provides a default implementation that subclasses can override. - * - * @return A string description of the logical condition - */ - @Override - public String getDescription() { - if (conditions.isEmpty()) { - return "No conditions"; - } - - String conditionType = (this instanceof AndCondition) ? "ALL of" : - ((this instanceof OrCondition) ? "ANY of" : "Logical group of"); - - StringBuilder sb = new StringBuilder(conditionType).append(": ("); - String separator = (this instanceof AndCondition) ? " AND " : - ((this instanceof OrCondition) ? " OR " : ", "); - - for (int i = 0; i < conditions.size(); i++) { - if (i > 0) sb.append(separator); - sb.append(conditions.get(i).getDescription()); - } - sb.append(")"); - return sb.toString(); - } - - /** - * Gets a description formatted for HTML display in UI components. - * This is useful for tooltips and other rich text displays. - * - * @param maxLength Maximum length of descriptions before truncating - * @return HTML formatted description - */ - public String getHtmlDescription(int maxLength) { - if (conditions.isEmpty()) { - return "No conditions"; - } - - StringBuilder sb = new StringBuilder(""); - - // Add operator with appropriate styling - if (this instanceof AndCondition) { - sb.append("ALL of: ("); - } else if (this instanceof OrCondition) { - sb.append("ANY of: ("); - } else { - sb.append("Logical group of: ("); - } - - // Add child conditions with appropriate separators - String separator = (this instanceof AndCondition) ? " AND " : - ((this instanceof OrCondition) ? " OR " : ", "); - - int totalLength = 0; - for (int i = 0; i < conditions.size(); i++) { - if (i > 0) sb.append(separator); - - String description = conditions.get(i).getDescription(); - - // Truncate long descriptions - if (maxLength > 0 && totalLength + description.length() > maxLength) { - description = description.substring(0, Math.max(10, maxLength - totalLength - 3)) + "..."; - } - - // Style based on satisfied state - if (conditions.get(i).isSatisfied()) { - sb.append("").append(description).append(""); - } else { - sb.append("").append(description).append(""); - } - - totalLength += description.length(); - } - - sb.append(")"); - return sb.toString(); - } - - /** - * Gets a simple HTML representation for use in tooltips. - * - * @return HTML formatted description - */ - public String getTooltipHtml() { - return getHtmlDescription(100); - } - /** - * Recursively finds all LockConditions within this LogicalCondition structure. - * This utility method is used by the break handler to detect locked conditions - * that should prevent breaks from occurring. - * - * @return List of all LockConditions found in the structure - */ - public List findAllLockConditions() { - List lockConditions = new ArrayList<>(); - - for (Condition condition : conditions) { - if (condition instanceof LockCondition) { - lockConditions.add((LockCondition) condition); - } else if (condition instanceof LogicalCondition) { - // Recursively search in nested logical conditions - lockConditions.addAll(((LogicalCondition) condition).findAllLockConditions()); - } else if (condition instanceof NotCondition) { - // Check if the wrapped condition is a LockCondition or contains LockConditions - Condition wrappedCondition = ((NotCondition) condition).getCondition(); - if (wrappedCondition instanceof LockCondition) { - lockConditions.add((LockCondition) wrappedCondition); - } else if (wrappedCondition instanceof LogicalCondition) { - lockConditions.addAll(((LogicalCondition) wrappedCondition).findAllLockConditions()); - } - } - } - - return lockConditions; - } - - /** - * Recursively finds all PredicateConditions within this LogicalCondition structure. - * This utility method is used by the break handler to detect predicate conditions - * that may prevent breaks from occurring when their predicate is not satisfied. - * - * @return List of all PredicateConditions found in the structure - */ - public List> findAllPredicateConditions() { - List> predicateConditions = new ArrayList<>(); - - for (Condition condition : conditions) { - if (condition instanceof PredicateCondition) { - predicateConditions.add((PredicateCondition) condition); - } else if (condition instanceof LogicalCondition) { - // Recursively search in nested logical conditions - predicateConditions.addAll(((LogicalCondition) condition).findAllPredicateConditions()); - } else if (condition instanceof NotCondition) { - // Check if the wrapped condition is a PredicateCondition or contains PredicateConditions - Condition wrappedCondition = ((NotCondition) condition).getCondition(); - if (wrappedCondition instanceof PredicateCondition) { - predicateConditions.add((PredicateCondition) wrappedCondition); - } else if (wrappedCondition instanceof LogicalCondition) { - predicateConditions.addAll(((LogicalCondition) wrappedCondition).findAllPredicateConditions()); - } - } - } - - return predicateConditions; - } - /** - * Recursively finds all TimeCondition instances in this logical condition structure. - * This searches through the entire hierarchy including nested logical conditions. - * - * @return A list of all TimeCondition instances found in this structure - */ - public List findTimeConditions() { - List timeConditions = new ArrayList<>(); - - // Recursively search for TimeCondition instances - for (Condition condition : conditions) { - // Check if this condition is a TimeCondition - if (condition.getType() == ConditionType.TIME) { - timeConditions.add(condition); - continue; - } - - // If this is a logical condition, search inside it recursively - if (condition instanceof LogicalCondition) { - timeConditions.addAll(((LogicalCondition) condition).findTimeConditions()); - continue; - } - - // Special case for NotCondition which wraps a single condition - if (condition instanceof NotCondition) { - Condition wrappedCondition = ((NotCondition) condition).getCondition(); - - // Check if the wrapped condition is a TimeCondition - if (wrappedCondition.getType() == ConditionType.TIME) { - timeConditions.add(wrappedCondition); - continue; - } - - // If the wrapped condition is a logical condition, search inside it - if (wrappedCondition instanceof LogicalCondition) { - timeConditions.addAll(((LogicalCondition) wrappedCondition).findTimeConditions()); - } - } - } - - return timeConditions; - } - - /** - * Recursively finds all non-TimeCondition instances in this logical condition structure. - * This searches through the entire hierarchy including nested logical conditions. - * - * @return A list of all non-TimeCondition instances found in this structure - */ - public List findNonTimeConditions() { - List nonTimeConditions = new ArrayList<>(); - - // Recursively search for non-TimeCondition instances - for (Condition condition : conditions) { - // Check if this condition is NOT a TimeCondition - if (condition.getType() != ConditionType.TIME) { - nonTimeConditions.add(condition); - } - - // If this is a logical condition, search inside it recursively - if (condition instanceof LogicalCondition) { - nonTimeConditions.addAll(((LogicalCondition) condition).findNonTimeConditions()); - continue; - } - - // Special case for NotCondition which wraps a single condition - if (condition instanceof NotCondition) { - Condition wrappedCondition = ((NotCondition) condition).getCondition(); - - // Check if the wrapped condition is NOT a TimeCondition - if (wrappedCondition.getType() != ConditionType.TIME) { - nonTimeConditions.add(wrappedCondition); - continue; - } - - // If the wrapped condition is a logical condition, search inside it - if (wrappedCondition instanceof LogicalCondition) { - nonTimeConditions.addAll(((LogicalCondition) wrappedCondition).findNonTimeConditions()); - } - } - } - - return nonTimeConditions; - } - - /** - * Checks if this logical condition structure contains only TimeCondition instances. - * - * @return true if all conditions in this structure are TimeConditions, false otherwise - */ - public boolean hasOnlyTimeConditions() { - return findNonTimeConditions().isEmpty(); - } - - /** - * Creates a new logical condition of the same type (AND/OR) that contains only - * TimeCondition instances from this logical structure. This preserves the nested structure - * of logical conditions rather than flattening. - * - * @return A new logical condition containing only TimeConditions with the same logical structure, - * or null if no time conditions exist in this structure - */ - public LogicalCondition createTimeOnlyLogicalStructure() { - // If there are no conditions at all, return null - if (conditions.isEmpty()) { - return null; - } - - // Create a new logical condition of the same type - LogicalCondition newLogical; - if (this instanceof AndCondition) { - newLogical = new AndCondition(); - } else if (this instanceof OrCondition) { - newLogical = new OrCondition(); - } else { - // For other logical types (like NOT), default to AND logic - newLogical = new AndCondition(); - } - - boolean hasAnyTimeConditions = false; - - // Process each condition, preserving the structure - for (Condition condition : conditions) { - if (condition.getType() == ConditionType.TIME) { - // Directly add time conditions - newLogical.addCondition(condition); - hasAnyTimeConditions = true; - } else if (condition instanceof LogicalCondition) { - // Recursively process nested logical conditions - LogicalCondition nestedTimeOnly = ((LogicalCondition) condition).createTimeOnlyLogicalStructure(); - if (nestedTimeOnly != null && !nestedTimeOnly.getConditions().isEmpty()) { - newLogical.addCondition(nestedTimeOnly); - hasAnyTimeConditions = true; - } - } else if (condition instanceof NotCondition) { - // Special handling for NOT conditions - Condition wrappedCondition = ((NotCondition) condition).getCondition(); - - // If the wrapped condition is a time condition, wrap it in a new NOT - if (wrappedCondition.getType() == ConditionType.TIME) { - newLogical.addCondition(new NotCondition(wrappedCondition)); - hasAnyTimeConditions = true; - } - // If the wrapped condition is a logical condition, process it recursively - else if (wrappedCondition instanceof LogicalCondition) { - LogicalCondition nestedTimeOnly = ((LogicalCondition) wrappedCondition).createTimeOnlyLogicalStructure(); - if (nestedTimeOnly != null && !nestedTimeOnly.getConditions().isEmpty()) { - newLogical.addCondition(new NotCondition(nestedTimeOnly)); - hasAnyTimeConditions = true; - } - } - } - } - - // If no time conditions were found, return null - if (!hasAnyTimeConditions) { - return null; - } - - // Post-processing: if we have a logical with only one nested condition, - // and that nested condition is a logical of the same type, we can flatten it - if (newLogical.getConditions().size() == 1) { - Condition singleCondition = newLogical.getConditions().get(0); - if (singleCondition instanceof LogicalCondition && - ((singleCondition instanceof AndCondition && newLogical instanceof AndCondition) || - (singleCondition instanceof OrCondition && newLogical instanceof OrCondition))) { - return (LogicalCondition) singleCondition; - } - } - - return newLogical; - } - - /** - * Evaluates whether this logical structure would be satisfied based solely on its - * time conditions. This creates a time-only logical structure with the same type (AND/OR) - * and checks if it is satisfied. - * - * @return true if the time-only structure is satisfied, false if not satisfied or no time conditions exist - */ - public boolean isTimeOnlyStructureSatisfied() { - LogicalCondition timeOnlyLogical = createTimeOnlyLogicalStructure(); - if (timeOnlyLogical == null) { - return false; // No time conditions, so can't be satisfied - } - - return timeOnlyLogical.isSatisfied(); - } - - /** - * Evaluates whether this logical structure would be satisfied if all non-time conditions - * were removed and only time conditions were evaluated. This helps determine if a plugin - * schedule would run based solely on its time conditions. - * - * @return true if time conditions alone would satisfy this structure, false otherwise - */ - public boolean wouldBeTimeOnlySatisfied() { - // If there are no time conditions at all, we can't satisfy this structure with time only - List timeConditions = findTimeConditions(); - if (timeConditions.isEmpty()) { - return false; - } - - // For AND logic, if all time conditions are satisfied, the overall structure would - // be satisfied if non-time conditions were not considered - if (this instanceof AndCondition) { - for (Condition condition : timeConditions) { - if (!condition.isSatisfied()) { - return false; - } - } - return true; - } - // For OR logic, if any time condition is satisfied, the overall structure would - // be satisfied if non-time conditions were not considered - else if (this instanceof OrCondition) { - for (Condition condition : timeConditions) { - if (condition.isSatisfied()) { - return true; - } - } - return false; - } - - // For other logic types, create a new structure and evaluate it - return isTimeOnlyStructureSatisfied(); - } - - - /** - * Updates this logical condition structure with new conditions from another logical condition. - * This is a convenience method that uses the default update mode (ADD_ONLY). - * - * @param newLogicalCondition The logical condition containing new conditions to add - * @return true if any changes were made, false if no changes were needed - */ - public boolean updateLogicalStructure(LogicalCondition newLogicalCondition) { - return updateLogicalStructure(newLogicalCondition, UpdateOption.SYNC, true); - } - - /** - * Updates this logical condition structure with new conditions from another logical condition. - * Provides fine-grained control over how conditions are merged. - * - * @param newLogicalCondition The logical condition containing new conditions to add - * @param updateMode Controls how conditions are merged (add only, sync, remove only, replace) - * @param preserveState If true, existing condition state is preserved when possible - * @return true if any changes were made, false if no changes were needed - */ - public boolean updateLogicalStructure(LogicalCondition newLogicalCondition, UpdateOption updateMode, boolean preserveState) { - if (newLogicalCondition == null) { - return false; - } - - if (updateMode == UpdateOption.REPLACE) { - // For REPLACE mode, just copy all conditions from the new structure - boolean anyChanges = false; - this.conditions.clear(); - - for (Condition newCondition : newLogicalCondition.getConditions()) { - this.addCondition(newCondition); - anyChanges = true; - } - - return anyChanges; - } - - boolean anyChanges = false; - - // First, handle removals if needed - if (updateMode == UpdateOption.SYNC || updateMode == UpdateOption.REMOVE_ONLY) { - anyChanges = removeNonMatchingConditions(newLogicalCondition) || anyChanges; - } - - // Then handle additions if needed - if (updateMode == UpdateOption.ADD_ONLY || updateMode == UpdateOption.SYNC) { - anyChanges = addNewConditions(newLogicalCondition, preserveState) || anyChanges; - } - - return anyChanges; - } - - /** - * Removes conditions from this logical structure that don't exist in the new structure. - * This creates a synchronized view between the two condition trees. - * - * @param newLogicalCondition The logical condition to compare against - * @return true if any conditions were removed, false otherwise - */ - private boolean removeNonMatchingConditions(LogicalCondition newLogicalCondition) { - boolean anyRemoved = false; - List toRemove = new ArrayList<>(); - - for (Condition existingCondition : conditions) { - // Check if the existing condition is found in the new structure - boolean foundMatch = false; - - // For non-logical conditions, check direct existence - if (!(existingCondition instanceof LogicalCondition) && !(existingCondition instanceof NotCondition)) { - foundMatch = newLogicalCondition.contains(existingCondition); - } - // For logical conditions, check by type and recursively - else if (existingCondition instanceof LogicalCondition) { - LogicalCondition existingLogical = (LogicalCondition) existingCondition; - - // Try to find a matching logical condition in the new structure - for (Condition newCondition : newLogicalCondition.getConditions()) { - if (newCondition instanceof LogicalCondition && - ((existingLogical instanceof AndCondition && newCondition instanceof AndCondition) || - (existingLogical instanceof OrCondition && newCondition instanceof OrCondition))) { - - // Found a logical condition of the same type, process it recursively - LogicalCondition newLogical = (LogicalCondition) newCondition; - existingLogical.removeNonMatchingConditions(newLogical); - foundMatch = true; - break; - } - } - } - // For not conditions, check if the wrapped condition exists - else if (existingCondition instanceof NotCondition) { - NotCondition existingNot = (NotCondition) existingCondition; - Condition wrappedExistingCondition = existingNot.getCondition(); - - // Check for matching NOT conditions - for (Condition newCondition : newLogicalCondition.getConditions()) { - if (newCondition instanceof NotCondition) { - NotCondition newNot = (NotCondition) newCondition; - if (newNot.getCondition().equals(wrappedExistingCondition)) { - foundMatch = true; - - // If the wrapped conditions are logical, recursively process them - if (wrappedExistingCondition instanceof LogicalCondition && - newNot.getCondition() instanceof LogicalCondition) { - ((LogicalCondition) wrappedExistingCondition).removeNonMatchingConditions( - (LogicalCondition) newNot.getCondition()); - } - break; - } - } - } - } - - // If no match was found, mark for removal - if (!foundMatch) { - toRemove.add(existingCondition); - } - } - - // Remove all conditions that weren't found in the new structure - for (Condition conditionToRemove : toRemove) { - conditions.remove(conditionToRemove); - anyRemoved = true; - log.debug("Removed condition from logical structure: {}", conditionToRemove.getDescription()); - } - - return anyRemoved; - } - - /** - * Identifies and removes conditions that can no longer trigger from a logical condition structure. - * This is useful for cleaning up one-time conditions that have already triggered and cannot trigger again. - * - * @param logicalCondition The logical condition structure to clean up - * @return true if any conditions were removed, false otherwise - */ - public static boolean removeNonTriggerableConditions(LogicalCondition logicalCondition) { - if (logicalCondition == null || logicalCondition.getConditions().isEmpty()) { - return false; - } - - boolean anyRemoved = false; - List conditionsToRemove = new ArrayList<>(); - - // First pass: identify conditions that can no longer trigger - for (Condition condition : logicalCondition.getConditions()) { - // Check direct non-triggerable conditions - if (condition instanceof TimeCondition && !((TimeCondition) condition).canTriggerAgain()) { - log.debug("Found non-triggerable time condition: {}", condition.getDescription()); - conditionsToRemove.add(condition); - continue; - } - - // Handle nested logical conditions - if (condition instanceof LogicalCondition) { - // Recursively clean up nested structure - if (removeNonTriggerableConditions((LogicalCondition) condition)) { - anyRemoved = true; - } - - // If this leaves the nested logical empty, mark it for removal too - if (((LogicalCondition) condition).getConditions().isEmpty()) { - conditionsToRemove.add(condition); - } - } - - // Handle NOT condition as a special case - if (condition instanceof NotCondition) { - NotCondition notCondition = (NotCondition) condition; - Condition wrappedCondition = notCondition.getCondition(); - - // If wrapped condition is a time condition that can't trigger - if (wrappedCondition instanceof TimeCondition && - !((TimeCondition) wrappedCondition).canTriggerAgain()) { - conditionsToRemove.add(condition); - } - // If wrapped condition is a logical, clean it up recursively - else if (wrappedCondition instanceof LogicalCondition) { - if (removeNonTriggerableConditions((LogicalCondition) wrappedCondition)) { - anyRemoved = true; - // If cleaned condition is now empty, mark the NOT for removal - if (((LogicalCondition) wrappedCondition).getConditions().isEmpty()) { - conditionsToRemove.add(condition); - } - } - } - } - } - - // Second pass: remove identified conditions - for (Condition conditionToRemove : conditionsToRemove) { - logicalCondition.removeCondition(conditionToRemove); - log.debug("Removed non-triggerable condition: {}", conditionToRemove.getDescription()); - anyRemoved = true; - } - - return anyRemoved; - } - - /** - * Adds new conditions from the provided structure that don't already exist in this structure. - * This preserves the existing condition state while adding new conditions. - * - * @param newLogicalCondition The logical condition containing new conditions to add - * @param preserveState If true, existing conditions with the same description are kept - * @return true if any conditions were added, false otherwise - */ - private boolean addNewConditions(LogicalCondition newLogicalCondition, boolean preserveState) { - boolean anyChanges = false; - - for (Condition newCondition : newLogicalCondition.getConditions()) { - // For non-logical conditions, check if we need to add them - if (!(newCondition instanceof LogicalCondition) && !(newCondition instanceof NotCondition)) { - if (!this.contains(newCondition)) { - this.addCondition(newCondition); - anyChanges = true; - } - continue; - } - - // Handle NotCondition as a special case - if (newCondition instanceof NotCondition) { - NotCondition newNotCondition = (NotCondition) newCondition; - Condition wrappedNewCondition = newNotCondition.getCondition(); - - // Check if we already have this NOT condition - boolean exists = false; - for (Condition existingCondition : this.conditions) { - if (existingCondition instanceof NotCondition) { - NotCondition existingNotCondition = (NotCondition) existingCondition; - Condition wrappedExistingCondition = existingNotCondition.getCondition(); - - // Check if the NOT conditions are wrapping the same condition - if (wrappedExistingCondition.equals(wrappedNewCondition)) { - exists = true; - break; - } - - // If both wrap logical conditions, we need to update recursively - if (wrappedExistingCondition instanceof LogicalCondition && - wrappedNewCondition instanceof LogicalCondition) { - if (((LogicalCondition) wrappedExistingCondition).updateLogicalStructure( - (LogicalCondition) wrappedNewCondition, - preserveState ? UpdateOption.ADD_ONLY : UpdateOption.SYNC, - preserveState)) { - anyChanges = true; - } - exists = true; - break; - } - } - } - - // If we don't have this NOT condition, add it - if (!exists) { - this.addCondition(newCondition); - anyChanges = true; - } - continue; - } - - // For logical conditions, recursively update if we find a matching logical type - LogicalCondition newLogical = (LogicalCondition) newCondition; - boolean foundMatchingLogical = false; - - for (Condition existingCondition : this.conditions) { - if (existingCondition instanceof LogicalCondition) { - LogicalCondition existingLogical = (LogicalCondition) existingCondition; - - // If they're the same type of logical condition (AND/OR), update recursively - if ((existingLogical instanceof AndCondition && newLogical instanceof AndCondition) || - (existingLogical instanceof OrCondition && newLogical instanceof OrCondition)) { - if (existingLogical.updateLogicalStructure( - newLogical, - preserveState ? UpdateOption.ADD_ONLY : UpdateOption.SYNC, - preserveState)) { - anyChanges = true; - } - foundMatchingLogical = true; - break; - } - } - } - - // If we didn't find a matching logical type, add the entire logical condition - if (!foundMatchingLogical) { - this.addCondition(newLogical); - anyChanges = true; - } - } - - return anyChanges; - } - - /** - * Validates the logical condition structure, checking for common issues - * like empty logical conditions or invalid condition nesting. - * - * @return A list of validation issues, or an empty list if no issues were found - */ - public List validateStructure() { - List issues = new ArrayList<>(); - - // Check for empty logical conditions - if (conditions.isEmpty()) { - issues.add("Empty logical condition: " + getClass().getSimpleName()); - } - - // Check for nested logical conditions of same type that could be flattened - for (Condition condition : conditions) { - if (condition instanceof LogicalCondition) { - LogicalCondition nestedLogical = (LogicalCondition) condition; - - // Recursively validate nested structures - issues.addAll(nestedLogical.validateStructure()); - - // Check for unnecessary nesting (same logical type) - if ((this instanceof AndCondition && nestedLogical instanceof AndCondition) || - (this instanceof OrCondition && nestedLogical instanceof OrCondition)) { - issues.add("Unnecessary nesting of " + getClass().getSimpleName() + - " contains nested " + nestedLogical.getClass().getSimpleName() + - " that could be flattened"); - } - - // Check for empty nested logical conditions - if (nestedLogical.getConditions().isEmpty()) { - issues.add("Empty nested logical condition: " + nestedLogical.getClass().getSimpleName()); - } - } - } - - return issues; - } - - /** - * Optimizes the logical condition structure by flattening unnecessary nesting - * and removing empty logical conditions. - * - * @return true if any optimizations were applied, false otherwise - */ - public boolean optimizeStructure() { - boolean anyChanges = false; - - // Remove empty nested logical conditions - for (int i = conditions.size() - 1; i >= 0; i--) { - Condition condition = conditions.get(i); - if (condition instanceof LogicalCondition) { - LogicalCondition nestedLogical = (LogicalCondition) condition; - - // Recursively optimize nested structure - if (nestedLogical.optimizeStructure()) { - anyChanges = true; - } - - // Remove if empty after optimization - if (nestedLogical.getConditions().isEmpty()) { - conditions.remove(i); - anyChanges = true; - continue; - } - - // Flatten nested logical conditions of same type - if ((this instanceof AndCondition && nestedLogical instanceof AndCondition) || - (this instanceof OrCondition && nestedLogical instanceof OrCondition)) { - - // Get all conditions from nested logical before we remove it - List nestedConditions = new ArrayList<>(nestedLogical.getConditions()); - - // Move all conditions from nested logical to this logical - // Need to iterate through a copy to avoid concurrent modification - for (Condition nestedCondition : nestedConditions) { - // Remove from nested logical first to avoid duplicates when we add to parent - nestedLogical.getConditions().remove(nestedCondition); - - // Add to parent logical if not already present - if (!this.contains(nestedCondition)) { - this.addCondition(nestedCondition); - } - } - - // Remove the now empty nested logical - conditions.remove(i); - anyChanges = true; - } - } - } - - return anyChanges; - } - - /** - * Updates this logical condition structure with new conditions from another logical condition. - * Only adds conditions that don't already exist in the structure. - * - * @param newLogicalCondition The logical condition containing new conditions to add - * @return true if any conditions were added, false if no changes were made - */ - public boolean updateLogicalStructureOld(LogicalCondition newLogicalCondition) { - if (newLogicalCondition == null || newLogicalCondition.getConditions().isEmpty()) { - return false; - } - - boolean anyChanges = false; - - for (Condition newCondition : newLogicalCondition.getConditions()) { - // For non-logical conditions, check if we need to add them - if (!(newCondition instanceof LogicalCondition) && !(newCondition instanceof NotCondition)) { - if (!this.contains(newCondition)) { - this.addCondition(newCondition); - anyChanges = true; - } - continue; - } - - // Handle NotCondition as a special case - if (newCondition instanceof NotCondition) { - NotCondition newNotCondition = (NotCondition) newCondition; - Condition wrappedNewCondition = newNotCondition.getCondition(); - - // Check if we already have this NOT condition - boolean exists = false; - for (Condition existingCondition : this.conditions) { - if (existingCondition instanceof NotCondition) { - NotCondition existingNotCondition = (NotCondition) existingCondition; - Condition wrappedExistingCondition = existingNotCondition.getCondition(); - - // Check if the NOT conditions are wrapping the same condition - if (wrappedExistingCondition.equals(wrappedNewCondition)) { - exists = true; - break; - } - - // If both wrap logical conditions, we need to update recursively - if (wrappedExistingCondition instanceof LogicalCondition && - wrappedNewCondition instanceof LogicalCondition) { - if (((LogicalCondition) wrappedExistingCondition).updateLogicalStructure( - (LogicalCondition) wrappedNewCondition)) { - anyChanges = true; - } - exists = true; - break; - } - } - } - - // If we don't have this NOT condition, add it - if (!exists) { - this.addCondition(newCondition); - anyChanges = true; - } - continue; - } - - // For logical conditions, recursively update if we find a matching logical type - LogicalCondition newLogical = (LogicalCondition) newCondition; - boolean foundMatchingLogical = false; - - for (Condition existingCondition : this.conditions) { - if (existingCondition instanceof LogicalCondition) { - LogicalCondition existingLogical = (LogicalCondition) existingCondition; - - // If they're the same type of logical condition (AND/OR), update recursively - if ((existingLogical instanceof AndCondition && newLogical instanceof AndCondition) || - (existingLogical instanceof OrCondition && newLogical instanceof OrCondition)) { - if (existingLogical.updateLogicalStructure(newLogical)) { - anyChanges = true; - } - foundMatchingLogical = true; - break; - } - } - } - - // If we didn't find a matching logical type, add the entire logical condition - if (!foundMatchingLogical) { - this.addCondition(newLogical); - anyChanges = true; - } - } - - return anyChanges; - } - - /** - * Compares this logical condition structure with another one and returns differences. - * This is useful for debugging and logging what changed during an update. - * - * @param otherLogical The logical condition to compare with - * @return A string describing the differences, or "No differences" if they're the same - */ - public String getStructureDifferences(LogicalCondition otherLogical) { - if (otherLogical == null) { - return "Other logical condition is null"; - } - - StringBuilder differences = new StringBuilder(); - - // Check for differences in logical type - if ((this instanceof AndCondition && !(otherLogical instanceof AndCondition)) || - (this instanceof OrCondition && !(otherLogical instanceof OrCondition))) { - differences.append("Different logical types: ") - .append(this.getClass().getSimpleName()) - .append(" vs ") - .append(otherLogical.getClass().getSimpleName()) - .append("\n"); - } - - // Find conditions in this that aren't in otherLogical - for (Condition thisCondition : this.conditions) { - if (!otherLogical.contains(thisCondition)) { - differences.append("Only in this: \n\t\t").append(thisCondition.getDescription()).append("\n"); - } - } - - // Find conditions in otherLogical that aren't in this - for (Condition otherCondition : otherLogical.conditions) { - if (!this.contains(otherCondition)) { - differences.append("Only in other: \n\t\t").append(otherCondition.getDescription()).append("\n"); - } - } - - // Check for nested differences in logical conditions - for (Condition thisCondition : this.conditions) { - if (thisCondition instanceof LogicalCondition) { - LogicalCondition thisLogical = (LogicalCondition) thisCondition; - - // Find the corresponding logical condition in otherLogical - for (Condition otherCondition : otherLogical.conditions) { - if (otherCondition instanceof LogicalCondition && - ((thisLogical instanceof AndCondition && otherCondition instanceof AndCondition) || - (thisLogical instanceof OrCondition && otherCondition instanceof OrCondition))) { - - LogicalCondition otherLogical2 = (LogicalCondition) otherCondition; - String nestedDifferences = thisLogical.getStructureDifferences(otherLogical2); - - if (!"No differences".equals(nestedDifferences)) { - differences.append("Nested differences in ") - .append(thisLogical.getClass().getSimpleName()) - .append(":\n") - .append(nestedDifferences) - .append("\n"); - } - break; - } - } - } - } - - return differences.length() > 0 ? differences.toString() : "No differences"; - } - - /** - * Gets a list of all conditions that are currently blocking this logical condition - * from being satisfied. This is useful for diagnosing why a complex condition tree - * is not being satisfied. - * is not being satisfied. - * - * The specific behavior depends on the type of logical condition: - * - For AND conditions: Returns all unsatisfied conditions - * - For OR conditions: Returns all conditions only if none are satisfied - * - * @return List of conditions that are preventing satisfaction - */ - public abstract List getBlockingConditions(); - - /** - * Gets a list of all "leaf" conditions that are blocking this logical condition. - * Leaf conditions are the non-logical conditions that represent the actual root causes - * for why the logical structure is not satisfied. - * - * @return List of leaf conditions that are preventing satisfaction - */ - public List getLeafBlockingConditions() { - List blockingLeaves = new ArrayList<>(); - - for (Condition condition : getBlockingConditions()) { - if (condition instanceof LogicalCondition) { - // Recursively get leaf conditions from nested logical conditions - blockingLeaves.addAll(((LogicalCondition) condition).getLeafBlockingConditions()); - } else { - // This is a leaf condition - blockingLeaves.add(condition); - } - } - - return blockingLeaves; - } - - /** - * Gets a human-readable explanation of why this logical condition is not satisfied, - * detailing the specific blocking conditions in the tree structure. - * - * @return A string explaining why the condition is not satisfied - */ - public String getBlockingExplanation() { - if (isSatisfied()) { - return "Condition is already satisfied"; - } - - StringBuilder explanation = new StringBuilder(); - explanation.append(getClass().getSimpleName()).append(" is not satisfied because:\n"); - - // Add explanations for each blocking condition - List blockingConditions = getBlockingConditions(); - for (int i = 0; i < blockingConditions.size(); i++) { - Condition condition = blockingConditions.get(i); - explanation.append(" ").append(i + 1).append(") "); - - if (condition instanceof LogicalCondition) { - // For nested logical conditions, include their blocking explanations - explanation.append(((LogicalCondition) condition).getBlockingExplanation().replace("\n", "\n ")); - } else { - // For leaf conditions, include their descriptions - explanation.append(condition.getDescription()) - .append(" (").append(condition.getClass().getSimpleName()).append(")"); - } - - if (i < blockingConditions.size() - 1) { - explanation.append("\n"); - } - } - - return explanation.toString(); - } - - /** - * Gets a concise summary of the root causes why this logical condition is not satisfied. - * This focuses only on the leaf conditions that are blocking satisfaction. - * - * @return A string summarizing the root causes for non-satisfaction - */ - public String getRootCausesSummary() { - if (isSatisfied()) { - return "Condition is satisfied"; - } - - List leafBlockingConditions = getLeafBlockingConditions(); - - if (leafBlockingConditions.isEmpty()) { - return "No specific blocking conditions found"; - } - - StringBuilder summary = new StringBuilder(); - summary.append("Root causes preventing satisfaction (").append(leafBlockingConditions.size()).append("):\n"); - - for (int i = 0; i < leafBlockingConditions.size(); i++) { - Condition condition = leafBlockingConditions.get(i); - summary.append(" ").append(i + 1).append(") ") - .append(condition.getDescription()) - .append(" (").append(condition.getClass().getSimpleName()).append(")"); - - // Add progress information if available - double progress = condition.getProgressPercentage(); - if (progress > 0 && progress < 100) { - summary.append(" - ").append(String.format("%.1f%%", progress)).append(" complete"); - } - - if (i < leafBlockingConditions.size() - 1) { - summary.append("\n"); - } - } - - return summary.toString(); - } - - /** - * Base implementation for estimated satisfaction time in logical conditions. - * This is overridden by specific logical condition types (And/Or) to provide - * appropriate logic for their semantics. - * - * @return Optional containing the estimated duration until satisfaction, or empty if not determinable - */ - @Override - public Optional getEstimatedTimeWhenIsSatisfied() { - if (conditions.isEmpty()) { - return Optional.of(Duration.ZERO); - } - - // This base implementation should be overridden by concrete classes - // Default behavior: return empty if we can't determine - return Optional.empty(); - } -} - - diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/NotCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/NotCondition.java deleted file mode 100644 index d14e7814a5a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/NotCondition.java +++ /dev/null @@ -1,263 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.logical; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import net.runelite.api.events.ItemContainerChanged; -import net.runelite.api.events.StatChanged; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.DayOfWeekCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition; - -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.Optional; - -/** - * Logical NOT operator - inverts a condition. - */ -@EqualsAndHashCode(callSuper = false) -public class NotCondition implements Condition { - @Getter - private final Condition condition; - - public NotCondition(Condition condition) { - this.condition = condition; - } - - @Override - public boolean isSatisfied() { - if (condition instanceof SingleTriggerTimeCondition) { - if (((SingleTriggerTimeCondition) condition).canTriggerAgain()) { - return !condition.isSatisfied(); - } - // should we only return true if the condition if it can trigger and is not satisfied? as we do it now - return false; - } - return !condition.isSatisfied(); - } - - @Override - public String getDescription() { - return "NOT (" + condition.getDescription() + ")"; - } - - @Override - public ConditionType getType() { - return ConditionType.LOGICAL; - } - - @Override - public void onStatChanged(StatChanged event) { - condition.onStatChanged(event); - } - - @Override - public void onItemContainerChanged(ItemContainerChanged event) { - condition.onItemContainerChanged(event); - } - - @Override - public void reset() { - condition.reset(); - } - @Override - public void reset(boolean randomize) { - condition.reset(randomize); - } - - @Override - public double getProgressPercentage() { - // Invert the progress for NOT conditions - double innerProgress = condition.getProgressPercentage(); - return 100.0 - innerProgress; - } - - @Override - public String getStatusInfo(int indent, boolean showProgress) { - StringBuilder sb = new StringBuilder(); - - // Add the NOT condition info - 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(")"); - } - } - - sb.append("\n"); - - // Add the nested condition with additional indent - sb.append(condition.getStatusInfo(indent + 2, showProgress)); - - return sb.toString(); - } - - /** - * Gets the next time this NOT condition will be satisfied. - * For TimeConditions, this attempts to determine when the inner condition will change state. - * - * @return Optional containing the next trigger time, or empty if none available - */ - @Override - public Optional getCurrentTriggerTime() { - // For NOT condition with a TimeCondition, we need to consider state transitions - if (condition instanceof TimeCondition) { - boolean innerSatisfied = condition.isSatisfied(); - - if (innerSatisfied) { - // Inner condition is satisfied (NOT is not satisfied) - // For DayOfWeekCondition, get the next non-active day - if (condition instanceof DayOfWeekCondition) { - DayOfWeekCondition dayCondition = (DayOfWeekCondition) condition; - return dayCondition.getNextNonActiveDay(); - } - // For TimeWindowCondition specifically, we can try to get its end time - else if (condition instanceof TimeWindowCondition) { - TimeWindowCondition timeWindow = (TimeWindowCondition) condition; - // If we have access to end time, we could return it - if (timeWindow.getCurrentEndDateTime() != null) { - return Optional.of(timeWindow.getCurrentEndDateTime() - .atZone(timeWindow.getZoneId())); - } - } - // For IntervalCondition, estimate when the interval would reset - else if (condition instanceof IntervalCondition) { - IntervalCondition intervalCondition = (IntervalCondition) condition; - // Calculate when the next interval would start after the current one - // This is our best estimate of when the NOT condition would become satisfied again - ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); - return Optional.of(now.plus(intervalCondition.getInterval())); - } - - // For other time conditions or if end time isn't available, - // we can't easily determine when the condition will stop being satisfied - return Optional.empty(); - } else { - // Inner condition is not satisfied (NOT is satisfied) - // The next notable time point is when the inner condition becomes satisfied - // (which would make the NOT condition unsatisfied) - Optional nextInnerTrigger = condition.getCurrentTriggerTime(); - - // If the inner condition has a next trigger time, that's when NOT will become unsatisfied - return nextInnerTrigger; - } - } - - // For non-TimeCondition, use the default behavior - // If the NOT is satisfied, return time in the past - if (isSatisfied()) { - return Optional.of(ZonedDateTime.now(ZoneId.systemDefault()).minusSeconds(1)); - } - - // If the NOT is not satisfied, we can't determine when it will become satisfied - return Optional.empty(); - } - - /** - * Returns a detailed description of the NOT condition with additional status information - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - // Basic description - sb.append("NOT Logical Condition: Inverts the inner condition\n"); - - // Status information - boolean satisfied = isSatisfied(); - sb.append("Status: ").append(satisfied ? "Satisfied" : "Not satisfied").append("\n"); - - // Progress information (inverted) - double progress = getProgressPercentage(); - sb.append(String.format("Inverted Progress: %.1f%%\n", progress)).append("\n"); - - // Inner condition information - sb.append("Inner Condition:\n"); - sb.append(" Type: ").append(condition.getClass().getSimpleName()).append("\n"); - sb.append(" Description: ").append(condition.getDescription()).append("\n"); - sb.append(" Status: ").append(condition.isSatisfied() ? "SATISFIED" : "NOT SATISFIED").append("\n"); - - // If the inner condition has a detailed description and it's not too complex - if (!(condition instanceof LogicalCondition)) { - sb.append("\nInner Condition Details:\n"); - - // Use reflection to safely try to access getDetailedDescription if available - try { - java.lang.reflect.Method detailedDescMethod = - condition.getClass().getMethod("getDetailedDescription"); - if (detailedDescMethod != null) { - String innerDetails = (String) detailedDescMethod.invoke(condition); - // Add indentation to inner details - innerDetails = " " + innerDetails.replace("\n", "\n "); - sb.append(innerDetails); - } - } catch (Exception e) { - // If detailed description isn't available, just use the regular description - sb.append(" ").append(condition.getDescription()); - } - } - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - // Basic information - sb.append("NotCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Type: NOT (Inverts inner condition)\n"); - sb.append(" │ Inner Condition: ").append(condition.getClass().getSimpleName()).append("\n"); - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - boolean satisfied = isSatisfied(); - sb.append(" │ Satisfied: ").append(satisfied).append("\n"); - sb.append(" │ Inner Satisfied: ").append(condition.isSatisfied()).append("\n"); - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Inner condition - sb.append(" └─ Inner Condition ─────────────────────────\n"); - - // Format the inner condition's toString with proper indentation - String innerString = condition.toString(); - String[] lines = innerString.split("\n"); - - // For simple conditions that might not have fancy toString - if (lines.length <= 1) { - sb.append(" ").append(condition.getDescription()); - } else { - // Skip the first line if it's just the class name - for (int i = (lines[0].contains("Condition:") ? 1 : 0); i < lines.length; i++) { - // Indent each line - sb.append(" ").append(lines[i]).append("\n"); - } - } - - return sb.toString(); - } - @Override - public void pause() { - - - - } - - @Override - public void resume() { - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/OrCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/OrCondition.java deleted file mode 100644 index 637ecd5faa5..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/OrCondition.java +++ /dev/null @@ -1,234 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.logical; - -import java.time.Duration; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import lombok.EqualsAndHashCode; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; - -/** - * Logical OR combination of conditions - any can be met. - */ -@EqualsAndHashCode(callSuper = true) -public class OrCondition extends LogicalCondition { - public OrCondition(Condition... conditions) { - super(conditions); - - } - @Override - public boolean isSatisfied() { - if (conditions.isEmpty()) return true; - return conditions.stream().anyMatch(Condition::isSatisfied); - } - - /** - * Gets the estimated time until this OR condition will be satisfied. - * For an OR condition, this returns the minimum (earliest) estimated time - * among all child conditions, since any one of them being satisfied - * will satisfy the entire OR condition. - * - * @return Optional containing the estimated duration until satisfaction, or empty if not determinable - */ - @Override - public Optional getEstimatedTimeWhenIsSatisfied() { - if (conditions.isEmpty()) { - return Optional.of(Duration.ZERO); - } - - // If any condition is already satisfied, return zero - if (isSatisfied()) { - return Optional.of(Duration.ZERO); - } - - Duration shortestTime = null; - boolean hasEstimate = false; - - for (Condition condition : conditions) { - Optional estimate = condition.getEstimatedTimeWhenIsSatisfied(); - if (estimate.isPresent()) { - hasEstimate = true; - Duration currentEstimate = estimate.get(); - - if (shortestTime == null || currentEstimate.compareTo(shortestTime) < 0) { - shortestTime = currentEstimate; - } - - // If any condition has zero duration (satisfied), return immediately - if (currentEstimate.isZero()) { - return Optional.of(Duration.ZERO); - } - } - } - - return hasEstimate ? Optional.of(shortestTime) : Optional.empty(); - } - - /** - * Gets the next time this OR condition will be satisfied. - * If any condition is satisfied, returns the trigger time of the first satisfied condition. - * If no condition is satisfied, returns the earliest next trigger time among TimeConditions. - * - * @return Optional containing the next trigger time, or empty if none available - */ - @Override - public Optional getCurrentTriggerTime() { - if (conditions.isEmpty()) { - return Optional.empty(); - } - - - // If none satisfied, find earliest trigger time among TimeConditions - ZonedDateTime earliestTimeSatisfied = null; - ZonedDateTime earliestTimeUnSatisfied = null; - int satisfiedCount = 0; - for (Condition condition : conditions) { - if (condition instanceof TimeCondition) { - Optional nextTrigger = condition.getCurrentTriggerTime(); - if (condition.isSatisfied()) { - satisfiedCount++; - if (earliestTimeSatisfied == null || nextTrigger.get().isBefore(earliestTimeSatisfied)) { - earliestTimeSatisfied = nextTrigger.get(); - } - }else{ - if (nextTrigger.isPresent()) { - ZonedDateTime triggerTime = nextTrigger.get(); - if (earliestTimeUnSatisfied == null || triggerTime.isBefore(earliestTimeUnSatisfied)) { - earliestTimeUnSatisfied = triggerTime; - } - } - } - } - } - if (satisfiedCount > 0) { - return earliestTimeSatisfied != null ? Optional.of(earliestTimeSatisfied) : Optional.empty(); - }else if (earliestTimeUnSatisfied != null) { - return Optional.of(earliestTimeUnSatisfied); - }else{ - return Optional.empty(); - } - - } - - /** - * For an OR condition, all conditions must be unsatisfied to block the entire OR. - * This method returns all child conditions if none are satisfied, or an empty list - * if at least one is satisfied (meaning the OR condition itself is satisfied). - * - * @return List of all child conditions if none are satisfied, otherwise an empty list - */ - @Override - public List getBlockingConditions() { - // For an OR condition, if any condition is satisfied, nothing is blocking - if (isSatisfied()) { - return new ArrayList<>(); - } - - // If we reach here, none are satisfied, so all conditions are blocking - return new ArrayList<>(conditions); - } - - /** - * Returns a detailed description of the OR condition with additional status information - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - // Basic description - sb.append("OR Logical Condition: Any condition can be satisfied\n"); - - // Status information - boolean satisfied = isSatisfied(); - sb.append("Status: ").append(satisfied ? "Satisfied" : "Not satisfied").append("\n"); - sb.append("Child Conditions: ").append(conditions.size()).append("\n"); - - // Progress information - double progress = getProgressPercentage(); - sb.append(String.format("Overall Progress: %.1f%%\n", progress)); - - // Count satisfied conditions - int satisfiedCount = 0; - for (Condition condition : conditions) { - if (condition.isSatisfied()) { - satisfiedCount++; - } - } - sb.append("Satisfied Conditions: ").append(satisfiedCount).append("/").append(conditions.size()).append("\n\n"); - - // List all child conditions - sb.append("Child Conditions:\n"); - for (int i = 0; i < conditions.size(); i++) { - Condition condition = conditions.get(i); - sb.append(String.format("%d. %s [%s]\n", - i + 1, - condition.getDescription(), - condition.isSatisfied() ? "SATISFIED" : "NOT SATISFIED")); - } - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - // Basic information - sb.append("OrCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Type: OR (Any condition can be satisfied)\n"); - sb.append(" │ Child Conditions: ").append(conditions.size()).append("\n"); - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - boolean anySatisfied = isSatisfied(); - sb.append(" │ Satisfied: ").append(anySatisfied).append("\n"); - - // Count satisfied conditions - int satisfiedCount = 0; - for (Condition condition : conditions) { - if (condition.isSatisfied()) { - satisfiedCount++; - } - } - sb.append(" │ Satisfied Conditions: ").append(satisfiedCount).append("/").append(conditions.size()).append("\n"); - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Child conditions - if (!conditions.isEmpty()) { - sb.append(" ├─ Child Conditions ────────────────────────\n"); - - for (int i = 0; i < conditions.size(); i++) { - Condition condition = conditions.get(i); - String prefix = (i == conditions.size() - 1) ? " └─ " : " ├─ "; - - sb.append(prefix).append(String.format("Condition %d: %s [%s]\n", - i + 1, - condition.getClass().getSimpleName(), - condition.isSatisfied() ? "SATISFIED" : "NOT SATISFIED")); - } - } else { - sb.append(" └─ No Child Conditions ───────────────────────\n"); - } - - return sb.toString(); - } - public void pause() { - // Pause all child conditions - for (Condition condition : conditions) { - condition.pause(); - } - - - } - - - public void resume() { - // Resume all child conditions - for (Condition condition : conditions) { - condition.resume(); - } - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/PredicateCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/PredicateCondition.java deleted file mode 100644 index fe24feef4c5..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/PredicateCondition.java +++ /dev/null @@ -1,196 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.logical; - -import java.time.ZonedDateTime; -import java.util.Optional; -import java.util.function.Predicate; -import java.util.function.Supplier; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; - -/** - * A condition that combines a manual lock with a predicate evaluation. - * The condition is satisfied only when: - * 1. It is not manually locked, AND - * 2. The predicate evaluates to true - * - * This allows plugins to define dynamic conditions that depend on the game state - * while still maintaining the ability to manually lock/unlock the condition. - */ -@Slf4j -@EqualsAndHashCode(callSuper = true) -public class PredicateCondition extends LockCondition { - - @Getter - private final Predicate predicate; - private final Supplier stateSupplier; - private final String predicateDescription; - - /** - * Creates a new predicate condition with a default reason. - * - * @param predicate The predicate to evaluate - * @param stateSupplier A supplier that provides the current state to evaluate against the predicate - * @param predicateDescription A human-readable description of what the predicate checks - * @throws IllegalArgumentException if predicate or stateSupplier is null - */ - public PredicateCondition(boolean withBreakHandlerLock , Predicate predicate, Supplier stateSupplier, String predicateDescription) { - super("Plugin is locked or predicate condition is not met", withBreakHandlerLock); - validateConstructorArguments(predicate, stateSupplier, predicateDescription); - this.predicate = predicate; - this.stateSupplier = stateSupplier; - this.predicateDescription = predicateDescription != null ? predicateDescription : "Unknown predicate"; - } - - /** - * Creates a new predicate condition with a specified reason. - * - * @param reason The reason why the plugin is locked - * @param predicate The predicate to evaluate - * @param stateSupplier A supplier that provides the current state to evaluate against the predicate - * @param predicateDescription A human-readable description of what the predicate checks - * @throws IllegalArgumentException if predicate or stateSupplier is null - */ - public PredicateCondition(String reason, boolean withBreakHandlerLock ,Predicate predicate, Supplier stateSupplier, String predicateDescription) { - super(reason, withBreakHandlerLock); - validateConstructorArguments(predicate, stateSupplier, predicateDescription); - this.predicate = predicate; - this.stateSupplier = stateSupplier; - this.predicateDescription = predicateDescription != null ? predicateDescription : "Unknown predicate"; - } - - /** - * Creates a new predicate condition with a specified reason and initial lock state. - * - * @param reason The reason why the plugin is locked - * @param defaultLocked The initial locked state of the condition. - * @param withBreakHandlerLock Whether this condition participates in BreakHandler coordination (lock hand-off), not an initial lock state. - * @param predicate The predicate to evaluate - * @param stateSupplier A supplier that provides the current state to evaluate against the predicate - * @param predicateDescription A human-readable description of what the predicate checks - * @throws IllegalArgumentException if predicate or stateSupplier is null - */ - public PredicateCondition(String reason, boolean defaultLocked, boolean withBreakHandlerLock, Predicate predicate, Supplier stateSupplier, String predicateDescription) { - super(reason, defaultLocked, withBreakHandlerLock); - validateConstructorArguments(predicate, stateSupplier, predicateDescription); - this.predicate = predicate; - this.stateSupplier = stateSupplier; - this.predicateDescription = predicateDescription != null ? predicateDescription : "Unknown predicate"; - } - - /** - * Validates that required constructor arguments are not null - * - * @param predicate The predicate to evaluate - * @param stateSupplier A supplier that provides the current state - * @param predicateDescription A description of the predicate - * @throws IllegalArgumentException if predicate or stateSupplier is null - */ - private void validateConstructorArguments(Predicate predicate, Supplier stateSupplier, String predicateDescription) { - if (predicate == null) { - log.error("Predicate cannot be null in PredicateCondition constructor"); - throw new IllegalArgumentException("Predicate cannot be null"); - } - if (stateSupplier == null) { - log.error("State supplier cannot be null in PredicateCondition constructor"); - throw new IllegalArgumentException("State supplier cannot be null"); - } - if (predicateDescription == null) { - log.warn("Predicate description is null, using default"); - } - } - - /** - * Evaluates the current state against the predicate. - * This method is thread-safe and handles exceptions safely. - * - * @return True if the predicate is satisfied, false otherwise or if an exception occurs - */ - public synchronized boolean evaluatePredicate() { - try { - if (stateSupplier == null) { - log.warn("State supplier is null in predicateDescription: {}", predicateDescription); - return false; - } - - T currentState = stateSupplier.get(); - - if (predicate == null) { - log.warn("Predicate is null in predicateDescription: {}", predicateDescription); - return false; - } - - return predicate.test(currentState); - } catch (Exception e) { - log.error("Exception in predicateDescription: {} - {}", predicateDescription, e.getMessage(), e); - return false; - } - } - - @Override - public synchronized boolean isSatisfied() { - try { - // The condition is satisfied only if: - // 1. It's not manually locked (from parent class) - // 2. The predicate evaluates to true - return super.isSatisfied() && evaluatePredicate(); - } catch (Exception e) { - log.error("Exception in isSatisfied for predicateDescription: {} - {}", predicateDescription, e.getMessage(), e); - return false; - } - } - - @Override - public synchronized String getDescription() { - try { - boolean predicateSatisfied = evaluatePredicate(); - return "Predicate Condition: " + - (isLocked() ? "\nLOCKED - " + getReason() : "\nUNLOCKED") + - "\nPredicate: " + predicateDescription + - "\nPredicate Satisfied: " + (predicateSatisfied ? "Yes" : "No"); - } catch (Exception e) { - log.error("Exception in getDescription for predicateDescription: {} - {}", predicateDescription, e.getMessage(), e); - return "Predicate Condition: [Error retrieving description]"; - } - } - - @Override - public synchronized String getDetailedDescription() { - return getDescription(); - } - - @Override - public ConditionType getType() { - return ConditionType.LOGICAL; - } - - @Override - public synchronized void reset(boolean randomize) { - try { - // Reset the lock state from parent class - super.reset(randomize); - // No need to reset predicate or supplier - } catch (Exception e) { - log.error("Exception in reset for predicateDescription: {} - {}", predicateDescription, e.getMessage(), e); - } - } - - @Override - public Optional getCurrentTriggerTime() { - // Predicate conditions don't have a specific trigger time - return Optional.empty(); - } - public void pause() { - - - } - - - public void resume() { - - - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/enums/UpdateOption.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/enums/UpdateOption.java deleted file mode 100644 index 4569b050a53..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/enums/UpdateOption.java +++ /dev/null @@ -1,28 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.enums; - -/** - * Update options for condition managers. - * Controls how plugin conditions are merged during updates. - */ -public enum UpdateOption { - /** - * Only add new conditions, preserve existing conditions - */ - ADD_ONLY, - - /** - * Synchronize conditions to match the new structure (add new and remove missing) - */ - SYNC, - - /** - * Only remove conditions that don't exist in the new structure - */ - REMOVE_ONLY, - - /** - * Replace the entire condition structure with the new one - */ - REPLACE -} - \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/serialization/LogicalConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/serialization/LogicalConditionAdapter.java deleted file mode 100644 index 7d0d9337c06..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/serialization/LogicalConditionAdapter.java +++ /dev/null @@ -1,117 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -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.LogicalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.OrCondition; - -import java.lang.reflect.Type; - -/** - * Serializes and deserializes LogicalCondition objects - */ -@Slf4j -public class LogicalConditionAdapter implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(LogicalCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add class info to distinguish different logical conditions - json.addProperty("class", src.getClass().getName()); - - // Serialize the conditions with proper type wrapping - JsonArray conditionsArray = new JsonArray(); - for (Condition condition : src.getConditions()) { - // Create a properly typed wrapper for each condition - JsonObject typedCondition = new JsonObject(); - typedCondition.addProperty("type", condition.getClass().getName()); - - // Serialize the condition data and add it to the wrapper - JsonElement conditionData = context.serialize(condition); - typedCondition.add("data", conditionData); - - conditionsArray.add(typedCondition); - } - json.add("conditions", conditionsArray); - - return json; - } - - @Override - public LogicalCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - try { - JsonObject jsonObject = json.getAsJsonObject(); - LogicalCondition logicalCondition; - - // Determine the concrete class using exact class name matching - if (jsonObject.has("class")) { - String className = jsonObject.get("class").getAsString(); - - // Use exact class name matching - if (className.endsWith(".OrCondition")) { - logicalCondition = new OrCondition(); - } else if (className.endsWith(".AndCondition")) { - logicalCondition = new AndCondition(); - } else { - // Default fallback - log.warn("Unknown logical condition class: {}, defaulting to AndCondition", className); - logicalCondition = new AndCondition(); - } - } else { - // Default if no class info - logicalCondition = new AndCondition(); - } - - // Handle conditions - if (jsonObject.has("conditions")) { - JsonArray conditionsArray = jsonObject.getAsJsonArray("conditions"); - for (JsonElement element : conditionsArray) { - try { - // Check if this is a wrapped condition from ConditionTypeAdapter - if (element.isJsonObject()) { - JsonObject conditionObj = element.getAsJsonObject(); - - // Handle the typed wrapper structure from ConditionTypeAdapter - if (conditionObj.has("type") && conditionObj.has("data")) { - // This is the format from ConditionTypeAdapter - Condition condition = context.deserialize(conditionObj, Condition.class); - if (condition != null) { - logicalCondition.addCondition(condition); - } - } else if (conditionObj.has("data")) { - // Try to get the condition directly from the data field - Condition condition = context.deserialize(conditionObj.get("data"), Condition.class); - if (condition != null) { - logicalCondition.addCondition(condition); - } - } else { - // Try to deserialize directly - Condition condition = context.deserialize(element, Condition.class); - if (condition != null) { - logicalCondition.addCondition(condition); - } - } - } else { - // Try to deserialize directly - Condition condition = context.deserialize(element, Condition.class); - if (condition != null) { - logicalCondition.addCondition(condition); - } - } - } catch (Exception e) { - log.warn("Failed to deserialize a condition in logical condition", e); - } - } - } - - return logicalCondition; - } catch (Exception e) { - log.error("Error deserializing LogicalCondition", e); - // Return empty AndCondition on error - return new AndCondition(); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/serialization/NotConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/serialization/NotConditionAdapter.java deleted file mode 100644 index 3be5d5b7a20..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/serialization/NotConditionAdapter.java +++ /dev/null @@ -1,69 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.NotCondition; - -import java.lang.reflect.Type; - -/** - * Serializes and deserializes NotCondition objects - */ -@Slf4j -public class NotConditionAdapter implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(NotCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add class info - json.addProperty("class", src.getClass().getName()); - - // NotCondition only has one inner condition - Condition innerCondition = src.getCondition(); - - // Create a properly typed wrapper for the inner condition - JsonObject typedCondition = new JsonObject(); - typedCondition.addProperty("type", innerCondition.getClass().getName()); - - // Serialize the condition data and add it to the wrapper - JsonElement conditionData = context.serialize(innerCondition); - typedCondition.add("data", conditionData); - - // Add the inner condition to the object - json.add("condition", typedCondition); - - return json; - } - - @Override - public NotCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - try { - JsonObject jsonObject = json.getAsJsonObject(); - - // Handle inner condition - if (jsonObject.has("condition")) { - log.info("Deserializing NOT condition inner condition: {}", - jsonObject.get("condition").toString()); - - JsonElement element = jsonObject.get("condition"); - Condition innerCondition = context.deserialize(element, Condition.class); - - if (innerCondition != null) { - return new NotCondition(innerCondition); - } else { - log.error("Failed to deserialize inner condition for NotCondition"); - } - } else { - log.error("NotCondition JSON missing 'condition' field"); - } - - // If we reach here, something went wrong - throw new JsonParseException("Invalid NotCondition JSON format"); - } catch (Exception e) { - log.error("Error deserializing NotCondition", e); - throw new JsonParseException("Failed to deserialize NotCondition: " + e.getMessage()); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/NpcCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/NpcCondition.java deleted file mode 100644 index e2fb3d00d55..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/NpcCondition.java +++ /dev/null @@ -1,46 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.npc; - -import java.util.regex.Pattern; - -import lombok.EqualsAndHashCode; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; - -/** - * Abstract base class for all NPC-based conditions. - */ -@EqualsAndHashCode(callSuper = false) -public abstract class NpcCondition implements Condition { - @Override - public ConditionType getType() { - return ConditionType.NPC; - } - - /** - * Creates a pattern for matching NPC names - */ - protected Pattern createNpcNamePattern(String npcName) { - if (npcName == null || npcName.isEmpty()) { - return Pattern.compile(".*"); - } - - // Check if the name is already a regex pattern - if (npcName.startsWith("^") || npcName.endsWith("$") || - npcName.contains(".*") || npcName.contains("[") || - npcName.contains("(")) { - return Pattern.compile(npcName); - } - - // Otherwise, create a contains pattern - return Pattern.compile(".*" + Pattern.quote(npcName) + ".*", Pattern.CASE_INSENSITIVE); - } - - public void pause() { - - } - - - public void resume() { - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/NpcKillCountCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/NpcKillCountCondition.java deleted file mode 100644 index f0210368978..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/NpcKillCountCondition.java +++ /dev/null @@ -1,470 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.npc; - -import lombok.Builder; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import net.runelite.api.Actor; -import net.runelite.api.NPC; -import net.runelite.api.events.InteractingChanged; -import net.runelite.api.events.NpcDespawned; -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.OrCondition; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.regex.Pattern; - -/** - * Condition that tracks the number of NPCs killed by the player. - * Is satisfied when the player has killed a certain number of NPCs. - */ -@Getter -@EqualsAndHashCode(callSuper = true) -public class NpcKillCountCondition extends NpcCondition { - - public static String getVersion() { - return "0.0.1"; - } - private final String npcName; - private final Pattern npcNamePattern; - private final int targetCountMin; - private final int targetCountMax; - private transient volatile int currentTargetCount; - private transient volatile int currentKillCount; - private transient volatile boolean satisfied = false; - private transient boolean registered = false; - - // Set to track NPCs we're currently interacting with - private final Set interactingNpcIndices = new HashSet<>(); - - private long startTimeMillis = System.currentTimeMillis(); - private long lastKillTimeMillis = 0; - - /** - * Creates a condition with a fixed target count - */ - - public NpcKillCountCondition(String npcName, int targetCount) { - this.npcName = npcName; - this.npcNamePattern = createNpcNamePattern(npcName); - this.targetCountMin = targetCount; - this.targetCountMax = targetCount; - this.currentTargetCount = targetCount; - - } - - /** - * Creates a condition with a randomized target count between min and max - */ - @Builder - public NpcKillCountCondition(String npcName, int targetCountMin, int targetCountMax) { - this.npcName = npcName; - this.npcNamePattern = createNpcNamePattern(npcName); - this.targetCountMin = Math.max(0, targetCountMin); - this.targetCountMax = Math.max(this.targetCountMin, targetCountMax); - this.currentTargetCount = Rs2Random.between(this.targetCountMin, this.targetCountMax); - } - - /** - * Creates a condition with randomized target between min and max - */ - public static NpcKillCountCondition createRandomized(String npcName, int minCount, int maxCount) { - return NpcKillCountCondition.builder() - .npcName(npcName) - .targetCountMin(minCount) - .targetCountMax(maxCount) - .build(); - } - - /** - * Creates an AND logical condition requiring kills for multiple NPCs with individual targets - * All conditions must be satisfied (must kill the required number of each NPC) - */ - public static LogicalCondition createAndCondition(List npcNames, List targetCountsMins, List targetCountsMaxs) { - if (npcNames == null || npcNames.isEmpty()) { - throw new IllegalArgumentException("NPC name list cannot be null or empty"); - } - - // Determine the smallest list size for safe iteration - int minSize = Math.min(npcNames.size(), - Math.min(targetCountsMins != null ? targetCountsMins.size() : 0, - targetCountsMaxs != null ? targetCountsMaxs.size() : 0)); - - // If target counts not provided or empty, default to single kill per NPC - if (targetCountsMins == null || targetCountsMins.isEmpty()) { - targetCountsMins = new ArrayList<>(npcNames.size()); - for (int i = 0; i < npcNames.size(); i++) { - targetCountsMins.add(1); - } - } - - if (targetCountsMaxs == null || targetCountsMaxs.isEmpty()) { - targetCountsMaxs = new ArrayList<>(targetCountsMins); - } - - // Create the logical condition - AndCondition andCondition = new AndCondition(); - - // Add a kill count condition for each NPC - for (int i = 0; i < minSize; i++) { - NpcKillCountCondition killCondition = NpcKillCountCondition.builder() - .npcName(npcNames.get(i)) - .targetCountMin(targetCountsMins.get(i)) - .targetCountMax(targetCountsMaxs.get(i)) - .build(); - - andCondition.addCondition(killCondition); - } - - return andCondition; - } - - /** - * Creates an OR logical condition requiring kills for multiple NPCs with individual targets - * Any condition can be satisfied (must kill the required number of any one NPC) - */ - public static LogicalCondition createOrCondition(List npcNames, List targetCountsMins, List targetCountsMaxs) { - if (npcNames == null || npcNames.isEmpty()) { - throw new IllegalArgumentException("NPC name list cannot be null or empty"); - } - - // Determine the smallest list size for safe iteration - int minSize = Math.min(npcNames.size(), - Math.min(targetCountsMins != null ? targetCountsMins.size() : 0, - targetCountsMaxs != null ? targetCountsMaxs.size() : 0)); - - // If target counts not provided or empty, default to single kill per NPC - if (targetCountsMins == null || targetCountsMins.isEmpty()) { - targetCountsMins = new ArrayList<>(npcNames.size()); - for (int i = 0; i < npcNames.size(); i++) { - targetCountsMins.add(1); - } - } - - if (targetCountsMaxs == null || targetCountsMaxs.isEmpty()) { - targetCountsMaxs = new ArrayList<>(targetCountsMins); - } - - // Create the logical condition - OrCondition orCondition = new OrCondition(); - - // Add a kill count condition for each NPC - for (int i = 0; i < minSize; i++) { - NpcKillCountCondition killCondition = NpcKillCountCondition.builder() - .npcName(npcNames.get(i)) - .targetCountMin(targetCountsMins.get(i)) - .targetCountMax(targetCountsMaxs.get(i)) - .build(); - - orCondition.addCondition(killCondition); - } - - return orCondition; - } - - /** - * Creates an AND logical condition requiring kills for multiple NPCs with the same target for all - * All conditions must be satisfied (must kill the required number of each NPC) - */ - public static LogicalCondition createAndCondition(List npcNames, int targetCountMin, int targetCountMax) { - if (npcNames == null || npcNames.isEmpty()) { - throw new IllegalArgumentException("NPC name list cannot be null or empty"); - } - - // Create the logical condition - AndCondition andCondition = new AndCondition(); - - // Add a kill count condition for each NPC with the same targets - for (String npcName : npcNames) { - NpcKillCountCondition killCondition = NpcKillCountCondition.builder() - .npcName(npcName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .build(); - - andCondition.addCondition(killCondition); - } - - return andCondition; - } - - /** - * Creates an OR logical condition requiring kills for multiple NPCs with the same target for all - * Any condition can be satisfied (must kill the required number of any one NPC) - */ - public static LogicalCondition createOrCondition(List npcNames, int targetCountMin, int targetCountMax) { - if (npcNames == null || npcNames.isEmpty()) { - throw new IllegalArgumentException("NPC name list cannot be null or empty"); - } - - // Create the logical condition - OrCondition orCondition = new OrCondition(); - - // Add a kill count condition for each NPC with the same targets - for (String npcName : npcNames) { - NpcKillCountCondition killCondition = NpcKillCountCondition.builder() - .npcName(npcName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .build(); - - orCondition.addCondition(killCondition); - } - - return orCondition; - } - - - @Override - public boolean isSatisfied() { - // Once satisfied, stay satisfied until reset - if (satisfied) { - return true; - } - - // Check if current count meets or exceeds target - if (currentKillCount >= currentTargetCount) { - satisfied = true; - return true; - } - - return false; - } - - @Override - public String getDescription() { - StringBuilder sb = new StringBuilder(); - String npcDisplayName = npcName != null && !npcName.isEmpty() ? npcName : "NPCs"; - - sb.append(String.format("Kill %d %s", currentTargetCount, npcDisplayName)); - - // Add randomization info if applicable - if (targetCountMin != targetCountMax) { - sb.append(String.format(" (randomized from %d-%d)", targetCountMin, targetCountMax)); - } - - // Add progress tracking - sb.append(String.format(" (%d/%d, %.1f%%)", - currentKillCount, - currentTargetCount, - getProgressPercentage())); - - return sb.toString(); - } - - /** - * Returns a detailed description of the kill condition with additional status information - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - String npcDisplayName = npcName != null && !npcName.isEmpty() ? npcName : "NPCs"; - - // Basic description - sb.append(String.format("Kill %d %s", currentTargetCount, npcDisplayName)); - - // Add randomization info if applicable - if (targetCountMin != targetCountMax) { - sb.append(String.format(" (randomized from %d-%d)", targetCountMin, targetCountMax)); - } - - sb.append("\n"); - - // Status information - sb.append("Status: ").append(satisfied ? "Satisfied" : "Not satisfied").append("\n"); - sb.append("Progress: ").append(String.format("%d/%d (%.1f%%)", - currentKillCount, - currentTargetCount, - getProgressPercentage())).append("\n"); - - // NPC information - if (npcName != null && !npcName.isEmpty()) { - sb.append("NPC Name: ").append(npcName).append("\n"); - - if (!npcNamePattern.pattern().equals(".*")) { - sb.append("Pattern: ").append(npcNamePattern.pattern()).append("\n"); - } - } else { - sb.append("NPC: Any\n"); - } - - // Tracking information - sb.append("Currently tracking ").append(interactingNpcIndices.size()).append(" NPCs"); - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - // Basic information - sb.append("NpcKillCountCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ NPC: ").append(npcName != null && !npcName.isEmpty() ? npcName : "Any").append("\n"); - - if (npcNamePattern != null && !npcNamePattern.pattern().equals(".*")) { - sb.append(" │ Pattern: ").append(npcNamePattern.pattern()).append("\n"); - } - - sb.append(" │ Target Count: ").append(currentTargetCount).append("\n"); - - // Randomization - sb.append(" ├─ Randomization ────────────────────────────\n"); - boolean hasRandomization = targetCountMin != targetCountMax; - sb.append(" │ Randomization: ").append(hasRandomization ? "Enabled" : "Disabled").append("\n"); - if (hasRandomization) { - sb.append(" │ Target Range: ").append(targetCountMin).append("-").append(targetCountMax).append("\n"); - } - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - sb.append(" │ Satisfied: ").append(satisfied).append("\n"); - sb.append(" │ Current Kill Count: ").append(currentKillCount).append("\n"); - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Tracking info - sb.append(" └─ Tracking ────────────────────────────────\n"); - sb.append(" Active Interactions: ").append(interactingNpcIndices.size()).append("\n"); - - // List tracked NPCs if there are any - if (!interactingNpcIndices.isEmpty()) { - sb.append(" Tracked NPCs: ").append(interactingNpcIndices.toString()).append("\n"); - } - - return sb.toString(); - } - - @Override - public void reset() { - reset(false); - } - - @Override - public void reset(boolean randomize) { - if (randomize && targetCountMin != targetCountMax) { - currentTargetCount = Rs2Random.between(targetCountMin, targetCountMax); - } - satisfied = false; - currentKillCount = 0; - interactingNpcIndices.clear(); - } - - @Override - public double getProgressPercentage() { - if (satisfied) { - return 100.0; - } - - if (currentTargetCount <= 0) { - return 100.0; - } - - return Math.min(100.0, (currentKillCount * 100.0) / currentTargetCount); - } - - // NOTE: This approach tracks player interactions with NPCs - // It may need adjustment based on how interaction events fire in the game - @Subscribe - public void onInteractingChanged(InteractingChanged event) { - // Only care if the player is doing the interaction - if (event.getSource() != Microbot.getClient().getLocalPlayer()) { - return; - } - - // If the player is now interacting with an NPC, track it - if (event.getTarget() instanceof NPC) { - NPC npc = (NPC) event.getTarget(); - - // Only track NPCs that match our pattern if we have one - if (npcName == null || npcName.isEmpty() || npcNamePattern.matcher(npc.getName()).matches()) { - interactingNpcIndices.add(npc.getIndex()); - } - } - } - - // NOTE: This part checks if an NPC that the player was interacting with died - // It assumes player killed it, which may not always be true in multi-combat areas - @Subscribe - public void onNpcDespawned(NpcDespawned event) { - NPC npc = event.getNpc(); - - // Check if we were tracking this NPC - if (interactingNpcIndices.contains(npc.getIndex())) { - // If the NPC is dead, count it as a kill - if (npc.isDead()) { - // Only count NPCs that match our pattern if we have one - if (npcName == null || npcName.isEmpty() || npcNamePattern.matcher(npc.getName()).matches()) { - currentKillCount++; - lastKillTimeMillis = System.currentTimeMillis(); - } - } - - // Remove the NPC from our tracking regardless - interactingNpcIndices.remove(npc.getIndex()); - } - } - - @Override - public int getTotalConditionCount() { - return 1; - } - - @Override - public int getMetConditionCount() { - return isSatisfied() ? 1 : 0; - } - - /** - * Manually increments the kill counter. - * Useful for testing or when external systems detect kills. - * - * @param count Number of kills to add - */ - public void incrementKillCount(int count) { - currentKillCount += count; - lastKillTimeMillis = System.currentTimeMillis(); - - // Update satisfaction status - if (currentKillCount >= currentTargetCount && !satisfied) { - satisfied = true; - } - } - - /** - * Gets the estimated kills per hour based on current progress. - * - * @return Kills per hour or 0 if not enough data - */ - public double getKillsPerHour() { - long timeElapsedMs = System.currentTimeMillis() - startTimeMillis; - - // Require at least 30 seconds of data and at least one kill - if (timeElapsedMs < 30000 || currentKillCount == 0) { - return 0; - } - - double hoursElapsed = timeElapsedMs / (1000.0 * 60 * 60); - return currentKillCount / hoursElapsed; - } - - /** - * Gets the time since the last kill in milliseconds. - * - * @return Time since last kill in ms, or -1 if no kills yet - */ - public long getTimeSinceLastKill() { - if (lastKillTimeMillis == 0) { - return -1; - } - - return System.currentTimeMillis() - lastKillTimeMillis; - } - - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/ReadMe.md b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/ReadMe.md deleted file mode 100644 index 05bf2207b72..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/ReadMe.md +++ /dev/null @@ -1,14 +0,0 @@ -``` Java -// Track kills for multiple NPCs with different requirements (must kill ALL to satisfy) -List npcNames = Arrays.asList("Goblin", "Cow", "Chicken"); -List minCounts = Arrays.asList(10, 5, 15); -List maxCounts = Arrays.asList(15, 10, 20); -LogicalCondition killAllCondition = NpcKillCountCondition.createAndCondition(npcNames, minCounts, maxCounts); - -// Track kills for multiple NPCs with same requirements (must kill ANY to satisfy) -LogicalCondition killAnyCondition = NpcKillCountCondition.createOrCondition( - Arrays.asList("Dragon", "Demon", "Giant"), 5, 10); - -// Add to condition manager -conditionManager.addCondition(killAllCondition); -``` \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/serialization/NpcKillCountConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/serialization/NpcKillCountConditionAdapter.java deleted file mode 100644 index ff773ddeb96..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/serialization/NpcKillCountConditionAdapter.java +++ /dev/null @@ -1,77 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.npc.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.npc.NpcKillCountCondition; - -import java.lang.reflect.Type; - -/** - * Serializes and deserializes NpcKillCountCondition objects - */ -@Slf4j -public class NpcKillCountConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(NpcKillCountCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add type information - json.addProperty("type", NpcKillCountCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Store NPC information - data.addProperty("npcName", src.getNpcName()); - data.addProperty("targetCountMin", src.getTargetCountMin()); - data.addProperty("targetCountMax", src.getTargetCountMax()); - data.addProperty("version", NpcKillCountCondition.getVersion()); - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public NpcKillCountCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(NpcKillCountCondition.getVersion())) { - throw new JsonParseException("Version mismatch in NpcKillCountCondition: expected " + - NpcKillCountCondition.getVersion() + ", got " + version); - } - } - - // Get NPC information - String npcName = dataObj.get("npcName").getAsString(); - int targetCountMin = dataObj.get("targetCountMin").getAsInt(); - int targetCountMax = dataObj.get("targetCountMax").getAsInt(); - - // Create using builder pattern - NpcKillCountCondition condition = NpcKillCountCondition.builder() - .npcName(npcName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .build(); - - return condition; - - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/BankItemCountCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/BankItemCountCondition.java deleted file mode 100644 index fe725e7ddb3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/BankItemCountCondition.java +++ /dev/null @@ -1,364 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource; - -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; -import lombok.Builder; -import lombok.Getter; -import net.runelite.api.InventoryID; -import net.runelite.api.events.ItemContainerChanged; -import net.runelite.client.eventbus.Subscribe; -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.OrCondition; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; - -/** - * Condition that tracks the number of items in bank. - * Is satisfied when we have a certain number of items in the bank, and stays satisfied until reset. - * TODO make proccesdItemCountCondition -> tracks the number of items processed in the inventory -> for now placeholder - * track first if we "get" it in the inventory, then count down the procces items (not counting item dropped or banked -> track if bank is open or the item was dropp be the player) - */ -@Getter -public class BankItemCountCondition extends ResourceCondition { - - public static String getVersion() { - return "0.0.1"; - } - private final String itemName; - private final int targetCountMin; - private final int targetCountMax; - - private int currentTargetCount; - private int currentItemCount; - private boolean satisfied = false; - - /** - * Creates a condition with fixed target count - */ - public BankItemCountCondition(String itemName, int targetCount) { - super(itemName); - this.itemName = itemName; - this.targetCountMin = targetCount; - this.targetCountMax = targetCount; - this.currentTargetCount = targetCount; - updateCurrentCount(); - } - - /** - * Creates a condition with target count range - */ - @Builder - public BankItemCountCondition(String itemName, int targetCountMin, int targetCountMax) { - super(itemName); - this.itemName = itemName; - this.targetCountMin = Math.max(0, targetCountMin); - this.targetCountMax = Math.max(this.targetCountMin, targetCountMax); - this.currentTargetCount = Rs2Random.between(this.targetCountMin, this.targetCountMax); - updateCurrentCount(); - } - - /** - * Creates a condition with randomized target between min and max - */ - public static BankItemCountCondition createRandomized(String itemName, int minCount, int maxCount) { - return BankItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(minCount) - .targetCountMax(maxCount) - .build(); - } - - @Override - public boolean isSatisfied() { - // A condition cannot be satisfied while paused - if (isPaused) { - return false; - } - - // Once satisfied, stay satisfied until reset - if (satisfied) { - return true; - } - - // Check if current count meets or exceeds target - if (currentItemCount >= currentTargetCount) { - satisfied = true; - return true; - } - - return false; - } - - @Override - public String getDescription() { - return String.format("Have %d %s in bank (%d/%d)", - currentTargetCount, - itemName, - currentItemCount, - currentTargetCount); - } - @Override - public String getDetailedDescription() { - return String.format("BankItemCountCondition: %s\n" + - "Target Count: %d - %d\n" + - "Current Count: %d\n" + - "Satisfied: %s", - itemName, - targetCountMin, - targetCountMax, - currentItemCount, - satisfied ? "YES" : "NO"); - } - - @Override - public void reset() { - reset(false); - } - - @Override - public void reset(boolean randomize) { - if (randomize && targetCountMin != targetCountMax) { - currentTargetCount = Rs2Random.between(targetCountMin, targetCountMax); - } - satisfied = false; - updateCurrentCount(); - } - - @Override - public double getProgressPercentage() { - if (satisfied) { - return 100.0; - } - - if (currentTargetCount <= 0) { - return 100.0; - } - - return Math.min(100.0, (currentItemCount * 100.0) / currentTargetCount); - } - - @Override - @Subscribe - public void onItemContainerChanged(ItemContainerChanged event) { - // Skip processing if paused - if (isPaused) { - return; - } - - // Update count when bank container changes - if (event.getContainerId() == InventoryID.BANK.getId()) { - updateCurrentCount(); - } - } - - private void updateCurrentCount() { - // Check if we're using a specific item name or counting all items - if (itemName == null || itemName.isEmpty()) { - // Count all items in bank - currentItemCount = Rs2Bank.getBankItemCount(); - } else { - // Count specific items by name using pattern matching - if (Rs2Bank.bankItems() != null && !Rs2Bank.bankItems().isEmpty()) { - currentItemCount = Rs2Bank.bankItems().stream() - .filter(item -> { - if (item == null) { - return false; - } - return itemPattern.matcher(item.getName()).matches(); - }) - .mapToInt(Rs2ItemModel::getQuantity) - .sum(); - } else { - // Fallback to direct count if bank items list isn't populated - currentItemCount = Rs2Bank.count(itemName, false); - } - } - } - - @Override - public int getTotalConditionCount() { - return 1; - } - - @Override - public int getMetConditionCount() { - return isSatisfied() ? 1 : 0; - } - - /** - * Creates a pattern for matching item names - */ - protected Pattern createItemPattern(String itemName) { - if (itemName == null || itemName.isEmpty()) { - return Pattern.compile(".*"); - } - - // Check if the name is already a regex pattern - if (itemName.startsWith("^") || itemName.endsWith("$") || - itemName.contains(".*") || itemName.contains("[") || - itemName.contains("(")) { - return Pattern.compile(itemName); - } - - // Otherwise, create a contains pattern - return Pattern.compile(".*" + Pattern.quote(itemName) + ".*", Pattern.CASE_INSENSITIVE); - } - - /** - * Creates an AND logical condition requiring multiple items with individual targets - * All conditions must be satisfied (must have the required number of each item) - */ - public static LogicalCondition createAndCondition(List itemNames, List targetCountsMins, List targetCountsMaxs) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Determine the smallest list size for safe iteration - int minSize = Math.min(itemNames.size(), - Math.min(targetCountsMins != null ? targetCountsMins.size() : 0, - targetCountsMaxs != null ? targetCountsMaxs.size() : 0)); - - // If target counts not provided or empty, default to single item each - if (targetCountsMins == null || targetCountsMins.isEmpty()) { - targetCountsMins = new ArrayList<>(itemNames.size()); - for (int i = 0; i < itemNames.size(); i++) { - targetCountsMins.add(1); - } - } - - if (targetCountsMaxs == null || targetCountsMaxs.isEmpty()) { - targetCountsMaxs = new ArrayList<>(targetCountsMins); - } - - // Create the logical condition - AndCondition andCondition = new AndCondition(); - - // Add a condition for each item - for (int i = 0; i < minSize; i++) { - BankItemCountCondition itemCondition = BankItemCountCondition.builder() - .itemName(itemNames.get(i)) - .targetCountMin(targetCountsMins.get(i)) - .targetCountMax(targetCountsMaxs.get(i)) - .build(); - - andCondition.addCondition(itemCondition); - } - - return andCondition; - } - - /** - * Creates an OR logical condition requiring multiple items with individual targets - * Any condition can be satisfied (must have the required number of any one item) - */ - public static LogicalCondition createOrCondition(List itemNames, List targetCountsMins, List targetCountsMaxs) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Determine the smallest list size for safe iteration - int minSize = Math.min(itemNames.size(), - Math.min(targetCountsMins != null ? targetCountsMins.size() : 0, - targetCountsMaxs != null ? targetCountsMaxs.size() : 0)); - - // If target counts not provided or empty, default to single item each - if (targetCountsMins == null || targetCountsMins.isEmpty()) { - targetCountsMins = new ArrayList<>(itemNames.size()); - for (int i = 0; i < itemNames.size(); i++) { - targetCountsMins.add(1); - } - } - - if (targetCountsMaxs == null || targetCountsMaxs.isEmpty()) { - targetCountsMaxs = new ArrayList<>(targetCountsMins); - } - - // Create the logical condition - OrCondition orCondition = new OrCondition(); - - // Add a condition for each item - for (int i = 0; i < minSize; i++) { - BankItemCountCondition itemCondition = BankItemCountCondition.builder() - .itemName(itemNames.get(i)) - .targetCountMin(targetCountsMins.get(i)) - .targetCountMax(targetCountsMaxs.get(i)) - .build(); - - orCondition.addCondition(itemCondition); - } - - return orCondition; - } - - /** - * Creates an AND logical condition requiring multiple items with the same target for all - * All conditions must be satisfied (must have the required number of each item) - */ - public static LogicalCondition createAndCondition(List itemNames, int targetCountMin, int targetCountMax) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Create the logical condition - AndCondition andCondition = new AndCondition(); - - // Add a condition for each item with the same targets - for (String itemName : itemNames) { - BankItemCountCondition itemCondition = BankItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .build(); - - andCondition.addCondition(itemCondition); - } - - return andCondition; - } - - /** - * Creates an OR logical condition requiring multiple items with the same target for all - * Any condition can be satisfied (must have the required number of any one item) - */ - public static LogicalCondition createOrCondition(List itemNames, int targetCountMin, int targetCountMax) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Create the logical condition - OrCondition orCondition = new OrCondition(); - - // Add a condition for each item with the same targets - for (String itemName : itemNames) { - BankItemCountCondition itemCondition = BankItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .build(); - - orCondition.addCondition(itemCondition); - } - - return orCondition; - } - - @Override - public void pause() { - // Call parent class pause method - super.pause(); - } - - @Override - public void resume() { - if (isPaused) { - // Call parent class resume method - super.resume(); - - // For snapshot-type conditions, refresh current state on resume - updateCurrentCount(); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/GatheredResourceCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/GatheredResourceCondition.java deleted file mode 100644 index 9ad5ede4647..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/GatheredResourceCondition.java +++ /dev/null @@ -1,645 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource; - -import lombok.Builder; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import net.runelite.api.gameval.AnimationID; -import net.runelite.api.InventoryID; -import net.runelite.api.Skill; -import net.runelite.api.events.AnimationChanged; -import net.runelite.api.events.GameTick; -import net.runelite.api.events.InteractingChanged; -import net.runelite.api.events.ItemContainerChanged; -import net.runelite.api.events.StatChanged; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; -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.OrCondition; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** - * Condition that tracks items gathered from resource nodes (mining, fishing, woodcutting, farming, etc.). - * Distinguishes between items gathered from resources versus those obtained from other sources. - */ -@Slf4j -@Getter -public class GatheredResourceCondition extends ResourceCondition { - - public static String getVersion() { - return "0.0.1"; - } - private final boolean includeNoted; - private final String itemName; - private final int targetCountMin; - private final int targetCountMax; - - private final List relevantSkills; - - // Gathering state tracking - private transient boolean isCurrentlyGathering = false; - private transient Instant lastGatheringActivity = Instant.now(); - private transient Map previousItemCounts = new HashMap<>(); - private transient Map gatheredItemCounts = new HashMap<>(); - private transient int currentTargetCount; - private transient int currentGatheredCount; - private transient volatile boolean satisfied = false; - - // Pause-related fields for cumulative tracking - private transient Map pausedItemCounts = new HashMap<>(); - private transient int pausedGatheredCount = 0; - - - // Animation tracking for gathering activities - private static final int[] GATHERING_ANIMATIONS = { - // Woodcutting - AnimationID.HUMAN_WOODCUTTING_BRONZE_AXE, AnimationID.HUMAN_WOODCUTTING_IRON_AXE, AnimationID.HUMAN_WOODCUTTING_STEEL_AXE, - AnimationID.HUMAN_WOODCUTTING_BLACK_AXE, AnimationID.HUMAN_WOODCUTTING_MITHRIL_AXE, AnimationID.HUMAN_WOODCUTTING_ADAMANT_AXE, - AnimationID.HUMAN_WOODCUTTING_RUNE_AXE, AnimationID.HUMAN_WOODCUTTING_DRAGON_AXE, AnimationID.HUMAN_WOODCUTTING_INFERNAL_AXE, - AnimationID.HUMAN_WOODCUTTING_3A_AXE, AnimationID.HUMAN_WOODCUTTING_CRYSTAL_AXE, AnimationID.HUMAN_WOODCUTTING_TRAILBLAZER_AXE_NO_INFERNAL, - - // Fishing - AnimationID.HUMAN_HARPOON, AnimationID.HUMAN_LOBSTER, AnimationID.HUMAN_LARGENET, - AnimationID.HUMAN_SMALLNET, AnimationID.HUMAN_FISHING_CASTING, AnimationID.HUMAN_FISH_ONSPOT_PEARL_OILY, - AnimationID.HUMAN_FISH_ONSPOT_PEARL,AnimationID.HUMAN_FISH_ONSPOT_PEARL_FLY,AnimationID.HUMAN_FISH_ONSPOT_PEARL_BRUT, - AnimationID.HUMAN_FISHING_CASTING_PEARL, - AnimationID.HUMAN_FISHING_CASTING_PEARL,AnimationID.HUMAN_FISHING_CASTING_PEARL_FLY,AnimationID.HUMAN_FISHING_CASTING_PEARL_BRUT, - - AnimationID.HUMAN_HARPOON_BARBED, AnimationID.HUMAN_HARPOON_DRAGON, AnimationID.HUMAN_HARPOON_INFERNAL, - AnimationID.HUMAN_HARPOON_TRAILBLAZER_NO_INFERNAL, AnimationID.HUMAN_HARPOON_CRYSTAL, AnimationID.HUMAN_HARPOON_GAUNTLET_HM, - AnimationID.BRUT_PLAYER_HAND_FISHING_READY, AnimationID.BRUT_PLAYER_HAND_FISHING_START, - AnimationID.BRUT_PLAYER_HAND_FISHING_END_SHARK_1, AnimationID.BRUT_PLAYER_HAND_FISHING_END_SHARK_2, - AnimationID.BRUT_PLAYER_HAND_FISHING_END_SWORDFISH_1, AnimationID.BRUT_PLAYER_HAND_FISHING_END_SWORDFISH_2, - AnimationID.BRUT_PLAYER_HAND_FISHING_END_TUNA_1, AnimationID.BRUT_PLAYER_HAND_FISHING_END_TUNA_2, - AnimationID.HUMAN_FISHING_CASTING_BRUT, AnimationID.HUMAN_FISHING_ONSPOT_BRUT, - AnimationID.SNAKEBOSS_SLICEEEL, //FISHING_CUTTING_SACRED_EELS - AnimationID.INFERNALEEL_BREAK, AnimationID.INFERNALEEL_BREAK_IMCANDO,//FISHING_CRUSHING_INFERNAL_EELS - AnimationID.HUMAN_OCTOPUS_POT,//FISHING_KARAMBWAN - // Mining - AnimationID.HUMAN_MINING_BRONZE_PICKAXE, AnimationID.HUMAN_MINING_IRON_PICKAXE, AnimationID.HUMAN_MINING_STEEL_PICKAXE, - AnimationID.HUMAN_MINING_BLACK_PICKAXE, AnimationID.HUMAN_MINING_MITHRIL_PICKAXE, AnimationID.HUMAN_MINING_ADAMANT_PICKAXE, - AnimationID.HUMAN_MINING_RUNE_PICKAXE, AnimationID.HUMAN_MINING_DRAGON_PICKAXE,AnimationID.HUMAN_MINING_DRAGON_PICKAXE_PRETTY, - AnimationID.HUMAN_MINING_ZALCANO_PICKAXE, AnimationID.HUMAN_MINING_CRYSTAL_PICKAXE,AnimationID.HUMAN_MINING_3A_PICKAXE, - AnimationID.HUMAN_MINING_TRAILBLAZER_PICKAXE_NO_INFERNAL,AnimationID.HUMAN_MINING_INFERNAL_PICKAXE, AnimationID.HUMAN_MINING_LEAGUE_TRAILBLAZER_PICKAXE, - AnimationID.HUMAN_MINING_ZALCANO_LEAGUE_TRAILBLAZER_PICKAXE, - - //CRASHEDSTAR mining - AnimationID.HUMAN_MINING_BRONZE_PICKAXE_NOREACHFORWARD, AnimationID.HUMAN_MINING_IRON_PICKAXE_NOREACHFORWARD, AnimationID.HUMAN_MINING_STEEL_PICKAXE_NOREACHFORWARD, - AnimationID.HUMAN_MINING_BLACK_PICKAXE_NOREACHFORWARD, AnimationID.HUMAN_MINING_MITHRIL_PICKAXE_NOREACHFORWARD, AnimationID.HUMAN_MINING_ADAMANT_PICKAXE_NOREACHFORWARD, - AnimationID.HUMAN_MINING_RUNE_PICKAXE_NOREACHFORWARD, AnimationID.HUMAN_MINING_DRAGON_PICKAXE_NOREACHFORWARD,AnimationID.HUMAN_MINING_DRAGON_PICKAXE_PRETTY_NOREACHFORWARD, - AnimationID.HUMAN_MINING_ZALCANO_PICKAXE_NOREACHFORWARD, AnimationID.HUMAN_MINING_CRYSTAL_PICKAXE_NOREACHFORWARD,AnimationID.HUMAN_MINING_3A_PICKAXE_NOREACHFORWARD, - AnimationID.HUMAN_MINING_TRAILBLAZER_PICKAXE_NO_INFERNAL_NOREACHFORWARD,AnimationID.HUMAN_MINING_INFERNAL_PICKAXE_NOREACHFORWARD, AnimationID.HUMAN_MINING_LEAGUE_TRAILBLAZER_PICKAXE_NOREACHFORWARD, - //Motherload Mine mining - AnimationID.HUMAN_MINING_BRONZE_PICKAXE_WALL, AnimationID.HUMAN_MINING_IRON_PICKAXE_WALL, AnimationID.HUMAN_MINING_STEEL_PICKAXE_WALL, - AnimationID.HUMAN_MINING_BLACK_PICKAXE_WALL, AnimationID.HUMAN_MINING_MITHRIL_PICKAXE_WALL, AnimationID.HUMAN_MINING_ADAMANT_PICKAXE_WALL, - AnimationID.HUMAN_MINING_RUNE_PICKAXE_WALL, AnimationID.HUMAN_MINING_DRAGON_PICKAXE_WALL,AnimationID.HUMAN_MINING_DRAGON_PICKAXE_PRETTY_WALL, - AnimationID.HUMAN_MINING_ZALCANO_PICKAXE_WALL, AnimationID.HUMAN_MINING_CRYSTAL_PICKAXE_WALL,AnimationID.HUMAN_MINING_3A_PICKAXE_WALL, - AnimationID.HUMAN_MINING_TRAILBLAZER_PICKAXE_NO_INFERNAL_WALL,AnimationID.HUMAN_MINING_INFERNAL_PICKAXE_WALL, AnimationID.HUMAN_MINING_LEAGUE_TRAILBLAZER_PICKAXE_WALL, - // Farming - AnimationID.ULTRACOMPOST_MAKE, AnimationID.HUMAN_FARMING, AnimationID.FARMING_RAKING, - AnimationID.FARMING_PICK_MUSHROOM - }; - - /** - * Basic constructor with only item name and target count - */ - public GatheredResourceCondition(String itemName, int targetCount, boolean includeNoted) { - super(itemName); - this.itemName = itemName; - this.targetCountMin = targetCount; - this.targetCountMax = targetCount; - this.currentTargetCount = targetCount; - this.includeNoted = includeNoted; - this.relevantSkills = determineRelevantSkills(itemName); - initializeItemCounts(); - } - - /** - * Full constructor with builder support - */ - @Builder - public GatheredResourceCondition(String itemName, int targetCountMin, int targetCountMax, - boolean includeNoted, List relevantSkills) { - super(itemName); - this.itemName = itemName; - this.targetCountMin = Math.max(0, targetCountMin); - this.targetCountMax = Math.min(Integer.MAX_VALUE, targetCountMax); - this.currentTargetCount = Rs2Random.between(this.targetCountMin, this.targetCountMax); - this.includeNoted = includeNoted; - this.relevantSkills = relevantSkills != null ? relevantSkills : determineRelevantSkills(itemName); - initializeItemCounts(); - } - - /** - * Initialize tracking of inventory item counts - */ - private void initializeItemCounts() { - updatePreviousItemCounts(); - gatheredItemCounts.clear(); - currentGatheredCount = 0; - } - - /** - * Create a map of current inventory item counts for tracking purposes - */ - private void updatePreviousItemCounts() { - previousItemCounts.clear(); - - // Include all inventory items matching our pattern - List items = new ArrayList<>(); - items.addAll(getUnNotedItems()); - if (includeNoted) { - items.addAll(getNotedItems()); - } - - // Count matching items - for (Rs2ItemModel item : items) { - if (item != null && itemPattern.matcher(item.getName()).matches()) { - String name = item.getName(); - previousItemCounts.put(name, previousItemCounts.getOrDefault(name, 0) + 1); - } - } - } - - /** - * Attempts to determine which skills are relevant for the specified item - */ - private List determineRelevantSkills(String itemName) { - List skills = new ArrayList<>(); - - // Pattern matching approach - could be more sophisticated with a proper mapping - String lowerName = itemName.toLowerCase(); - - // Mining related - if (lowerName.contains("ore") || lowerName.contains("rock") || lowerName.contains("coal") || - lowerName.contains("gem") || lowerName.contains("granite") || lowerName.contains("sandstone") || - lowerName.contains("clay")) { - skills.add(Skill.MINING); - } - - // Fishing related - if (lowerName.contains("fish") || lowerName.contains("shrimp") || lowerName.contains("trout") || - lowerName.contains("salmon") || lowerName.contains("lobster") || lowerName.contains("shark") || - lowerName.contains("karambwan") || lowerName.contains("monkfish") || lowerName.contains("anglerfish")) { - skills.add(Skill.FISHING); - } - - // Woodcutting related - if (lowerName.contains("log") || lowerName.contains("root") || lowerName.contains("bark")) { - skills.add(Skill.WOODCUTTING); - } - - // Farming related - if (lowerName.contains("seed") || lowerName.contains("sapling") || lowerName.contains("herb") || - lowerName.contains("leaf") || lowerName.contains("fruit") || lowerName.contains("berry") || - lowerName.contains("vegetable") || lowerName.contains("coconut") || lowerName.contains("banana") || - lowerName.contains("papaya") || lowerName.contains("watermelon") || lowerName.contains("strawberry") || - lowerName.contains("tomato") || lowerName.contains("potato") || lowerName.contains("onion") || - lowerName.contains("cabbage")) { - skills.add(Skill.FARMING); - } - - // Default to all resource-gathering skills if no match - if (skills.isEmpty()) { - skills.add(Skill.MINING); - skills.add(Skill.FISHING); - skills.add(Skill.WOODCUTTING); - skills.add(Skill.FARMING); - skills.add(Skill.HUNTER); - } - - return skills; - } - - /** - * Checks if the player is currently performing a gathering animation - */ - private boolean isCurrentlyGatheringAnimation() { - int currentAnimation = Rs2Player.getAnimation(); - for (int animation : GATHERING_ANIMATIONS) { - if (currentAnimation == animation) { - return true; - } - } - return false; - } - - @Override - public boolean isSatisfied() { - // A condition cannot be satisfied while paused - if (isPaused) { - return false; - } - - // Once satisfied, stay satisfied until reset - if (satisfied) { - return true; - } - - // Check if gathered count meets or exceeds target - if (currentGatheredCount >= currentTargetCount) { - satisfied = true; - return true; - } - - return false; - } - - @Override - public void reset() { - reset(false); - } - - @Override - public void reset(boolean randomize) { - if (randomize && targetCountMin != targetCountMax) { - currentTargetCount = Rs2Random.between(targetCountMin, targetCountMax); - } - satisfied = false; - isCurrentlyGathering = false; - gatheredItemCounts.clear(); - currentGatheredCount = 0; - updatePreviousItemCounts(); - } - - @Override - public ConditionType getType() { - return ConditionType.RESOURCE; - } - - @Override - public String getDescription() { - String itemTypeDesc = includeNoted ? " (including noted)" : ""; - String randomRangeInfo = ""; - - if (targetCountMin != targetCountMax) { - randomRangeInfo = String.format(" (randomized from %d-%d)", targetCountMin, targetCountMax); - } - - return String.format("Gather %d %s%s%s (%d/%d, %.1f%%)", - currentTargetCount, - itemName != null && !itemName.isEmpty() ? itemName : "resources", - itemTypeDesc, - randomRangeInfo, - currentGatheredCount, - currentTargetCount, - getProgressPercentage()); - } - - @Override - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - // Basic description - sb.append("Gathered Resource Condition: ").append(itemName != null && !itemName.isEmpty() ? itemName : "Any resource").append("\n"); - - // Add randomization info if applicable - if (targetCountMin != targetCountMax) { - sb.append("Target Range: ").append(targetCountMin).append("-").append(targetCountMax) - .append(" (current target: ").append(currentTargetCount).append(")\n"); - } else { - sb.append("Target Count: ").append(currentTargetCount).append("\n"); - } - - // Status information - sb.append("Status: ").append(isSatisfied() ? "Satisfied" : "Not satisfied").append("\n"); - sb.append("Progress: ").append(currentGatheredCount).append("/").append(currentTargetCount) - .append(" (").append(String.format("%.1f%%", getProgressPercentage())).append(")\n"); - - // Configuration information - sb.append("Include Noted Items: ").append(includeNoted ? "Yes" : "No").append("\n"); - sb.append("Currently Gathering: ").append(isCurrentlyGathering ? "Yes" : "No").append("\n"); - - // Relevant skills - sb.append("Tracking XP in skills: "); - for (int i = 0; i < relevantSkills.size(); i++) { - sb.append(relevantSkills.get(i).getName()); - if (i < relevantSkills.size() - 1) { - sb.append(", "); - } - } - - return sb.toString(); - } - - @Override - public double getProgressPercentage() { - if (satisfied) { - return 100.0; - } - - if (currentTargetCount <= 0) { - return 100.0; - } - - return Math.min(100.0, (currentGatheredCount * 100.0) / currentTargetCount); - } - - @Override - public void onAnimationChanged(AnimationChanged event) { - // Check if this is our player - if (event.getActor() != Microbot.getClient().getLocalPlayer()) { - return; - } - - // Update gathering state based on animation - if (isCurrentlyGatheringAnimation()) { - isCurrentlyGathering = true; - lastGatheringActivity = Instant.now(); - } - } - - @Override - public void onStatChanged(StatChanged event) { - // Check if XP was gained in a relevant skill - if (relevantSkills.contains(event.getSkill()) && event.getXp() > 0) { - isCurrentlyGathering = true; - lastGatheringActivity = Instant.now(); - } - } - - @Override - public void onInteractingChanged(InteractingChanged event) { - // Check if this is our player - if (event.getSource() != Microbot.getClient().getLocalPlayer()) { - return; - } - - // If player starts interacting with something, consider it gathering - if (event.getTarget() != null) { - isCurrentlyGathering = true; - lastGatheringActivity = Instant.now(); - } - } - - @Override - public void onGameTick(GameTick event) { - // Check if we've timed out on gathering activity - if (isCurrentlyGathering && Instant.now().minusSeconds(5).isAfter(lastGatheringActivity)) { - isCurrentlyGathering = false; - } - } - - @Override - public void onItemContainerChanged(ItemContainerChanged event) { - // Only process inventory changes - if (event.getContainerId() != InventoryID.INVENTORY.getId()) { - return; - } - - // Don't process changes if bank is open (banking items) - if (Rs2Bank.isOpen()) { - updatePreviousItemCounts(); - return; - } - - // Process inventory changes only when actively gathering or within 3 seconds of gathering - if (isCurrentlyGathering || Instant.now().minusSeconds(3).isBefore(lastGatheringActivity)) { - processInventoryChanges(); - } else { - // Just update previous counts if not gathering - updatePreviousItemCounts(); - } - } - - /** - * Process inventory changes to detect newly gathered items - */ - private void processInventoryChanges() { - Map currentCounts = new HashMap<>(); - - // Get current inventory counts - List currentItems = new ArrayList<>(); - currentItems.addAll(getUnNotedItems()); - if (includeNoted) { - currentItems.addAll(getNotedItems()); - } - - // Count matching items - for (Rs2ItemModel item : currentItems) { - if (item != null && itemPattern.matcher(item.getName()).matches()) { - String name = item.getName(); - currentCounts.put(name, currentCounts.getOrDefault(name, 0) + item.getQuantity()); - } - } - - // Calculate differences - for (Map.Entry entry : currentCounts.entrySet()) { - String itemName = entry.getKey(); - int currentCount = entry.getValue(); - int previousCount = previousItemCounts.getOrDefault(itemName, 0); - - // If current count is higher, items were gathered - if (currentCount > previousCount) { - int newItems = currentCount - previousCount; - log.debug("Detected {} newly gathered {}", newItems, itemName); - - // Add to gathered count - gatheredItemCounts.put(itemName, gatheredItemCounts.getOrDefault(itemName, 0) + newItems); - currentGatheredCount += newItems; - } - } - - // Update previous counts for next comparison - previousItemCounts = currentCounts; - } - - /** - * Creates a condition with randomized target between min and max - */ - public static GatheredResourceCondition createRandomized(String itemName, int minCount, int maxCount, - boolean includeNoted) { - return GatheredResourceCondition.builder() - .itemName(itemName) - .targetCountMin(minCount) - .targetCountMax(maxCount) - .includeNoted(includeNoted) - .build(); - } - - /** - * Creates an AND logical condition requiring multiple gathered items with individual targets - */ - public static LogicalCondition createAndCondition(List itemNames, - List targetCountsMins, - List targetCountsMaxs, - boolean includeNoted) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Handle missing target counts - int minSize = Math.min(itemNames.size(), - Math.min(targetCountsMins != null ? targetCountsMins.size() : 0, - targetCountsMaxs != null ? targetCountsMaxs.size() : 0)); - - if (targetCountsMins == null || targetCountsMins.isEmpty()) { - targetCountsMins = new ArrayList<>(itemNames.size()); - for (int i = 0; i < itemNames.size(); i++) { - targetCountsMins.add(1); - } - } - - if (targetCountsMaxs == null || targetCountsMaxs.isEmpty()) { - targetCountsMaxs = new ArrayList<>(targetCountsMins); - } - - // Create AND condition - AndCondition andCondition = new AndCondition(); - - // Add condition for each item - for (int i = 0; i < minSize; i++) { - GatheredResourceCondition itemCondition = GatheredResourceCondition.builder() - .itemName(itemNames.get(i)) - .targetCountMin(targetCountsMins.get(i)) - .targetCountMax(targetCountsMaxs.get(i)) - .includeNoted(includeNoted) - .build(); - - andCondition.addCondition(itemCondition); - } - - return andCondition; - } - - /** - * Creates an OR logical condition requiring any of multiple gathered items with individual targets - */ - public static LogicalCondition createOrCondition(List itemNames, - List targetCountsMins, - List targetCountsMaxs, - boolean includeNoted) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Handle missing target counts - int minSize = Math.min(itemNames.size(), - Math.min(targetCountsMins != null ? targetCountsMins.size() : 0, - targetCountsMaxs != null ? targetCountsMaxs.size() : 0)); - - if (targetCountsMins == null || targetCountsMins.isEmpty()) { - targetCountsMins = new ArrayList<>(itemNames.size()); - for (int i = 0; i < itemNames.size(); i++) { - targetCountsMins.add(1); - } - } - - if (targetCountsMaxs == null || targetCountsMaxs.isEmpty()) { - targetCountsMaxs = new ArrayList<>(targetCountsMins); - } - - // Create OR condition - OrCondition orCondition = new OrCondition(); - - // Add condition for each item - for (int i = 0; i < minSize; i++) { - GatheredResourceCondition itemCondition = GatheredResourceCondition.builder() - .itemName(itemNames.get(i)) - .targetCountMin(targetCountsMins.get(i)) - .targetCountMax(targetCountsMaxs.get(i)) - .includeNoted(includeNoted) - .build(); - - orCondition.addCondition(itemCondition); - } - - return orCondition; - } - - @Override - public void pause() { - if (!isPaused) { - // Call parent class pause method - super.pause(); - - // Capture current inventory state at pause time - pausedItemCounts.clear(); - - // Get current inventory counts for items matching our pattern - List items = new ArrayList<>(); - items.addAll(getUnNotedItems()); - if (includeNoted) { - items.addAll(getNotedItems()); - } - - // Count matching items at pause time - for (Rs2ItemModel item : items) { - if (item != null && itemPattern.matcher(item.getName()).matches()) { - String name = item.getName(); - pausedItemCounts.put(name, pausedItemCounts.getOrDefault(name, 0) + item.getQuantity()); - } - } - - // Save current gathered count - pausedGatheredCount = currentGatheredCount; - - log.debug("GatheredResourceCondition paused. Captured pause state with {} gathered", - pausedGatheredCount); - } - } - - @Override - public void resume() { - if (isPaused) { - // Calculate items gained during pause - Map currentCounts = new HashMap<>(); - - // Get current inventory counts - List currentItems = new ArrayList<>(); - currentItems.addAll(getUnNotedItems()); - if (includeNoted) { - currentItems.addAll(getNotedItems()); - } - - // Count matching items now - for (Rs2ItemModel item : currentItems) { - if (item != null && itemPattern.matcher(item.getName()).matches()) { - String name = item.getName(); - currentCounts.put(name, currentCounts.getOrDefault(name, 0) + item.getQuantity()); - } - } - - // Calculate items gained during pause and adjust gathered counts - int itemsGainedDuringPause = 0; - for (Map.Entry entry : currentCounts.entrySet()) { - String itemName = entry.getKey(); - int currentCount = entry.getValue(); - int pausedCount = pausedItemCounts.getOrDefault(itemName, 0); - - if (currentCount > pausedCount) { - int gainedDuringPause = currentCount - pausedCount; - itemsGainedDuringPause += gainedDuringPause; - - // Remove the pause-period gains from our gathered counts - int existingGathered = gatheredItemCounts.getOrDefault(itemName, 0); - gatheredItemCounts.put(itemName, Math.max(0, existingGathered - gainedDuringPause)); - } - } - - // Adjust total gathered count to exclude pause-period gains - currentGatheredCount = Math.max(0, currentGatheredCount - itemsGainedDuringPause); - - // Call parent class resume method - super.resume(); - - // Update baseline inventory counts for future comparisons - updatePreviousItemCounts(); - - log.debug("GatheredResourceCondition resumed. Adjusted gathered count by {} items gained during pause. " + - "New gathered count: {}", itemsGainedDuringPause, currentGatheredCount); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/InventoryItemCountCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/InventoryItemCountCondition.java deleted file mode 100644 index 42123df2d86..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/InventoryItemCountCondition.java +++ /dev/null @@ -1,383 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource; - -import java.util.ArrayList; -import java.util.List; - - -import lombok.Builder; -import lombok.Getter; -import net.runelite.api.InventoryID; -import net.runelite.api.events.GameTick; -import net.runelite.api.events.ItemContainerChanged; -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.OrCondition; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; - -/** - * Condition that tracks the total number of items in inventory. - * Can be set to track noted items as well. TODO Rename into GatheredItemCountCondition - */ -@Getter -public class InventoryItemCountCondition extends ResourceCondition { - - public static String getVersion() { - return "0.0.1"; - } - private final boolean includeNoted; - private final int targetCountMin; - private final int targetCountMax; - - private transient int currentTargetCount; - private transient int currentItemCount; - private transient volatile boolean satisfied = false; - private transient boolean initialInventoryLoaded = false; - public InventoryItemCountCondition(String itemName, int targetCount, boolean includeNoted) { - super(itemName); - this.includeNoted = includeNoted; - - this.targetCountMin = targetCount; - this.targetCountMax = targetCount; - this.currentTargetCount = targetCount; - updateCurrentCount(); - } - - @Builder - public InventoryItemCountCondition(String itemName,int targetCountMin, int targetCountMax, boolean includeNoted) { - super(itemName); - this.includeNoted = includeNoted; - this.targetCountMin = Math.max(0, targetCountMin); - - // If not tracking noted items, limit max count to inventory size (28) - this.targetCountMax = includeNoted ? - Math.min(Integer.MAX_VALUE, targetCountMax) : - Math.min(28, targetCountMax); - - this.currentTargetCount = Rs2Random.between(this.targetCountMin, this.targetCountMax); - updateCurrentCount(); - } - - /** - * Creates a condition with randomized target between min and max - */ - public static InventoryItemCountCondition createRandomized(int minCount, int maxCount, boolean includeNoted) { - return InventoryItemCountCondition.builder() - .targetCountMin(minCount) - .targetCountMax(maxCount) - .includeNoted(includeNoted) - .build(); - } - - /** - * Creates an AND logical condition requiring multiple items with individual targets - * All conditions must be satisfied (must have the required number of each item) - */ - public static LogicalCondition createAndCondition(List itemNames, - List targetCountsMins, List targetCountsMaxs, boolean includeNoted) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Determine the smallest list size for safe iteration - int minSize = Math.min(itemNames.size(), - Math.min(targetCountsMins != null ? targetCountsMins.size() : 0, - targetCountsMaxs != null ? targetCountsMaxs.size() : 0)); - - // If target counts not provided or empty, default to single item each - if (targetCountsMins == null || targetCountsMins.isEmpty()) { - targetCountsMins = new ArrayList<>(itemNames.size()); - for (int i = 0; i < itemNames.size(); i++) { - targetCountsMins.add(1); - } - } - - if (targetCountsMaxs == null || targetCountsMaxs.isEmpty()) { - targetCountsMaxs = new ArrayList<>(targetCountsMins); - } - - // Create the logical condition - AndCondition andCondition = new AndCondition(); - - // Add a condition for each item - for (int i = 0; i < minSize; i++) { - InventoryItemCountCondition itemCondition = InventoryItemCountCondition.builder() - .itemName(itemNames.get(i)) - .targetCountMin(targetCountsMins.get(i)) - .targetCountMax(targetCountsMaxs.get(i)) - .includeNoted(includeNoted) - .build(); - - andCondition.addCondition(itemCondition); - } - - return andCondition; - } - - /** - * Creates an OR logical condition requiring multiple items with individual targets - * Any condition can be satisfied (must have the required number of any one item) - */ - public static LogicalCondition createOrCondition(List itemNames, List targetCountsMins, List targetCountsMaxs, boolean includeNoted) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Determine the smallest list size for safe iteration - int minSize = Math.min(itemNames.size(), - Math.min(targetCountsMins != null ? targetCountsMins.size() : 0, - targetCountsMaxs != null ? targetCountsMaxs.size() : 0)); - - // If target counts not provided or empty, default to single item each - if (targetCountsMins == null || targetCountsMins.isEmpty()) { - targetCountsMins = new ArrayList<>(itemNames.size()); - for (int i = 0; i < itemNames.size(); i++) { - targetCountsMins.add(1); - } - } - - if (targetCountsMaxs == null || targetCountsMaxs.isEmpty()) { - targetCountsMaxs = new ArrayList<>(targetCountsMins); - } - - // Create the logical condition - OrCondition orCondition = new OrCondition(); - - // Add a condition for each item - for (int i = 0; i < minSize; i++) { - InventoryItemCountCondition itemCondition = InventoryItemCountCondition.builder() - .itemName(itemNames.get(i)) - .targetCountMin(targetCountsMins.get(i)) - .targetCountMax(targetCountsMaxs.get(i)) - .includeNoted(includeNoted) - .build(); - - orCondition.addCondition(itemCondition); - } - - return orCondition; - } - - /** - * Creates an AND logical condition requiring multiple items with the same target for all - * All conditions must be satisfied (must have the required number of each item) - */ - public static LogicalCondition createAndCondition(List itemNames, int targetCountMin, int targetCountMax, boolean includeNoted) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Create the logical condition - AndCondition andCondition = new AndCondition(); - - // Add a condition for each item with the same targets - for (String itemName : itemNames) { - InventoryItemCountCondition itemCondition = InventoryItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .includeNoted(includeNoted) - .build(); - - andCondition.addCondition(itemCondition); - } - - return andCondition; - } - - /** - * Creates an OR logical condition requiring multiple items with the same target for all - * Any condition can be satisfied (must have the required number of any one item) - */ - public static LogicalCondition createOrCondition(List itemNames, int targetCountMin, int targetCountMax, boolean includeNoted) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Create the logical condition - OrCondition orCondition = new OrCondition(); - - // Add a condition for each item with the same targets - for (String itemName : itemNames) { - InventoryItemCountCondition itemCondition = InventoryItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .includeNoted(includeNoted) - .build(); - - orCondition.addCondition(itemCondition); - } - - return orCondition; - } - - @Override - public boolean isSatisfied() { - // A condition cannot be satisfied while paused - if (isPaused) { - return false; - } - - // Once satisfied, stay satisfied until reset - if (satisfied) { - return true; - } - - // Check if current count meets or exceeds target - if (currentItemCount >= currentTargetCount) { - satisfied = true; - return true; - } - - return false; - } - - @Override - public String getDescription() { - String itemTypeDesc = includeNoted ? " (including noted)" : ""; - String randomRangeInfo = ""; - - if (targetCountMin != targetCountMax) { - randomRangeInfo = String.format(" (randomized from %d-%d)", targetCountMin, targetCountMax); - } - - if (getItemName() == null || getItemName().isEmpty()) { - return String.format("Have %d total items in inventory%s%s (%d/%d, %.1f%%)", - currentTargetCount, - itemTypeDesc, - randomRangeInfo, - currentItemCount, - currentTargetCount, - getProgressPercentage()); - } else { - return String.format("Have %d %s in inventory%s%s (%d/%d, %.1f%%)", - currentTargetCount, - getItemName(), - itemTypeDesc, - randomRangeInfo, - currentItemCount, - currentTargetCount, - getProgressPercentage()); - } - } - public String getDetailedDescription() { - return String.format("Inventory Item Count Condition: %s\n" + - "Target Count: %d (current: %d)\n" + - "Include Noted: %s\n" + - "Progress: %.1f%%", - getItemName(), - currentTargetCount, - currentItemCount, - includeNoted ? "Yes" : "No", - getProgressPercentage()); - } - @Override - public void reset() { - reset(false); - } - - @Override - public void reset(boolean randomize) { - if (randomize && targetCountMin != targetCountMax) { - currentTargetCount = Rs2Random.between(targetCountMin, targetCountMax); - } - satisfied = false; - initialInventoryLoaded = false; - updateCurrentCount(); - } - - @Override - public double getProgressPercentage() { - if (satisfied) { - return 100.0; - } - - if (currentTargetCount <= 0) { - return 100.0; - } - - return Math.min(100.0, (currentItemCount * 100.0) / currentTargetCount); - } - - @Override - public void onItemContainerChanged(ItemContainerChanged event) { - // Skip processing if paused - if (isPaused) { - return; - } - - if (event.getContainerId() == InventoryID.INVENTORY.getId()) { - if (Rs2Bank.isOpen()) { - return; - } - updateCurrentCount(); - } - } - - @Override - public void onGameTick(GameTick event) { - // Skip processing if paused - if (isPaused) { - return; - } - - // Load initial inventory if not yet loaded - if (!initialInventoryLoaded) { - updateCurrentCount(); - initialInventoryLoaded = true; - } - } - - private void updateCurrentCount() { - - // Count specific items by name (using existing pattern matching) - - int currentItemCountNoted = getNotedItems().stream().filter(item -> { - if (item == null) { - return false; - } ; - return itemPattern.matcher(item.getName()).matches(); - }).mapToInt(item -> item.getQuantity()).sum(); - int currentItemCountUnNoted = getUnNotedItems().stream().filter(item -> { - if (item == null) { - return false; - } - return itemPattern.matcher(item.getName()).matches(); - }).mapToInt(item -> item.getQuantity()).sum(); - if (includeNoted) { - currentItemCount = currentItemCountNoted + currentItemCountUnNoted; - } else { - currentItemCount = currentItemCountUnNoted; - } - - } - - @Override - public int getTotalConditionCount() { - return 1; - } - - @Override - public int getMetConditionCount() { - return isSatisfied() ? 1 : 0; - } - - @Override - public void pause() { - // Call parent class pause method - super.pause(); - } - - @Override - public void resume() { - if (isPaused) { - // Call parent class resume method - super.resume(); - - // For snapshot-type conditions, refresh current state on resume - updateCurrentCount(); - initialInventoryLoaded = true; // Mark as loaded after resume - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/LootItemCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/LootItemCondition.java deleted file mode 100644 index 156d5968231..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/LootItemCondition.java +++ /dev/null @@ -1,884 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource; - -import lombok.Builder; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Client; -import net.runelite.api.GameState; -import net.runelite.api.InventoryID; -import net.runelite.api.TileItem; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.ItemContainerChanged; -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.ItemDespawned; -import net.runelite.api.events.ItemSpawned; - -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.OrCondition; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItem; -import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; -import net.runelite.client.plugins.microbot.util.models.RS2Item; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -import static net.runelite.api.TileItem.OWNERSHIP_SELF; - -import static net.runelite.api.TileItem.OWNERSHIP_GROUP; -import static net.runelite.api.TileItem.OWNERSHIP_OTHER; -import static net.runelite.api.TileItem.OWNERSHIP_NONE; - -/** - * Condition that tracks a specific looted item quantity. - * Focuses on tracking a single item for clarity and precision. - */ -@Getter -@Slf4j -public class LootItemCondition extends ResourceCondition { - - public static String getVersion() { - return "0.0.1"; - } - private final int targetAmountMin; - private final int targetAmountMax; - private final boolean includeNoted; - private final boolean includeNoneOwner; - private final boolean ignorePlayerDropped; - - private transient int currentTargetAmount; - private transient int currentTrackedCount; - private transient int lastInventoryCount; - - // Pause/resume state for cumulative tracking - private transient int pausedInventoryCount; - private transient int pausedTrackedCount; - - - private final Map trackedItemQuantities = new HashMap<>(); - - // Keep track of recently looted items to avoid double counting - private final Map recentlyLootedItems = new HashMap<>(); - - // Key is a composite key of location and item ID to track multiple items at the same spot - private static class TrackedItem { - public final WorldPoint location; - public final int itemId; - public final int quantity; - public final long timestamp; - public final String itemName; - - public TrackedItem(WorldPoint location, int itemId, int quantity, String itemName) { - this.location = location; - this.itemId = itemId; - this.quantity = quantity; - this.timestamp = System.currentTimeMillis(); - this.itemName = itemName; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TrackedItem that = (TrackedItem) o; - return itemId == that.itemId && - location.equals(that.location); - } - - @Override - public int hashCode() { - return Objects.hash(location, itemId); - } - } - - // Replace the existing tracking collections with new ones - private final Set trackedItems = new HashSet<>(); - private final Map> itemsByLocation = new HashMap<>(); - - public LootItemCondition(String itemName, int targetAmount, boolean includeNoted) { - super(itemName); - - this.targetAmountMin = targetAmount; - this.targetAmountMax = targetAmount; - this.includeNoted = includeNoted; - this.currentTargetAmount = Rs2Random.between(targetAmountMin, targetAmountMax); - this.currentTrackedCount = 0; - this.lastInventoryCount = 0; - this.includeNoneOwner = false; - this.isIncludeNoneOwner(); - this.ignorePlayerDropped = false; - } - - @Builder - public LootItemCondition(String itemName, int targetAmountMin, - int targetAmountMax, - boolean includeNoted, - boolean includeNoneOwner, - boolean ignorePlayerDropped) { - super(itemName); - this.targetAmountMin = targetAmountMin; - this.targetAmountMax = targetAmountMax; - this.currentTargetAmount = Rs2Random.between(targetAmountMin, targetAmountMax); - this.currentTrackedCount = 0; - this.lastInventoryCount = 0; - this.includeNoted = includeNoted; - this.includeNoneOwner = includeNoneOwner; - this.ignorePlayerDropped = ignorePlayerDropped; - // Initialize with existing items on the ground - scanForExistingItems(); - } - - /** - * Creates a condition with randomized target between min and max - */ - public static LootItemCondition createRandomized(String itemName, int minAmount, int maxAmount, boolean includeNoted, boolean includeNoneOwner) { - return LootItemCondition.builder() - .itemName(itemName) - .targetAmountMin(minAmount) - .targetAmountMax(maxAmount) - .includeNoted(includeNoted) - .includeNoneOwner(includeNoneOwner) - .build(); - } - - - /** - * Creates an AND logical condition requiring multiple looted items with individual targets - */ - public static LogicalCondition createAndCondition(List itemNames, List targetAmountsMins, List targetAmountsMaxs,boolean includeNoted, boolean includeNoneOwner) { - // Validate input - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Determine the smallest list size for safe iteration - int minSize = Math.min(itemNames.size(), - Math.min(targetAmountsMins != null ? targetAmountsMins.size() : 0, - targetAmountsMaxs != null ? targetAmountsMaxs.size() : 0)); - - // If target amounts not provided or empty, default to single item each - if (targetAmountsMins == null || targetAmountsMins.isEmpty()) { - targetAmountsMins = new ArrayList<>(itemNames.size()); - for (int i = 0; i < itemNames.size(); i++) { - targetAmountsMins.add(1); - } - } - - if (targetAmountsMaxs == null || targetAmountsMaxs.isEmpty()) { - targetAmountsMaxs = new ArrayList<>(targetAmountsMins); - } - - // Create the logical condition - AndCondition andCondition = new AndCondition(); - - // Add a condition for each item - for (int i = 0; i < minSize; i++) { - LootItemCondition itemCondition = LootItemCondition.builder() - .itemName(itemNames.get(i)) - .targetAmountMin(targetAmountsMins.get(i)) - .targetAmountMax(targetAmountsMaxs.get(i)) - .includeNoted(includeNoted) - .includeNoneOwner(includeNoneOwner) - .build(); - - andCondition.addCondition(itemCondition); - } - - return andCondition; - } - - /** - * Creates an OR logical condition requiring multiple looted items with individual targets - */ - public static LogicalCondition createOrCondition(List itemNames, - List targetAmountsMins, List targetAmountsMaxs, - boolean includeNoted, boolean includeNoneOwner) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Determine the smallest list size for safe iteration - int minSize = Math.min(itemNames.size(), - Math.min(targetAmountsMins != null ? targetAmountsMins.size() : 0, - targetAmountsMaxs != null ? targetAmountsMaxs.size() : 0)); - - // If target amounts not provided or empty, default to single item each - if (targetAmountsMins == null || targetAmountsMins.isEmpty()) { - targetAmountsMins = new ArrayList<>(itemNames.size()); - for (int i = 0; i < itemNames.size(); i++) { - targetAmountsMins.add(1); - } - } - - if (targetAmountsMaxs == null || targetAmountsMaxs.isEmpty()) { - targetAmountsMaxs = new ArrayList<>(targetAmountsMins); - } - - // Create the logical condition - OrCondition orCondition = new OrCondition(); - - // Add a condition for each item - for (int i = 0; i < minSize; i++) { - LootItemCondition itemCondition = LootItemCondition.builder() - .itemName(itemNames.get(i)) - .targetAmountMin(targetAmountsMins.get(i)) - .targetAmountMax(targetAmountsMaxs.get(i)) - .includeNoted(includeNoted) - .includeNoneOwner(includeNoneOwner) - .build(); - - orCondition.addCondition(itemCondition); - } - - return orCondition; - } - - /** - * Creates an AND logical condition requiring multiple looted items with the same target for all - */ - public static LogicalCondition createAndCondition(List itemNames, int targetAmountMin, int targetAmountMax,boolean includeNoted, boolean includeNoneOwner) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Create the logical condition - AndCondition andCondition = new AndCondition(); - - // Add a condition for each item with the same targets - for (String itemName : itemNames) { - LootItemCondition itemCondition = LootItemCondition.builder() - .itemName(itemName) - .targetAmountMin(targetAmountMin) - .targetAmountMax(targetAmountMax) - .includeNoted(includeNoted) - .includeNoneOwner(includeNoneOwner) - .build(); - - andCondition.addCondition(itemCondition); - } - - return andCondition; - } - - /** - * Creates an OR logical condition requiring multiple looted items with the same target for all - */ - public static LogicalCondition createOrCondition(List itemNames, int targetAmountMin, int targetAmountMax,boolean includeNoted, boolean includeNoneOwner) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Create the logical condition - OrCondition orCondition = new OrCondition(); - - // Add a condition for each item with the same targets - for (String itemName : itemNames) { - LootItemCondition itemCondition = LootItemCondition.builder() - .itemName(itemName) - .targetAmountMin(targetAmountMin) - .targetAmountMax(targetAmountMax) - .includeNoted(includeNoted) - .includeNoneOwner(includeNoneOwner) - .build(); - - orCondition.addCondition(itemCondition); - } - - return orCondition; - } - - @Override - public boolean isSatisfied() { - // Return false if paused to prevent condition from being satisfied during pause - if (isPaused()) { - return false; - } - return currentTrackedCount >= currentTargetAmount; - } - - @Override - public String getDescription() { - StringBuilder sb = new StringBuilder(); - String noteState = includeNoted ? " (including noted)" : ""; - String ownerState = includeNoneOwner ? " (any owner)" : " (player owned)"; - String randomRangeInfo = ""; - - if (targetAmountMin != targetAmountMax) { - randomRangeInfo = String.format(" (randomized from %d-%d)", targetAmountMin, targetAmountMax); - } - - sb.append(String.format("Loot %d %s%s%s", - currentTargetAmount, - noteState, - ownerState, - randomRangeInfo)); - - // Add progress tracking - sb.append(String.format(" (%d/%d, %.1f%%)", - currentTrackedCount, - currentTargetAmount, - getProgressPercentage())); - - return sb.toString(); - } - - /** - * Returns a detailed description of the loot item condition with additional status information - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - // Basic description - sb.append(String.format("Loot %d %s", currentTargetAmount, getItemPattern().pattern())); - - // Add randomization info if applicable - if (targetAmountMin != targetAmountMax) { - sb.append(String.format(" (randomized from %d-%d)", targetAmountMin, targetAmountMax)); - } - - sb.append("\n"); - - // Status information - sb.append("Status: ").append(isSatisfied() ? "Satisfied" : "Not satisfied").append("\n"); - sb.append("Progress: ").append(String.format("%d/%d (%.1f%%)", - currentTrackedCount, - currentTargetAmount, - getProgressPercentage())).append("\n"); - - // Configuration information - sb.append("Item Pattern: ").append(itemPattern.pattern()).append("\n"); - sb.append("Include Noted Items: ").append(includeNoted ? "Yes" : "No").append("\n"); - sb.append("Include Items from Other Players: ").append(includeNoneOwner ? "Yes" : "No").append("\n"); - - // Tracking information - sb.append("Currently Tracking: ").append(itemsByLocation.size()).append(" ground locations\n"); - sb.append("Current Inventory Count: ").append(lastInventoryCount); - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - String itemName = getItemName(); - // Basic information - sb.append("LootItemCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Item: ").append(itemName != null && !itemName.isEmpty() ? itemName : "Any").append("\n"); - - if (itemPattern != null && !itemPattern.pattern().equals(".*")) { - sb.append(" │ Pattern: ").append(itemPattern.pattern()).append("\n"); - } - - sb.append(" │ Target Amount: ").append(currentTargetAmount).append("\n"); - sb.append(" │ Include Noted: ").append(includeNoted ? "Yes" : "No").append("\n"); - sb.append(" │ Track Non-Owned: ").append(includeNoneOwner ? "Yes" : "No").append("\n"); - - // Randomization - sb.append(" ├─ Randomization ────────────────────────────\n"); - boolean hasRandomization = targetAmountMin != targetAmountMax; - sb.append(" │ Randomization: ").append(hasRandomization ? "Enabled" : "Disabled").append("\n"); - if (hasRandomization) { - sb.append(" │ Target Range: ").append(targetAmountMin).append("-").append(targetAmountMax).append("\n"); - } - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - sb.append(" │ Satisfied: ").append(isSatisfied()).append("\n"); - sb.append(" │ Current Count: ").append(currentTrackedCount).append("\n"); - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Tracking info - sb.append(" └─ Tracking ────────────────────────────────\n"); - sb.append(" Ground Locations: ").append(itemsByLocation.size()).append("\n"); - sb.append(" Inventory Count: ").append(lastInventoryCount).append("\n"); - sb.append(" Recent Loots: ").append(recentlyLootedItems.size()); - - return sb.toString(); - } - - @Override - public void reset() { - reset(false); - } - - @Override - public void reset(boolean randomize) { - if (randomize) { - this.currentTargetAmount = Rs2Random.between(targetAmountMin, targetAmountMax); - } - this.currentTrackedCount = 0; - this.lastInventoryCount = getCurrentInventoryCount(); - this.trackedItems.clear(); - this.itemsByLocation.clear(); - this.recentlyLootedItems.clear(); - // Re-scan for items after reset - scanForExistingItems(); - } - - @Override - public double getProgressPercentage() { - if (currentTargetAmount <= 0) { - return 100.0; - } - - double ratio = (double) currentTrackedCount / currentTargetAmount; - return Math.min(100.0, ratio * 100.0); - } - - /** - * Called when an item spawns on the ground - we check if it matches our target item - */ - @Override - public void onItemSpawned(ItemSpawned event) { - // Skip updates if paused - if (isPaused()) { - return; - } - - TileItem tileItem = event.getItem(); - WorldPoint location = event.getTile().getWorldLocation(); - - Client client = Microbot.getClient(); - if (client == null) { - return; - } - - boolean isPlayerOwned = tileItem.getOwnership() == OWNERSHIP_SELF || - tileItem.getOwnership() == OWNERSHIP_GROUP || - (includeNoneOwner && - (tileItem.getOwnership() == OWNERSHIP_NONE || - tileItem.getOwnership() == OWNERSHIP_OTHER)); - - if (isPlayerOwned) { - // Get the item name - String spawnedItemName = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemComposition(tileItem.getId()).getName() - ).orElse(""); - - // Check if this item was likely dropped by the player - boolean playerDropped = isLikelyPlayerDroppedItem(location, System.currentTimeMillis()); - - // Add to pending events - if (!playerDropped || !ignorePlayerDropped) { - if (itemPattern.matcher(spawnedItemName).matches()) { - pendingEvents.add(new ItemTrackingEvent( - System.currentTimeMillis(), - location, - spawnedItemName, - tileItem.getId(), - tileItem.getQuantity(), - isPlayerOwned, - ItemTrackingEvent.EventType.ITEM_SPAWNED - )); - } - } - } - } - - /** - * Called when an item despawns from the ground - check if it was one of our tracked items - */ - @Override - public void onItemDespawned(ItemDespawned event) { - // Skip updates if paused - if (isPaused()) { - return; - } - - WorldPoint location = event.getTile().getWorldLocation(); - TileItem tileItem = event.getItem(); - - // Get the item name - String despawnedItemName = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemComposition(tileItem.getId()).getName() - ).orElse(""); - boolean isPlayerOwned = tileItem.getOwnership() == OWNERSHIP_SELF || - tileItem.getOwnership() == OWNERSHIP_GROUP || - (includeNoneOwner && - (tileItem.getOwnership() == OWNERSHIP_NONE || - tileItem.getOwnership() == OWNERSHIP_OTHER)); - - // Check if we have any tracked items at this location - if (itemsByLocation.containsKey(location)) { - // Find the specific item that despawned - List itemsAtLocation = itemsByLocation.get(location); - - // Look for a matching item at this location - for (TrackedItem trackedItem : itemsAtLocation) { - if (trackedItem.itemId == tileItem.getId() && - itemPattern.matcher(despawnedItemName).matches()) { - - if(Microbot.isDebug()) { - log.info("Item despawned that we were tracking: {} x{} at {}", - despawnedItemName, trackedItem.quantity, location); - } - - pendingEvents.add(new ItemTrackingEvent( - System.currentTimeMillis(), - location, - despawnedItemName, - tileItem.getId(), - trackedItem.quantity, - isPlayerOwned, // We know it's tracked, so it must be player-owned-> it should be playerowned - ItemTrackingEvent.EventType.ITEM_DESPAWNED - )); - - break; // Found the specific item - } - } - } - } - @Override - public void onGroundObjectSpawned(GroundObjectSpawned event) { - // Not used for this implementation - } - - @Override - public void onGroundObjectDespawned(GroundObjectDespawned event) { - // Not used for this implementation - } - - /** - * Called when item containers change (inventory, bank, etc.) - * We need to check if we gained any of our target items. - */ - @Override - public void onItemContainerChanged(ItemContainerChanged event) { - // Skip updates if paused - if (isPaused()) { - return; - } - - // Only interested in inventory changes - if (event.getContainerId() != InventoryID.INVENTORY.getId()) { - return; - } - log.info("Item container changed: {}", event.getContainerId()); - int currentCount = getCurrentInventoryCount(); - - // Add to pending events if inventory count changed - if (currentCount != lastInventoryCount) { - pendingEvents.add(new ItemTrackingEvent( - System.currentTimeMillis(), - null, // No specific location for inventory changes - getItemName(), - -1, // No specific ID for general inventory changes - Math.abs(currentCount - lastInventoryCount), - true, // Always player owned - ItemTrackingEvent.EventType.INVENTORY_CHANGED - )); - - // Update the last count right away to detect further changes - lastInventoryCount = currentCount; - } - } - - /** - * Process all pending tracking events at the end of a game tick - */ - @Override - protected void processPendingEvents() { - // Sort events by timestamp to process in the correct sequence - pendingEvents.sort(Comparator.comparing(e -> e.timestamp)); - - // Track inventory changes and despawned items in this tick - boolean hadInventoryIncrease = false; - int inventoryGained = 0; - int totalDespawnedQuantity = 0; - List despawnedItems = new ArrayList<>(); - - // First pass - collect information - for (ItemTrackingEvent event : pendingEvents) { - switch (event.eventType) { - case ITEM_SPAWNED: - // Track this new item - if (event.isPlayerOwned && itemPattern.matcher(event.itemName).matches()) { - // Create a new tracked item with unique instance ID (timestamp-based) - TrackedItem newItem = new TrackedItem( - event.location, - event.itemId, - event.quantity, - event.itemName - ); - - // Add to our tracked sets - trackedItems.add(newItem); - - // Add to location mapping - itemsByLocation.computeIfAbsent(event.location, k -> new ArrayList<>()) - .add(newItem); - - if (Microbot.isDebug()) { - log.info("Tracking new item: {} x{} at {} (id: {})", - event.itemName, event.quantity, event.location, newItem.timestamp); - } - } - break; - - case ITEM_DESPAWNED: - // Save despawned events for correlation - despawnedItems.add(event); - - // Find and remove the tracked item - if (itemsByLocation.containsKey(event.location)) { - List itemsAtLocation = itemsByLocation.get(event.location); - - // Find first matching item by ID for removal - ONLY REMOVE ONE INSTANCE - boolean itemRemoved = false; - TrackedItem itemToRemove = null; - - // Find the matching item to remove - for (TrackedItem item : itemsAtLocation) { - if (item.itemId == event.itemId && !itemRemoved) { - itemToRemove = item; - totalDespawnedQuantity += item.quantity; - itemRemoved = true; - break; - } - } - - // Remove the specific item - if (itemToRemove != null) { - itemsAtLocation.remove(itemToRemove); - trackedItems.remove(itemToRemove); - - if (Microbot.isDebug()) { - log.info("Item despawned: {} x{} at {} (id: {})", - event.itemName, itemToRemove.quantity, event.location, itemToRemove.timestamp); - } - } - - // Remove the location entry if no more items there - if (itemsAtLocation.isEmpty()) { - itemsByLocation.remove(event.location); - } - - // Record that we just looted from this location - recentlyLootedItems.put(event.location, System.currentTimeMillis()); - } - break; - - case INVENTORY_CHANGED: - // Only consider increases for looting - if (event.quantity > 0) { - hadInventoryIncrease = true; - inventoryGained += event.quantity; - } - break; - } - } - - // Second pass - correlate despawns with inventory increases - if (hadInventoryIncrease && !despawnedItems.isEmpty()) { - // We had both despawns and inventory increases, likely a loot event - if (Microbot.isDebug()) { - log.info("Correlated loot event: gained {} items after {} despawns totaling {} quantity", - inventoryGained, despawnedItems.size(), totalDespawnedQuantity); - } - - // If we have enough evidence that items were looted - if (totalDespawnedQuantity > 0) { - // If inventory gained matches exactly what was despawned, use that number - // Otherwise, use the despawn quantity as it might be more accurate for stacked items - int countToAdd = (inventoryGained == totalDespawnedQuantity) ? - inventoryGained : totalDespawnedQuantity; - - // Count this as looted items - currentTrackedCount += countToAdd; - - if (Microbot.isDebug()) { - log.info("Added {} to tracking count (now {})", countToAdd, currentTrackedCount); - } - } else { - // Fallback to inventory changes if we can't correlate with despawns - currentTrackedCount += inventoryGained; - } - } - - // Clean up old entries from recently looted map (older than 5 seconds) - long now = System.currentTimeMillis(); - recentlyLootedItems.entrySet().removeIf(entry -> now - entry.getValue() > 5000); - - // Clear processed events - pendingEvents.clear(); - } - - /** - * Check inventory every game tick to catch changes that might not trigger ItemContainerChanged - */ - @Override - public void onGameTick(GameTick gameTick) { - // Skip updates if paused - if (isPaused()) { - return; - } - - // Update player position for dropped item tracking - updatePlayerPosition(); - - // Process any pending events - processPendingEvents(); - - // Also do a final inventory check - int currentCount = getCurrentInventoryCount(); - if (currentCount != lastInventoryCount) { - if (currentCount > lastInventoryCount) { - int gained = currentCount - lastInventoryCount; - if (Microbot.isDebug()) { - log.info("Game tick detected uncaught inventory increase of {} {}", gained, getItemName()); - } - - // Count as looted if we're tracking items on the ground - currentTrackedCount += gained; - } - - lastInventoryCount = currentCount; - } - } - - @Override - public void pause() { - super.pause(); - - // Snapshot current state for adjustment on resume - pausedInventoryCount = getCurrentInventoryCount(); - pausedTrackedCount = currentTrackedCount; - - if (Microbot.isDebug()) { - log.info("LootItemCondition paused: inventory={}, tracked={}", - pausedInventoryCount, pausedTrackedCount); - } - } - - @Override - public void resume() { - super.resume(); - - // Adjust tracked count to exclude items gained during pause - int currentInventoryCount = getCurrentInventoryCount(); - int inventoryGainedDuringPause = Math.max(0, currentInventoryCount - pausedInventoryCount); - - // Subtract the gain from our tracked count to exclude pause progress - currentTrackedCount = Math.max(0, pausedTrackedCount - inventoryGainedDuringPause); - - // Update last inventory count to current state - lastInventoryCount = currentInventoryCount; - - if (Microbot.isDebug()) { - log.info("LootItemCondition resumed: adjusted tracked count from {} to {} (excluded {} items gained during pause)", - pausedTrackedCount, currentTrackedCount, inventoryGainedDuringPause); - } - } - - /** - * Gets the current count of this item in the inventory - */ - private int getCurrentInventoryCount() { - - int currentItemCountNoted = getNotedItems().stream().filter(item -> { - if (item == null) { - return false; - } - - return itemPattern.matcher(item.getName()).matches(); - }).mapToInt(Rs2ItemModel::getQuantity).sum(); - - int currentItemCountUnNoted = getUnNotedItems().stream().filter(item -> { - if (item == null) { - return false; - } - return itemPattern.matcher(item.getName()).matches(); - }).mapToInt(Rs2ItemModel::getQuantity).sum(); - - - if (includeNoted) { - return currentItemCountNoted + currentItemCountUnNoted; - } - return currentItemCountUnNoted; - } - @Override - public void onGameStateChanged(GameStateChanged gameStateChanged) { - // Reset the condition if we log out or change worlds - if (gameStateChanged.getGameState() == GameState.LOGGED_IN ){ - scanForExistingItems(); - } - } - /** - * Scans for existing ground items that match our criteria and adds them to tracking - */ - private void scanForExistingItems() { - // Scan a generous range (maximum view distance) - int scanRange = 32; - if (Microbot.getClient() == null || !Microbot.isLoggedIn()) { - return; - } - // Get all items on the ground - RS2Item[] groundItems = Rs2GroundItem.getAll(scanRange); - - if (Microbot.isDebug()) { - log.info("Scanning for existing ground items - found {} total items", groundItems.length); - } - - // Filter and track matching items - for (RS2Item rs2Item : groundItems) { - if (rs2Item == null) continue; - - // Check if this item matches our criteria - boolean isPlayerOwned = rs2Item.getTileItem().getOwnership() == OWNERSHIP_SELF || - rs2Item.getTileItem().getOwnership() == OWNERSHIP_GROUP || - (includeNoneOwner && - (rs2Item.getTileItem().getOwnership() == OWNERSHIP_NONE || - rs2Item.getTileItem().getOwnership() == OWNERSHIP_OTHER)); - - if (isPlayerOwned && itemPattern.matcher(rs2Item.getItem().getName()).matches()) { - WorldPoint location = rs2Item.getTile().getWorldLocation(); - - // Check if this item was likely dropped by the player - boolean playerDropped = isLikelyPlayerDroppedItem(location, System.currentTimeMillis()); - - // Only track if not player-dropped or we're including player-dropped items - if (!playerDropped || !ignorePlayerDropped) { - // Create a tracked item - TrackedItem newItem = new TrackedItem( - location, - rs2Item.getItem().getId(), - rs2Item.getTileItem().getQuantity(), - rs2Item.getItem().getName() - ); - - // Add to our tracking collections - trackedItems.add(newItem); - - // Add to location mapping - itemsByLocation.computeIfAbsent(location, k -> new ArrayList<>()) - .add(newItem); - - if (Microbot.isDebug()) { - log.info("Found existing item to track: {} x{} at {}", - rs2Item.getItem().getName(), - rs2Item.getTileItem().getQuantity(), - location); - } - } - } - } - - if (Microbot.isDebug()) { - log.info("Now tracking {} items at {} locations after initial scan", - trackedItems.size(), itemsByLocation.size()); - } - } - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ProcessItemCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ProcessItemCondition.java deleted file mode 100644 index 45fdef2c16a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ProcessItemCondition.java +++ /dev/null @@ -1,804 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource; - -import lombok.Builder; -import lombok.Data; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.InventoryID; -import net.runelite.api.events.GameTick; -import net.runelite.api.events.ItemContainerChanged; -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.OrCondition; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - - -/** - * Condition that tracks item processing activities (smithing, herblore, crafting, etc.) - * Can track both source items being consumed and target items being produced - */ -@Slf4j -@Getter -public class ProcessItemCondition extends ResourceCondition { - public static String getVersion() { - return "0.0.1"; - } - // Tracking options - private final List sourceItems; // Items being consumed - private final List targetItems; // Items being produced - private final TrackingMode trackingMode; // How we want to track progress - - // Target count configuration - private final int targetCountMin; - private final int targetCountMax; - private final boolean includeBankForPauseResume; // Whether to include bank items when detecting processes during pause/resume - private transient int currentTargetCount; - - // State tracking - private transient Map previousInventoryCounts = new HashMap<>(); - private transient int processedCount = 0; - private transient volatile boolean satisfied = false; - private transient Instant lastInventoryChange = Instant.now(); - private transient boolean isProcessingActive = false; - private transient boolean initialInventoryLoaded = false; - - // Pause/resume state for cumulative tracking - private transient Map pausedInventoryCounts = new HashMap<>(); - private transient int pausedProcessedCount; - public ListgetInputItemPatterns() { - return sourceItems.stream().map(ItemTracker::getItemPattern).collect(Collectors.toList()); - } - public List getInputItemPatternStrings() { - return sourceItems.stream().map( (trakedItem)-> trakedItem.getItemPattern().toString()).collect(Collectors.toList()); - } - public ListgetOutputItemPatterns() { - return targetItems.stream().map(ItemTracker::getItemPattern).collect(Collectors.toList()); - } - public List getOutputItemPatternStrings() { - return targetItems.stream().map( (trakedItem)-> trakedItem.getItemPattern().toString()).collect(Collectors.toList()); - } - /** - * Tracking mode determines what we're counting - */ - public enum TrackingMode { - SOURCE_CONSUMPTION, // Count when source items are consumed - TARGET_PRODUCTION, // Count when target items are produced - EITHER, // Count either source consumption or target production - BOTH // Require both source consumption and target production - } - - /** - * Inner class to track details about items being processed - */ - @Data - public static class ItemTracker { - private final Pattern itemPattern; - private final int quantityPerProcess; // How many of this item are consumed/produced per process - - public ItemTracker(String itemName, int quantityPerProcess) { - this.itemPattern = createItemPattern(itemName); - this.quantityPerProcess = quantityPerProcess; - } - - private static Pattern createItemPattern(String itemName) { - if (itemName == null || itemName.isEmpty()) { - return Pattern.compile(".*"); - } - - // Check if the name is already a regex pattern - if (itemName.startsWith("^") || itemName.endsWith("$") || - itemName.contains(".*") || itemName.contains("[") || - itemName.contains("(")) { - return Pattern.compile(itemName); - } - - // Otherwise, create a contains pattern - return Pattern.compile(".*" + Pattern.quote(itemName) + ".*", Pattern.CASE_INSENSITIVE); - } - public String getItemName() { - // Extract a clean item name from the pattern for display - String patternStr = itemPattern.pattern(); - // If it's a "contains" pattern (created with .*pattern.*) - if (patternStr.startsWith(".*") && patternStr.endsWith(".*")) { - patternStr = patternStr.substring(2, patternStr.length() - 2); - } - // Handle patterns that were created with Pattern.quote() which escapes special characters - if (patternStr.startsWith("\\Q") && patternStr.endsWith("\\E")) { - patternStr = patternStr.substring(2, patternStr.length() - 2); - } - return patternStr; - } - } - - /** - * Full constructor with Builder support - */ - @Builder - public ProcessItemCondition( - List sourceItems, - List targetItems, - TrackingMode trackingMode, - int targetCountMin, - int targetCountMax, - Boolean includeBankForPauseResume) { - super(); - this.sourceItems = sourceItems != null ? sourceItems : new ArrayList<>(); - this.targetItems = targetItems != null ? targetItems : new ArrayList<>(); - this.trackingMode = trackingMode != null ? trackingMode : TrackingMode.EITHER; - this.targetCountMin = Math.max(0, targetCountMin); - this.targetCountMax = Math.max(this.targetCountMin, targetCountMax); - this.includeBankForPauseResume = includeBankForPauseResume != null ? includeBankForPauseResume : true; // Default to true for better accuracy - this.currentTargetCount = Rs2Random.between(this.targetCountMin, this.targetCountMax); - } - - /** - * Create a condition for tracking production of a specific item - */ - public static ProcessItemCondition forProduction(String targetItemName, int count) { - return forProduction(targetItemName, 1, count); - } - - /** - * Create a condition for tracking production of a specific item with quantity per process - */ - public static ProcessItemCondition forProduction(String targetItemName, int quantityPerProcess, int count) { - List targetItems = new ArrayList<>(); - targetItems.add(new ItemTracker(targetItemName, quantityPerProcess)); - - return ProcessItemCondition.builder() - .targetItems(targetItems) - .trackingMode(TrackingMode.TARGET_PRODUCTION) - .targetCountMin(count) - .targetCountMax(count) - .build(); - } - - /** - * Create a condition for tracking consumption of a specific source item - */ - public static ProcessItemCondition forConsumption(String sourceItemName, int count) { - return forConsumption(sourceItemName, 1, count); - } - - /** - * Create a condition for tracking consumption of a specific source item with quantity per process - */ - public static ProcessItemCondition forConsumption(String sourceItemName, int quantityPerProcess, int count) { - List sourceItems = new ArrayList<>(); - sourceItems.add(new ItemTracker(sourceItemName, quantityPerProcess)); - - return ProcessItemCondition.builder() - .sourceItems(sourceItems) - .trackingMode(TrackingMode.SOURCE_CONSUMPTION) - .targetCountMin(count) - .targetCountMax(count) - .build(); - } - - /** - * Create a condition for tracking a complete recipe (source items and target items) - */ - public static ProcessItemCondition forRecipe(String sourceItemName, int sourceQuantity, - String targetItemName, int targetQuantity, int count) { - return forRecipe(sourceItemName, sourceQuantity, targetItemName, targetQuantity, count, true); - } - - /** - * Create a condition for tracking a complete recipe (source items and target items) - * @param includeBankForPauseResume whether to include bank items when detecting processes during pause/resume - */ - public static ProcessItemCondition forRecipe(String sourceItemName, int sourceQuantity, - String targetItemName, int targetQuantity, int count, boolean includeBankForPauseResume) { - List sourceItems = new ArrayList<>(); - sourceItems.add(new ItemTracker(sourceItemName, sourceQuantity)); - - List targetItems = new ArrayList<>(); - targetItems.add(new ItemTracker(targetItemName, targetQuantity)); - - return ProcessItemCondition.builder() - .sourceItems(sourceItems) - .targetItems(targetItems) - .trackingMode(TrackingMode.BOTH) - .targetCountMin(count) - .targetCountMax(count) - .includeBankForPauseResume(includeBankForPauseResume) - .build(); - } - - /** - * Create a condition for tracking multiple source items being consumed - */ - public static ProcessItemCondition forMultipleConsumption( - List sourceItemNames, - List sourceQuantities, - int count) { - List sourceItems = new ArrayList<>(); - for (int i = 0; i < sourceItemNames.size(); i++) { - int quantity = i < sourceQuantities.size() ? sourceQuantities.get(i) : 1; - sourceItems.add(new ItemTracker(sourceItemNames.get(i), quantity)); - } - - return ProcessItemCondition.builder() - .sourceItems(sourceItems) - .trackingMode(TrackingMode.SOURCE_CONSUMPTION) - .targetCountMin(count) - .targetCountMax(count) - .build(); - } - - /** - * Create a condition for tracking multiple target items being produced - */ - public static ProcessItemCondition forMultipleProduction( - List targetItemNames, - List targetQuantities, - int count) { - List targetItems = new ArrayList<>(); - for (int i = 0; i < targetItemNames.size(); i++) { - int quantity = i < targetQuantities.size() ? targetQuantities.get(i) : 1; - targetItems.add(new ItemTracker(targetItemNames.get(i), quantity)); - } - - return ProcessItemCondition.builder() - .targetItems(targetItems) - .trackingMode(TrackingMode.TARGET_PRODUCTION) - .targetCountMin(count) - .targetCountMax(count) - .build(); - } - - /** - * Create a randomized condition for production - */ - public static ProcessItemCondition createRandomizedProduction(String targetItemName, int minCount, int maxCount) { - List targetItems = new ArrayList<>(); - targetItems.add(new ItemTracker(targetItemName, 1)); - - return ProcessItemCondition.builder() - .targetItems(targetItems) - .trackingMode(TrackingMode.TARGET_PRODUCTION) - .targetCountMin(minCount) - .targetCountMax(maxCount) - .build(); - } - - /** - * Create a randomized condition for consumption - */ - public static ProcessItemCondition createRandomizedConsumption(String sourceItemName, int minCount, int maxCount) { - List sourceItems = new ArrayList<>(); - sourceItems.add(new ItemTracker(sourceItemName, 1)); - - return ProcessItemCondition.builder() - .sourceItems(sourceItems) - .trackingMode(TrackingMode.SOURCE_CONSUMPTION) - .targetCountMin(minCount) - .targetCountMax(maxCount) - .build(); - } - - @Override - public boolean isSatisfied() { - // Return false if paused to prevent condition from being satisfied during pause - if (isPaused()) { - return false; - } - - // Once satisfied, stay satisfied until reset - if (satisfied) { - return true; - } - - // Check if processed count meets or exceeds target - if (processedCount >= currentTargetCount) { - satisfied = true; - return true; - } - - return false; - } - - @Override - public String getDescription() { - StringBuilder sb = new StringBuilder(); - - if (trackingMode == TrackingMode.SOURCE_CONSUMPTION || trackingMode == TrackingMode.BOTH) { - sb.append("Process "); - for (int i = 0; i < sourceItems.size(); i++) { - ItemTracker item = sourceItems.get(i); - sb.append(item.getQuantityPerProcess()).append(" ").append(item.getItemName()); - if (i < sourceItems.size() - 1) { - sb.append(", "); - } - } - } - - if (trackingMode == TrackingMode.TARGET_PRODUCTION || trackingMode == TrackingMode.BOTH) { - if (trackingMode == TrackingMode.BOTH) { - sb.append(" into "); - } else { - sb.append("Create "); - } - - for (int i = 0; i < targetItems.size(); i++) { - ItemTracker item = targetItems.get(i); - sb.append(item.getQuantityPerProcess()).append(" ").append(item.getItemName()); - if (i < targetItems.size() - 1) { - sb.append(", "); - } - } - } - - // Add target info - sb.append(": ").append(processedCount).append("/").append(currentTargetCount); - - if (targetCountMin != targetCountMax) { - sb.append(" (randomized from ").append(targetCountMin).append("-").append(targetCountMax).append(")"); - } - - return sb.toString(); - } - - @Override - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - // Basic description - sb.append("Process Item Condition\n"); - - // Source items - if (!sourceItems.isEmpty()) { - sb.append("Source Items: "); - for (int i = 0; i < sourceItems.size(); i++) { - ItemTracker item = sourceItems.get(i); - sb.append(item.getQuantityPerProcess()).append("x ").append(item.getItemName()); - if (i < sourceItems.size() - 1) { - sb.append(", "); - } - } - sb.append("\n"); - } - - // Target items - if (!targetItems.isEmpty()) { - sb.append("Target Items: "); - for (int i = 0; i < targetItems.size(); i++) { - ItemTracker item = targetItems.get(i); - sb.append(item.getQuantityPerProcess()).append("x ").append(item.getItemName()); - if (i < targetItems.size() - 1) { - sb.append(", "); - } - } - sb.append("\n"); - } - - // Tracking mode - sb.append("Tracking Mode: ").append(trackingMode).append("\n"); - - // Status - sb.append("Status: ").append(isSatisfied() ? "Satisfied" : "Not Satisfied").append("\n"); - sb.append("Progress: ").append(processedCount).append("/").append(currentTargetCount) - .append(" (").append(String.format("%.1f%%", getProgressPercentage())).append(")\n"); - - if (targetCountMin != targetCountMax) { - sb.append("Target Range: ").append(targetCountMin).append("-").append(targetCountMax).append("\n"); - } - - // Current state - sb.append("Processing Active: ").append(isProcessingActive ? "Yes" : "No").append("\n"); - - return sb.toString(); - } - - @Override - public void reset() { - reset(false); - } - - @Override - public void reset(boolean randomize) { - if (randomize && targetCountMin != targetCountMax) { - currentTargetCount = Rs2Random.between(targetCountMin, targetCountMax); - } - satisfied = false; - processedCount = 0; - previousInventoryCounts.clear(); - initialInventoryLoaded = false; - } - - @Override - public double getProgressPercentage() { - if (satisfied) { - return 100.0; - } - - if (currentTargetCount <= 0) { - return 100.0; - } - - return Math.min(100.0, (processedCount * 100.0) / currentTargetCount); - } - @Override - public void onItemContainerChanged(ItemContainerChanged event) { - // Skip updates if paused - if (isPaused()) { - return; - } - - // Only process inventory changes - if (event.getContainerId() != InventoryID.INVENTORY.getId()) { - return; - } - - // Don't process changes if bank is open (banking items) - if (Rs2Bank.isOpen()) { - return; - } - - // Update item tracking - updateItemTracking(); - } - - @Override - public void onGameTick(GameTick event) { - // Skip updates if paused - if (isPaused()) { - return; - } - - // Load initial inventory if not yet loaded - if (!initialInventoryLoaded) { - updateItemTracking(); - initialInventoryLoaded = true; - } - } - - /** - * Update the tracking of items and detect processing - */ - private void updateItemTracking() { - Map currentCounts = getCurrentInventoryCounts(); - - // Skip first inventory load - if (previousInventoryCounts.isEmpty()) { - previousInventoryCounts = currentCounts; - return; - } - - // Detect changes in inventory - boolean sourceConsumed = false; - boolean targetProduced = false; - - // Check source items consumption - if (trackingMode == TrackingMode.SOURCE_CONSUMPTION || - trackingMode == TrackingMode.EITHER || - trackingMode == TrackingMode.BOTH) { - - sourceConsumed = detectSourceConsumption(previousInventoryCounts, currentCounts); - } - - // Check target items production - if (trackingMode == TrackingMode.TARGET_PRODUCTION || - trackingMode == TrackingMode.EITHER || - trackingMode == TrackingMode.BOTH) { - - targetProduced = detectTargetProduction(previousInventoryCounts, currentCounts); - } - - // Update processed count based on tracking mode - boolean processDetected = false; - - switch (trackingMode) { - case SOURCE_CONSUMPTION: - processDetected = sourceConsumed; - break; - case TARGET_PRODUCTION: - processDetected = targetProduced; - break; - case EITHER: - processDetected = sourceConsumed || targetProduced; - break; - case BOTH: - processDetected = sourceConsumed && targetProduced; - break; - } - - if (processDetected) { - processedCount++; - isProcessingActive = true; - lastInventoryChange = Instant.now(); - log.debug("Process detected: {} -> {}/{}", - trackingMode, processedCount, currentTargetCount); - } else { - // If no changes detected for 3 seconds, consider processing inactive - if (isProcessingActive && Instant.now().minusSeconds(3).isAfter(lastInventoryChange)) { - isProcessingActive = false; - } - } - - // Update previous counts for next comparison - previousInventoryCounts = currentCounts; - } - - /** - * Detect if source items were consumed by comparing previous and current inventory - */ - private boolean detectSourceConsumption(Map previous, Map current) { - if (sourceItems.isEmpty()) { - return false; - } - - // Check if all source items were reduced by expected amounts - for (ItemTracker sourceItem : sourceItems) { - int prevCount = previous.getOrDefault(sourceItem.getItemName(), 0); - int currCount = current.getOrDefault(sourceItem.getItemName(), 0); - - // Check if the item was consumed by the expected amount - if (currCount >= prevCount || prevCount - currCount != sourceItem.getQuantityPerProcess()) { - return false; - } - } - - return true; - } - - /** - * Detect if target items were produced by comparing previous and current inventory - */ - private boolean detectTargetProduction(Map previous, Map current) { - if (targetItems.isEmpty()) { - return false; - } - - // Check if all target items were increased by expected amounts - for (ItemTracker targetItem : targetItems) { - int prevCount = previous.getOrDefault(targetItem.getItemName(), 0); - int currCount = current.getOrDefault(targetItem.getItemName(), 0); - - // Check if the item was produced by the expected amount - if (currCount <= prevCount || currCount - prevCount != targetItem.getQuantityPerProcess()) { - return false; - } - } - - return true; - } - - /** - * Get current inventory counts for relevant items - */ - private Map getCurrentInventoryCounts() { - return getCurrentItemCounts(false); - } - - /** - * Get current total item counts (inventory + bank) for relevant items - */ - private Map getCurrentTotalItemCounts() { - return getCurrentItemCounts(true); - } - - /** - * Get current item counts for relevant items - * @param includeBank whether to include banked items in the count - */ - private Map getCurrentItemCounts(boolean includeBank) { - Map counts = new HashMap<>(); - - // Get inventory items - List invItems = Rs2Inventory.all(); - - // Get bank items if requested and bank data is available - List bankItems = new ArrayList<>(); - if (includeBank) { - try { - List bankData = Rs2Bank.bankItems(); - if (bankData != null) { - bankItems.addAll(bankData); - } - } catch (Exception e) { - // Bank might not be accessible, continue with inventory only - if (Microbot.isDebug()) { - log.debug("Could not access bank data: {}", e.getMessage()); - } - } - } - - // Count source items - for (ItemTracker sourceItem : sourceItems) { - int invTotal = countItems(invItems, sourceItem.getItemPattern()); - int bankTotal = includeBank ? countItems(bankItems, sourceItem.getItemPattern()) : 0; - counts.put(sourceItem.getItemName(), invTotal + bankTotal); - } - - // Count target items - for (ItemTracker targetItem : targetItems) { - int invTotal = countItems(invItems, targetItem.getItemPattern()); - int bankTotal = includeBank ? countItems(bankItems, targetItem.getItemPattern()) : 0; - counts.put(targetItem.getItemName(), invTotal + bankTotal); - } - - return counts; - } - - /** - * Count items in inventory that match a pattern - */ - private int countItems(List items, Pattern pattern) { - return items.stream() - .filter(item -> pattern.matcher(item.getName()).matches()) - .mapToInt(Rs2ItemModel::getQuantity) - .sum(); - } - - // Factory methods for logical conditions - - /** - * Creates an AND logical condition requiring multiple processing conditions - */ - public static LogicalCondition createAndCondition(List conditions) { - AndCondition andCondition = new AndCondition(); - for (ProcessItemCondition condition : conditions) { - andCondition.addCondition(condition); - } - return andCondition; - } - - /** - * Creates an OR logical condition requiring any of multiple processing conditions - */ - public static LogicalCondition createOrCondition(List conditions) { - OrCondition orCondition = new OrCondition(); - for (ProcessItemCondition condition : conditions) { - orCondition.addCondition(condition); - } - return orCondition; - } - - @Override - public void pause() { - super.pause(); - - // Snapshot current state for adjustment on resume - // Use total counts (inventory + bank) if configured, otherwise inventory only - if (includeBankForPauseResume) { - pausedInventoryCounts = new HashMap<>(getCurrentTotalItemCounts()); - if (Microbot.isDebug()) { - log.info("ProcessItemCondition paused: processed={}, total item counts (inv+bank) captured", pausedProcessedCount); - } - } else { - pausedInventoryCounts = new HashMap<>(getCurrentInventoryCounts()); - if (Microbot.isDebug()) { - log.info("ProcessItemCondition paused: processed={}, inventory counts captured", pausedProcessedCount); - } - } - pausedProcessedCount = processedCount; - } - - @Override - public void resume() { - // Only proceed if actually paused - if (!isPaused()) { - return; - } - - // Get current item counts for comparison (use same method as pause) - Map currentCounts = includeBankForPauseResume ? - getCurrentTotalItemCounts() : getCurrentInventoryCounts(); - - // Calculate how many processes occurred during pause - int processesDetectedDuringPause = 0; - - // For processing conditions, we need to detect actual processing that occurred - // Use the same counting method (inventory vs total) as used during pause - if (!pausedInventoryCounts.isEmpty()) { - // Check if we can detect processing based on our tracking mode - switch (trackingMode) { - case SOURCE_CONSUMPTION: - processesDetectedDuringPause = detectProcessesDuringPauseByConsumption(pausedInventoryCounts, currentCounts); - break; - case TARGET_PRODUCTION: - processesDetectedDuringPause = detectProcessesDuringPauseByProduction(pausedInventoryCounts, currentCounts); - break; - case EITHER: - // Take the maximum of consumption or production detected - int consumptionProcesses = detectProcessesDuringPauseByConsumption(pausedInventoryCounts, currentCounts); - int productionProcesses = detectProcessesDuringPauseByProduction(pausedInventoryCounts, currentCounts); - processesDetectedDuringPause = Math.max(consumptionProcesses, productionProcesses); - break; - case BOTH: - // For BOTH mode, we need to detect the minimum of both (since both are required) - int consumptionDetected = detectProcessesDuringPauseByConsumption(pausedInventoryCounts, currentCounts); - int productionDetected = detectProcessesDuringPauseByProduction(pausedInventoryCounts, currentCounts); - processesDetectedDuringPause = Math.min(consumptionDetected, productionDetected); - break; - } - } - - // Adjust processed count to exclude progress made during pause - processedCount = Math.max(0, pausedProcessedCount - processesDetectedDuringPause); - - // Call parent class resume method - super.resume(); - - // Update baseline inventory counts for future tracking (inventory only for regular processing) - previousInventoryCounts = getCurrentInventoryCounts(); - - if (Microbot.isDebug()) { - String countingMethod = includeBankForPauseResume ? "total counts (inv+bank)" : "inventory counts"; - log.info("ProcessItemCondition resumed: detected {} processes during pause using {}, " + - "adjusted processed count from {} to {}", - processesDetectedDuringPause, countingMethod, pausedProcessedCount, processedCount); - } - } - - /** - * Detect processes during pause by looking at source item consumption - */ - private int detectProcessesDuringPauseByConsumption(Map pausedCounts, Map currentCounts) { - if (sourceItems.isEmpty()) { - return 0; - } - - int minProcesses = Integer.MAX_VALUE; - boolean anyConsumptionDetected = false; - - // Check each source item to see how much was consumed - for (ItemTracker sourceItem : sourceItems) { - String itemName = sourceItem.getItemName(); - int pausedCount = pausedCounts.getOrDefault(itemName, 0); - int currentCount = currentCounts.getOrDefault(itemName, 0); - - if (pausedCount > currentCount) { - // Items were consumed during pause - int consumed = pausedCount - currentCount; - int processes = consumed / sourceItem.getQuantityPerProcess(); - minProcesses = Math.min(minProcesses, processes); - anyConsumptionDetected = true; - } - } - - return anyConsumptionDetected ? minProcesses : 0; - } - - /** - * Detect processes during pause by looking at target item production - */ - private int detectProcessesDuringPauseByProduction(Map pausedCounts, Map currentCounts) { - if (targetItems.isEmpty()) { - return 0; - } - - int minProcesses = Integer.MAX_VALUE; - boolean anyProductionDetected = false; - - // Check each target item to see how much was produced - for (ItemTracker targetItem : targetItems) { - String itemName = targetItem.getItemName(); - int pausedCount = pausedCounts.getOrDefault(itemName, 0); - int currentCount = currentCounts.getOrDefault(itemName, 0); - - if (currentCount > pausedCount) { - // Items were produced during pause - int produced = currentCount - pausedCount; - int processes = produced / targetItem.getQuantityPerProcess(); - minProcesses = Math.min(minProcesses, processes); - anyProductionDetected = true; - } - } - - return anyProductionDetected ? minProcesses : 0; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/README.md b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/README.md deleted file mode 100644 index 7ff356273ad..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/README.md +++ /dev/null @@ -1,30 +0,0 @@ -``` JAVA -// Track making 50 adamant daggers -ProcessItemCondition makeDaggers = ProcessItemCondition.forProduction("adamant dagger", 50); - -// Track using 100 adamant bars (regardless of what they make) -ProcessItemCondition useAdamantBars = ProcessItemCondition.forConsumption("adamant bar", 100); - -// Track making platebodies specifically (which use 5 bars each) -ProcessItemCondition makePlatebodies = ProcessItemCondition.forRecipe( - "adamant bar", 5, // Source: 5 adamant bars - "adamant platebody", 1, // Target: 1 platebody - 20 // Make 20 platebodies -); - -// Track making any herblore potions with ranarr weed -ProcessItemCondition useRanarrs = ProcessItemCondition.forConsumption("ranarr weed", 50); - -// Track making prayer potions specifically -ProcessItemCondition makePrayerPots = ProcessItemCondition.forMultipleConsumption( - Arrays.asList("ranarr potion (unf)", "snape grass"), - Arrays.asList(1, 1), - 50 // Make 50 prayer potions -); - -// Randomized condition - make between 25-35 necklaces of crafting -ProcessItemCondition makeCraftingNecklaces = ProcessItemCondition.createRandomizedProduction( - "necklace of crafting", 25, 35 -); - -``` \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ResourceCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ResourceCondition.java deleted file mode 100644 index a9e9e8ae30b..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ResourceCondition.java +++ /dev/null @@ -1,397 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.ItemComposition; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.GameTick; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; - -import java.time.Duration; -import java.util.List; -import java.util.Optional; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -/** - * Abstract base class for all resource-based conditions. - * Provides common functionality for tracking items, materials and other resources. - */ -@Slf4j -public abstract class ResourceCondition implements Condition { - @Getter - protected final Pattern itemPattern; - - /** - * Queue of item events waiting to be processed at the end of a game tick. - * Events are accumulated during the tick and processed together for efficiency. - */ - protected final List pendingEvents = new ArrayList<>(); - - /** - * Map tracking recently dropped items by the player, keyed by world location. - * Values are timestamps when the items were dropped, used to identify player actions. - */ - protected final Map playerDroppedItems = new HashMap<>(); - - /** - * The player's last known position in the game world. - * Used for determining if items appearing nearby were likely dropped by the player. - */ - protected WorldPoint lastPlayerPosition = null; - - // Pause-related fields - @Getter - protected transient boolean isPaused = false; - public ResourceCondition() { - this.itemPattern = null; - } - public ResourceCondition(String itemPatternString) { - this.itemPattern = createItemPattern(itemPatternString); - } - public final String getItemPatternString() { - return itemPattern == null ? null : itemPattern.pattern().toString(); - } - /** - * Returns the condition type for resource conditions - */ - @Override - public ConditionType getType() { - return ConditionType.RESOURCE; - } - - /** - * Default implementation for detailed description - subclasses should override - */ - @Override - public String getDetailedDescription() { - return "Resource Condition: " + getDescription(); - } - - /** - * Default implementation for calculating progress percentage - * Subclasses should override for more specific calculations - */ - @Override - public double getProgressPercentage() { - return isSatisfied() ? 100.0 : 0.0; - } - - /** - * Gets the estimated time until this resource condition will be satisfied. - * Resource conditions typically cannot provide reliable time estimates since they - * depend on player actions, game events, or unpredictable external factors. - * - * Subclasses may override this method if they can provide meaningful estimates - * (e.g., based on historical data or known resource generation rates). - * - * @return Optional.empty() for most resource conditions, as time cannot be reliably estimated - */ - @Override - public Optional getEstimatedTimeWhenIsSatisfied() { - // If the condition is already satisfied, return zero duration - if (isSatisfied()) { - return Optional.of(Duration.ZERO); - } - - // Resource conditions generally cannot provide reliable time estimates - // since they depend on unpredictable factors like: - // - Player actions and behavior - // - Random game events - // - External market conditions - // - Item drop rates - // - NPC spawning patterns - - return Optional.empty(); - } - - /** - * Checks if an item is in noted form - * @param itemId The item ID to check - * @return true if the item is noted, false otherwise - */ - public static boolean isNoted(int itemId) { - try { - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - ItemComposition itemComposition = Microbot.getItemManager().getItemComposition(itemId); - - int linkedId = itemComposition.getLinkedNoteId(); - if (linkedId <= 0) { - return false; - } - ItemComposition linkedItemComposition = Microbot.getItemManager().getItemComposition(linkedId); - boolean isNoted = itemComposition.getNote() == 799; - - boolean isNoteable = isNoteable(itemId); - - return isNoted && !isNoteable; - }).orElse(null); - } catch (Exception e) { - log.error("Error checking if item is noted, itemId: {}, error: {}", itemId, e.getMessage()); - return false; - } - } - - /** - * Checks if an item can be noted - * @param itemId The item ID to check - * @return true if the item can be noted, false otherwise - */ - public static boolean isNoteable(int itemId) { - if (itemId < 0) { - return false; - } - try { - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - ItemComposition itemComposition = Microbot.getItemManager().getItemComposition(itemId); - int linkedId = itemComposition.getLinkedNoteId(); - if (linkedId <= 0) { - return false; - } - - ItemComposition linkedItemComposition = Microbot.getItemManager().getItemComposition(linkedId); - - boolean unlinkedIsNoted = itemComposition.getNote() == 799; - return !unlinkedIsNoted; - }).orElse(false); - } catch (Exception e) { - log.error("Error checking if item is noteable, itemId: {}, error: {}", itemId, e.getMessage()); - return false; - } - } - - /** - * Gets a list of noted items from the inventory - * @return List of noted item models - */ - public static List getNotedItems() { - return Rs2Inventory.all().stream() - .filter(item -> item.isNoted() && item.isStackable()) - .collect(Collectors.toList()); - } - - /** - * Gets a list of un-noted items from the inventory that could be noted - * @return List of un-noted item models - */ - public static List getUnNotedItems() { - return Rs2Inventory.all().stream() - .filter(item -> (!item.isNoted()) ) - .collect(Collectors.toList()); - } - - /** - * Checks if an item model represents a noted item - * @param notedItem The item model to check - * @return true if the item is noted, false otherwise - */ - public static boolean isNoted(Rs2ItemModel notedItem) { - return notedItem != null && isNoted(notedItem.getId()); - } - - /** - * Checks if an item model represents an un-noted item - * @param item The item model to check - * @return true if the item is un-noted, false otherwise - */ - public static boolean isUnNoted(Rs2ItemModel item) { - if (item == null) { - return false; - } - return !isNoted(item.getId()); - } - - /** - * Normalizes an item name for comparison (lowercase and trim) - * @param name The item name to normalize - * @return The normalized item name - */ - protected String normalizeItemName(String name) { - if (name == null) return ""; - return name.toLowerCase().trim(); - } - - /** - * Creates a pattern for matching item names - * @param itemName The item name pattern to match - * @return A compiled regex pattern for matching the item name - */ - protected Pattern createItemPattern(String itemName) { - if (itemName == null || itemName.isEmpty()) { - return Pattern.compile(".*", Pattern.CASE_INSENSITIVE); - } - - // Check if the name is already a regex pattern - if (itemName.startsWith("^") || itemName.endsWith("$") || - itemName.contains(".*") || itemName.contains("[") || - itemName.contains("(")) { - return Pattern.compile(itemName,Pattern.CASE_INSENSITIVE); - } - - // Otherwise, create a contains pattern - return Pattern.compile(".*" + Pattern.quote(itemName) + ".*", Pattern.CASE_INSENSITIVE); - } - /** - * Extracts a clean, readable item name from the item's regex pattern. - * - * This method processes the pattern string to remove special regex characters: - * - For "contains" patterns (wrapped in ".*"), the wrapping characters are removed - * - For patterns created with Pattern.quote(), the \Q and \E escape sequences are removed - * - * @return A cleaned string representation of the item name suitable for display - */ - public String getItemName() { - // Extract a clean item name from the pattern for display - String patternStr = itemPattern.pattern(); - // If it's a "contains" pattern (created with .*pattern.*) - if (patternStr.startsWith(".*") && patternStr.endsWith(".*")) { - patternStr = patternStr.substring(2, patternStr.length() - 2); - } - // Handle patterns that were created with Pattern.quote() which escapes special characters - if (patternStr.startsWith("\\Q") && patternStr.endsWith("\\E")) { - patternStr = patternStr.substring(2, patternStr.length() - 2); - } - return patternStr; - } - - - /** - * Class for tracking resource-related events and their metadata. - * Used to record when items are spawned, despawned, or inventory changes occur. - */ - protected static class ItemTrackingEvent { - public final long timestamp; - public final WorldPoint location; - public final String itemName; - public final int itemId; - public final int quantity; - public final boolean isPlayerOwned; - public final EventType eventType; - - /** - * Defines the different types of item events that can be tracked. - */ - public enum EventType { - ITEM_SPAWNED, - ITEM_DESPAWNED, - INVENTORY_CHANGED - } - - /** - * Creates a new item tracking event with the specified parameters. - * - * @param timestamp The time when the event occurred - * @param location The world location where the event occurred - * @param itemName The name of the item involved - * @param itemId The ID of the item involved - * @param quantity The quantity of items involved - * @param isPlayerOwned Whether the item is owned by the player - * @param eventType The type of event (spawn, despawn, inventory change) - */ - public ItemTrackingEvent(long timestamp, WorldPoint location, String itemName, - int itemId, int quantity, boolean isPlayerOwned, EventType eventType) { - this.timestamp = timestamp; - this.location = location; - this.itemName = itemName; - this.itemId = itemId; - this.quantity = quantity; - this.isPlayerOwned = isPlayerOwned; - this.eventType = eventType; - } - } - - - - /** - * Determines if an item spawned at a location was likely dropped by the player. - * This is estimated based on proximity to the player's last known position and - * previously tracked player-dropped items. - * - * @param location The world location where the item appeared - * @param timestamp When the item appeared (millisecond timestamp) - * @return true if the item was likely dropped by the player, false otherwise - */ - protected boolean isLikelyPlayerDroppedItem(WorldPoint location, long timestamp) { - // Check if this is near the player's last position - if (lastPlayerPosition != null) { - int distance = location.distanceTo2D(lastPlayerPosition); - // Items dropped by players typically appear at their location or 1 tile away - if (distance <= 1) { - // Track this as a likely player-dropped item - playerDroppedItems.put(location, timestamp); - return true; - } - } - return playerDroppedItems.containsKey(location); - } - - /** - * Processes all pending item tracking events that accumulated during the game tick. - * This method should be called at the end of each game tick to update resource tracking. - * Base implementation only clears events; subclasses should override with specific logic. - */ - protected void processPendingEvents() { - // Default implementation just clears events - // Subclasses should override with specific implementation - pendingEvents.clear(); - - // Clean up old entries from player dropped items map (older than 10 seconds) - long now = System.currentTimeMillis(); - playerDroppedItems.entrySet().removeIf(entry -> now - entry.getValue() > 10000); - } - - /** - * Updates the player's current position for use in dropped item tracking. - * Called each game tick to maintain accurate position information. - */ - protected void updatePlayerPosition() { - if (Microbot.getClient() != null && Microbot.getClient().getLocalPlayer() != null) { - lastPlayerPosition = Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - } - - /** - * Handles the GameTick event from RuneLite's event system. - * Updates player position and processes any accumulated item events. - * - * @param event The game tick event object - */ - @Override - public void onGameTick(GameTick event) { - // Skip processing if paused - if (isPaused) { - return; - } - - // Update player position - updatePlayerPosition(); - - // Process any pending events - processPendingEvents(); - } - - @Override - public void pause() { - if (!isPaused) { - isPaused = true; - log.debug("Resource condition paused for item pattern: {}", - itemPattern != null ? itemPattern.pattern() : "any"); - } - } - - @Override - public void resume() { - if (isPaused) { - isPaused = false; - log.debug("Resource condition resumed for item pattern: {}", - itemPattern != null ? itemPattern.pattern() : "any"); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/BankItemCountConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/BankItemCountConditionAdapter.java deleted file mode 100644 index c7a7b7a6791..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/BankItemCountConditionAdapter.java +++ /dev/null @@ -1,76 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.BankItemCountCondition; - -import java.lang.reflect.Type; - -/** - * Adapter for handling serialization and deserialization of BankItemCountCondition objects. - */ -@Slf4j -public class BankItemCountConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(BankItemCountCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Add type information - result.addProperty("type", BankItemCountCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Add version information - data.addProperty("version", BankItemCountCondition.getVersion()); - - // Add specific properties for BankItemCountCondition - data.addProperty("itemName", src.getItemName()); - data.addProperty("targetCountMin", src.getTargetCountMin()); - data.addProperty("targetCountMax", src.getTargetCountMax()); - // Add data to wrapper - result.add("data", data); - - return result; - } - - @Override - public BankItemCountCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(BankItemCountCondition.getVersion())) { - - throw new JsonParseException("Version mismatch in BankItemCountCondition: expected " + - BankItemCountCondition.getVersion() + ", got " + version); - } - } - - // Extract basic properties - String itemName = dataObj.has("itemName") ? dataObj.get("itemName").getAsString() : ""; - int targetCountMin = dataObj.has("targetCountMin") ? dataObj.get("targetCountMin").getAsInt() : 1; - int targetCountMax = dataObj.has("targetCountMax") ? dataObj.get("targetCountMax").getAsInt() : targetCountMin; - - // Create the condition - return BankItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .build(); - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/GatheredResourceConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/GatheredResourceConditionAdapter.java deleted file mode 100644 index b9d794c5bd5..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/GatheredResourceConditionAdapter.java +++ /dev/null @@ -1,106 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Skill; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.GatheredResourceCondition; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; - -/** - * Adapter for handling serialization and deserialization of GatheredResourceCondition objects. - */ -@Slf4j -public class GatheredResourceConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(GatheredResourceCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Add type information - result.addProperty("type", GatheredResourceCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Add version information - data.addProperty("version", GatheredResourceCondition.getVersion()); - - // Add specific properties for GatheredResourceCondition - data.addProperty("itemName", src.getItemName()); - data.addProperty("targetCountMin", src.getTargetCountMin()); - data.addProperty("targetCountMax", src.getTargetCountMax()); - data.addProperty("includeNoted", src.isIncludeNoted()); - - // Add relevant skills array - JsonArray skillsArray = new JsonArray(); - for (Skill skill : src.getRelevantSkills()) { - skillsArray.add(skill.name()); - } - data.add("relevantSkills", skillsArray); - - // Add data to wrapper - result.add("data", data); - - return result; - } - - @Override - public GatheredResourceCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(GatheredResourceCondition.getVersion())) { - log.warn("Version mismatch in GatheredResourceCondition: expected {}, got {}", - GatheredResourceCondition.getVersion(), version); - throw new JsonParseException("Version mismatch in GatheredResourceCondition: expected " + - GatheredResourceCondition.getVersion() + ", got " + version); - } - } - - // Extract basic properties - String itemName = dataObj.has("itemName") ? dataObj.get("itemName").getAsString() : ""; - int targetCountMin = dataObj.has("targetCountMin") ? dataObj.get("targetCountMin").getAsInt() : 1; - int targetCountMax = dataObj.has("targetCountMax") ? dataObj.get("targetCountMax").getAsInt() : targetCountMin; - boolean includeNoted = dataObj.has("includeNoted") && dataObj.get("includeNoted").getAsBoolean(); - - // Extract relevant skills if present - List relevantSkills = new ArrayList<>(); - if (dataObj.has("relevantSkills")) { - JsonArray skillsArray = dataObj.getAsJsonArray("relevantSkills"); - for (JsonElement skillElement : skillsArray) { - try { - Skill skill = Skill.valueOf(skillElement.getAsString()); - relevantSkills.add(skill); - } catch (IllegalArgumentException e) { - log.warn("Unknown skill: {}", skillElement.getAsString()); - } - } - } - - // Create the condition - return GatheredResourceCondition.builder() - .itemName(itemName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .includeNoted(includeNoted) - .relevantSkills(relevantSkills.isEmpty() ? null : relevantSkills) - .build(); - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/InventoryItemCountConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/InventoryItemCountConditionAdapter.java deleted file mode 100644 index da6e8abed61..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/InventoryItemCountConditionAdapter.java +++ /dev/null @@ -1,90 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.InventoryItemCountCondition; - -import java.lang.reflect.Type; - -/** - * Adapter for handling serialization and deserialization of InventoryItemCountCondition objects. - */ -@Slf4j -public class InventoryItemCountConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(InventoryItemCountCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Add type information - result.addProperty("type", InventoryItemCountCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Add version information - data.addProperty("version", InventoryItemCountCondition.getVersion()); - - // Add specific properties for InventoryItemCountCondition - data.addProperty("itemName", src.getItemName()); - data.addProperty("targetCountMin", src.getTargetCountMin()); - data.addProperty("targetCountMax", src.getTargetCountMax()); - data.addProperty("includeNoted", src.isIncludeNoted()); - data.addProperty("currentTargetCount", src.getCurrentTargetCount()); - data.addProperty("currentItemCount", src.getCurrentItemCount()); - - // Add data to wrapper - result.add("data", data); - - return result; - } - - @Override - public InventoryItemCountCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - try { - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(InventoryItemCountCondition.getVersion())) { - log.warn("Version mismatch in InventoryItemCountCondition: expected {}, got {}", - InventoryItemCountCondition.getVersion(), version); - } - } - - // Extract basic properties - String itemName = dataObj.has("itemName") ? dataObj.get("itemName").getAsString() : ""; - int targetCountMin = dataObj.has("targetCountMin") ? dataObj.get("targetCountMin").getAsInt() : 1; - int targetCountMax = dataObj.has("targetCountMax") ? dataObj.get("targetCountMax").getAsInt() : targetCountMin; - boolean includeNoted = dataObj.has("includeNoted") && dataObj.get("includeNoted").getAsBoolean(); - - // Create the condition - return InventoryItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .includeNoted(includeNoted) - .build(); - } catch (Exception e) { - log.error("Error deserializing InventoryItemCountCondition", e); - // Return a default condition on error - return InventoryItemCountCondition.builder() - .itemName("Unknown") - .targetCountMin(1) - .targetCountMax(1) - .includeNoted(false) - .build(); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/LootItemConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/LootItemConditionAdapter.java deleted file mode 100644 index bfc30d622a4..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/LootItemConditionAdapter.java +++ /dev/null @@ -1,84 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.LootItemCondition; - -import java.lang.reflect.Type; - -/** - * Adapter for handling serialization and deserialization of LootItemCondition objects. - */ -@Slf4j -public class LootItemConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(LootItemCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Add type information - result.addProperty("type", LootItemCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Add version information - data.addProperty("version", LootItemCondition.getVersion()); - - // Add specific properties for LootItemCondition - data.addProperty("itemName", src.getItemName()); - data.addProperty("targetAmountMin", src.getTargetAmountMin()); - data.addProperty("targetAmountMax", src.getTargetAmountMax()); - data.addProperty("includeNoneOwner", src.isIncludeNoneOwner()); - data.addProperty("includeNoted", src.isIncludeNoted()); - - // Add data to wrapper - result.add("data", data); - - return result; - } - - @Override - public LootItemCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(LootItemCondition.getVersion())) { - log.warn("Version mismatch in LootItemCondition: expected {}, got {}", - LootItemCondition.getVersion(), version); - throw new JsonParseException("Version mismatch in LootItemCondition: expected " + - LootItemCondition.getVersion() + ", got " + version); - } - } - - // Extract basic properties - String itemName = dataObj.has("itemName") ? dataObj.get("itemName").getAsString() : ""; - int targetAmountMin = dataObj.has("targetAmountMin") ? dataObj.get("targetAmountMin").getAsInt() : 1; - int targetAmountMax = dataObj.has("targetAmountMax") ? dataObj.get("targetAmountMax").getAsInt() : targetAmountMin; - boolean includeNoted = dataObj.has("includeNoted") && dataObj.get("includeNoted").getAsBoolean(); - boolean includeNoneOwner = dataObj.has("includeNoneOwner") && dataObj.get("includeNoneOwner").getAsBoolean(); - - // Create the condition - return LootItemCondition.builder() - .itemName(itemName) - .targetAmountMin(targetAmountMin) - .targetAmountMax(targetAmountMax) - .includeNoted(includeNoted) - .includeNoneOwner(includeNoneOwner) - .build(); - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/ProcessItemConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/ProcessItemConditionAdapter.java deleted file mode 100644 index 39ebdacb977..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/ProcessItemConditionAdapter.java +++ /dev/null @@ -1,147 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ProcessItemCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ProcessItemCondition.ItemTracker; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ProcessItemCondition.TrackingMode; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; - -/** - * Adapter for handling serialization and deserialization of ProcessItemCondition objects. - */ -@Slf4j -public class ProcessItemConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ProcessItemCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Add type information - result.addProperty("type", ProcessItemCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Add version information - data.addProperty("version", ProcessItemCondition.getVersion()); - - // Add specific properties for ProcessItemCondition - data.addProperty("targetCountMin", src.getTargetCountMin()); - data.addProperty("targetCountMax", src.getTargetCountMax()); - data.addProperty("trackingMode", src.getTrackingMode().name()); - - // Serialize sourceItems - if (src.getSourceItems() != null && !src.getSourceItems().isEmpty()) { - JsonArray sourceItemsArray = new JsonArray(); - for (ItemTracker item : src.getSourceItems()) { - JsonObject itemObj = new JsonObject(); - itemObj.addProperty("patternString", item.getItemPattern().pattern()); - itemObj.addProperty("quantityPerProcess", item.getQuantityPerProcess()); - sourceItemsArray.add(itemObj); - } - data.add("sourceItems", sourceItemsArray); - } - - // Serialize targetItems - if (src.getTargetItems() != null && !src.getTargetItems().isEmpty()) { - JsonArray targetItemsArray = new JsonArray(); - for (ItemTracker item : src.getTargetItems()) { - JsonObject itemObj = new JsonObject(); - itemObj.addProperty("patternString", item.getItemPattern().pattern()); - itemObj.addProperty("quantityPerProcess", item.getQuantityPerProcess()); - targetItemsArray.add(itemObj); - } - data.add("targetItems", targetItemsArray); - } - - // Add data to wrapper - result.add("data", data); - - return result; - } - - @Override - public ProcessItemCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(ProcessItemCondition.getVersion())) { - throw new JsonParseException("Version mismatch in ProcessItemCondition: expected " + - ProcessItemCondition.getVersion() + ", got " + version); - } - } - - // Extract basic properties - int targetCountMin = dataObj.has("targetCountMin") ? dataObj.get("targetCountMin").getAsInt() : 1; - int targetCountMax = dataObj.has("targetCountMax") ? dataObj.get("targetCountMax").getAsInt() : targetCountMin; - - // Deserialize tracking mode - TrackingMode trackingMode = TrackingMode.EITHER; // Default - if (dataObj.has("trackingMode")) { - try { - trackingMode = TrackingMode.valueOf(dataObj.get("trackingMode").getAsString()); - } catch (IllegalArgumentException e) { - log.warn("Invalid tracking mode: {}", dataObj.get("trackingMode").getAsString()); - } - } - - // Deserialize sourceItems - List sourceItems = new ArrayList<>(); - if (dataObj.has("sourceItems")) { - JsonArray sourceItemsArray = dataObj.getAsJsonArray("sourceItems"); - for (JsonElement element : sourceItemsArray) { - JsonObject itemObj = element.getAsJsonObject(); - String patternString = itemObj.get("patternString").getAsString(); - int quantity = itemObj.get("quantityPerProcess").getAsInt(); - - // Create ItemTracker directly since its constructor needs pattern - ItemTracker tracker = new ItemTracker(patternString, quantity); - sourceItems.add(tracker); - } - } - - // Deserialize targetItems - List targetItems = new ArrayList<>(); - if (dataObj.has("targetItems")) { - JsonArray targetItemsArray = dataObj.getAsJsonArray("targetItems"); - for (JsonElement element : targetItemsArray) { - JsonObject itemObj = element.getAsJsonObject(); - String patternString = itemObj.get("patternString").getAsString(); - int quantity = itemObj.get("quantityPerProcess").getAsInt(); - - // Create ItemTracker directly since its constructor needs pattern - ItemTracker tracker = new ItemTracker(patternString, quantity); - targetItems.add(tracker); - } - } - - // Create the condition - return ProcessItemCondition.builder() - .sourceItems(sourceItems) - .targetItems(targetItems) - .trackingMode(trackingMode) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .build(); - - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/ResourceConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/ResourceConditionAdapter.java deleted file mode 100644 index 0e65cf23d03..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/ResourceConditionAdapter.java +++ /dev/null @@ -1,66 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ResourceCondition; - -import java.lang.reflect.Type; -import java.util.regex.Pattern; - -/** - * Adapter for handling serialization and deserialization of ResourceCondition objects. - */ -@Slf4j -public class ResourceConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ResourceCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Add type information - result.addProperty("type", src.getClass().getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Add version information - use specific version if available, or default - try { - String version = (String) src.getClass().getMethod("getVersion").invoke(null); - data.addProperty("version", version); - } catch (Exception e) { - data.addProperty("version", "0.0.1"); - log.debug("Could not get version for {}, using default", src.getClass().getName()); - } - - // Add itemName pattern - a common property for all resource conditions - data.addProperty("itemName", src.getItemName()); - - // Add data to wrapper - result.add("data", data); - - return result; - } - - @Override - public ResourceCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - // This base adapter doesn't handle deserialization directly - // It's expected that specific subclass adapters will handle their own types - throw new JsonParseException("Cannot deserialize abstract ResourceCondition directly"); - } - - /** - * Helper method to extract a Pattern object from JSON - */ - protected Pattern deserializePattern(JsonObject jsonObject, String fieldName) { - if (jsonObject.has(fieldName)) { - String patternStr = jsonObject.get(fieldName).getAsString(); - try { - return Pattern.compile(patternStr); - } catch (Exception e) { - log.warn("Failed to parse pattern: {}", patternStr, e); - } - } - return null; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ui/ResourceConditionPanelUtil.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ui/ResourceConditionPanelUtil.java deleted file mode 100644 index 62052d30cfc..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ui/ResourceConditionPanelUtil.java +++ /dev/null @@ -1,1405 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ui; - -import java.util.ArrayList; -import java.util.List; - -import javax.swing.BorderFactory; -import javax.swing.ButtonGroup; -import javax.swing.DefaultListModel; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JPanel; -import javax.swing.JRadioButton; -import javax.swing.JScrollPane; -import javax.swing.JSpinner; -import javax.swing.JTextField; -import javax.swing.SpinnerNumberModel; -import javax.swing.border.TitledBorder; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Skill; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.BankItemCountCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.GatheredResourceCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.InventoryItemCountCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.LootItemCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ProcessItemCondition; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Dimension; -import java.awt.FlowLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridLayout; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LogicalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.OrCondition; - - - - -@Slf4j -public class ResourceConditionPanelUtil { - /** - * Creates a panel for configuring Inventory Item Count conditions - */ - public static void createInventoryItemCountPanel(JPanel panel, GridBagConstraints gbc) { - // Title - JLabel titleLabel = new JLabel("Inventory Item Count:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // Item name input - gbc.gridy++; - JPanel itemNamePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - itemNamePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel itemNameLabel = new JLabel("Item Name (leave empty for any):"); - itemNameLabel.setForeground(Color.WHITE); - itemNamePanel.add(itemNameLabel); - - JTextField itemNameField = new JTextField(15); - itemNameField.setForeground(Color.WHITE); - itemNameField.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - itemNamePanel.add(itemNameField); - - panel.add(itemNamePanel, gbc); - - // Count range - gbc.gridy++; - JPanel countPanel = new JPanel(new GridLayout(2, 2, 5, 5)); - countPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel minCountLabel = new JLabel("Min Count:"); - minCountLabel.setForeground(Color.WHITE); - countPanel.add(minCountLabel); - - SpinnerNumberModel minCountModel = new SpinnerNumberModel(10, 0, Integer.MAX_VALUE, 1); - JSpinner minCountSpinner = new JSpinner(minCountModel); - countPanel.add(minCountSpinner); - - JLabel maxCountLabel = new JLabel("Max Count:"); - maxCountLabel.setForeground(Color.WHITE); - countPanel.add(maxCountLabel); - - SpinnerNumberModel maxCountModel = new SpinnerNumberModel(10, 0, Integer.MAX_VALUE, 1); - JSpinner maxCountSpinner = new JSpinner(maxCountModel); - countPanel.add(maxCountSpinner); - - // Link the min and max spinners - minCountSpinner.addChangeListener(e -> { - int minValue = (Integer) minCountSpinner.getValue(); - int maxValue = (Integer) maxCountSpinner.getValue(); - if (minValue > maxValue) { - maxCountSpinner.setValue(minValue); - } - }); - - maxCountSpinner.addChangeListener(e -> { - int minValue = (Integer) minCountSpinner.getValue(); - int maxValue = (Integer) maxCountSpinner.getValue(); - if (maxValue < minValue) { - minCountSpinner.setValue(maxValue); - } - }); - - panel.add(countPanel, gbc); - - // Options panel - gbc.gridy++; - JPanel optionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - optionsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JCheckBox includeNotedCheckbox = new JCheckBox("Include noted items"); - includeNotedCheckbox.setForeground(Color.WHITE); - includeNotedCheckbox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - includeNotedCheckbox.setSelected(true); - optionsPanel.add(includeNotedCheckbox); - - - - panel.add(optionsPanel, gbc); - - // Add a helpful description - gbc.gridy++; - JLabel descriptionLabel = new JLabel("Plugin will stop when you have the target number of items"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - gbc.gridy++; - JLabel regexLabel = new JLabel("Item name supports regex patterns (.*bones.*)"); - regexLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - regexLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(regexLabel, gbc); - - // Store the components for later access - panel.putClientProperty("itemNameField", itemNameField); - panel.putClientProperty("minCountSpinner", minCountSpinner); - panel.putClientProperty("maxCountSpinner", maxCountSpinner); - panel.putClientProperty("includeNotedCheckbox", includeNotedCheckbox); - } - public static InventoryItemCountCondition createInventoryItemCountCondition(JPanel configPanel) { - JTextField itemNameField = (JTextField) configPanel.getClientProperty("itemNameField"); - JSpinner minCountSpinner = (JSpinner) configPanel.getClientProperty("minCountSpinner"); - JSpinner maxCountSpinner = (JSpinner) configPanel.getClientProperty("maxCountSpinner"); - JCheckBox includeNotedCheckbox = (JCheckBox) configPanel.getClientProperty("includeNotedCheckbox"); - - - String itemName = itemNameField.getText().trim(); - int minCount = (Integer) minCountSpinner.getValue(); - int maxCount = (Integer) maxCountSpinner.getValue(); - boolean includeNoted = includeNotedCheckbox.isSelected(); - - if (minCount != maxCount) { - return InventoryItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(minCount) - .targetCountMax(maxCount) - .includeNoted(includeNoted) - .build(); - } else { - return InventoryItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(minCount) - .targetCountMax(minCount) - .includeNoted(includeNoted) - .build(); - } - } - /** - * Creates a panel for configuring Bank Item Count conditions - */ - public static void createBankItemCountPanel(JPanel panel, GridBagConstraints gbc, JPanel configPanel) { - // Title - JLabel titleLabel = new JLabel("Bank Item Count:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // Item name input - gbc.gridy++; - JPanel itemNamePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - itemNamePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel itemNameLabel = new JLabel("Item Name (leave empty for total items):"); - itemNameLabel.setForeground(Color.WHITE); - itemNamePanel.add(itemNameLabel); - - JTextField itemNameField = new JTextField(15); - itemNameField.setForeground(Color.WHITE); - itemNameField.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - itemNamePanel.add(itemNameField); - - panel.add(itemNamePanel, gbc); - - // Count range - gbc.gridy++; - JPanel countPanel = new JPanel(new GridLayout(2, 2, 5, 5)); - countPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel minCountLabel = new JLabel("Min Count:"); - minCountLabel.setForeground(Color.WHITE); - countPanel.add(minCountLabel); - - SpinnerNumberModel minCountModel = new SpinnerNumberModel(100, 0, Integer.MAX_VALUE, 10); - JSpinner minCountSpinner = new JSpinner(minCountModel); - countPanel.add(minCountSpinner); - - JLabel maxCountLabel = new JLabel("Max Count:"); - maxCountLabel.setForeground(Color.WHITE); - countPanel.add(maxCountLabel); - - SpinnerNumberModel maxCountModel = new SpinnerNumberModel(100, 0, Integer.MAX_VALUE, 10); - JSpinner maxCountSpinner = new JSpinner(maxCountModel); - countPanel.add(maxCountSpinner); - - // Link the min and max spinners - minCountSpinner.addChangeListener(e -> { - int minValue = (Integer) minCountSpinner.getValue(); - int maxValue = (Integer) maxCountSpinner.getValue(); - if (minValue > maxValue) { - maxCountSpinner.setValue(minValue); - } - }); - - maxCountSpinner.addChangeListener(e -> { - int minValue = (Integer) minCountSpinner.getValue(); - int maxValue = (Integer) maxCountSpinner.getValue(); - if (maxValue < minValue) { - minCountSpinner.setValue(maxValue); - } - }); - - panel.add(countPanel, gbc); - - // Options panel - gbc.gridy++; - JPanel optionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - optionsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - - - panel.add(optionsPanel, gbc); - - // Add a helpful description - gbc.gridy++; - JLabel descriptionLabel = new JLabel("Plugin will stop when you have the target number of items in bank"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - gbc.gridy++; - JLabel regexLabel = new JLabel("Item name supports regex patterns (.*rune.*)"); - regexLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - regexLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(regexLabel, gbc); - - // Store the components for later access - configPanel.putClientProperty("bankItemNameField", itemNameField); - configPanel.putClientProperty("bankMinCountSpinner", minCountSpinner); - configPanel.putClientProperty("bankMaxCountSpinner", maxCountSpinner); - } - - - - public static BankItemCountCondition createBankItemCountCondition(JPanel configPanel) { - JTextField itemNameField = (JTextField) configPanel.getClientProperty("bankItemNameField"); - JSpinner minCountSpinner = (JSpinner) configPanel.getClientProperty("bankMinCountSpinner"); - JSpinner maxCountSpinner = (JSpinner) configPanel.getClientProperty("bankMaxCountSpinner"); - - - String itemName = itemNameField.getText().trim(); - int minCount = (Integer) minCountSpinner.getValue(); - int maxCount = (Integer) maxCountSpinner.getValue(); - - if (minCount != maxCount) { - return BankItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(minCount) - .targetCountMax(maxCount) - .build(); - } else { - return BankItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(minCount) - .targetCountMax(minCount) - .build(); - } - } - /** - * Creates a panel for configuring item collection conditions with enhanced options. - */ - public static void createItemConfigPanel(JPanel panel, GridBagConstraints gbc, JPanel configPanel, boolean stopConditionPanel) { - // Section title - JLabel titleLabel = new JLabel(stopConditionPanel ? "Collect Items to Stop:" : "Required Items to Start:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - panel.add(titleLabel, gbc); - - gbc.gridy++; - - // Item names input - JLabel itemsLabel = new JLabel("Item Names (comma-separated):"); - itemsLabel.setForeground(Color.WHITE); - panel.add(itemsLabel, gbc); - - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - JTextField itemsField = new JTextField(); - itemsField.setToolTipText("Item names are automatically detected as exact matches or regex patterns:
" + - "- Simple names: 'Dragon scimitar', 'Bones', 'Shark' → exact match
" + - "- Pattern names: 'Dragon.*', '^Rune.*sword$', '.*bones.*' → regex match
" + - "- Multiple items: 'Bones, Dragon.*' → mixed exact and regex matching"); - panel.add(itemsField, gbc); - - // Matching mode information - gbc.gridy++; - JPanel matchingPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - matchingPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel matchingInfoLabel = new JLabel("Item name matching: Automatic detection (exact names or regex patterns)"); - matchingInfoLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - matchingInfoLabel.setFont(FontManager.getRunescapeSmallFont()); - matchingInfoLabel.setToolTipText("Item names are automatically detected as either exact matches or regex patterns.
" + - "Simple names like 'Dragon scimitar' are matched exactly.
" + - "Patterns like 'Dragon.*' or '^Rune.*sword$' are treated as regex."); - matchingPanel.add(matchingInfoLabel); - - panel.add(matchingPanel, gbc); - - // Logical operator selection (AND/OR) - only visible with multiple items - gbc.gridy++; - JPanel logicalPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - logicalPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel logicalLabel = new JLabel("Multiple items logic:"); - logicalLabel.setForeground(Color.WHITE); - logicalPanel.add(logicalLabel); - - JRadioButton andRadioButton = new JRadioButton("Need ALL items (AND)"); - andRadioButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - andRadioButton.setForeground(Color.WHITE); - andRadioButton.setSelected(true); - andRadioButton.setToolTipText("All specified items must be collected to satisfy the condition"); - - JRadioButton orRadioButton = new JRadioButton("Need ANY item (OR)"); - orRadioButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - orRadioButton.setForeground(Color.WHITE); - orRadioButton.setToolTipText("Any of the specified items will satisfy the condition"); - - ButtonGroup logicGroup = new ButtonGroup(); - logicGroup.add(andRadioButton); - logicGroup.add(orRadioButton); - - logicalPanel.add(andRadioButton); - logicalPanel.add(orRadioButton); - - // Initially hide the panel - will be shown only when there are commas in the text field - logicalPanel.setVisible(false); - panel.add(logicalPanel, gbc); - - // Listen for changes in the text field to show/hide the logical panel - itemsField.getDocument().addDocumentListener(new DocumentListener() { - private void updateLogicalPanelVisibility() { - String text = itemsField.getText().trim(); - boolean hasMultipleItems = text.contains(","); - logicalPanel.setVisible(hasMultipleItems); - panel.revalidate(); - panel.repaint(); - } - - @Override - public void insertUpdate(DocumentEvent e) { - updateLogicalPanelVisibility(); - } - - @Override - public void removeUpdate(DocumentEvent e) { - updateLogicalPanelVisibility(); - } - - @Override - public void changedUpdate(DocumentEvent e) { - updateLogicalPanelVisibility(); - } - }); - - // Target amount panel - gbc.gridy++; - JPanel amountPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - amountPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel amountLabel = new JLabel("Target Amount:"); - amountLabel.setForeground(Color.WHITE); - amountPanel.add(amountLabel); - - JCheckBox sameAmountCheckBox = new JCheckBox("Same amount for all items"); - sameAmountCheckBox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - sameAmountCheckBox.setForeground(Color.WHITE); - sameAmountCheckBox.setSelected(true); - sameAmountCheckBox.setVisible(false); // Only show with multiple items - sameAmountCheckBox.setToolTipText("Use the same target amount for all items"); - amountPanel.add(sameAmountCheckBox); - - panel.add(amountPanel, gbc); - - // Amount configuration panel (always visible) - gbc.gridy++; - JPanel amountConfigPanel = new JPanel(new GridLayout(1, 4, 5, 0)); - amountConfigPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel minLabel = new JLabel("Min:"); - minLabel.setForeground(Color.WHITE); - amountConfigPanel.add(minLabel); - - SpinnerNumberModel minModel = new SpinnerNumberModel(1, 1, Integer.MAX_VALUE, 1); - JSpinner minSpinner = new JSpinner(minModel); - amountConfigPanel.add(minSpinner); - - JLabel maxLabel = new JLabel("Max:"); - maxLabel.setForeground(Color.WHITE); - amountConfigPanel.add(maxLabel); - - SpinnerNumberModel maxModel = new SpinnerNumberModel(1, 1, Integer.MAX_VALUE, 1); - JSpinner maxSpinner = new JSpinner(maxModel); - amountConfigPanel.add(maxSpinner); - - panel.add(amountConfigPanel, gbc); - - // Additional options panel - gbc.gridy++; - JPanel optionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - optionsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JCheckBox includeNotedCheckBox = new JCheckBox("Include noted items"); - includeNotedCheckBox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - includeNotedCheckBox.setForeground(Color.WHITE); - includeNotedCheckBox.setSelected(true); - includeNotedCheckBox.setToolTipText("If checked, will also count noted versions of the items
" + - "For example, 'Bones' would match both normal and noted bones"); - optionsPanel.add(includeNotedCheckBox); - - JCheckBox includeNoneOwnerCheckBox = new JCheckBox("Include unowned items"); - includeNoneOwnerCheckBox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - includeNoneOwnerCheckBox.setForeground(Color.WHITE); - includeNoneOwnerCheckBox.setToolTipText("If checked, items that don't belong to you will also be tracked
" + - "By default, only items that belong to you are counted"); - optionsPanel.add(includeNoneOwnerCheckBox); - - panel.add(optionsPanel, gbc); - - - - - // Link sameAmountCheckBox visibility to comma presence - itemsField.getDocument().addDocumentListener(new DocumentListener() { - private void updateCheckBoxVisibility() { - String text = itemsField.getText().trim(); - boolean hasMultipleItems = text.contains(","); - sameAmountCheckBox.setVisible(hasMultipleItems); - panel.revalidate(); - panel.repaint(); - } - - @Override - public void insertUpdate(DocumentEvent e) { - updateCheckBoxVisibility(); - } - - @Override - public void removeUpdate(DocumentEvent e) { - updateCheckBoxVisibility(); - } - - @Override - public void changedUpdate(DocumentEvent e) { - updateCheckBoxVisibility(); - } - }); - - // Add value change listeners for min/max validation - minSpinner.addChangeListener(e -> { - int min = (Integer) minSpinner.getValue(); - int max = (Integer) maxSpinner.getValue(); - - if (min > max) { - maxSpinner.setValue(min); - } - }); - - maxSpinner.addChangeListener(e -> { - int min = (Integer) minSpinner.getValue(); - int max = (Integer) maxSpinner.getValue(); - - if (max < min) { - minSpinner.setValue(max); - } - }); - - // Description - gbc.gridy++; - JLabel descriptionLabel; - if (stopConditionPanel) { - descriptionLabel = new JLabel("Plugin will stop when target amount of items is collected"); - } else { - descriptionLabel = new JLabel("Plugin will only start when you have the required items"); - } - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Add help text - gbc.gridy++; - JLabel helpLabel = new JLabel("Tip: Use simple names like 'Dragon scimitar' or regex patterns like 'Dragon.*'"); - helpLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - helpLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(helpLabel, gbc); - - // Store components - configPanel.putClientProperty("itemsField", itemsField); - configPanel.putClientProperty("andRadioButton", andRadioButton); - configPanel.putClientProperty("sameAmountCheckBox", sameAmountCheckBox); - configPanel.putClientProperty("minAmountSpinner", minSpinner); - configPanel.putClientProperty("maxAmountSpinner", maxSpinner); - configPanel.putClientProperty("includeNotedCheckBox", includeNotedCheckBox); - configPanel.putClientProperty("includeNoneOwnerCheckBox", includeNoneOwnerCheckBox); - } - /** - * Creates an appropriate LootItemCondition or logical condition based on user input - */ - public static Condition createItemCondition(JPanel configPanel) { - JTextField itemsField = (JTextField) configPanel.getClientProperty("itemsField"); - JRadioButton andRadioButton = (JRadioButton) configPanel.getClientProperty("andRadioButton"); - JCheckBox sameAmountCheckBox = (JCheckBox) configPanel.getClientProperty("sameAmountCheckBox"); - JSpinner minAmountSpinner = (JSpinner) configPanel.getClientProperty("minAmountSpinner"); - JSpinner maxAmountSpinner = (JSpinner) configPanel.getClientProperty("maxAmountSpinner"); - JCheckBox includeNotedCheckBox = (JCheckBox) configPanel.getClientProperty("includeNotedCheckBox"); - JCheckBox includeNoneOwnerCheckBox = (JCheckBox) configPanel.getClientProperty("includeNoneOwnerCheckBox"); - - // Handle potential component errors - if (itemsField == null) { - log.error("Items field component not found"); - return null; - } - - // Get configuration values - boolean includeNoted = includeNotedCheckBox != null && includeNotedCheckBox.isSelected(); - boolean includeNoneOwner = includeNoneOwnerCheckBox != null && includeNoneOwnerCheckBox.isSelected(); - - int minAmount = minAmountSpinner != null ? (Integer) minAmountSpinner.getValue() : 1; - int maxAmount = maxAmountSpinner != null ? (Integer) maxAmountSpinner.getValue() : minAmount; - - // Ensure max >= min - if (maxAmount < minAmount) { - maxAmount = minAmount; - } - - String itemNamesString = itemsField.getText().trim(); - if (itemNamesString.isEmpty()) { - return null; // Invalid item name - } - - // Split by comma and trim each item name - String[] itemNamesArray = itemNamesString.split(","); - List itemNames = new ArrayList<>(); - - for (String itemName : itemNamesArray) { - itemName = itemName.trim(); - if (!itemName.isEmpty()) { - // Add the item name as-is - the ResourceCondition.createItemPattern() method - // will automatically detect if it's a regex pattern or exact match - itemNames.add(itemName); - } - } - - if (itemNames.isEmpty()) { - return null; - } - - // If only one item, create a simple LootItemCondition - if (itemNames.size() == 1) { - return LootItemCondition.createRandomized(itemNames.get(0), minAmount, maxAmount, includeNoted, includeNoneOwner); - } - - // For multiple items, create a logical condition based on selection - boolean useAndLogic = andRadioButton == null || andRadioButton.isSelected(); - boolean useSameAmount = sameAmountCheckBox != null && sameAmountCheckBox.isSelected(); - - if (useSameAmount) { - if (useAndLogic) { - return LootItemCondition.createAndCondition(itemNames, minAmount, maxAmount, includeNoted, includeNoneOwner); - } else { - return LootItemCondition.createOrCondition(itemNames, minAmount, maxAmount, includeNoted, includeNoneOwner); - } - } else { - // Create lists of min/max amounts for each item (currently using same values for all) - List minAmounts = new ArrayList<>(); - List maxAmounts = new ArrayList<>(); - - for (int i = 0; i < itemNames.size(); i++) { - minAmounts.add(minAmount); - maxAmounts.add(maxAmount); - } - - if (useAndLogic) { - return LootItemCondition.createAndCondition(itemNames, minAmounts, maxAmounts, includeNoted, includeNoneOwner); - } else { - return LootItemCondition.createOrCondition(itemNames, minAmounts, maxAmounts, includeNoted, includeNoneOwner); - } - } - } - - /** - * Creates a panel for configuring GatheredResourceCondition - */ - public static void createGatheredResourcePanel(JPanel panel, GridBagConstraints gbc, JPanel configPanel) { - // Section title - JLabel titleLabel = new JLabel("Gathered Resource Condition:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // Item name input - gbc.gridy++; - JPanel itemNamePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - itemNamePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel itemNameLabel = new JLabel("Resource Name:"); - itemNameLabel.setForeground(Color.WHITE); - itemNamePanel.add(itemNameLabel); - - JTextField itemNameField = new JTextField(15); - itemNameField.setForeground(Color.WHITE); - itemNameField.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - itemNamePanel.add(itemNameField); - - panel.add(itemNamePanel, gbc); - - // Resource type selection (helps with skill detection) - gbc.gridy++; - JPanel resourceTypePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - resourceTypePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel resourceTypeLabel = new JLabel("Resource Type:"); - resourceTypeLabel.setForeground(Color.WHITE); - resourceTypePanel.add(resourceTypeLabel); - - String[] resourceTypes = {"Auto-detect", "Mining", "Fishing", "Woodcutting", "Farming", "Hunter"}; - JComboBox resourceTypeComboBox = new JComboBox<>(resourceTypes); - resourceTypePanel.add(resourceTypeComboBox); - - panel.add(resourceTypePanel, gbc); - - // Count panel - gbc.gridy++; - JPanel countPanel = new JPanel(new GridLayout(2, 2, 5, 5)); - countPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel countLabel = new JLabel("Target Count:"); - countLabel.setForeground(Color.WHITE); - countPanel.add(countLabel); - - - - JPanel minMaxPanel = new JPanel(new GridLayout(1, 4, 5, 0)); - minMaxPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel minLabel = new JLabel("Min:"); - minLabel.setForeground(Color.WHITE); - minMaxPanel.add(minLabel); - - SpinnerNumberModel minModel = new SpinnerNumberModel(50, 1, 10000, 10); - JSpinner minSpinner = new JSpinner(minModel); - minMaxPanel.add(minSpinner); - - JLabel maxLabel = new JLabel("Max:"); - maxLabel.setForeground(Color.WHITE); - minMaxPanel.add(maxLabel); - - SpinnerNumberModel maxModel = new SpinnerNumberModel(150, 1, 10000, 10); - JSpinner maxSpinner = new JSpinner(maxModel); - minMaxPanel.add(maxSpinner); - - minMaxPanel.setVisible(false); - countPanel.add(minMaxPanel); - - - - panel.add(countPanel, gbc); - - // Options panel - gbc.gridy++; - JPanel optionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - optionsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JCheckBox includeNotedCheckbox = new JCheckBox("Include noted items"); - includeNotedCheckbox.setForeground(Color.WHITE); - includeNotedCheckbox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - includeNotedCheckbox.setSelected(true); - includeNotedCheckbox.setToolTipText("Count both noted and unnoted versions of the gathered resource"); - optionsPanel.add(includeNotedCheckbox); - - panel.add(optionsPanel, gbc); - - // Example resources by type - gbc.gridy++; - JPanel examplesPanel = new JPanel(new BorderLayout()); - examplesPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel examplesLabel = new JLabel("Examples by type:"); - examplesLabel.setForeground(Color.WHITE); - examplesPanel.add(examplesLabel, BorderLayout.NORTH); - - JLabel examplesContentLabel = new JLabel("Mining: Coal, Iron ore, Gold ore
" + - "Fishing: Shrimp, Trout, Tuna, Shark
" + - "Woodcutting: Logs, Oak logs, Yew logs
" + - "Farming: Potato, Strawberry, Herbs
" + - "Hunter: Bird meat, Rabbit, Chinchompa"); - examplesContentLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - examplesContentLabel.setFont(FontManager.getRunescapeSmallFont()); - examplesPanel.add(examplesContentLabel, BorderLayout.CENTER); - - panel.add(examplesPanel, gbc); - - // Description - gbc.gridy++; - JLabel descriptionLabel = new JLabel("Tracks resources gathered from skilling activities"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Store components for later access - configPanel.putClientProperty("gatheredItemNameField", itemNameField); - configPanel.putClientProperty("gatheredResourceType", resourceTypeComboBox); - configPanel.putClientProperty("gatheredMinSpinner", minSpinner); - configPanel.putClientProperty("gatheredMaxSpinner", maxSpinner); - configPanel.putClientProperty("gatheredIncludeNotedCheckbox", includeNotedCheckbox); - } - - /** - * Creates a GatheredResourceCondition from the panel configuration - */ - @SuppressWarnings("unchecked") - public static GatheredResourceCondition createGatheredResourceCondition(JPanel configPanel) { - JTextField itemNameField = (JTextField) configPanel.getClientProperty("gatheredItemNameField"); - JComboBox resourceTypeComboBox = (JComboBox) configPanel.getClientProperty("gatheredResourceType"); - JSpinner minSpinner = (JSpinner) configPanel.getClientProperty("gatheredMinSpinner"); - JSpinner maxSpinner = (JSpinner) configPanel.getClientProperty("gatheredMaxSpinner"); - JCheckBox includeNotedCheckbox = (JCheckBox) configPanel.getClientProperty("gatheredIncludeNotedCheckbox"); - - // Get item name - String itemName = itemNameField.getText().trim(); - if (itemName.isEmpty()) { - itemName = "resources"; // Default generic name - } - - // Get relevant skills based on resource type selection - List relevantSkills = new ArrayList<>(); - String selectedResourceType = (String) resourceTypeComboBox.getSelectedItem(); - - if (!"Auto-detect".equals(selectedResourceType)) { - // Add specific skill based on selection - switch (selectedResourceType) { - case "Mining": - relevantSkills.add(Skill.MINING); - break; - case "Fishing": - relevantSkills.add(Skill.FISHING); - break; - case "Woodcutting": - relevantSkills.add(Skill.WOODCUTTING); - break; - case "Farming": - relevantSkills.add(Skill.FARMING); - break; - case "Hunter": - relevantSkills.add(Skill.HUNTER); - break; - } - } - - // Get target count - int minCount, maxCount; - - minCount = (Integer) minSpinner.getValue(); - maxCount = (Integer) maxSpinner.getValue(); - - - - boolean includeNoted = includeNotedCheckbox.isSelected(); - - // Create the condition - return GatheredResourceCondition.builder() - .itemName(itemName) - .targetCountMin(minCount) - .targetCountMax(maxCount) - .includeNoted(includeNoted) - .relevantSkills(relevantSkills.isEmpty() ? null : relevantSkills) - .build(); - } - - /** - * Creates a panel for configuring ProcessItemCondition - */ -public static void createProcessItemPanel(JPanel panel, GridBagConstraints gbc, JPanel configPanel) { - // Section title - JLabel titleLabel = new JLabel("Process Item Condition:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // Tracking mode selection - gbc.gridy++; - JPanel trackingPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - trackingPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel trackingLabel = new JLabel("Tracking Mode:"); - trackingLabel.setForeground(Color.WHITE); - trackingPanel.add(trackingLabel); - - ButtonGroup trackingGroup = new ButtonGroup(); - - JRadioButton sourceButton = new JRadioButton("Source Consumption"); - sourceButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - sourceButton.setForeground(Color.WHITE); - sourceButton.setSelected(true); - trackingGroup.add(sourceButton); - trackingPanel.add(sourceButton); - - JRadioButton targetButton = new JRadioButton("Target Production"); - targetButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - targetButton.setForeground(Color.WHITE); - trackingGroup.add(targetButton); - trackingPanel.add(targetButton); - - JRadioButton eitherButton = new JRadioButton("Either"); - eitherButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - eitherButton.setForeground(Color.WHITE); - trackingGroup.add(eitherButton); - trackingPanel.add(eitherButton); - - JRadioButton bothButton = new JRadioButton("Both"); - bothButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - bothButton.setForeground(Color.WHITE); - trackingGroup.add(bothButton); - trackingPanel.add(bothButton); - - panel.add(trackingPanel, gbc); - - // Source items panel - gbc.gridy++; - JPanel sourcePanel = new JPanel(new BorderLayout()); - sourcePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - sourcePanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Source Items (items consumed)", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeSmallFont(), - Color.WHITE - )); - - JPanel sourceInputPanel = new JPanel(new GridLayout(0, 3, 5, 5)); - sourceInputPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel sourceNameLabel = new JLabel("Item Name"); - sourceNameLabel.setForeground(Color.WHITE); - sourceInputPanel.add(sourceNameLabel); - - JLabel sourceQuantityLabel = new JLabel("Quantity Per Process"); - sourceQuantityLabel.setForeground(Color.WHITE); - sourceInputPanel.add(sourceQuantityLabel); - - // Empty label for alignment - sourceInputPanel.add(new JLabel("")); - - JTextField sourceNameField = new JTextField(); - sourceInputPanel.add(sourceNameField); - - SpinnerNumberModel sourceQuantityModel = new SpinnerNumberModel(1, 1, 100, 1); - JSpinner sourceQuantitySpinner = new JSpinner(sourceQuantityModel); - sourceInputPanel.add(sourceQuantitySpinner); - - JButton addSourceButton = new JButton("+"); - addSourceButton.setBackground(ColorScheme.BRAND_ORANGE); - addSourceButton.setForeground(Color.WHITE); - sourceInputPanel.add(addSourceButton); - - sourcePanel.add(sourceInputPanel, BorderLayout.NORTH); - - // Source items list (will be populated dynamically) - DefaultListModel sourceItemsModel = new DefaultListModel<>(); - JList sourceItemsList = new JList<>(sourceItemsModel); - sourceItemsList.setBackground(ColorScheme.DARKER_GRAY_COLOR); - sourceItemsList.setForeground(Color.WHITE); - JScrollPane sourceScrollPane = new JScrollPane(sourceItemsList); - sourceScrollPane.setPreferredSize(new Dimension(0, 80)); - sourcePanel.add(sourceScrollPane, BorderLayout.CENTER); - - // Add source item button action - addSourceButton.addActionListener(e -> { - String itemName = sourceNameField.getText().trim(); - if (!itemName.isEmpty()) { - int quantity = (Integer) sourceQuantitySpinner.getValue(); - sourceItemsModel.addElement(quantity + "x " + itemName); - sourceNameField.setText(""); - } - }); - - panel.add(sourcePanel, gbc); - - // Target items panel (similar structure to source panel) - gbc.gridy++; - JPanel targetPanel = new JPanel(new BorderLayout()); - targetPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - targetPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Target Items (items produced)", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeSmallFont(), - Color.WHITE - )); - - JPanel targetInputPanel = new JPanel(new GridLayout(0, 3, 5, 5)); - targetInputPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel targetNameLabel = new JLabel("Item Name"); - targetNameLabel.setForeground(Color.WHITE); - targetInputPanel.add(targetNameLabel); - - JLabel targetQuantityLabel = new JLabel("Quantity Per Process"); - targetQuantityLabel.setForeground(Color.WHITE); - targetInputPanel.add(targetQuantityLabel); - - // Empty label for alignment - targetInputPanel.add(new JLabel("")); - - JTextField targetNameField = new JTextField(); - targetInputPanel.add(targetNameField); - - SpinnerNumberModel targetQuantityModel = new SpinnerNumberModel(1, 1, 100, 1); - JSpinner targetQuantitySpinner = new JSpinner(targetQuantityModel); - targetInputPanel.add(targetQuantitySpinner); - - JButton addTargetButton = new JButton("+"); - addTargetButton.setBackground(ColorScheme.BRAND_ORANGE); - addTargetButton.setForeground(Color.WHITE); - targetInputPanel.add(addTargetButton); - - targetPanel.add(targetInputPanel, BorderLayout.NORTH); - - // Target items list - DefaultListModel targetItemsModel = new DefaultListModel<>(); - JList targetItemsList = new JList<>(targetItemsModel); - targetItemsList.setBackground(ColorScheme.DARKER_GRAY_COLOR); - targetItemsList.setForeground(Color.WHITE); - JScrollPane targetScrollPane = new JScrollPane(targetItemsList); - targetScrollPane.setPreferredSize(new Dimension(0, 80)); - targetPanel.add(targetScrollPane, BorderLayout.CENTER); - - // Add target item button action - addTargetButton.addActionListener(e -> { - String itemName = targetNameField.getText().trim(); - if (!itemName.isEmpty()) { - int quantity = (Integer) targetQuantitySpinner.getValue(); - targetItemsModel.addElement(quantity + "x " + itemName); - targetNameField.setText(""); - } - }); - - panel.add(targetPanel, gbc); - - // Count panel - gbc.gridy++; - JPanel countPanel = new JPanel(new GridLayout(2, 3, 5, 5)); - countPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel countLabel = new JLabel("Target Process Count:"); - countLabel.setForeground(Color.WHITE); - countPanel.add(countLabel); - - // Empty space - countPanel.add(new JLabel("")); - - - - JPanel minMaxPanel = new JPanel(new GridLayout(1, 4, 5, 0)); - minMaxPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel minLabel = new JLabel("Min:"); - minLabel.setForeground(Color.WHITE); - minMaxPanel.add(minLabel); - - SpinnerNumberModel minModel = new SpinnerNumberModel(25, 1, 10000, 1); - JSpinner minSpinner = new JSpinner(minModel); - minMaxPanel.add(minSpinner); - - JLabel maxLabel = new JLabel("Max:"); - maxLabel.setForeground(Color.WHITE); - minMaxPanel.add(maxLabel); - - SpinnerNumberModel maxModel = new SpinnerNumberModel(75, 1, 10000, 1); - JSpinner maxSpinner = new JSpinner(maxModel); - minMaxPanel.add(maxSpinner); - - minMaxPanel.setVisible(false); - - - - countPanel.add(minMaxPanel); - - panel.add(countPanel, gbc); - - // Description - gbc.gridy++; - JLabel descriptionLabel = new JLabel("Tracks items being processed (crafting, cooking, etc.)"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Store components for later access - configPanel.putClientProperty("procSourceRadio", sourceButton); - configPanel.putClientProperty("procTargetRadio", targetButton); - configPanel.putClientProperty("procEitherRadio", eitherButton); - configPanel.putClientProperty("procBothRadio", bothButton); - configPanel.putClientProperty("procSourceItemsModel", sourceItemsModel); - configPanel.putClientProperty("procTargetItemsModel", targetItemsModel); - configPanel.putClientProperty("procMinSpinner", minSpinner); - configPanel.putClientProperty("procMaxSpinner", maxSpinner); -} - -/** - * Creates a ProcessItemCondition from the panel configuration - */ -@SuppressWarnings("unchecked") -public static ProcessItemCondition createProcessItemCondition(JPanel configPanel) { - JRadioButton sourceButton = (JRadioButton) configPanel.getClientProperty("procSourceRadio"); - JRadioButton targetButton = (JRadioButton) configPanel.getClientProperty("procTargetRadio"); - JRadioButton eitherButton = (JRadioButton) configPanel.getClientProperty("procEitherRadio"); - - DefaultListModel sourceItemsModel = (DefaultListModel) configPanel.getClientProperty("procSourceItemsModel"); - DefaultListModel targetItemsModel = (DefaultListModel) configPanel.getClientProperty("procTargetItemsModel"); - - JSpinner minSpinner = (JSpinner) configPanel.getClientProperty("procMinSpinner"); - JSpinner maxSpinner = (JSpinner) configPanel.getClientProperty("procMaxSpinner"); - - // Determine tracking mode - ProcessItemCondition.TrackingMode trackingMode; - if (sourceButton.isSelected()) { - trackingMode = ProcessItemCondition.TrackingMode.SOURCE_CONSUMPTION; - } else if (targetButton.isSelected()) { - trackingMode = ProcessItemCondition.TrackingMode.TARGET_PRODUCTION; - } else if (eitherButton.isSelected()) { - trackingMode = ProcessItemCondition.TrackingMode.EITHER; - } else { - trackingMode = ProcessItemCondition.TrackingMode.BOTH; - } - - // Parse source items - List sourceItems = new ArrayList<>(); - for (int i = 0; i < sourceItemsModel.getSize(); i++) { - String entry = sourceItemsModel.getElementAt(i); - // Parse "Nx ItemName" format - int xIndex = entry.indexOf('x'); - if (xIndex > 0) { - try { - int quantity = Integer.parseInt(entry.substring(0, xIndex).trim()); - String itemName = entry.substring(xIndex + 1).trim(); - sourceItems.add(new ProcessItemCondition.ItemTracker(itemName, quantity)); - } catch (NumberFormatException e) { - // If parsing fails, default to quantity 1 - sourceItems.add(new ProcessItemCondition.ItemTracker(entry, 1)); - } - } else { - sourceItems.add(new ProcessItemCondition.ItemTracker(entry, 1)); - } - } - - // Parse target items - List targetItems = new ArrayList<>(); - for (int i = 0; i < targetItemsModel.getSize(); i++) { - String entry = targetItemsModel.getElementAt(i); - // Parse "Nx ItemName" format - int xIndex = entry.indexOf('x'); - if (xIndex > 0) { - try { - int quantity = Integer.parseInt(entry.substring(0, xIndex).trim()); - String itemName = entry.substring(xIndex + 1).trim(); - targetItems.add(new ProcessItemCondition.ItemTracker(itemName, quantity)); - } catch (NumberFormatException e) { - // If parsing fails, default to quantity 1 - targetItems.add(new ProcessItemCondition.ItemTracker(entry, 1)); - } - } else { - targetItems.add(new ProcessItemCondition.ItemTracker(entry, 1)); - } - } - - // Get target count - int minCount, maxCount; - - minCount = (Integer) minSpinner.getValue(); - maxCount = (Integer) maxSpinner.getValue(); - - - - - // Build the condition - return ProcessItemCondition.builder() - .sourceItems(sourceItems) - .targetItems(targetItems) - .trackingMode(trackingMode) - .targetCountMin(minCount) - .targetCountMax(maxCount) - .build(); -} - -/** - * Sets up panel with values from an existing condition - * - * @param panel The panel containing the UI components - * @param condition The resource condition to read values from - */ -public static void setupResourceCondition(JPanel panel, Condition condition) { - if (condition == null) { - return; - } - - if (condition instanceof InventoryItemCountCondition) { - setupInventoryItemCountCondition(panel, (InventoryItemCountCondition) condition); - } else if (condition instanceof BankItemCountCondition) { - setupBankItemCountCondition(panel, (BankItemCountCondition) condition); - } else if (condition instanceof LootItemCondition || condition instanceof LogicalCondition) { - setupLootItemCondition(panel, condition); - } else if (condition instanceof ProcessItemCondition) { - setupProcessItemCondition(panel, (ProcessItemCondition) condition); - } else if (condition instanceof GatheredResourceCondition) { - setupGatheredResourceCondition(panel, (GatheredResourceCondition) condition); - } -} - -/** - * Sets up inventory item count condition panel - */ -private static void setupInventoryItemCountCondition(JPanel panel, InventoryItemCountCondition condition) { - JTextField itemNameField = (JTextField) panel.getClientProperty("itemNameField"); - JSpinner minCountSpinner = (JSpinner) panel.getClientProperty("minCountSpinner"); - JSpinner maxCountSpinner = (JSpinner) panel.getClientProperty("maxCountSpinner"); - JCheckBox includeNotedCheckbox = (JCheckBox) panel.getClientProperty("includeNotedCheckbox"); - - if (itemNameField != null) { - itemNameField.setText(condition.getItemPattern().toString()); - } - - if (minCountSpinner != null && maxCountSpinner != null) { - - minCountSpinner.setValue(condition.getTargetCountMin()); - maxCountSpinner.setValue(condition.getTargetCountMax()); - - - } - if (includeNotedCheckbox != null) { - includeNotedCheckbox.setSelected(condition.isIncludeNoted()); - } -} - -/** - * Sets up bank item count condition panel - */ -private static void setupBankItemCountCondition(JPanel panel, BankItemCountCondition condition) { - JTextField itemNameField = (JTextField) panel.getClientProperty("bankItemNameField"); - JSpinner minCountSpinner = (JSpinner) panel.getClientProperty("bankMinCountSpinner"); - JSpinner maxCountSpinner = (JSpinner) panel.getClientProperty("bankMaxCountSpinner"); - - - if (itemNameField != null) { - itemNameField.setText(condition.getItemPattern().toString()); - } - - if (minCountSpinner != null && maxCountSpinner != null) { - - minCountSpinner.setValue(condition.getTargetCountMin()); - maxCountSpinner.setValue(condition.getTargetCountMax()); - - } -} - -/** - * Sets up item collection condition panel - */ -private static void setupLootItemCondition(JPanel panel, Condition condition) { - // Retrieve the UI components - JTextField itemsField = (JTextField) panel.getClientProperty("itemsField"); - JRadioButton andRadioButton = (JRadioButton) panel.getClientProperty("andRadioButton"); - JCheckBox sameAmountCheckBox = (JCheckBox) panel.getClientProperty("sameAmountCheckBox"); - JSpinner minAmountSpinner = (JSpinner) panel.getClientProperty("minAmountSpinner"); - JSpinner maxAmountSpinner = (JSpinner) panel.getClientProperty("maxAmountSpinner"); - JCheckBox includeNotedCheckBox = (JCheckBox) panel.getClientProperty("includeNotedCheckBox"); - JCheckBox includeNoneOwnerCheckBox = (JCheckBox) panel.getClientProperty("includeNoneOwnerCheckBox"); - - if (condition instanceof LootItemCondition || condition instanceof LogicalCondition) { - Condition conditionBaseCondition = condition; - boolean isAndLogic = true; - if (!(condition instanceof LootItemCondition)) { - conditionBaseCondition = ((LogicalCondition) condition).getConditions().get(0); - if (condition instanceof OrCondition) { - isAndLogic = false; - } - } - LootItemCondition itemCondition = (LootItemCondition) conditionBaseCondition; - - // Set item names - if (itemsField != null) { - // For single condition, just use the pattern - String itemPatternString = itemCondition.getItemPatternString(); - if (itemPatternString != null) { - // Clean up the pattern for display - String displayName = itemPatternString; - if (displayName.startsWith(".*") && displayName.endsWith(".*") && !displayName.contains("|")) { - // Remove surrounding .* for cleaner display - displayName = displayName.substring(2, displayName.length() - 2); - } - - // Handle patterns that were created with Pattern.quote() which escapes special characters - if (displayName.startsWith("\\Q") && displayName.endsWith("\\E")) { - displayName = displayName.substring(2, displayName.length() - 2); - } - - itemsField.setText(displayName); - } - } - - // Set logical operator - if (andRadioButton != null) { - andRadioButton.setSelected(isAndLogic); - } - - // Set min/max amounts - if (minAmountSpinner != null && maxAmountSpinner != null) { - minAmountSpinner.setValue(itemCondition.getTargetAmountMin()); - maxAmountSpinner.setValue(itemCondition.getTargetAmountMax()); - } - - // Set same amount for all - if (sameAmountCheckBox != null) { - sameAmountCheckBox.setSelected(true); // Default to same amount - } - - // Set options - if (includeNotedCheckBox != null) { - includeNotedCheckBox.setSelected(itemCondition.isIncludeNoted()); - } - if (includeNoneOwnerCheckBox != null) { - includeNoneOwnerCheckBox.setSelected(itemCondition.isIncludeNoneOwner()); - } - } -} - -/** - * Sets up process item condition panel - */ -@SuppressWarnings("unchecked") -private static void setupProcessItemCondition(JPanel panel, ProcessItemCondition condition) { - JRadioButton sourceButton = (JRadioButton) panel.getClientProperty("procSourceRadio"); - JRadioButton targetButton = (JRadioButton) panel.getClientProperty("procTargetRadio"); - JRadioButton eitherButton = (JRadioButton) panel.getClientProperty("procEitherRadio"); - JRadioButton procBothRadio = (JRadioButton) panel.getClientProperty("procBothRadio"); - - DefaultListModel sourceItemsModel = (DefaultListModel) panel.getClientProperty("procSourceItemsModel"); - DefaultListModel targetItemsModel = (DefaultListModel) panel.getClientProperty("procTargetItemsModel"); - JSpinner minCountSpinner = (JSpinner) panel.getClientProperty("procMinSpinner"); - JSpinner maxCountSpinner = (JSpinner) panel.getClientProperty("procMaxSpinner"); - - // Set tracking mode - ProcessItemCondition.TrackingMode trackingMode = condition.getTrackingMode(); - switch (trackingMode) { - case SOURCE_CONSUMPTION: - if (sourceButton != null) sourceButton.setSelected(true); - break; - case TARGET_PRODUCTION: - if (targetButton != null) targetButton.setSelected(true); - break; - case EITHER: - if (eitherButton != null) eitherButton.setSelected(true); - break; - case BOTH: - if (procBothRadio != null) procBothRadio.setSelected(true); - break; - } - - // Populate source items list - if (sourceItemsModel != null) { - sourceItemsModel.clear(); - for (ProcessItemCondition.ItemTracker tracker : condition.getSourceItems()) { - sourceItemsModel.addElement(tracker.getQuantityPerProcess() + "x " + tracker.getItemName()); - } - } - - // Populate target items list - if (targetItemsModel != null) { - targetItemsModel.clear(); - for (ProcessItemCondition.ItemTracker tracker : condition.getTargetItems()) { - targetItemsModel.addElement(tracker.getQuantityPerProcess() + "x " + tracker.getItemName()); - } - } - - // Set count values - if (minCountSpinner != null && maxCountSpinner != null) { - minCountSpinner.setValue(condition.getTargetCountMin()); - maxCountSpinner.setValue(condition.getTargetCountMax()); - } -} - -/** - * Sets up gathered resource condition panel - */ -@SuppressWarnings("unchecked") -private static void setupGatheredResourceCondition(JPanel panel, GatheredResourceCondition condition) { - JComboBox resourceTypeComboBox = (JComboBox) panel.getClientProperty("gatheredResourceType"); - JTextField resourceNameField = (JTextField) panel.getClientProperty("gatheredItemNameField"); - JSpinner minCountSpinner = (JSpinner) panel.getClientProperty("gatheredMinSpinner"); - JSpinner maxCountSpinner = (JSpinner) panel.getClientProperty("gatheredMaxSpinner"); - JCheckBox includeNotedCheckbox = (JCheckBox) panel.getClientProperty("gatheredIncludeNotedCheckbox"); - - if (resourceTypeComboBox != null) { - resourceTypeComboBox.setSelectedItem("Auto-detect"); - } - - if (resourceNameField != null) { - resourceNameField.setText(condition.getItemName()); - } - - if (includeNotedCheckbox != null) { - includeNotedCheckbox.setSelected(condition.isIncludeNoted()); - } - - if (minCountSpinner != null && maxCountSpinner != null) { - minCountSpinner.setValue(condition.getTargetCountMin()); - maxCountSpinner.setValue(condition.getTargetCountMax()); - } -} - -/** - * Checks if an item name string appears to be a regex pattern - * This method uses the same logic as ResourceCondition.createItemPattern() - * @param itemName The item name to check - * @return true if the item name appears to be a regex pattern, false otherwise - */ - public static boolean isRegexPattern(String itemName) { - if (itemName == null || itemName.isEmpty()) { - return false; - } - - // Check for regex special characters that indicate a pattern - return itemName.startsWith("^") || itemName.endsWith("$") || - itemName.contains(".*") || itemName.contains("[") || - itemName.contains("(") || itemName.contains("|"); - } - - /** - * Formats a list of item names for display, showing whether they are exact matches or regex patterns - * @param itemNames The list of item names - * @return A formatted string showing the item names and their matching mode - */ - public static String formatItemNamesForDisplay(List itemNames) { - if (itemNames == null || itemNames.isEmpty()) { - return ""; - } - - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < itemNames.size(); i++) { - String itemName = itemNames.get(i); - if (i > 0) { - sb.append(", "); - } - sb.append(itemName); - if (isRegexPattern(itemName)) { - sb.append(" (regex)"); - } - } - return sb.toString(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillCondition.java deleted file mode 100644 index 8d0c483c918..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillCondition.java +++ /dev/null @@ -1,422 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.skill; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.GameState; -import net.runelite.api.Skill; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.StatChanged; -import net.runelite.client.game.SkillIconManager; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.util.ImageUtil; - -import javax.swing.*; - -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.awt.image.BufferedImage; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Abstract base class for skill-based conditions. - */ -@Getter -@EqualsAndHashCode(callSuper = false) -@Slf4j -public abstract class SkillCondition implements Condition { - // Static icon cache to prevent repeated loading of the same icons - private static final ConcurrentHashMap ICON_CACHE = new ConcurrentHashMap<>(); - private static Icon OVERALL_ICON = null; - - // Static skill data caching for performance improvements - private static final ConcurrentHashMap SKILL_LEVELS = new ConcurrentHashMap<>(); - private static final ConcurrentHashMap SKILL_XP = new ConcurrentHashMap<>(); - private static transient int TOTAL_LEVEL = 0; - private static transient long TOTAL_XP = 0; - private static transient boolean SKILL_DATA_INITIALIZED = false; - private static transient long LAST_UPDATE_TIME = 0; - private static final long UPDATE_THROTTLE_MS = 600; // Update at most once every 600ms - - private static final int ICON_SIZE = 24; // Standard size for all skill icons - protected final Skill skill; - - // Pause-related fields - @Getter - protected transient boolean isPaused = false; - protected transient Map pausedSkillLevels = new HashMap<>(); - protected transient Map pausedSkillXp = new HashMap<>(); - protected transient int pausedTotalLevel = 0; - protected transient long pausedTotalXp = 0; - - /** - * Constructor requiring a skill to be set - */ - protected SkillCondition(Skill skill) { - this.skill = skill; - - // Initialize skill data if needed - if (!SKILL_DATA_INITIALIZED) { - initializeSkillData(); - } - } - - /** - * Gets the skill associated with this condition - */ - public Skill getSkill() { - return skill; - } - - /** - * Checks if this condition is for the total of all skills - */ - public boolean isTotal() { - return skill == null || skill == Skill.OVERALL; - } - - /** - * Gets all skills to be considered for total calculations - * Excludes TOTAL itself and other non-tracked skills - */ - protected Skill[] getAllTrackableSkills() { - return Skill.values(); - } - - /** - * Gets a properly scaled icon for the skill (24x24 pixels) - * Uses a cache to avoid repeatedly loading the same icons - */ - public Icon getSkillIcon() { - try { - // First check if we have a cached icon - if (isTotal()) { - if (OVERALL_ICON != null) { - return OVERALL_ICON; - } - } else if (skill != null && ICON_CACHE.containsKey(skill)) { - return ICON_CACHE.get(skill); - } - - // If not in cache, create the icon and cache it - Icon icon = createSkillIcon(); - - // Store in the appropriate cache - if (isTotal()) { - OVERALL_ICON = icon; - } else if (skill != null) { - ICON_CACHE.put(skill, icon); - } - - return icon; - } catch (Exception e) { - // Fall back to generic skill icon - return null; - } - } - - /** - * Creates a skill icon - now separated from getSkillIcon to support caching - */ - private Icon createSkillIcon() { - try { - // This only needs to be done once per skill, not on every UI render - SkillIconManager iconManager = Microbot.getClientThread().runOnClientThreadOptional( - () -> Microbot.getInjector().getInstance(SkillIconManager.class)).orElse(null); - - if (iconManager != null) { - // Get the skill image (small=true for smaller version) - BufferedImage skillImage; - String skillName = isTotal() ? "overall" : skill.getName().toLowerCase(); - if (isTotal()) { - String skillIconPath = "/skill_icons/" + skillName + ".png"; - skillImage = ImageUtil.loadImageResource(getClass(), skillIconPath); - } else { - skillImage = iconManager.getSkillImage(skill, true); - } - - // Scale the image if needed - if (skillImage.getWidth() != ICON_SIZE || skillImage.getHeight() != ICON_SIZE) { - skillImage = ImageUtil.resizeImage(skillImage, ICON_SIZE, ICON_SIZE); - } - - return new ImageIcon(skillImage); - } - } catch (Exception e) { - // Silently fail and return null - } - return null; - } - - /** - * Reset condition - must be implemented by subclasses - */ - @Override - public void reset() { - reset(false); - } - - /** - * Reset condition with option to randomize targets - */ - public abstract void reset(boolean randomize); - - /** - * Initializes skill data tracking for better performance - */ - private static void initializeSkillData() { - if (!Microbot.isLoggedIn()){ - SKILL_DATA_INITIALIZED = false; - return; - } - if (SKILL_DATA_INITIALIZED) { - return; - } - Microbot.getClientThread().invoke(() -> { - try { - // Initialize skill level and XP caches - for (Skill skill : Skill.values()) { - SKILL_LEVELS.put(skill, Microbot.getClient().getRealSkillLevel(skill)); - SKILL_XP.put(skill, (long) Microbot.getClient().getSkillExperience(skill)); - } - TOTAL_LEVEL = Microbot.getClient().getTotalLevel(); - TOTAL_XP = Microbot.getClient().getOverallExperience(); - SKILL_DATA_INITIALIZED = true; - LAST_UPDATE_TIME = System.currentTimeMillis(); - } catch (Exception e) { - // Ignore errors during initialization - } - }); - - } - - /** - * Gets the current level for a skill from the cache - */ - public static int getSkillLevel(Skill skill) { - if (!SKILL_DATA_INITIALIZED) { - initializeSkillData(); - } - - // If the skill is null or OVERALL, return the total level - if (skill == null || skill == Skill.OVERALL) { - return TOTAL_LEVEL; - } - - return SKILL_LEVELS.getOrDefault(skill, 0); - } - - /** - * Gets the current XP for a skill from the cache - */ - public static long getSkillXp(Skill skill) { - if (!SKILL_DATA_INITIALIZED) { - initializeSkillData(); - } - - // If the skill is null or OVERALL, return the total XP - if (skill == null || skill == Skill.OVERALL) { - return TOTAL_XP; - } - - return SKILL_XP.getOrDefault(skill, 0L); - } - - /** - * Gets the current total level from the cache - */ - public static int getTotalLevel() { - if (!SKILL_DATA_INITIALIZED) { - initializeSkillData(); - } - return TOTAL_LEVEL; - } - - /** - * Gets the current total XP from the cache - */ - public static long getTotalXp() { - if (!SKILL_DATA_INITIALIZED) { - initializeSkillData(); - } - return TOTAL_XP; - } - - /** - * Forces an update of all skill data (throttled to prevent performance issues) - */ - public static void forceUpdate() { - // Only update once every UPDATE_THROTTLE_MS milliseconds - long currentTime = System.currentTimeMillis(); - if (currentTime - LAST_UPDATE_TIME < UPDATE_THROTTLE_MS) { - return; - } - SKILL_DATA_INITIALIZED = false; - initializeSkillData(); - sleepUntil(()-> SKILL_DATA_INITIALIZED); - LAST_UPDATE_TIME = currentTime; - } - - /** - * Updates skill data when stats change - */ - @Override - public void onStatChanged(StatChanged event) { - - if (!SKILL_DATA_INITIALIZED) { - initializeSkillData(); - return; - } - - Skill updatedSkill = event.getSkill(); - - // Update throttling - only update once every UPDATE_THROTTLE_MS milliseconds - long currentTime = System.currentTimeMillis(); - if (currentTime - LAST_UPDATE_TIME < UPDATE_THROTTLE_MS) { - return; - } - - // Update cached values - Microbot.getClientThread().invokeLater(() -> { - try { - // Update the specific skill - int newLevel = Microbot.getClient().getRealSkillLevel(updatedSkill); - long newXp = Microbot.getClient().getSkillExperience(updatedSkill); - - SKILL_LEVELS.put(updatedSkill, newLevel); - SKILL_XP.put(updatedSkill, newXp); - - // Update total level and XP - TOTAL_LEVEL = Microbot.getClient().getTotalLevel(); - TOTAL_XP = Microbot.getClient().getOverallExperience(); - LAST_UPDATE_TIME = currentTime; - } catch (Exception e) { - // Ignore errors during update - } - - }); - } - @Override - public void onGameStateChanged(GameStateChanged gameStateChanged) { - if (gameStateChanged.getGameState() == GameState.LOGGED_IN) { - SKILL_DATA_INITIALIZED = false; - initializeSkillData(); - }else{ - SKILL_DATA_INITIALIZED = false; - } - } - - @Override - public void pause() { - if (!isPaused) { - isPaused = true; - - // Capture current skill values at pause time - this.pausedSkillLevels.clear(); - this.pausedSkillXp.clear(); - - // Force update skill data before capturing pause state - forceUpdate(); - - StringBuilder pauseStateLog = new StringBuilder("Captured pause state for skills:\n"); - - // Capture all individual skill levels and XP - for (Skill skill : Skill.values()) { - pausedSkillLevels.put(skill, SKILL_LEVELS.getOrDefault(skill, 0)); - pausedSkillXp.put(skill, SKILL_XP.getOrDefault(skill, 0L)); - pauseStateLog.append("\t") - .append(skill.getName()) - .append("\tLevel: ") - .append(pausedSkillLevels.get(skill)) - .append("\tXP: ") - .append(pausedSkillXp.get(skill)) - .append("\n"); - } - - - - // Capture total values - pausedTotalLevel = TOTAL_LEVEL; - pausedTotalXp = TOTAL_XP; - pauseStateLog.append("\nSkill condition paused. Captured pause state for skill tracking."); - log.debug(pauseStateLog.toString()); - } - } - - @Override - public void resume() { - if (isPaused) { - isPaused = false; - - // Force update skill data to get current values after resume - forceUpdate(); - // Log current skill values after resume using StringBuilder - StringBuilder resumeStateLog = new StringBuilder("Skill condition resumed. Current skill values:\n"); - for (Skill skill : Skill.values()) { - resumeStateLog.append("\t") - .append(skill.getName()) - .append("\tLevel: ") - .append(SKILL_LEVELS.get(skill)) - .append("\tXP: ") - .append(SKILL_XP.get(skill)) - .append("\n"); - } - resumeStateLog.append("\nSkill condition resumed. Pause state cleared for skill tracking."); - log.debug(resumeStateLog.toString()); - } - } - - /** - * Gets the amount of XP or levels gained while paused for a specific skill. - * This is used to adjust baseline values during resume. - * - * @param skill The skill to check - * @return XP gained during pause, or 0 if not paused or skill not tracked - */ - protected long getXpGainedDuringPause(Skill skill) { - long currentXp = SKILL_XP.getOrDefault(skill, 0L); - long pausedXp = this.pausedSkillXp.getOrDefault(skill, 0L); - return Math.max(0, currentXp - pausedXp); - } - - /** - * Gets the number of levels gained while paused for a specific skill. - * - * @param skill The skill to check - * @return Levels gained during pause, or 0 if not paused or skill not tracked - */ - protected int getLevelsGainedDuringPause(Skill skill) { - int currentLevel = SKILL_LEVELS.getOrDefault(skill, 0); - int pausedLevel = this.pausedSkillLevels.getOrDefault(skill, 0); - return Math.max(0, currentLevel - pausedLevel); - } - - /** - * Gets the total XP gained during pause across all skills. - * - * @return Total XP gained during pause, or 0 if not paused - */ - protected long getTotalXpGainedDuringPause() { - if (!isPaused) { - return 0; - } - - return Math.max(0, TOTAL_XP - pausedTotalXp); - } - - /** - * Gets the total levels gained during pause across all skills. - * - * @return Total levels gained during pause, or 0 if not paused - */ - protected int getTotalLevelsGainedDuringPause() { - if (!isPaused) { - return 0; - } - - return Math.max(0, TOTAL_LEVEL - pausedTotalLevel); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillLevelCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillLevelCondition.java deleted file mode 100755 index c5acf993864..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillLevelCondition.java +++ /dev/null @@ -1,450 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.skill; - - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Skill; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; - -/** - * Skill level-based condition for script execution. - */ -@Getter -@EqualsAndHashCode(callSuper = true) -@Slf4j -public class SkillLevelCondition extends SkillCondition { - /** - * Version of the condition class. - * Used for serialization and deserialization. - */ - public static String getVersion() { - return "0.0.1"; - } - private transient int currentTargetLevel; - @Getter - private final int targetLevelMin; - @Getter - private final int targetLevelMax; - private transient int startLevel; - private transient int[] startLevelsBySkill; // Used for total level tracking - private transient boolean SKILL_LEVEL_INITIALIZED = false; - @Getter - private final boolean randomized; - @Getter - private final boolean relative; // Whether this is a relative or absolute level target - - /** - * Creates an absolute level condition (must reach a specific level) - */ - public SkillLevelCondition(Skill skill, int targetLevel) { - super(skill); // Call parent constructor with skill - this.currentTargetLevel = targetLevel; - this.targetLevelMin = targetLevel; - this.targetLevelMax = targetLevel; - this.randomized = false; - this.relative = false; // Absolute level target - initializeLevelTracking(); - } - - /** - * Creates a randomized absolute level condition (must reach a level within a range) - */ - public SkillLevelCondition(Skill skill, int targetMinLevel, int targetMaxLevel) { - super(skill); // Call parent constructor with skill - targetMinLevel = Math.max(1, targetMinLevel); - targetMaxLevel = Math.min(99, targetMaxLevel); - this.currentTargetLevel = Rs2Random.between(targetMinLevel, targetMaxLevel); - this.targetLevelMin = targetMinLevel; - this.targetLevelMax = targetMaxLevel; - this.randomized = true; - this.relative = false; // Absolute level target - initializeLevelTracking(); - } - - /** - * Creates a relative level condition (must gain specific levels from current) - */ - public SkillLevelCondition(Skill skill, int targetLevel, boolean relative) { - super(skill); // Call parent constructor with skill - this.currentTargetLevel = targetLevel; - this.targetLevelMin = targetLevel; - this.targetLevelMax = targetLevel; - this.randomized = false; - this.relative = relative; - initializeLevelTracking(); - } - - /** - * Creates a randomized relative level condition (must gain a random number of levels from current) - */ - public SkillLevelCondition(Skill skill, int targetMinLevel, int targetMaxLevel, boolean relative) { - super(skill); // Call parent constructor with skill - targetMinLevel = Math.max(1, targetMinLevel); - targetMaxLevel = Math.min(99, targetMaxLevel); - this.currentTargetLevel = Rs2Random.between(targetMinLevel, targetMaxLevel); - this.targetLevelMin = targetMinLevel; - this.targetLevelMax = targetMaxLevel; - this.randomized = true; - this.relative = relative; - initializeLevelTracking(); - } - - /** - * Initialize level tracking for individual skill or all skills if total - */ - private void initializeLevelTracking() { - - if (!Microbot.isLoggedIn()){ - this.SKILL_LEVEL_INITIALIZED = false; - return; // Don't initialize if not logged in - } - if( SKILL_LEVEL_INITIALIZED) { - return; // Already initialized, no need to re-initialize - } - log.info("\n\t--Initializing level tracking for skill: \"{}\"", skill); - super.forceUpdate(); - if (isTotal()) { - Skill[] skills = getAllTrackableSkills(); - startLevelsBySkill = new int[skills.length]; - startLevel = getTotalLevel(); - } else { - startLevel = getCurrentLevel(); - } - } - - @Override - public void reset(boolean randomize) { - if (randomize) { - currentTargetLevel = Rs2Random.between(targetLevelMin, targetLevelMax); - } - SKILL_LEVEL_INITIALIZED = false; // Reset skill data initialization flag - initializeLevelTracking(); - } - - /** - * Create an absolute skill level condition with random target between min and max - */ - public static SkillLevelCondition createRandomized(Skill skill, int minLevel, int maxLevel) { - if (minLevel == maxLevel) { - return new SkillLevelCondition(skill, minLevel); - } - - return new SkillLevelCondition(skill, minLevel, maxLevel); - } - - /** - * Create a relative skill level condition (gain levels from current) - */ - public static SkillLevelCondition createRelative(Skill skill, int targetLevel) { - return new SkillLevelCondition(skill, targetLevel, true); - } - - /** - * Create a relative skill level condition with random target between min and max - */ - public static SkillLevelCondition createRelativeRandomized(Skill skill, int minLevel, int maxLevel) { - if (minLevel == maxLevel) { - return new SkillLevelCondition(skill, minLevel, true); - } - - return new SkillLevelCondition(skill, minLevel, maxLevel, true); - } - - @Override - public boolean isSatisfied() { - // A condition cannot be satisfied while paused - if (isPaused) { - return false; - } - - if (relative) { - // For relative mode, check if we've gained the target number of levels - return getLevelsGained() >= currentTargetLevel; - } else { - // For absolute mode, check if our current level is at or above the target - return getCurrentLevel() >= currentTargetLevel; - } - } - - /** - * Gets the number of levels gained since condition was created - */ - public int getLevelsGained() { - return getCurrentLevel() - startLevel; - } - - /** - * Gets the number of levels remaining to reach target - */ - public int getLevelsRemaining() { - if (relative) { - return Math.max(0, currentTargetLevel - getLevelsGained()); - } else { - return Math.max(0, currentTargetLevel - getCurrentLevel()); - } - } - - /** - * Gets the current skill level or total level if this is a total skill condition - * Uses the SkillCondition's cached data to avoid client thread calls - */ - public int getCurrentLevel() { - // Use static cached data from SkillCondition class - if (isTotal()) { - return SkillCondition.getTotalLevel(); - } - return SkillCondition.getSkillLevel(skill); - } - - /** - * Gets the starting skill level - */ - public int getStartingLevel() { - return startLevel; - } - - /** - * Gets the target skill level to reach (for absolute mode), - * or the target level gain (for relative mode) - */ - public int getCurrentTargetLevel() { - return currentTargetLevel; - } - - @Override - public String getDescription() { - String skillName = isTotal() ? "Total" : skill.getName(); - String randomRangeInfo = ""; - - if (targetLevelMin != targetLevelMax) { - randomRangeInfo = String.format(" (randomized from %d-%d)", targetLevelMin, targetLevelMax); - } - - if (relative) { - int levelsGained = getLevelsGained(); - - return String.format("Gain %d %s levels%s (gained: %d - %.1f%%)", - currentTargetLevel, - skillName, - randomRangeInfo, - levelsGained, - getProgressPercentage()); - } else { - int currentLevel = getCurrentLevel(); - int levelsNeeded = Math.max(0, currentTargetLevel - currentLevel); - - if (levelsNeeded <= 0) { - return String.format("%s level %d or higher%s (currently %d, goal reached)", - skillName, currentTargetLevel, randomRangeInfo, currentLevel); - } else { - return String.format("%s level %d or higher%s (currently %d, need %d more)", - skillName, currentTargetLevel, randomRangeInfo, currentLevel, levelsNeeded); - } - } - } - - /** - * Returns a detailed description of the level condition with additional status information - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - String skillName = isTotal() ? "Total" : skill.getName(); - - // Basic description - if (relative) { - sb.append("Skill Level Condition: Gain ").append(currentTargetLevel) - .append(" ").append(skillName).append(" levels from starting level\n"); - } else { - sb.append("Skill Level Condition: Reach ").append(currentTargetLevel) - .append(" ").append(skillName).append(" level\n"); - } - - // Randomization info if applicable - if (targetLevelMin != targetLevelMax) { - sb.append("Target Range: ").append(targetLevelMin) - .append("-").append(targetLevelMax).append(" (randomized)\n"); - } - - // Status information - int currentLevel = getCurrentLevel(); - boolean satisfied = isSatisfied(); - sb.append("Status: ").append(satisfied ? "Satisfied" : "Not satisfied").append("\n"); - - // Progress information - int levelsGained = getLevelsGained(); - sb.append("Starting Level: ").append(startLevel).append("\n"); - sb.append("Current Level: ").append(currentLevel).append("\n"); - sb.append("Levels Gained: ").append(levelsGained).append("\n"); - - if (!satisfied) { - sb.append("Levels Remaining: ").append(getLevelsRemaining()).append("\n"); - } - - sb.append("Progress: ").append(String.format("%.1f%%", getProgressPercentage())); - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - String skillName = isTotal() ? "Total" : skill.getName(); - - // Basic information - sb.append("SkillLevelCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Skill: ").append(skillName).append("\n"); - - if (relative) { - sb.append(" │ Mode: Relative (gain from current)\n"); - sb.append(" │ Target Level gain: ").append(currentTargetLevel).append("\n"); - } else { - sb.append(" │ Mode: Absolute (reach total)\n"); - sb.append(" │ Target Level: ").append(currentTargetLevel).append("\n"); - } - - // Randomization - boolean hasRandomization = targetLevelMin != targetLevelMax; - if (hasRandomization) { - sb.append(" │ Randomization: Enabled\n"); - sb.append(" │ Target Range: ").append(targetLevelMin).append("-").append(targetLevelMax).append("\n"); - } - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - int currentLevel = getCurrentLevel(); - boolean satisfied = isSatisfied(); - sb.append(" │ Satisfied: ").append(satisfied).append("\n"); - - if (relative) { - sb.append(" │ Levels Gained: ").append(getLevelsGained()).append("\n"); - - if (!satisfied) { - sb.append(" │ Levels Remaining: ").append(getLevelsRemaining()).append("\n"); - } - } else { - if (currentLevel >= currentTargetLevel) { - sb.append(" │ Current Level: ").append(currentLevel).append(" (goal reached)\n"); - } else { - sb.append(" │ Current Level: ").append(currentLevel).append("\n"); - sb.append(" │ Levels Remaining: ").append(getLevelsRemaining()).append("\n"); - } - } - - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Current state - sb.append(" └─ Current State ──────────────────────────\n"); - sb.append(" Starting Level: ").append(startLevel).append("\n"); - sb.append(" Current Level: ").append(currentLevel); - - return sb.toString(); - } - - @Override - public ConditionType getType() { - return ConditionType.SKILL; - } - - @Override - public double getProgressPercentage() { - if (relative) { - int levelsGained = getLevelsGained(); - - if (levelsGained >= currentTargetLevel) { - return 100.0; - } - - if (currentTargetLevel <= 0) { - return 100.0; - } - - return (100.0 * levelsGained) / currentTargetLevel; - } else { - int currentLevel = getCurrentLevel(); - int startingLevel = getStartingLevel(); - int targetLevel = getCurrentTargetLevel(); - if (currentLevel==0 || startingLevel == 0 || targetLevel ==0){ - return 0.0; - } - if (currentLevel >= targetLevel) { - return 100.0; - } - - if (currentLevel == startingLevel) { - // If we haven't gained any levels yet, estimate progress using XP - // This provides better feedback for the user - Skill skill = getSkill(); - if (skill != null) { - // Use cached XP data - long currentXp = SkillCondition.getSkillXp(skill); - - long levelStartXp = net.runelite.api.Experience.getXpForLevel(currentLevel); - long nextLevelXp = net.runelite.api.Experience.getXpForLevel(currentLevel + 1); - long xpInLevel = currentXp - levelStartXp; - long xpNeededForNextLevel = nextLevelXp - levelStartXp; - - // Progress within the current level (0-100%) - double levelProgress = (100.0 * xpInLevel) / xpNeededForNextLevel; - - // Total levels needed and percentage of one level - int levelsNeeded = targetLevel - currentLevel; - double oneLevel = 100.0 / levelsNeeded; - - // Return progress for partially completed level - return (levelProgress * oneLevel) / 100.0; - } - } - - int levelsGained = currentLevel - startingLevel; - int levelsNeeded = targetLevel - startingLevel; - - if (levelsNeeded <= 0) { - return 100.0; - } - - return (100.0 * levelsGained) / levelsNeeded; - } - } - - @Override - public void pause() { - // Call parent class pause method to capture pause state - super.pause(); - } - - @Override - public void resume() { - if (isPaused) { - // Call parent class resume method to clear pause state FIRST - super.resume(); - // Calculate level gains during pause BEFORE calling super.resume() which clears pause state - int levelsGainedDuringPause; - if (isTotal()) { - levelsGainedDuringPause = getTotalLevelsGainedDuringPause(); - } else { - levelsGainedDuringPause = getLevelsGainedDuringPause(skill); - } - // Now adjust baselines to exclude levels gained during pause - if (levelsGainedDuringPause > 0) { - if (relative) { - // For relative mode, adjust the starting baseline - startLevel += levelsGainedDuringPause; - log.debug("Adjusted {} level baseline by {} levels gained during pause for relative mode. New startLevel: {}", - isTotal() ? "Total" : skill.getName(), levelsGainedDuringPause, startLevel); - } else { - // For absolute mode, increase target to exclude paused gains - currentTargetLevel += levelsGainedDuringPause; - log.debug("Adjusted {} level target by {} levels gained during pause for absolute mode. New target: {}", - isTotal() ? "Total" : skill.getName(), levelsGainedDuringPause, currentTargetLevel); - } - } else { - log.debug("No level adjustment needed for {} - no gains during pause", - isTotal() ? "Total" : skill.getName()); - } - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillXpCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillXpCondition.java deleted file mode 100644 index 7b1e1950f9a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillXpCondition.java +++ /dev/null @@ -1,455 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.skill; - - - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.GameState; -import net.runelite.api.Skill; -import net.runelite.api.events.GameStateChanged; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; - -/** - * Skill XP-based condition for script execution. - */ -@Getter -@EqualsAndHashCode(callSuper = true) -@Slf4j -public class SkillXpCondition extends SkillCondition { - - - public static String getVersion() { - return "0.0.1"; - } - private transient long currentTargetXp;// relative and absolute mode difference - private final long targetXpMin; - private final long targetXpMax; - private transient long startXp = -1; - private transient long[] startXpBySkill; // Used for total XP tracking - private transient boolean SKILL_DATA_INITIALIZED = false; - private final boolean randomized; - @Getter - private final boolean relative; // Whether this is a relative or absolute XP target - - /** - * Creates an absolute XP condition (must reach specific XP amount) - */ - public SkillXpCondition(Skill skill, long targetXp) { - super(skill); - this.currentTargetXp = targetXp; - this.targetXpMin = targetXp; - this.targetXpMax = targetXp; - this.randomized = false; - this.relative = false; // Absolute XP target - initializeXpTracking(); - } - - /** - * Creates a randomized absolute XP condition (must reach specific XP amount within a range) - */ - public SkillXpCondition(Skill skill, long targetXpMin, long targetXpMax) { - super(skill); - targetXpMin = Math.max(0, targetXpMin); - targetXpMax = Math.min(Long.MAX_VALUE, targetXpMax); - - this.currentTargetXp = Rs2Random.between((int)targetXpMin, (int)targetXpMax); - this.targetXpMin = targetXpMin; - this.targetXpMax = targetXpMax; - this.randomized = true; - this.relative = false; // Absolute XP target - initializeXpTracking(); - } - - /** - * Creates a relative XP condition (must gain specific amount of XP from current) - */ - public SkillXpCondition(Skill skill, long targetXp, boolean relative) { - super(skill); - this.currentTargetXp = targetXp; - this.targetXpMin = targetXp; - this.targetXpMax = targetXp; - this.randomized = false; - this.relative = relative; - initializeXpTracking(); - } - - /** - * Creates a randomized relative XP condition (must gain a random amount of XP from current) - */ - public SkillXpCondition(Skill skill, long targetXpMin, long targetXpMax, boolean relative) { - super(skill); - targetXpMin = Math.max(0, targetXpMin); - targetXpMax = Math.min(Long.MAX_VALUE, targetXpMax); - - this.currentTargetXp = Rs2Random.between((int)targetXpMin, (int)targetXpMax); - this.targetXpMin = targetXpMin; - this.targetXpMax = targetXpMax; - this.randomized = true; - this.relative = relative; - initializeXpTracking(); - } - - /** - * Initialize XP tracking for individual skill or all skills if total - */ - private void initializeXpTracking() { - - if (!Microbot.isLoggedIn()){ - this.SKILL_DATA_INITIALIZED = false; - return; // Don't initialize if not logged in - } - if( SKILL_DATA_INITIALIZED) { - - return; // Already initialized, no need to re-initialize - } - log.info("\n\t--Initializing XP tracking for skill: \"{}\"", skill); - super.forceUpdate(); - if (isTotal()) { - Skill[] skills = getAllTrackableSkills(); - this.startXpBySkill = new long[skills.length]; - long totalXp = getTotalXp(); - this.startXp = totalXp; - } else { - this.startXp = getCurrentXp(); - } - SKILL_DATA_INITIALIZED = true; - } - - @Override - public void reset(boolean randomize) { - if (randomize) { - currentTargetXp = (long)Rs2Random.between((int)targetXpMin, (int)targetXpMax); - } - SKILL_DATA_INITIALIZED = false; // Reset initialization state - initializeXpTracking(); - } - - /** - * Create an absolute skill XP condition with random target between min and max - */ - public static SkillXpCondition createRandomized(Skill skill, long minXp, long maxXp) { - if (minXp == maxXp) { - return new SkillXpCondition(skill, minXp); - } - - return new SkillXpCondition(skill, minXp, maxXp); - } - - /** - * Create a relative skill XP condition (gain XP from current) - */ - public static SkillXpCondition createRelative(Skill skill, long targetXp) { - return new SkillXpCondition(skill, targetXp, true); - } - - /** - * Create a relative skill XP condition with random target between min and max - */ - public static SkillXpCondition createRelativeRandomized(Skill skill, long minXp, long maxXp) { - if (minXp == maxXp) { - return new SkillXpCondition(skill, minXp, true); - } - - return new SkillXpCondition(skill, minXp, maxXp, true); - } - - @Override - public boolean isSatisfied() { - // A condition cannot be satisfied while paused - if (isPaused) { - return false; - } - - if (relative) { - // For relative mode, we need to check if we've gained the target amount of XP - return getXpGained() >= currentTargetXp; - } else { - // For absolute mode, we need to check if our current XP is at or above the target - return getCurrentXp() >= currentTargetXp; - } - } - - /** - * Gets the amount of XP gained since condition was created - */ - public long getXpGained() { - if (startXp != -1){ - if (isTotal()) { - return getTotalXp() - startXp; - } else { - return getCurrentXp() - startXp; - } - }else{ - return 0; - } - } - - - - /** - * Gets the amount of XP remaining to reach target - */ - public long getXpRemaining() { - if (relative) { - return Math.max(0, currentTargetXp - getXpGained()); - } else { - return Math.max(0, currentTargetXp - getCurrentXp()); - } - } - - /** - * Gets the current XP - * Uses static cached data from SkillCondition - */ - public long getCurrentXp() { - if (isTotal()) { - return getTotalXp(); - } - - // Use static cached data from SkillCondition class - return SkillCondition.getSkillXp(skill); - } - - /** - * Gets the starting XP - */ - public long getStartingXp() { - return startXp; - } - - /** - * Gets progress percentage towards target - */ - @Override - public double getProgressPercentage() { - if (relative) { - long xpGained = getXpGained(); - if (xpGained >= currentTargetXp) { - return 100.0; - } - - if (currentTargetXp <= 0) { - return 0; - } - - return (100.0 * xpGained) / currentTargetXp; - } else { - // For absolute targets, we need to calculate progress from 0 to target - long currentXp = getCurrentXp(); - - if (currentXp >= currentTargetXp) { - return 100.0; - } - - if (currentTargetXp <= 0) { - return 100.0; - } - - return (100.0 * currentXp) / currentTargetXp; - } - } - - @Override - public String getDescription() { - String skillName = isTotal() ? "Total" : skill.getName(); - - if (relative) { - long xpGained = getXpGained(); - long currentXp = getCurrentXp(); - long startXp = getStartingXp(); - String randomRangeInfo = ""; - - if (targetXpMin != targetXpMax) { - randomRangeInfo = String.format(" (randomized from %d-%d)", targetXpMin, targetXpMax); - } - - return String.format("Gain Relative %d %s XP%s (gained: %d - %.1f%%, current total: %d starting: %d)", - currentTargetXp, - skillName, - randomRangeInfo, - xpGained, - getProgressPercentage(),currentXp, startXp); - } else { - long currentXp = getCurrentXp(); - String randomRangeInfo = ""; - - if (targetXpMin != targetXpMax) { - randomRangeInfo = String.format(" (randomized from %d-%d)", targetXpMin, targetXpMax); - } - - if (currentXp >= currentTargetXp) { - return String.format("Reach Total %d %s XP%s (currently: %d, goal reached)", - currentTargetXp, - skillName, - randomRangeInfo, - currentXp); - } else { - return String.format("Reach Total %d %s XP%s (currently: %d, need %d more ( %.1f%%))", - currentTargetXp, - skillName, - randomRangeInfo, - currentXp, - getXpRemaining(), - getProgressPercentage() - ); - } - } - } - - @Override - public ConditionType getType() { - return ConditionType.SKILL; - } - - /** - * Returns a detailed description of the XP condition with additional status information - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - String skillName = isTotal() ? "Total" : skill.getName(); - - // Basic description - if (relative) { - sb.append("Skill XP Condition: Gain ").append(currentTargetXp) - .append(" ").append(skillName).append(" XP from starting XP\n"); - } else { - sb.append("Skill XP Condition: Reach ").append(currentTargetXp) - .append(" ").append(skillName).append(" XP total\n"); - } - - // Randomization info if applicable - if (targetXpMin != targetXpMax) { - sb.append("Target Range: ").append(targetXpMin) - .append("-").append(targetXpMax).append(" XP (randomized)\n"); - } - - // Status information - boolean satisfied = isSatisfied(); - sb.append("Status: ").append(satisfied ? "Satisfied" : "Not satisfied").append("\n"); - - // Progress information - long xpGained = getXpGained(); - sb.append("XP Gained: ").append(xpGained).append("\n"); - sb.append("Starting XP: ").append(startXp).append("\n"); - sb.append("Current XP: ").append(getCurrentXp()).append("\n"); - - if (!satisfied) { - sb.append("XP Remaining: ").append(getXpRemaining()).append("\n"); - } - - sb.append("Progress: ").append(String.format("%.1f%%", getProgressPercentage())); - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - String skillName = isTotal() ? "Total" : skill.getName(); - - // Basic information - sb.append("\nSkillXpCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Skill: ").append(skillName).append("\n"); - - if (relative) { - sb.append(" │ Mode: Relative (gain from current)\n"); - sb.append(" │ Target XP gain: ").append(currentTargetXp).append("\n"); - } else { - sb.append(" │ Mode: Absolute (reach total)\n"); - sb.append(" │ Target XP total: ").append(currentTargetXp).append("\n"); - } - - // Randomization - boolean hasRandomization = targetXpMin != targetXpMax; - if (hasRandomization) { - sb.append(" │ Randomization: Enabled\n"); - sb.append(" │ Target Range: ").append(targetXpMin).append("-").append(targetXpMax).append(" XP\n"); - } - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - boolean satisfied = isSatisfied(); - sb.append(" │ Satisfied: ").append(satisfied).append("\n"); - - if (relative) { - sb.append(" │ XP Gained: ").append(getXpGained()).append("\n"); - - if (!satisfied) { - sb.append(" │ XP Remaining: ").append(getXpRemaining()).append("\n"); - } - } else { - long currentXp = getCurrentXp(); - if (currentXp >= currentTargetXp) { - sb.append(" │ Current XP: ").append(currentXp).append(" (goal reached)\n"); - } else { - sb.append(" │ Current XP: ").append(currentXp).append("\n"); - sb.append(" │ XP Remaining: ").append(getXpRemaining()).append("\n"); - } - } - - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Current state - sb.append(" └─ Current State ──────────────────────────\n"); - sb.append(" Starting XP: ").append(startXp).append("\n"); - sb.append(" Current XP: ").append(getCurrentXp()); - - return sb.toString(); - } - @Override - public void onGameStateChanged(GameStateChanged gameStateChanged) { - if (gameStateChanged.getGameState() == GameState.LOGGED_IN) { - super.onGameStateChanged(gameStateChanged); - log.debug("Game state changed to LOGGED_IN, re-initializing XP tracking for skill: {}", skill); - initializeXpTracking(); - }else{ - - } - } - @Override - public void pause() { - // Call parent class pause method to capture pause state - super.pause(); - } - - @Override - public void resume() { - if (isPaused) { - // Call parent class resume method to clear pause state - super.resume(); - // For both relative and absolute mode, we need to adjust baselines to exclude XP gained during pause - if (isTotal()) { //tracking total XP - // Adjust total XP baseline - long xpGainedDuringPause = getTotalXpGainedDuringPause(); - if (relative) { - startXp += xpGainedDuringPause; - log.debug("Adjusted total XP baseline by {} XP gained during pause for relative mode", xpGainedDuringPause); - } else { - // For absolute mode, increase target to exclude paused gains - currentTargetXp += xpGainedDuringPause; - log.debug("Adjusted total XP target by {} XP gained during pause for absolute mode. New target: {}", - xpGainedDuringPause, currentTargetXp); - } - } else { - // Adjust individual skill XP baseline - long xpGainedDuringPause = getXpGainedDuringPause(skill); - if (relative) { - startXp += xpGainedDuringPause; - log.info("Adjusted {} XP baseline by {} XP gained during pause for relative mode", - skill.getName(), xpGainedDuringPause); - } else { - // For absolute mode, increase target to exclude paused gains - currentTargetXp += xpGainedDuringPause; - log.info("Adjusted {} XP target by {} XP gained during pause for absolute mode. New target: {}", - skill.getName(), xpGainedDuringPause, currentTargetXp); - } - } - - - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/serialization/SkillLevelConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/serialization/SkillLevelConditionAdapter.java deleted file mode 100644 index 6ce80f5a685..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/serialization/SkillLevelConditionAdapter.java +++ /dev/null @@ -1,85 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Skill; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.SkillLevelCondition; - -import java.lang.reflect.Type; - -/** - * Serializes and deserializes SkillLevelCondition objects - */ -@Slf4j -public class SkillLevelConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(SkillLevelCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add type information - json.addProperty("type", SkillLevelCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Store skill information - Skill skill = src.getSkill(); - data.addProperty("skill", skill != null ? skill.name() : "OVERALL"); - - // Store level information - data.addProperty("targetLevelMin", src.getTargetLevelMin()); - data.addProperty("targetLevelMax", src.getTargetLevelMax()); - data.addProperty("relative", src.isRelative()); - data.addProperty("version", SkillLevelCondition.getVersion()); - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public SkillLevelCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(SkillLevelCondition.getVersion())) { - log.warn("Version mismatch in SkillLevelCondition: expected {}, got {}", - SkillLevelCondition.getVersion(), version); - throw new JsonParseException("Version mismatch in SkillLevelCondition: expected " + - SkillLevelCondition.getVersion() + ", got " + version); - } - } - - // Get skill - Skill skill; - if (dataObj.get("skill").getAsString().equals("OVERALL")) { - skill = Skill.OVERALL; - } else { - skill = Skill.valueOf(dataObj.get("skill").getAsString()); - } - - // Get level information - int targetLevelMin = dataObj.get("targetLevelMin").getAsInt(); - int targetLevelMax = dataObj.get("targetLevelMax").getAsInt(); - boolean relative = dataObj.get("relative").getAsBoolean(); - - // Create condition - return new SkillLevelCondition(skill, targetLevelMin, targetLevelMax, relative); - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/serialization/SkillXpConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/serialization/SkillXpConditionAdapter.java deleted file mode 100644 index ee0090d4f2d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/serialization/SkillXpConditionAdapter.java +++ /dev/null @@ -1,84 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Skill; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.SkillXpCondition; - -import java.lang.reflect.Type; - -/** - * Serializes and deserializes SkillXpCondition objects - */ -@Slf4j -public class SkillXpConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(SkillXpCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add type information - json.addProperty("type", SkillXpCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Store skill information - Skill skill = src.getSkill(); - data.addProperty("skill", skill != null ? skill.name() : "OVERALL"); - - // Store XP information - data.addProperty("targetXpMin", src.getTargetXpMin()); - data.addProperty("targetXpMax", src.getTargetXpMax()); - data.addProperty("relative", src.isRelative()); - data.addProperty("version", src.getVersion()); - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public SkillXpCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(SkillXpCondition.getVersion())) { - throw new JsonParseException("Version mismatch in SkillXpCondition: expected " + - SkillXpCondition.getVersion() + ", got " + version); - } - } - - // Get skill - Skill skill; - if (dataObj.get("skill").getAsString().equals("OVERALL")) { - skill = Skill.OVERALL; - } else { - skill = Skill.valueOf(dataObj.get("skill").getAsString()); - } - - // Get XP information - long targetXpMin = dataObj.get("targetXpMin").getAsLong(); - long targetXpMax = dataObj.get("targetXpMax").getAsLong(); - boolean relative = dataObj.get("relative").getAsBoolean(); - - // Create condition - return new SkillXpCondition(skill, targetXpMin, targetXpMax, relative); - - - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/ui/SkillConditionPanelUtil.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/ui/SkillConditionPanelUtil.java deleted file mode 100644 index 8e655b5356e..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/ui/SkillConditionPanelUtil.java +++ /dev/null @@ -1,662 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.ui; - -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSpinner; -import javax.swing.SpinnerNumberModel; -import javax.swing.JToggleButton; -import javax.swing.ButtonGroup; -import javax.swing.JRadioButton; -import javax.swing.Box; - -import java.awt.Color; -import java.awt.Dimension; -import java.awt.FlowLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridLayout; -import java.awt.BorderLayout; - -import net.runelite.api.Skill; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.SkillLevelCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.SkillXpCondition; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -public class SkillConditionPanelUtil { - /** - * Creates the skill level condition configuration panel - */ - public static void createSkillLevelConfigPanel(JPanel panel, GridBagConstraints gbc, boolean stopConditionPanel) { - // --- Add skill selection section --- - JComboBox skillComboBox = createSkillSelector(); - addSkillSelectionSection(panel, gbc, skillComboBox); - - // --- Add mode selection (relative vs absolute) --- - JRadioButton relativeButton = new JRadioButton(); - JRadioButton absoluteButton = new JRadioButton(); - JPanel modePanel = createModeSelectionPanel(relativeButton, absoluteButton); - gbc.gridy++; - panel.add(modePanel, gbc); - - // --- Target level section --- - JLabel targetLevelLabel = new JLabel("Levels to gain:"); - targetLevelLabel.setForeground(Color.WHITE); - targetLevelLabel.setFont(FontManager.getRunescapeSmallFont()); - - SpinnerNumberModel levelModel = new SpinnerNumberModel(1, 1, 99, 1); - JSpinner levelSpinner = new JSpinner(levelModel); - levelSpinner.setPreferredSize(new Dimension(70, levelSpinner.getPreferredSize().height)); - - JCheckBox randomizeCheckBox = new JCheckBox("Randomize"); - randomizeCheckBox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - randomizeCheckBox.setForeground(Color.WHITE); - - JPanel levelPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - levelPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - levelPanel.add(targetLevelLabel); - levelPanel.add(levelSpinner); - levelPanel.add(randomizeCheckBox); - - gbc.gridy++; - panel.add(levelPanel, gbc); - - // --- Min/Max level panel --- - JPanel minMaxPanel = createMinMaxPanel("Min Level:", "Max Level:", 1, 99, 1); - minMaxPanel.setVisible(false); - gbc.gridy++; - panel.add(minMaxPanel, gbc); - - // Save references to the spinners - JSpinner minSpinner = (JSpinner) minMaxPanel.getClientProperty("minSpinner"); - JSpinner maxSpinner = (JSpinner) minMaxPanel.getClientProperty("maxSpinner"); - - // --- Mode selection listener --- - setupModeListeners(relativeButton, absoluteButton, targetLevelLabel, "Levels to gain:", "Target level:"); - - // --- Randomize checkbox behavior --- - setupRandomizeListener(randomizeCheckBox, levelSpinner, minSpinner, maxSpinner, - relativeButton, minMaxPanel, panel); - - // --- Min/Max validation --- - setupMinMaxValidation(minSpinner, maxSpinner); - - // --- Description --- - addDescriptionLabel(panel, gbc, stopConditionPanel); - - // Store components in configPanel client properties for later access - storeConfigComponents(panel, skillComboBox, levelSpinner, minSpinner, maxSpinner, - randomizeCheckBox, relativeButton, absoluteButton, targetLevelLabel); - } - - /** - * Creates the skill XP condition configuration panel - */ - public static void createSkillXpConfigPanel(JPanel panel, GridBagConstraints gbc, JPanel configPanel) { - // --- Add skill selection section --- - JComboBox skillComboBox = createSkillSelector(); - addSkillSelectionSection(panel, gbc, skillComboBox); - - // --- Add mode selection (relative vs absolute) --- - JRadioButton relativeButton = new JRadioButton(); - JRadioButton absoluteButton = new JRadioButton(); - JPanel modePanel = createModeSelectionPanel(relativeButton, absoluteButton); - gbc.gridy++; - panel.add(modePanel, gbc); - - // --- Target XP section --- - JLabel targetXpLabel = new JLabel("XP to gain:"); - targetXpLabel.setForeground(Color.WHITE); - targetXpLabel.setFont(FontManager.getRunescapeSmallFont()); - - SpinnerNumberModel xpModel = new SpinnerNumberModel(10000, 1, 200000000, 1000); - JSpinner xpSpinner = new JSpinner(xpModel); - xpSpinner.setPreferredSize(new Dimension(100, xpSpinner.getPreferredSize().height)); - - JCheckBox randomizeCheckBox = new JCheckBox("Randomize"); - randomizeCheckBox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - randomizeCheckBox.setForeground(Color.WHITE); - - JPanel xpPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - xpPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - xpPanel.add(targetXpLabel); - xpPanel.add(xpSpinner); - xpPanel.add(randomizeCheckBox); - - gbc.gridy++; - panel.add(xpPanel, gbc); - - // --- Min/Max XP panel --- - JPanel minMaxPanel = createMinMaxPanel("Min XP:", "Max XP:", 1, 200000000, 1000); - minMaxPanel.setVisible(false); - gbc.gridy++; - panel.add(minMaxPanel, gbc); - - // Save references to the spinners - JSpinner minSpinner = (JSpinner) minMaxPanel.getClientProperty("minSpinner"); - JSpinner maxSpinner = (JSpinner) minMaxPanel.getClientProperty("maxSpinner"); - - // --- Mode selection listener --- - setupModeListeners(relativeButton, absoluteButton, targetXpLabel, "XP to gain:", "Target XP:"); - - // --- Randomize checkbox behavior --- - setupXpRandomizeListener(randomizeCheckBox, xpSpinner, minSpinner, maxSpinner, - minMaxPanel, panel); - - // --- Min/Max validation --- - setupMinMaxValidation(minSpinner, maxSpinner); - - // --- Description --- - gbc.gridy++; - JLabel descriptionLabel = new JLabel(); - descriptionLabel.setText("XP is tracked from the time condition is created. Relative mode tracks gains from that point."); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Store components in configPanel client properties for later access - configPanel.putClientProperty("xpSkillComboBox", skillComboBox); - configPanel.putClientProperty("xpSpinner", xpSpinner); - configPanel.putClientProperty("minXpSpinner", minSpinner); - configPanel.putClientProperty("maxXpSpinner", maxSpinner); - configPanel.putClientProperty("randomizeSkillXp", randomizeCheckBox); - configPanel.putClientProperty("xpRelativeMode", relativeButton); - configPanel.putClientProperty("xpAbsoluteMode", absoluteButton); - configPanel.putClientProperty("xpTargetLabel", targetXpLabel); - } - - /** - * Creates a combo box with all skills and "Total" option - */ - private static JComboBox createSkillSelector() { - JComboBox skillComboBox = new JComboBox<>(); - for (Skill skill : Skill.values()) { - skillComboBox.addItem(skill.getName()); - } - skillComboBox.addItem("Total"); - skillComboBox.setPreferredSize(new Dimension(150, skillComboBox.getPreferredSize().height)); - return skillComboBox; - } - - /** - * Adds the skill selection section to the panel - */ - private static void addSkillSelectionSection(JPanel panel, GridBagConstraints gbc, JComboBox skillComboBox) { - JPanel skillSelectionPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - skillSelectionPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel skillLabel = new JLabel("Skill:"); - skillLabel.setForeground(Color.WHITE); - skillLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - skillSelectionPanel.add(skillLabel); - skillSelectionPanel.add(skillComboBox); - - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(skillSelectionPanel, gbc); - } - - /** - * Creates the mode selection panel (relative vs absolute) - */ - private static JPanel createModeSelectionPanel(JRadioButton relativeButton, JRadioButton absoluteButton) { - JPanel modePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - modePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel modeLabel = new JLabel("Mode:"); - modeLabel.setForeground(Color.WHITE); - modeLabel.setFont(FontManager.getRunescapeSmallFont()); - modePanel.add(modeLabel); - - relativeButton.setText("Relative (gain from current)"); - relativeButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - relativeButton.setForeground(Color.WHITE); - relativeButton.setSelected(true); // Default to relative mode - relativeButton.setToolTipText("Track gains from the current value when the condition starts"); - - absoluteButton.setText("Absolute (reach specific)"); - absoluteButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - absoluteButton.setForeground(Color.WHITE); - absoluteButton.setToolTipText("Track progress toward a specific target value"); - - ButtonGroup modeGroup = new ButtonGroup(); - modeGroup.add(relativeButton); - modeGroup.add(absoluteButton); - - modePanel.add(relativeButton); - modePanel.add(absoluteButton); - return modePanel; - } - - /** - * Creates a min/max panel for randomization - */ - private static JPanel createMinMaxPanel(String minLabel, String maxLabel, int minValue, int maxValue, int step) { - JPanel minMaxPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - minMaxPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel minLabelComponent = new JLabel(minLabel); - minLabelComponent.setForeground(Color.WHITE); - minLabelComponent.setFont(FontManager.getRunescapeSmallFont()); - - SpinnerNumberModel minModel = new SpinnerNumberModel(minValue, minValue, maxValue, step); - JSpinner minSpinner = new JSpinner(minModel); - minSpinner.setPreferredSize(new Dimension(100, minSpinner.getPreferredSize().height)); - - JLabel maxLabelComponent = new JLabel(maxLabel); - maxLabelComponent.setForeground(Color.WHITE); - maxLabelComponent.setFont(FontManager.getRunescapeSmallFont()); - - SpinnerNumberModel maxModel = new SpinnerNumberModel( - Math.min(maxValue, minValue + (step * 10)), - minValue, - maxValue, - step - ); - JSpinner maxSpinner = new JSpinner(maxModel); - maxSpinner.setPreferredSize(new Dimension(100, maxSpinner.getPreferredSize().height)); - - minMaxPanel.add(minLabelComponent); - minMaxPanel.add(minSpinner); - minMaxPanel.add(maxLabelComponent); - minMaxPanel.add(maxSpinner); - - // Store references for later access - minMaxPanel.putClientProperty("minSpinner", minSpinner); - minMaxPanel.putClientProperty("maxSpinner", maxSpinner); - - return minMaxPanel; - } - - /** - * Sets up listeners for the mode buttons - */ - private static void setupModeListeners(JRadioButton relativeButton, JRadioButton absoluteButton, - JLabel targetLabel, String relativeText, String absoluteText) { - relativeButton.addActionListener(e -> { - targetLabel.setText(relativeText); - }); - - absoluteButton.addActionListener(e -> { - targetLabel.setText(absoluteText); - }); - } - - /** - * Sets up the randomize checkbox behavior for level conditions - */ - private static void setupRandomizeListener(JCheckBox randomizeCheckBox, JSpinner mainSpinner, - JSpinner minSpinner, JSpinner maxSpinner, - JRadioButton relativeButton, JPanel minMaxPanel, - JPanel parentPanel) { - randomizeCheckBox.addChangeListener(e -> { - minMaxPanel.setVisible(randomizeCheckBox.isSelected()); - mainSpinner.setEnabled(!randomizeCheckBox.isSelected()); - - // If enabling randomize, set min/max from current value - if (randomizeCheckBox.isSelected()) { - int value = (Integer) mainSpinner.getValue(); - if (relativeButton.isSelected()) { - // For relative mode, set reasonable min/max values - minSpinner.setValue(Math.max(1, value - 1)); - maxSpinner.setValue(Math.min(99, value + 1)); - } else { - // For absolute mode, set wider range - minSpinner.setValue(Math.max(1, value - 5)); - maxSpinner.setValue(Math.min(99, value + 5)); - } - } - - parentPanel.revalidate(); - parentPanel.repaint(); - }); - } - - /** - * Sets up the randomize checkbox behavior for XP conditions - */ - private static void setupXpRandomizeListener(JCheckBox randomizeCheckBox, JSpinner xpSpinner, - JSpinner minSpinner, JSpinner maxSpinner, - JPanel minMaxPanel, JPanel parentPanel) { - randomizeCheckBox.addChangeListener(e -> { - minMaxPanel.setVisible(randomizeCheckBox.isSelected()); - xpSpinner.setEnabled(!randomizeCheckBox.isSelected()); - - // If enabling randomize, set min/max from current XP - if (randomizeCheckBox.isSelected()) { - int xp = (Integer) xpSpinner.getValue(); - // Set min/max values based on percentage of target XP - int variation = Math.max(1000, xp / 5); // 20% variation or at least 1000 XP - minSpinner.setValue(Math.max(1, xp - variation)); - maxSpinner.setValue(Math.min(200000000, xp + variation)); - } - - parentPanel.revalidate(); - parentPanel.repaint(); - }); - } - - /** - * Sets up validation for min/max spinners to ensure min <= max - */ - private static void setupMinMaxValidation(JSpinner minSpinner, JSpinner maxSpinner) { - // Ensure min doesn't exceed max - minSpinner.addChangeListener(e -> { - int min = (Integer) minSpinner.getValue(); - int max = (Integer) maxSpinner.getValue(); - - if (min > max) { - maxSpinner.setValue(min); - } - }); - - // Ensure max doesn't go below min - maxSpinner.addChangeListener(e -> { - int min = (Integer) minSpinner.getValue(); - int max = (Integer) maxSpinner.getValue(); - - if (max < min) { - minSpinner.setValue(max); - } - }); - } - - /** - * Adds a description label to the panel - */ - private static void addDescriptionLabel(JPanel panel, GridBagConstraints gbc, boolean stopConditionPanel) { - gbc.gridy++; - JLabel descriptionLabel; - if (stopConditionPanel) { - descriptionLabel = new JLabel("Plugin will stop when skill reaches target level"); - } else { - descriptionLabel = new JLabel("Plugin will only start when skill is at or above target level"); - } - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - } - - /** - * Stores components in the configPanel for later access - */ - private static void storeConfigComponents(JPanel configPanel, JComboBox skillComboBox, - JSpinner levelSpinner, JSpinner minSpinner, JSpinner maxSpinner, - JCheckBox randomizeCheckBox, JRadioButton relativeButton, - JRadioButton absoluteButton, JLabel targetLabel) { - configPanel.putClientProperty("skillComboBox", skillComboBox); - configPanel.putClientProperty("levelSpinner", levelSpinner); - configPanel.putClientProperty("minLevelSpinner", minSpinner); - configPanel.putClientProperty("maxLevelSpinner", maxSpinner); - configPanel.putClientProperty("randomizeSkillLevel", randomizeCheckBox); - configPanel.putClientProperty("relativeMode", relativeButton); - configPanel.putClientProperty("absoluteMode", absoluteButton); - configPanel.putClientProperty("skillLevelLabel", targetLabel); - } - - /** - * Update min/max spinner ranges based on mode - */ - private static void updateMinMaxRanges(JSpinner levelSpinner, JSpinner minSpinner, JSpinner maxSpinner) { - int currentValue = (Integer) levelSpinner.getValue(); - - // Ensure all spinners use consistent min/max values - SpinnerNumberModel levelModel = (SpinnerNumberModel) levelSpinner.getModel(); - SpinnerNumberModel minModel = (SpinnerNumberModel) minSpinner.getModel(); - SpinnerNumberModel maxModel = (SpinnerNumberModel) maxSpinner.getModel(); - - levelModel.setMinimum(1); - levelModel.setMaximum(99); - minModel.setMinimum(1); - minModel.setMaximum(99); - maxModel.setMinimum(1); - maxModel.setMaximum(99); - - // Update range based on current value - minSpinner.setValue(Math.max(1, Math.min(currentValue - 1, 98))); - maxSpinner.setValue(Math.min(99, Math.max(currentValue + 1, 2))); - } - - /** - * Creates a skill level condition based on UI components - */ - public static SkillLevelCondition createSkillLevelCondition(JPanel configPanel) { - JComboBox skillComboBox = (JComboBox) configPanel.getClientProperty("skillComboBox"); - if (skillComboBox == null) { - throw new IllegalStateException("Skill combo box not found. Please check the panel configuration."); - } - - JCheckBox randomizeCheckBox = (JCheckBox) configPanel.getClientProperty("randomizeSkillLevel"); - if (randomizeCheckBox == null) { - throw new IllegalStateException("Randomize checkbox not found. Please check the panel configuration."); - } - - JRadioButton relativeButton = (JRadioButton) configPanel.getClientProperty("relativeMode"); - if (relativeButton == null) { - throw new IllegalStateException("Mode buttons not found. Please check the panel configuration."); - } - - boolean isRelative = relativeButton.isSelected(); - - String skillName = (String) skillComboBox.getSelectedItem(); - if (skillName == null) { - // Provide a default if somehow no skill is selected - skillName = Skill.ATTACK.getName(); - } - - Skill skill = null; - if (skillName.equals("Total")) { - skill = null; - } else { - skill = Skill.valueOf(skillName.toUpperCase()); - } - - if (randomizeCheckBox.isSelected()) { - JSpinner minLevelSpinner = (JSpinner) configPanel.getClientProperty("minLevelSpinner"); - JSpinner maxLevelSpinner = (JSpinner) configPanel.getClientProperty("maxLevelSpinner"); - - if (minLevelSpinner == null || maxLevelSpinner == null) { - throw new IllegalStateException("Min/max level spinners not found. Please check the panel configuration."); - } - - int minLevel = (Integer) minLevelSpinner.getValue(); - int maxLevel = (Integer) maxLevelSpinner.getValue(); - - if (isRelative) { - return SkillLevelCondition.createRelativeRandomized(skill, minLevel, maxLevel); - } else { - return SkillLevelCondition.createRandomized(skill, minLevel, maxLevel); - } - } else { - JSpinner levelSpinner = (JSpinner) configPanel.getClientProperty("levelSpinner"); - if (levelSpinner == null) { - throw new IllegalStateException("Level spinner not found. Please check the panel configuration."); - } - - int level = (Integer) levelSpinner.getValue(); - - if (isRelative) { - return SkillLevelCondition.createRelative(skill, level); - } else { - return new SkillLevelCondition(skill, level); - } - } - } - - /** - * Creates a skill XP condition based on UI components - */ - public static SkillXpCondition createSkillXpCondition(JPanel configPanel) { - JComboBox skillComboBox = (JComboBox) configPanel.getClientProperty("xpSkillComboBox"); - if (skillComboBox == null) { - throw new IllegalStateException("XP skill combo box not found. Please check the panel configuration."); - } - - JCheckBox randomizeCheckBox = (JCheckBox) configPanel.getClientProperty("randomizeSkillXp"); - if (randomizeCheckBox == null) { - throw new IllegalStateException("Randomize XP checkbox not found. Please check the panel configuration."); - } - - JRadioButton relativeButton = (JRadioButton) configPanel.getClientProperty("xpRelativeMode"); - if (relativeButton == null) { - throw new IllegalStateException("XP mode buttons not found. Please check the panel configuration."); - } - - boolean isRelative = relativeButton.isSelected(); - - String skillName = (String) skillComboBox.getSelectedItem(); - if (skillName == null) { - // Provide a default if somehow no skill is selected - skillName = Skill.ATTACK.getName(); - } - - Skill skill = null; - if (skillName.equals("Total")) { - skill = null; - } else { - skill = Skill.valueOf(skillName.toUpperCase()); - } - - if (randomizeCheckBox.isSelected()) { - JSpinner minXpSpinner = (JSpinner) configPanel.getClientProperty("minXpSpinner"); - JSpinner maxXpSpinner = (JSpinner) configPanel.getClientProperty("maxXpSpinner"); - - if (minXpSpinner == null || maxXpSpinner == null) { - throw new IllegalStateException("Min/max XP spinners not found. Please check the panel configuration."); - } - - int minXp = (Integer) minXpSpinner.getValue(); - int maxXp = (Integer) maxXpSpinner.getValue(); - - if (isRelative) { - return SkillXpCondition.createRelativeRandomized(skill, minXp, maxXp); - } else { - return SkillXpCondition.createRandomized(skill, minXp, maxXp); - } - } else { - JSpinner xpSpinner = (JSpinner) configPanel.getClientProperty("xpSpinner"); - if (xpSpinner == null) { - throw new IllegalStateException("XP spinner not found. Please check the panel configuration."); - } - - int xp = (Integer) xpSpinner.getValue(); - - if (isRelative) { - return SkillXpCondition.createRelative(skill, xp); - } else { - return new SkillXpCondition(skill, xp); - } - } - } - - /** - * Sets up UI components based on an existing condition - */ - public static void setupSkillCondition(JPanel panel, Condition condition) { - if (condition == null) { - return; - } - - if (condition instanceof SkillLevelCondition) { - setupSkillLevelCondition(panel, (SkillLevelCondition) condition); - } else if (condition instanceof SkillXpCondition) { - setupSkillXpCondition(panel, (SkillXpCondition) condition); - } - } - - /** - * Sets up UI for an existing skill level condition - */ - private static void setupSkillLevelCondition(JPanel panel, SkillLevelCondition condition) { - JComboBox skillComboBox = (JComboBox) panel.getClientProperty("skillComboBox"); - JSpinner levelSpinner = (JSpinner) panel.getClientProperty("levelSpinner"); - JSpinner minLevelSpinner = (JSpinner) panel.getClientProperty("minLevelSpinner"); - JSpinner maxLevelSpinner = (JSpinner) panel.getClientProperty("maxLevelSpinner"); - JCheckBox randomizeCheckBox = (JCheckBox) panel.getClientProperty("randomizeSkillLevel"); - JRadioButton relativeButton = (JRadioButton) panel.getClientProperty("relativeMode"); - JRadioButton absoluteButton = (JRadioButton) panel.getClientProperty("absoluteMode"); - JLabel levelLabel = (JLabel) panel.getClientProperty("skillLevelLabel"); - - if (skillComboBox != null) { - Skill skill = condition.getSkill(); - String skillName = skill == null ? "Total" : skill.getName(); - skillComboBox.setSelectedItem(skillName); - } - - // Set mode - if (relativeButton != null && absoluteButton != null) { - boolean isRelative = condition.isRelative(); - relativeButton.setSelected(isRelative); - absoluteButton.setSelected(!isRelative); - - // Update label text based on mode - if (levelLabel != null) { - levelLabel.setText(isRelative ? "Levels to gain:" : "Target level:"); - } - } - - if (randomizeCheckBox != null) { - boolean isRandomized = condition.isRandomized(); - randomizeCheckBox.setSelected(isRandomized); - - if (isRandomized) { - if (minLevelSpinner != null && maxLevelSpinner != null) { - minLevelSpinner.setValue(condition.getTargetLevelMin()); - maxLevelSpinner.setValue(condition.getTargetLevelMax()); - } - } else if (levelSpinner != null) { - levelSpinner.setValue(condition.getCurrentTargetLevel()); - } - } - } - - /** - * Sets up UI for an existing skill XP condition - */ - private static void setupSkillXpCondition(JPanel panel, SkillXpCondition condition) { - JComboBox skillComboBox = (JComboBox) panel.getClientProperty("xpSkillComboBox"); - JSpinner xpSpinner = (JSpinner) panel.getClientProperty("xpSpinner"); - JSpinner minXpSpinner = (JSpinner) panel.getClientProperty("minXpSpinner"); - JSpinner maxXpSpinner = (JSpinner) panel.getClientProperty("maxXpSpinner"); - JCheckBox randomizeCheckBox = (JCheckBox) panel.getClientProperty("randomizeSkillXp"); - JRadioButton relativeButton = (JRadioButton) panel.getClientProperty("xpRelativeMode"); - JRadioButton absoluteButton = (JRadioButton) panel.getClientProperty("xpAbsoluteMode"); - JLabel xpLabel = (JLabel) panel.getClientProperty("xpTargetLabel"); - - if (skillComboBox != null) { - Skill skill = condition.getSkill(); - String skillName = skill == null ? "Total" : skill.getName(); - skillComboBox.setSelectedItem(skillName); - } - - // Set mode - if (relativeButton != null && absoluteButton != null) { - boolean isRelative = condition.isRelative(); - relativeButton.setSelected(isRelative); - absoluteButton.setSelected(!isRelative); - - // Update label text based on mode - if (xpLabel != null) { - xpLabel.setText(isRelative ? "XP to gain:" : "Target XP:"); - } - } - - if (randomizeCheckBox != null) { - boolean isRandomized = condition.isRandomized(); - randomizeCheckBox.setSelected(isRandomized); - - if (isRandomized) { - if (minXpSpinner != null && maxXpSpinner != null) { - minXpSpinner.setValue((int)condition.getTargetXpMin()); - maxXpSpinner.setValue((int)condition.getTargetXpMax()); - } - } else if (xpSpinner != null) { - xpSpinner.setValue((int)condition.getCurrentTargetXp()); - } - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/DayOfWeekCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/DayOfWeekCondition.java deleted file mode 100644 index 84b6f12e1ae..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/DayOfWeekCondition.java +++ /dev/null @@ -1,1057 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.events.GameTick; -import net.runelite.client.eventbus.Subscribe; -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.OrCondition; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; - -import java.time.DayOfWeek; -import java.time.Duration; -import java.time.LocalDate; - -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.temporal.ChronoUnit; - -import java.time.temporal.WeekFields; -import java.util.*; -import java.util.stream.Collectors; - -/** - * Condition that is met on specific days of the week. - * This allows scheduling tasks to run only on certain days. - */ -@Getter -@Slf4j -@EqualsAndHashCode(callSuper = true) -public class DayOfWeekCondition extends TimeCondition { - - public static String getVersion() { - return "0.0.2"; - } - private final Set activeDays; - private final long maxRepeatsPerDay; - private final long maxRepeatsPerWeek; - - @Getter - @Setter - private transient Map dailyResetCounts = new HashMap<>(); - @Getter - @Setter - private transient Map weeklyResetCounts = new HashMap<>(); - - private final AndCondition combinedCondition; - public void setIntervalCondition(IntervalCondition intervalCondition) { - combinedCondition.addCondition(intervalCondition); - } - public Optional getIntervalCondition() { - assert(combinedCondition.getConditions().size() >= 1 && combinedCondition.getConditions().size() <= 2); - for (Condition condition : combinedCondition.getConditions()) { - if (condition instanceof IntervalCondition) { - return Optional.of((IntervalCondition) condition); - } - } - return Optional.empty(); - } - public boolean hasIntervalCondition() { - return getIntervalCondition().isPresent(); - } - /** - * Creates a day of week condition for the specified days. - * This condition will be satisfied when the current day of the week matches any day in the provided set. - * - * @param maximumNumberOfRepeats The maximum number of times this condition can trigger (0 for unlimited) - * @param activeDays The set of days on which this condition should be active - */ - public DayOfWeekCondition(long maximumNumberOfRepeats, Set activeDays) { - this(maximumNumberOfRepeats, 0, 0, activeDays); - } - - /** - * Creates a day of week condition for the specified days with limits on per-day usage. - * This condition will be satisfied when the current day of the week matches any day in - * the provided set and hasn't exceeded the daily repeat limit. - * - * @param maximumNumberOfRepeats The maximum number of times this condition can trigger overall (0 for unlimited) - * @param maxRepeatsPerDay The maximum number of times this condition can trigger per day (0 for unlimited) - * @param activeDays The set of days on which this condition should be active - */ - public DayOfWeekCondition(long maximumNumberOfRepeats, long maxRepeatsPerDay, Set activeDays) { - this(maximumNumberOfRepeats, maxRepeatsPerDay, 0, activeDays); - } - - /** - * Creates a day of week condition for the specified days with limits on per-day and per-week usage. - * - * @param maximumNumberOfRepeats The maximum number of times this condition can trigger overall (0 for unlimited) - * @param maxRepeatsPerDay The maximum number of times this condition can trigger per day (0 for unlimited) - * @param maxRepeatsPerWeek The maximum number of times this condition can trigger per week (0 for unlimited) - * @param activeDays The set of days on which this condition should be active - */ - public DayOfWeekCondition(long maximumNumberOfRepeats, long maxRepeatsPerDay, long maxRepeatsPerWeek, Set activeDays) { - super(maximumNumberOfRepeats); - this.activeDays = EnumSet.copyOf(activeDays); - this.maxRepeatsPerDay = maxRepeatsPerDay; - this.maxRepeatsPerWeek = maxRepeatsPerWeek; - this.dailyResetCounts = new HashMap<>(); - this.weeklyResetCounts = new HashMap<>(); - updateNextTriggerDay(getNow()); - this.combinedCondition = new AndCondition(); - this.combinedCondition.addCondition(this); - } - - /** - * Creates a day of week condition for the specified days. - * This condition will be satisfied when the current day of the week matches any of the provided days. - * - * @param maximumNumberOfRepeats The maximum number of times this condition can trigger (0 for unlimited) - * @param days The array of days on which this condition should be active - */ - public DayOfWeekCondition(long maximumNumberOfRepeats, DayOfWeek... days) { - this(maximumNumberOfRepeats, 0, 0, days); - } - - /** - * Creates a day of week condition for the specified days with limits on per-day usage. - * - * @param maximumNumberOfRepeats The maximum number of times this condition can trigger overall (0 for unlimited) - * @param maxRepeatsPerDay The maximum number of times this condition can trigger per day (0 for unlimited) - * @param days The array of days on which this condition should be active - */ - public DayOfWeekCondition(long maximumNumberOfRepeats, long maxRepeatsPerDay, DayOfWeek... days) { - this(maximumNumberOfRepeats, maxRepeatsPerDay, 0, days); - } - - /** - * Creates a day of week condition for the specified days with limits on per-day and per-week usage. - * - * @param maximumNumberOfRepeats The maximum number of times this condition can trigger overall (0 for unlimited) - * @param maxRepeatsPerDay The maximum number of times this condition can trigger per day (0 for unlimited) - * @param maxRepeatsPerWeek The maximum number of times this condition can trigger per week (0 for unlimited) - * @param days The array of days on which this condition should be active - */ - public DayOfWeekCondition(long maximumNumberOfRepeats, long maxRepeatsPerDay, long maxRepeatsPerWeek, DayOfWeek... days) { - super(maximumNumberOfRepeats); - this.activeDays = EnumSet.noneOf(DayOfWeek.class); - this.activeDays.addAll(Arrays.asList(days)); - this.maxRepeatsPerDay = maxRepeatsPerDay; - this.maxRepeatsPerWeek = maxRepeatsPerWeek; - this.dailyResetCounts = new HashMap<>(); - this.weeklyResetCounts = new HashMap<>(); - updateNextTriggerDay(getNow()); - this.combinedCondition = new AndCondition(); - this.combinedCondition.addCondition(this); - } - - /** - * Creates a condition for weekdays (Monday through Friday) - */ - public static DayOfWeekCondition weekdays() { - return new DayOfWeekCondition(0, - DayOfWeek.MONDAY, - DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, - DayOfWeek.FRIDAY); - } - - /** - * Creates a condition for weekends (Saturday and Sunday) - */ - public static DayOfWeekCondition weekends() { - return new DayOfWeekCondition(0, - DayOfWeek.SATURDAY, - DayOfWeek.SUNDAY); - } - - /** - * Creates a condition for weekdays with specified daily limits - */ - public static DayOfWeekCondition weekdaysWithDailyLimit(long maxRepeatsPerDay) { - return new DayOfWeekCondition(0, maxRepeatsPerDay, - DayOfWeek.MONDAY, - DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, - DayOfWeek.FRIDAY); - } - - /** - * Creates a condition for weekends with specified daily limits - */ - public static DayOfWeekCondition weekendsWithDailyLimit(long maxRepeatsPerDay) { - return new DayOfWeekCondition(0, maxRepeatsPerDay, - DayOfWeek.SATURDAY, - DayOfWeek.SUNDAY); - } - - /** - * Creates a condition for weekdays with specified weekly limit - */ - public static DayOfWeekCondition weekdaysWithWeeklyLimit(long maxRepeatsPerWeek) { - return new DayOfWeekCondition(0, 0, maxRepeatsPerWeek, - DayOfWeek.MONDAY, - DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, - DayOfWeek.FRIDAY); - } - - /** - * Creates a condition for weekends with specified weekly limit - */ - public static DayOfWeekCondition weekendsWithWeeklyLimit(long maxRepeatsPerWeek) { - return new DayOfWeekCondition(0, 0, maxRepeatsPerWeek, - DayOfWeek.SATURDAY, - DayOfWeek.SUNDAY); - } - - /** - * Creates a condition for all days with specified daily and weekly limits - */ - public static DayOfWeekCondition allDaysWithLimits(long maxRepeatsPerDay, long maxRepeatsPerWeek) { - return new DayOfWeekCondition(0, maxRepeatsPerDay, maxRepeatsPerWeek, - DayOfWeek.MONDAY, - DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, - DayOfWeek.FRIDAY, - DayOfWeek.SATURDAY, - DayOfWeek.SUNDAY); - } - - /** - * Creates a day of week condition from a ZonedDateTime, using its day of week. - * - * @param dateTime The date/time to extract day of week from - * @return A condition for that specific day of the week - */ - public static DayOfWeekCondition fromZonedDateTime(ZonedDateTime dateTime) { - DayOfWeek day = dateTime.getDayOfWeek(); - return new DayOfWeekCondition(0, day); - } - - /** - * Creates a randomized DayOfWeekCondition with a random number of consecutive days. - * - * @param minDays Minimum number of consecutive days - * @param maxDays Maximum number of consecutive days - * @return A condition with randomly selected consecutive days - */ - public static DayOfWeekCondition createRandomized(int minDays, int maxDays) { - int numDays = Rs2Random.between(minDays, maxDays); - numDays = Math.min(numDays, 7); // Cap at 7 days - - // Randomly pick a starting day - int startDayIndex = Rs2Random.between(0, 6); - Set selectedDays = EnumSet.noneOf(DayOfWeek.class); - - for (int i = 0; i < numDays; i++) { - int dayIndex = (startDayIndex + i) % 7; - selectedDays.add(DayOfWeek.of(dayIndex == 0 ? 7 : dayIndex)); // DayOfWeek is 1-based - } - - return new DayOfWeekCondition(0, selectedDays); - } - - /** - * {@inheritDoc} - * Determines if the condition is currently satisfied based on whether the current - * day of the week is in the set of active days and the condition can still trigger. - * Also checks if the daily and weekly reset counts haven't exceeded the maximum allowed. - * - * @return true if today is one of the active days and neither the overall, daily, nor weekly - * limits have been exceeded, false otherwise - */ - @Override - public boolean isSatisfied() { - return isSatisfiedAt(getNextTriggerTimeWithPause().orElse(getNow())); - } - @Override - public boolean isSatisfiedAt(ZonedDateTime triggerAt) { - if (isPaused()) { - return false; - } - // First make sure this condition can trigger again - if (!canTriggerAgain()) { - return false; - } - - // Check if today is an active day - ZonedDateTime now = getNow(); - DayOfWeek today = now.getDayOfWeek(); - if (!activeDays.contains(today)) { - return false; - } - - // Check daily reset count hasn't been exceeded - LocalDate todayDate = now.toLocalDate(); - int todayCount = dailyResetCounts.getOrDefault(todayDate, 0); - if (maxRepeatsPerDay > 0 && todayCount >= maxRepeatsPerDay) { - return false; - } - - // Check weekly reset count hasn't been exceeded - if (maxRepeatsPerWeek > 0) { - int currentWeek = getCurrentWeekNumber(now); - int weekCount = weeklyResetCounts.getOrDefault(currentWeek, 0); - if (weekCount >= maxRepeatsPerWeek) { - return false; - } - } - - // If we have an interval condition, it must also be satisfied - IntervalCondition intervalCondition = getIntervalCondition().orElse(null); - if (intervalCondition != null && !intervalCondition.isSatisfied()) { - return false; - } - - return true; - } - - /** - * {@inheritDoc} - * Provides a user-friendly description of this condition, showing which days are active. - * Special cases are handled for "Every day", "Weekdays", and "Weekends". - * - * @return A human-readable string describing the active days for this condition - */ - @Override - public String getDescription() { - if (activeDays.isEmpty()) { - return "No active days"; - } - - String daysDescription; - if (activeDays.size() == 7) { - daysDescription = "Every day"; - } else if (activeDays.size() == 5 && - activeDays.contains(DayOfWeek.MONDAY) && - activeDays.contains(DayOfWeek.TUESDAY) && - activeDays.contains(DayOfWeek.WEDNESDAY) && - activeDays.contains(DayOfWeek.THURSDAY) && - activeDays.contains(DayOfWeek.FRIDAY)) { - daysDescription = "Weekdays"; - } else if (activeDays.size() == 2 && - activeDays.contains(DayOfWeek.SATURDAY) && - activeDays.contains(DayOfWeek.SUNDAY)) { - daysDescription = "Weekends"; - } else { - daysDescription = "On " + activeDays.stream() - .map(day -> day.toString().charAt(0) + day.toString().substring(1).toLowerCase()) - .collect(Collectors.joining(", ")); - } - - StringBuilder sb = new StringBuilder(daysDescription); - - if (maxRepeatsPerDay > 0) { - sb.append(" (max ").append(maxRepeatsPerDay).append(" per day)"); - } - - if (maxRepeatsPerWeek > 0) { - sb.append(" (max ").append(maxRepeatsPerWeek).append(" per week)"); - } - - return sb.toString(); - } - - /** - * Provides a detailed description of the condition, including the current day, - * whether today is active, the next upcoming active day, progress percentage, - * and basic condition information. - * - * @return A detailed human-readable description with status information - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - sb.append(getDescription()); - - ZonedDateTime now = getNow(); - DayOfWeek today = now.getDayOfWeek(); - LocalDate todayDate = now.toLocalDate(); - int currentWeek = getCurrentWeekNumber(now); - - sb.append("\nToday is ").append(today.toString().charAt(0) + today.toString().substring(1).toLowerCase()); - sb.append(" (").append(activeDays.contains(today) ? "active" : "inactive").append(")"); - - // Daily and weekly usage - if (activeDays.contains(today)) { - int todayUsage = dailyResetCounts.getOrDefault(todayDate, 0); - sb.append("\nToday's usage: ").append(todayUsage); - if (maxRepeatsPerDay > 0) { - sb.append("/").append(maxRepeatsPerDay); - } - - int weekUsage = weeklyResetCounts.getOrDefault(currentWeek, 0); - sb.append("\nThis week's usage: ").append(weekUsage); - if (maxRepeatsPerWeek > 0) { - sb.append("/").append(maxRepeatsPerWeek); - } - } - - // Show next trigger day if today is not active or limits are reached - boolean dailyLimitReached = maxRepeatsPerDay > 0 && - dailyResetCounts.getOrDefault(todayDate, 0) >= maxRepeatsPerDay; - boolean weeklyLimitReached = maxRepeatsPerWeek > 0 && - weeklyResetCounts.getOrDefault(currentWeek, 0) >= maxRepeatsPerWeek; - - if (!activeDays.contains(today) || dailyLimitReached || weeklyLimitReached) { - if (getNextTriggerTimeWithPause().orElse(null) != null) { - DayOfWeek nextDay = getNextTriggerTimeWithPause().get().getDayOfWeek(); - long daysUntil = ChronoUnit.DAYS.between(now.toLocalDate(), getNextTriggerTimeWithPause().get().toLocalDate()); - - sb.append("\nNext active day: ") - .append(nextDay.toString().charAt(0) + nextDay.toString().substring(1).toLowerCase()) - .append(" (in ").append(daysUntil).append(daysUntil == 1 ? " day)" : " days)"); - - if (weeklyLimitReached) { - // Calculate days until next week starts - LocalDate today_date = now.toLocalDate(); - LocalDate startOfNextWeek = today_date.plusDays(8 - today.getValue()); // Monday is 1, Sunday is 7 - long daysUntilNextWeek = ChronoUnit.DAYS.between(today_date, startOfNextWeek); - - sb.append("\nWeekly limit reached. New week starts in ").append(daysUntilNextWeek) - .append(daysUntilNextWeek == 1 ? " day" : " days"); - } - } - } - - // Include interval condition details if present - getIntervalCondition().ifPresent(intervalCondition -> { - sb.append("\n\nInterval condition: ").append(intervalCondition.getDescription()); - sb.append("\nInterval satisfied: ").append(intervalCondition.isSatisfied()); - - // Add next trigger time if available - intervalCondition.getCurrentTriggerTime().ifPresent(triggerTime -> { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - sb.append("\nNext interval trigger: ").append(triggerTime.format(formatter)); - - if (now.isBefore(triggerTime)) { - Duration timeUntil = Duration.between(now, triggerTime); - long seconds = timeUntil.getSeconds(); - sb.append(" (in ").append(String.format("%02d:%02d:%02d", - seconds / 3600, (seconds % 3600) / 60, seconds % 60)).append(")"); - } else { - sb.append(" (ready now)"); - } - }); - }); - - sb.append("\nProgress: ").append(String.format("%.1f%%", getProgressPercentage())); - sb.append("\n").append(super.getDescription()); - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - - // Basic information - sb.append("DayOfWeekCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Active Days: ").append(getDescription()).append("\n"); - if (maxRepeatsPerDay > 0) { - sb.append(" │ Max Repeats Per Day: ").append(maxRepeatsPerDay).append("\n"); - } - if (maxRepeatsPerWeek > 0) { - sb.append(" │ Max Repeats Per Week: ").append(maxRepeatsPerWeek).append("\n"); - } - -// Add interval condition information if present - getIntervalCondition().ifPresent(intervalCondition -> { - sb.append(" │ Interval: ").append(intervalCondition.toString()).append("\n"); - }); - // Staus information - sb.append(" ├─ Status ──────────────────────────────────\n"); - sb.append(" │ Satisfied: ").append(isSatisfied()).append("\n"); - sb.append(" │ Paused: ").append(isPaused()).append("\n"); - - ZonedDateTime now = getNow(); - DayOfWeek today = now.getDayOfWeek(); - LocalDate todayDate = now.toLocalDate(); - int currentWeek = getCurrentWeekNumber(now); - - sb.append(" │ Current Day: ").append(today.toString().charAt(0) + today.toString().substring(1).toLowerCase()) - .append(" (").append(activeDays.contains(today) ? "active" : "inactive").append(")\n"); - - if (activeDays.contains(today)) { - int todayUsage = dailyResetCounts.getOrDefault(todayDate, 0); - sb.append(" │ Today's Usage: ").append(todayUsage); - if (maxRepeatsPerDay > 0) { - sb.append("/").append(maxRepeatsPerDay); - } - sb.append("\n"); - - int weekUsage = weeklyResetCounts.getOrDefault(currentWeek, 0); - sb.append(" │ This Week's Usage: ").append(weekUsage); - if (maxRepeatsPerWeek > 0) { - sb.append("/").append(maxRepeatsPerWeek); - } - sb.append("\n"); - } - - // If not active today or limits reached, show next active day - boolean dailyLimitReached = maxRepeatsPerDay > 0 && - dailyResetCounts.getOrDefault(todayDate, 0) >= maxRepeatsPerDay; - boolean weeklyLimitReached = maxRepeatsPerWeek > 0 && - weeklyResetCounts.getOrDefault(currentWeek, 0) >= maxRepeatsPerWeek; - - if ((!activeDays.contains(today) || dailyLimitReached || weeklyLimitReached) - && getNextTriggerTimeWithPause().get() != null) { - - DayOfWeek nextDay = getNextTriggerTimeWithPause().get().getDayOfWeek(); - long daysUntil = ChronoUnit.DAYS.between(now.toLocalDate(), getNextTriggerTimeWithPause().get().toLocalDate()); - - sb.append(" │ Next Active Day: ") - .append(nextDay.toString().charAt(0) + nextDay.toString().substring(1).toLowerCase()) - .append(" (").append(getNextTriggerTimeWithPause().get().toLocalDate()) - .append(", in ").append(daysUntil).append(daysUntil == 1 ? " day)\n" : " days)\n"); - - if (weeklyLimitReached) { - LocalDate today_date = now.toLocalDate(); - LocalDate startOfNextWeek = today_date.plusDays(8 - today.getValue()); - long daysUntilNextWeek = ChronoUnit.DAYS.between(today_date, startOfNextWeek); - - sb.append(" │ Weekly Limit Reached: New week starts in ") - .append(daysUntilNextWeek).append(daysUntilNextWeek == 1 ? " day\n" : " days\n"); - } - } - - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Tracking info - sb.append(" └─ Tracking ────────────────────────────────\n"); - sb.append(" Reset Count: ").append(currentValidResetCount); - if (this.getMaximumNumberOfRepeats() > 0) { - sb.append("/").append(getMaximumNumberOfRepeats()); - } else { - sb.append(" (unlimited)"); - } - sb.append("\n"); - if (lastValidResetTime != null) { - sb.append(" Last Reset: ").append(lastValidResetTime.format(dateTimeFormatter)).append("\n"); - } - sb.append(" Can Trigger Again: ").append(canTriggerAgain()).append("\n"); - - // If paused, show pause information - if (isPaused()) { - Duration currentPauseDuration = Duration.between(pauseStartTime, getNow()); - sb.append(" Current Pause Duration: ").append(formatDuration(currentPauseDuration)).append("\n"); - } - if (totalPauseDuration.getSeconds() > 0) { - sb.append(" Total Pause Duration: ").append(formatDuration(totalPauseDuration)).append("\n"); - } - - return sb.toString(); - } - - /** - * Updates the tracking of which days have been used and how many times - * Also tracks weekly usage - */ - private void updateDailyResetCount() { - ZonedDateTime now = getNow(); - LocalDate today = now.toLocalDate(); - int currentWeek = getCurrentWeekNumber(now); - - // Increment today's reset count - dailyResetCounts.put(today, dailyResetCounts.getOrDefault(today, 0) + 1); - - // Increment this week's reset count - weeklyResetCounts.put(currentWeek, weeklyResetCounts.getOrDefault(currentWeek, 0) + 1); - - // Clean up old entries (optional, to prevent map growth) - Set oldDates = new HashSet<>(); - for (LocalDate date : dailyResetCounts.keySet()) { - if (ChronoUnit.DAYS.between(date, today) > 14) { // Keep only last two weeks - oldDates.add(date); - } - } - for (LocalDate oldDate : oldDates) { - dailyResetCounts.remove(oldDate); - } - - // Clean up old week entries - Set oldWeeks = new HashSet<>(); - for (Integer week : weeklyResetCounts.keySet()) { - if (week < currentWeek - 2) { // Keep only last few weeks - oldWeeks.add(week); - } - } - for (Integer oldWeek : oldWeeks) { - weeklyResetCounts.remove(oldWeek); - } - } - - /** - * Gets the current ISO week number in the year - */ - private int getCurrentWeekNumber(ZonedDateTime dateTime) { - WeekFields weekFields = WeekFields.of(Locale.getDefault()); - return dateTime.get(weekFields.weekOfWeekBasedYear()); - } - - /** - * Updates the next trigger day based on current day and daily/weekly limits - */ - private void updateNextTriggerDay(ZonedDateTime nextPossibleDateTime) { - - DayOfWeek possibleDay = nextPossibleDateTime.getDayOfWeek(); - LocalDate possibleDate = nextPossibleDateTime.toLocalDate(); - int currentWeek = getCurrentWeekNumber(nextPossibleDateTime); - - // If today is active and hasn't reached limits, it's the trigger day - if (activeDays.contains(possibleDay) && - (maxRepeatsPerDay <= 0 || dailyResetCounts.getOrDefault(possibleDate, 0) < maxRepeatsPerDay) && - (maxRepeatsPerWeek <= 0 || weeklyResetCounts.getOrDefault(currentWeek, 0) < maxRepeatsPerWeek)) { - super.setNextTriggerTime(nextPossibleDateTime.minusSeconds(1)); - - return; - } - - // Check if we've hit the weekly limit but not the daily limit - boolean weeklyLimitReached = (maxRepeatsPerWeek > 0 && - weeklyResetCounts.getOrDefault(currentWeek, 0) >= maxRepeatsPerWeek); - - // If weekly limit reached, find first active day in next week - if (weeklyLimitReached) { - // Start from next Monday (first day of next week) - LocalDate mondayNextWeek = possibleDate.plusDays(8 - possibleDay.getValue()); - ZonedDateTime nextWeekStart = nextPossibleDateTime.with(mondayNextWeek.atStartOfDay()); - - // Find first active day in next week - for (int daysToAdd = 0; daysToAdd < 7; daysToAdd++) { - ZonedDateTime checkDay = nextWeekStart.plusDays(daysToAdd); - if (activeDays.contains(checkDay.getDayOfWeek())) { - super.setNextTriggerTime(checkDay); - return; - } - } - } - - // Otherwise, find the next active day in current week that hasn't reached daily limit - int daysToAdd = 1; - while (daysToAdd <= 7) { - ZonedDateTime checkDay = nextPossibleDateTime.plusDays(daysToAdd); - DayOfWeek checkDayOfWeek = checkDay.getDayOfWeek(); - LocalDate checkDate = checkDay.toLocalDate(); - int checkWeek = getCurrentWeekNumber(checkDay); - - // Skip to next week if current week has reached limit - if (weeklyLimitReached && checkWeek == currentWeek) { - // Skip to Monday of next week - int daysToMonday = 8 - possibleDay.getValue(); - daysToAdd = daysToMonday; - continue; - } - - if (activeDays.contains(checkDayOfWeek) && - (maxRepeatsPerDay <= 0 || dailyResetCounts.getOrDefault(checkDate, 0) < maxRepeatsPerDay) && - (maxRepeatsPerWeek <= 0 || weeklyResetCounts.getOrDefault(checkWeek, 0) < maxRepeatsPerWeek)) { - // Found the next active day that hasn't reached limits - super.setNextTriggerTime( nextPossibleDateTime.plusDays(daysToAdd).truncatedTo(ChronoUnit.DAYS)); - return; - } - - daysToAdd++; - } - - // If no active days found within next 7 days, set to null - super.setNextTriggerTime( null); - - } - - /** - * {@inheritDoc} - * Resets the condition state and increments the reset counter. - * For day of week conditions, this tracks when the condition was last reset, - * updates daily and weekly usage counters, and recalculates the next trigger day. - * - * @param randomize Whether to randomize aspects of the condition (not used for this condition type) - */ - @Override - public void reset(boolean randomize) { - updateValidReset(); - getIntervalCondition() - .ifPresent(IntervalCondition::reset); - updateDailyResetCount(); - updateNextTriggerDay(getNow()); - } - @Override - public void hardReset() { - super.hardReset(); - dailyResetCounts.clear(); - weeklyResetCounts.clear(); - resume(); - updateNextTriggerDay(getNow()); - } - - /** - * {@inheritDoc} - * Calculates a percentage indicating progress toward the next active day. - * If today is an active day and hasn't reached limits, returns 100%. - * Otherwise, returns a percentage based on how close we are to the next active day. - * - * @return A percentage from 0-100 indicating progress toward the next active day - */ - @Override - public double getProgressPercentage() { - if (isSatisfied()) { - return 100.0; - } - - // If no active days, return 0 - if (activeDays.isEmpty()) { - return 0.0; - } - - ZonedDateTime now = getNow(); - DayOfWeek today = now.getDayOfWeek(); - LocalDate todayDate = now.toLocalDate(); - int currentWeek = getCurrentWeekNumber(now); - - // If today is an active day but reached daily limit - if (activeDays.contains(today) && maxRepeatsPerDay > 0) { - int todayCount = dailyResetCounts.getOrDefault(todayDate, 0); - - if (todayCount >= maxRepeatsPerDay) { - // Calculate hours until midnight as progress toward next day - long hoursUntilMidnight = 24 - now.getHour(); - return 100.0 * ((24.0 - hoursUntilMidnight) / 24.0); - } - } - - // If weekly limit reached - if (maxRepeatsPerWeek > 0) { - int weekCount = weeklyResetCounts.getOrDefault(currentWeek, 0); - - if (weekCount >= maxRepeatsPerWeek) { - // Calculate days until next week as progress - int daysUntilNextWeek = 8 - today.getValue(); // Days until next Monday - return 100.0 * ((7.0 - daysUntilNextWeek) / 7.0); - } - } - - // Calculate days until the next active day - if (getNextTriggerTimeWithPause().orElse(null) == null) { - updateNextTriggerDay(getNow()); - } - - if (getNextTriggerTimeWithPause().orElse(null) != null) { - long daysUntil = ChronoUnit.DAYS.between(now.toLocalDate(), getNextTriggerTimeWithPause().get().toLocalDate()); - if (daysUntil == 0) { - return 100.0; // Same day - } else if (daysUntil >= 7) { - return 0.0; // A week or more away - } else { - return 100.0 * (1.0 - (daysUntil / 7.0)); - } - } - - return 0.0; - } - - /** - * {@inheritDoc} - * Calculates the next time this condition will be satisfied. - * This accounts for daily and weekly limits and finding the next valid active day. - * - * @return An Optional containing the time when this condition will next be satisfied, - * or empty if the condition cannot trigger again or has no active days - */ - @Override - public Optional getCurrentTriggerTime() { - if (!canTriggerAgain()) { - return Optional.empty(); - } - - ZonedDateTime now = getNow(); - DayOfWeek today = now.getDayOfWeek(); - LocalDate todayDate = now.toLocalDate(); - int currentWeek = getCurrentWeekNumber(now); - - // If today is active and hasn't reached daily or weekly limits - if (activeDays.contains(today) && - (maxRepeatsPerDay <= 0 || dailyResetCounts.getOrDefault(todayDate, 0) < maxRepeatsPerDay) && - (maxRepeatsPerWeek <= 0 || weeklyResetCounts.getOrDefault(currentWeek, 0) < maxRepeatsPerWeek)) { - IntervalCondition intervalCondition = getIntervalCondition().orElse(null); - if (intervalCondition != null) { - return Optional.of(intervalCondition.getCurrentTriggerTime().orElse(null)); - } - return Optional.of(now.minusSeconds(1)); - - } - - // If no active days defined, return empty - if (activeDays.isEmpty()) { - return Optional.empty(); - } - - // If nextTriggerDay isn't calculated yet or needs update - if (getNextTriggerTimeWithPause().orElse(null) == null) { - updateNextTriggerDay(getNow()); - } - - return getNextTriggerTimeWithPause(); - } - - /** - * Returns the count of resets that have occurred on the given date. - * This can be used to track usage across days. - * - * @param date The date to check - * @return The number of resets that occurred on that date - */ - public int getResetCountForDate(LocalDate date) { - return dailyResetCounts.getOrDefault(date, 0); - } - - /** - * Returns the count of resets that have occurred in the given week. - * This can be used to track usage across weeks. - * - * @param weekNumber The ISO week number to check - * @return The number of resets that occurred in that week - */ - public int getResetCountForWeek(int weekNumber) { - return weeklyResetCounts.getOrDefault(weekNumber, 0); - } - - /** - * Returns the count of resets that have occurred in the current week. - * - * @return The number of resets that occurred in the current week - */ - public int getCurrentWeekResetCount() { - ZonedDateTime now = getNow(); - int currentWeek = getCurrentWeekNumber(now); - return weeklyResetCounts.getOrDefault(currentWeek, 0); - } - - /** - * Checks if this DayOfWeekCondition has reached its daily limit for the current day. - * - * @return true if the daily limit has been reached, false otherwise (including if there's no limit) - */ - public boolean isDailyLimitReached() { - if (maxRepeatsPerDay <= 0) { - return false; // No daily limit - } - - ZonedDateTime now = getNow(); - LocalDate today = now.toLocalDate(); - int todayCount = dailyResetCounts.getOrDefault(today, 0); - - return todayCount >= maxRepeatsPerDay; - } - - /** - * Checks if this DayOfWeekCondition has reached its weekly limit for the current week. - * - * @return true if the weekly limit has been reached, false otherwise (including if there's no limit) - */ - public boolean isWeeklyLimitReached() { - if (maxRepeatsPerWeek <= 0) { - return false; // No weekly limit - } - - ZonedDateTime now = getNow(); - int currentWeek = getCurrentWeekNumber(now); - int weekCount = weeklyResetCounts.getOrDefault(currentWeek, 0); - - return weekCount >= maxRepeatsPerWeek; - } - - /** - * Finds the next day that is NOT an active day. - * This can be useful for determining when the condition will stop being satisfied. - * - * @return An Optional containing the start time of the next non-active day, - * or empty if all days are active - */ - public Optional getNextNonActiveDay() { - ZonedDateTime now = getNow(); - DayOfWeek today = now.getDayOfWeek(); - - // If today is already non-active, return today - if (!activeDays.contains(today)) { - return Optional.of(now.truncatedTo(ChronoUnit.DAYS)); - } - - // Otherwise, find the next non-active day - int daysToAdd = 1; - while (daysToAdd <= 7) { - DayOfWeek nextDay = today.plus(daysToAdd); - if (!activeDays.contains(nextDay)) { - // Found the next non-active day - ZonedDateTime nextNonActiveDay = now.plusDays(daysToAdd) - .truncatedTo(ChronoUnit.DAYS); // Start of the day - return Optional.of(nextNonActiveDay); - } - daysToAdd++; - } - - // If all days are active, return empty - return Optional.empty(); - } - - /** - * Gets the date when the next week starts (Monday) - * - * @return The date of next Monday - */ - public LocalDate getNextWeekStartDate() { - ZonedDateTime now = getNow(); - DayOfWeek today = now.getDayOfWeek(); - return now.toLocalDate().plusDays(8 - today.getValue()); // Monday is 1, Sunday is 7 - } - - /** - * Handles game tick events from the RuneLite event bus. - * This method exists primarily to ensure the condition stays registered with the event bus. - * - * @param tick The game tick event - */ - @Subscribe - public void onGameTick(GameTick tick) { - // Just used to ensure we stay registered with the event bus - } - - /** - * Creates a combined condition that requires both specific days of the week and a time interval. - * This factory method makes it easy to create a schedule that runs for specific durations on certain days. - * - * @param dayCondition The day of week condition that specifies on which days the condition is active - * @param intervalDuration The duration for the interval condition (how long to run) - * @return An AndCondition that combines both conditions - */ - public static DayOfWeekCondition withInterval(DayOfWeekCondition dayCondition, Duration intervalDuration) { - dayCondition.setIntervalCondition( new IntervalCondition(intervalDuration)); - return dayCondition; - } - - /** - * Creates a combined condition that requires both specific days of the week and a randomized time interval. - * - * @param dayCondition The day of week condition that specifies on which days the condition is active - * @param minDuration The minimum duration for the interval - * @param maxDuration The maximum duration for the interval - * @return An AndCondition that combines both conditions - */ - public static DayOfWeekCondition withRandomizedInterval(DayOfWeekCondition dayCondition, - Duration minDuration, - Duration maxDuration) { - dayCondition.setIntervalCondition (IntervalCondition.createRandomized(minDuration, maxDuration)); - return dayCondition; - } - - /** - * Factory method to create a condition that runs only on weekdays with a specified interval. - * - * @param intervalHours The number of hours to run for - * @return A combined condition that runs for the specified duration on weekdays - */ - public static DayOfWeekCondition weekdaysWithHourLimit(int intervalHours) { - return withInterval(weekdays(), Duration.ofHours(intervalHours)); - } - - /** - * Factory method to create a condition that runs only on weekends with a specified interval. - * - * @param intervalHours The number of hours to run for - * @return A combined condition that runs for the specified duration on weekends - */ - public static DayOfWeekCondition weekendsWithHourLimit(int intervalHours) { - return withInterval(weekends(), Duration.ofHours(intervalHours)); - } - - /** - * Factory method to create a condition for specific days with a limit on triggers per day - * and a time limit per session. - * - * @param days The days on which the condition should be active - * @param maxRepeatsPerDay Maximum number of times the condition can trigger per day - * @param sessionDuration How long each session should last - * @return A combined condition with both day and interval constraints - */ - public static DayOfWeekCondition createDailySchedule(Set days, long maxRepeatsPerDay, Duration sessionDuration) { - DayOfWeekCondition dayCondition = new DayOfWeekCondition(0, maxRepeatsPerDay, days); - return withInterval(dayCondition, sessionDuration); - } - - /** - * Factory method to create a condition for specific days with a limit on triggers per day - * and a randomized time limit per session. - * - * @param days The days on which the condition should be active - * @param maxRepeatsPerDay Maximum number of times the condition can trigger per day - * @param minSessionDuration Minimum session duration - * @param maxSessionDuration Maximum session duration - * @return A combined condition with both day and randomized interval constraints - */ - public static DayOfWeekCondition createRandomizedDailySchedule(Set days, - long maxRepeatsPerDay, - Duration minSessionDuration, - Duration maxSessionDuration) { - DayOfWeekCondition dayCondition = new DayOfWeekCondition(0, maxRepeatsPerDay, days); - return withRandomizedInterval(dayCondition, minSessionDuration, maxSessionDuration); - } - - /** - * Factory method to create a humanized play schedule that mimics natural human gaming patterns. - * Creates a schedule that: - * - Plays more on weekends (2 sessions of 1-3 hours each) - * - Plays less on weekdays (1 session of 30min-1.5hrs) - * - * @return A natural-seeming play schedule - */ - public static OrCondition createHumanizedPlaySchedule() { - // Create weekday condition: 1 session per day, 30-90 minutes each - DayOfWeekCondition weekdayCondition = new DayOfWeekCondition(0, 1, EnumSet.of( - DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, DayOfWeek.FRIDAY)); - Condition weekdaySchedule = withRandomizedInterval( - weekdayCondition, - Duration.ofMinutes(30), - Duration.ofMinutes(90)); - - // Create weekend condition: 2 sessions per day, 1-3 hours each - DayOfWeekCondition weekendCondition = new DayOfWeekCondition(0, 2, EnumSet.of( - DayOfWeek.SATURDAY, DayOfWeek.SUNDAY)); - Condition weekendSchedule = withRandomizedInterval( - weekendCondition, - Duration.ofHours(1), - Duration.ofHours(3)); - - // Use OrCondition to combine them - OrCondition combinedSchedule = - new OrCondition(); - combinedSchedule.addCondition(weekdaySchedule); - combinedSchedule.addCondition(weekendSchedule); - - return combinedSchedule; - } - - @Override - protected void onResume(Duration pauseDuration) { - if (isPaused) { - return; - } - ZonedDateTime nextTriggerTimeWithPauseDuration = getNextTriggerTimeWithPause().orElse(null); - if (nextTriggerTimeWithPauseDuration != null) { - // Shift the next trigger time by the pause duration - // getNextTriggerTimeWithPause() provide old next trigger time -> we are resumed.. - nextTriggerTimeWithPauseDuration = nextTriggerTimeWithPauseDuration.plus(pauseDuration); - updateNextTriggerDay(nextTriggerTimeWithPauseDuration); - log.info("DayOfWeekCondition resumed, next trigger time shifted to: {}", getNextTriggerTimeWithPause().get()); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/IntervalCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/IntervalCondition.java deleted file mode 100644 index 5d16378fe14..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/IntervalCondition.java +++ /dev/null @@ -1,781 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.questhelper.requirements.zone.Zone; - -import java.time.Duration; -import java.time.LocalDateTime; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Optional; -import java.util.concurrent.ThreadLocalRandom; - - - -/** - * Condition that is met at regular intervals. - * Can be used for periodic tasks or for creating natural breaks. - */ - -@Slf4j -@EqualsAndHashCode(callSuper = false , exclude = {"nextTriggerTime"}) -public class IntervalCondition extends TimeCondition { - /** - * Version of the IntervalCondition class - */ - public static String getVersion() { - return "0.0.4"; - } - - /** - * The base/average interval between triggers, primarily used for display purposes - */ - @Getter - private final Duration interval; - - /** - * The next time this condition should trigger - */ - //@Getter - //@Setter - //private transient ZonedDateTime nextTriggerTime; - - /** - * The variation factor (0.0-1.0) representing how much intervals can vary from the mean - * For example, 0.2 means intervals can vary by ±20% from the mean value - */ - @Getter - private final double randomFactor; - - /** - * The minimum possible interval duration when randomization is enabled - */ - @Getter - private final Duration minInterval; - - /** - * The maximum possible interval duration when randomization is enabled - */ - @Getter - private final Duration maxInterval; - - /** - * Whether this interval uses randomization (true) or fixed intervals (false) - */ - @Getter - private final boolean randomize; - - /** - * Optional condition for initial delay before first trigger - * When present, this condition must be satisfied before the interval triggers can begin - */ - @Getter - private final SingleTriggerTimeCondition initialDelayCondition; - /** - * Creates an interval condition that triggers at regular intervals - * - * @param interval The time interval between triggers - */ - public IntervalCondition(Duration interval) { - this(interval, false, 0.0, 0); - } - - /** - * Creates an interval condition with optional randomization - * - * @param interval The base time interval - * @param randomize Whether to randomize intervals - * @param variationFactor Variation factor (0-1.0) - how much to vary the interval by percentage - * @param maximumNumberOfRepeats Maximum number of times this condition can trigger - */ - public IntervalCondition(Duration interval, boolean randomize, double variationFactor, long maximumNumberOfRepeats) { - this(interval, randomize, variationFactor, maximumNumberOfRepeats, null); - } - - /** - * Creates an interval condition with optional randomization and initial delay - * - * @param interval The base time interval - * @param randomize Whether to randomize intervals - * @param variationFactor Variation factor (0-1.0) - how much to vary the interval by percentage - * @param maximumNumberOfRepeats Maximum number of times this condition can trigger - * @param initialDelaySeconds Initial delay in seconds before first trigger - */ - public IntervalCondition(Duration interval, boolean randomize, double variationFactor, long maximumNumberOfRepeats, Long initialDelaySeconds) { - super(maximumNumberOfRepeats); - this.interval = interval; - this.randomFactor = Math.max(0, Math.min(1.0, variationFactor)); - - // Set min/max intervals based on variation factor - if (randomize && variationFactor > 0) { - long baseMillis = interval.toMillis(); - long variation = (long) (baseMillis * variationFactor); - this.minInterval = Duration.ofMillis(Math.max(0, baseMillis - variation)); - this.maxInterval = Duration.ofMillis(baseMillis + variation); - this.randomize = true; - } else { - this.minInterval = interval; - this.maxInterval = interval; - this.randomize = false; - } - - // Initialize initial delay if specified - if (initialDelaySeconds != null && initialDelaySeconds > 0) { - this.initialDelayCondition = SingleTriggerTimeCondition.afterDelay(initialDelaySeconds); - } else { - this.initialDelayCondition = null; - } - - setNextTriggerTime (calculateNextTriggerTime()); - } - - /** - * Private constructor with explicit min/max interval values - * - * @param interval The base/average time interval for display purposes - * @param minInterval Minimum possible interval duration - * @param maxInterval Maximum possible interval duration - * @param randomize Whether to randomize intervals - * @param variationFactor Variation factor (0-1.0) representing how much variation is allowed - * @param maximumNumberOfRepeats Maximum number of times this condition can trigger - */ - private IntervalCondition(Duration interval, Duration minInterval, Duration maxInterval, - boolean randomize, double variationFactor, long maximumNumberOfRepeats) { - this(interval, minInterval, maxInterval, randomize, variationFactor, maximumNumberOfRepeats, 0L); - } - - /** - * Private constructor with explicit min/max interval values and initial delay - * - * @param interval The base/average time interval for display purposes - * @param minInterval Minimum possible interval duration - * @param maxInterval Maximum possible interval duration - * @param randomize Whether to randomize intervals - * @param variationFactor Variation factor (0-1.0) representing how much variation is allowed - * @param maximumNumberOfRepeats Maximum number of times this condition can trigger - * @param initialDelaySeconds Initial delay in seconds before first trigger - */ - public IntervalCondition(Duration interval, Duration minInterval, Duration maxInterval, - boolean randomize, double variationFactor, long maximumNumberOfRepeats, - Long initialDelaySeconds) { - super(maximumNumberOfRepeats); - this.interval = interval; - this.randomFactor = Math.max(0, Math.min(1.0, variationFactor)); - this.minInterval = minInterval; - this.maxInterval = maxInterval; - // We consider it randomized if min and max are different - this.randomize = !minInterval.equals(maxInterval); - - // Initialize initial delay if specified - if (initialDelaySeconds != null && initialDelaySeconds > 0) { - this.initialDelayCondition = SingleTriggerTimeCondition.afterDelay(initialDelaySeconds); - } else { - this.initialDelayCondition = null; - } - - setNextTriggerTime(calculateNextTriggerTime()); - } - - /** - * Private constructor with explicit min/max interval values and initial delay - * - * @param interval The base/average time interval for display purposes - * @param minInterval Minimum possible interval duration - * @param maxInterval Maximum possible interval duration - * @param randomize Whether to randomize intervals - * @param variationFactor Variation factor (0-1.0) representing how much variation is allowed - * @param maximumNumberOfRepeats Maximum number of times this condition can trigger - * @param initialDelaySeconds Initial delay in seconds before first trigger - */ - public IntervalCondition(Duration interval, Duration minInterval, Duration maxInterval, - boolean randomize, double variationFactor, long maximumNumberOfRepeats, - SingleTriggerTimeCondition initialDelayCondition) { - super(maximumNumberOfRepeats); - this.interval = interval; - this.randomFactor = Math.max(0, Math.min(1.0, variationFactor)); - this.minInterval = minInterval; - this.maxInterval = maxInterval; - // We consider it randomized if min and max are different - this.randomize = !minInterval.equals(maxInterval); - - // Initialize initial delay if specified - if (initialDelayCondition != null) { - this.initialDelayCondition = initialDelayCondition.copy(); - } else { - this.initialDelayCondition = null; - } - - setNextTriggerTime(calculateNextTriggerTime()); - } - - /** - * Creates an interval condition with minutes - */ - public static IntervalCondition everyMinutes(int minutes) { - return new IntervalCondition(Duration.ofMinutes(minutes)); - } - - /** - * Creates an interval condition with hours - */ - public static IntervalCondition everyHours(int hours) { - return new IntervalCondition(Duration.ofHours(hours)); - } - - /** - * Creates an interval condition with randomized timing using a base time and variation factor - * - * @param baseMinutes The average interval duration in minutes - * @param variationFactor How much the interval can vary (0-1.0, e.g., 0.2 means ±20%) - * @param maximumNumberOfRepeats Maximum number of times this condition can trigger - * @return A randomized interval condition - */ - public static IntervalCondition randomizedMinutesWithVariation(int baseMinutes, double variationFactor, long maximumNumberOfRepeats) { - return new IntervalCondition(Duration.ofMinutes(baseMinutes), true, variationFactor, maximumNumberOfRepeats); - } - - /** - * Creates an interval condition that triggers at intervals between the provided min and max durations. - * - * @param minDuration Minimum interval duration - * @param maxDuration Maximum interval duration - * @return A randomized interval condition - */ - public static IntervalCondition createRandomized(Duration minDuration, Duration maxDuration) { - // Validate inputs - if (minDuration.compareTo(maxDuration) > 0) { - throw new IllegalArgumentException("Minimum duration must be less than or equal to maximum duration"); - } - - // Create an average interval for display purposes - long minMillis = minDuration.toMillis(); - long maxMillis = maxDuration.toMillis(); - Duration avgInterval = Duration.ofMillis((minMillis + maxMillis) / 2); - - // Calculate a randomization factor - represents how much the intervals can vary - // from the mean value (as a percentage of the mean) - double variationFactor = 0.0; - if (minMillis < maxMillis) { - // Calculate as a percentage of the average - long halfRange = (maxMillis - minMillis) / 2; - variationFactor = halfRange / (double) avgInterval.toMillis(); - } - - log.debug("createRandomized: min={}, max={}, avg={}, variationFactor={}", - minDuration, maxDuration, avgInterval, variationFactor); - - return new IntervalCondition(avgInterval, minDuration, maxDuration, true, variationFactor, 0); - } - - /** - * Creates an interval condition with randomized timing using seconds range - * - * @param minSeconds Minimum interval in seconds - * @param maxSeconds Maximum interval in seconds - * @return A randomized interval condition - */ - public static IntervalCondition randomizedSeconds(int minSeconds, int maxSeconds) { - return createRandomized(Duration.ofSeconds(minSeconds), Duration.ofSeconds(maxSeconds)); - } - - /** - * Creates an interval condition with randomized timing using minutes range - * - * @param minMinutes Minimum interval in minutes - * @param maxMinutes Maximum interval in minutes - * @return A randomized interval condition - */ - public static IntervalCondition randomizedMinutes(int minMinutes, int maxMinutes) { - return createRandomized(Duration.ofMinutes(minMinutes), Duration.ofMinutes(maxMinutes)); - } - - /** - * Creates an interval condition with randomized timing using hours range - * - * @param minHours Minimum interval in hours - * @param maxHours Maximum interval in hours - * @return A randomized interval condition - */ - public static IntervalCondition randomizedHours(int minHours, int maxHours) { - return createRandomized(Duration.ofHours(minHours), Duration.ofHours(maxHours)); - } - - /** - * Creates an interval condition with minutes and an initial delay - * - * @param minutes The interval in minutes - * @param initialDelaySeconds The initial delay in seconds before first trigger - */ - public static IntervalCondition everyMinutesWithDelay(int minutes, Long initialDelaySeconds) { - return new IntervalCondition(Duration.ofMinutes(minutes), false, 0.0, 0, initialDelaySeconds); - } - - /** - * Creates an interval condition with hours and an initial delay - * - * @param hours The interval in hours - * @param initialDelaySeconds The initial delay in seconds before first trigger - */ - public static IntervalCondition everyHoursWithDelay(int hours, Long initialDelaySeconds) { - return new IntervalCondition(Duration.ofHours(hours), false, 0.0, 0, initialDelaySeconds); - } - - /** - * Creates an interval condition with randomized timing using a base time and variation factor, - * plus an initial delay before the first trigger - * - * @param baseMinutes The average interval duration in minutes - * @param variationFactor How much the interval can vary (0-1.0, e.g., 0.2 means ±20%) - * @param maximumNumberOfRepeats Maximum number of times this condition can trigger - * @param initialDelaySeconds The initial delay in seconds before first trigger - * @return A randomized interval condition with initial delay - */ - public static IntervalCondition randomizedMinutesWithVariationAndDelay( - int baseMinutes, double variationFactor, long maximumNumberOfRepeats, Long initialDelaySeconds) { - return new IntervalCondition(Duration.ofMinutes(baseMinutes), - true, variationFactor, maximumNumberOfRepeats, initialDelaySeconds); - } - - /** - * Creates an interval condition with randomized timing using seconds range and an initial delay - * - * @param minSeconds Minimum interval in seconds - * @param maxSeconds Maximum interval in seconds - * @param initialDelaySeconds The initial delay in seconds before first trigger - * @return A randomized interval condition with initial delay - */ - public static IntervalCondition randomizedSecondsWithDelay(int minSeconds, int maxSeconds, Long initialDelaySeconds) { - IntervalCondition condition = createRandomized(Duration.ofSeconds(minSeconds), Duration.ofSeconds(maxSeconds)); - return new IntervalCondition( - condition.interval, - condition.minInterval, - condition.maxInterval, - condition.randomize, - condition.randomFactor, - 0, - initialDelaySeconds); - } - - /** - * Creates an interval condition with randomized timing using minutes range and an initial delay - * - * @param minMinutes Minimum interval in minutes - * @param maxMinutes Maximum interval in minutes - * @param initialDelaySeconds The initial delay in seconds before first trigger - * @return A randomized interval condition with initial delay - */ - public static IntervalCondition randomizedMinutesWithDelay(int minMinutes, int maxMinutes, Long initialDelaySeconds) { - IntervalCondition condition = createRandomized(Duration.ofMinutes(minMinutes), Duration.ofMinutes(maxMinutes)); - return new IntervalCondition( - condition.interval, - condition.minInterval, - condition.maxInterval, - condition.randomize, - condition.randomFactor, - 0, - initialDelaySeconds); - } - - /** - * Creates an interval condition with randomized timing using hours range and an initial delay - * - * @param minHours Minimum interval in hours - * @param maxHours Maximum interval in hours - * @param initialDelaySeconds The initial delay in seconds before first trigger - * @return A randomized interval condition with initial delay - */ - public static IntervalCondition randomizedHoursWithDelay(int minHours, int maxHours, Long initialDelaySeconds) { - IntervalCondition condition = createRandomized(Duration.ofHours(minHours), Duration.ofHours(maxHours)); - return new IntervalCondition( - condition.interval, - condition.minInterval, - condition.maxInterval, - condition.randomize, - condition.randomFactor, - 0, - initialDelaySeconds); - } - - @Override - public boolean isSatisfied() { - return isSatisfiedAt(getNextTriggerTimeWithPause().orElse(getNow())); - } - @Override - public boolean isSatisfiedAt(ZonedDateTime triggerTime) { - if (triggerTime == null) { - return false; - } - if(!canTriggerAgain()) { - return false; - } - - // Check if condition is paused (handled by superclass, but adding for clarity) - if (isPaused) { - return false; - } - - // Check initial delay condition first (if exists) - if (initialDelayCondition != null && !initialDelayCondition.isSatisfiedAt(initialDelayCondition.getNextTriggerTimeWithPause().orElse(getNow()))) { - return false; // Initial delay hasn't been met yet - } - - ZonedDateTime now = getNow(); - if (now.isAfter(triggerTime) || now.isEqual(triggerTime)) { - return true; - } - return false; - } - @Override - public String getDescription() { - ZonedDateTime now = getNow(); - String timeLeft = ""; - String initialDelayInfo = ""; - String pauseInfo = isPaused ? " (PAUSED)" : ""; - - // Check initial delay status - if (initialDelayCondition != null && !initialDelayCondition.isSatisfied()) { - Duration initialDelayRemaining = Duration.between(now, initialDelayCondition.getNextTriggerTimeWithPause().orElse(now)); - long seconds = initialDelayRemaining.getSeconds(); - if (seconds > 0) { - initialDelayInfo = String.format(" (initial delay: %02d:%02d:%02d)", - seconds / 3600, (seconds % 3600) / 60, seconds % 60); - } - } - - if (getNextTriggerTimeWithPause().orElse(null) != null && (initialDelayCondition == null || initialDelayCondition.isSatisfied())) { - if (now.isAfter(getNextTriggerTimeWithPause().orElse(getNow()))) { - timeLeft = " (ready now)"; - } else { - Duration remaining = Duration.between(now, getNextTriggerTimeWithPause().orElse(getNow())); - long seconds = remaining.getSeconds(); - timeLeft = String.format(" (next in %02d:%02d:%02d)", - seconds / 3600, (seconds % 3600) / 60, seconds % 60); - } - } - - // The condition was randomized if min and max intervals are different - if (randomize) { - // Show as a range when we have min and max - return String.format("Every %s-%s%s%s%s", - formatDuration(minInterval), - formatDuration(maxInterval), - timeLeft, - initialDelayInfo, - pauseInfo); - } else { - // Fixed interval - return String.format("Every %s%s%s%s", formatDuration(interval), timeLeft, initialDelayInfo, pauseInfo); - } - } - - /** - * Returns a detailed description of the interval condition with additional status information - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - sb.append(getDescription()).append("\n"); - - ZonedDateTime now = getNow(); - if (getNextTriggerTimeWithPause().orElse(null) != null) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - sb.append("Next trigger at: ").append(getNextTriggerTimeWithPause().orElse(getNow()).format(formatter)).append("\n"); - - if (now.isAfter(getNextTriggerTimeWithPause().orElse(getNow()))) { - sb.append("Status: Ready to trigger\n"); - } else { - Duration remaining = Duration.between(now, getNextTriggerTimeWithPause().orElse(getNow())); - long seconds = remaining.getSeconds(); - sb.append("Time remaining: ") - .append(String.format("%02d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, seconds % 60)) - .append("\n"); - } - } - - sb.append("Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - if (randomize) { - sb.append("Randomization: Enabled (±").append(String.format("%.0f", randomFactor * 100)).append("%)\n"); - } - - // Add lastValidResetTime information - if (lastValidResetTime != null && currentValidResetCount > 0) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - sb.append("Last reset: ").append(lastValidResetTime.format(formatter)).append("\n"); - - // Calculate time since the last reset - Duration sinceLastReset = Duration.between(lastValidResetTime, LocalDateTime.now()); - long seconds = sinceLastReset.getSeconds(); - sb.append("Time since last reset: ") - .append(String.format("%02d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, seconds % 60)) - .append("\n"); - } - - sb.append(super.getDescription()); - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - - // Basic information - sb.append("IntervalCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Interval: ").append(formatDuration(interval)).append("\n"); - - // Initial Delay information - if (initialDelayCondition != null) { - sb.append(" │ Initial Delay: "); - ZonedDateTime now = getNow(); - if (initialDelayCondition.isSatisfied()) { - sb.append("Completed\n"); - } else { - Duration initialDelayRemaining = Duration.between(now, initialDelayCondition.getNextTriggerTimeWithPause().orElse(now)); - long seconds = initialDelayRemaining.getSeconds(); - if (seconds > 0) { - sb.append(String.format("%02d:%02d:%02d remaining\n", - seconds / 3600, (seconds % 3600) / 60, seconds % 60)); - } else { - sb.append("Ready\n"); - } - } - } - - // Randomization - sb.append(" ├─ Randomization ────────────────────────────\n"); - if (randomize) { - sb.append(" │ Min Interval: ").append(formatDuration(minInterval)).append("\n"); - sb.append(" │ Max Interval: ").append(formatDuration(maxInterval)).append("\n"); - sb.append(" │ Randomization: Enabled\n"); - sb.append(" │ Random Factor: ±").append(String.format("%.0f%%", randomFactor * 100)).append("\n"); - } else { - sb.append(" │ Randomization: Disabled\n"); - } - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - sb.append(" │ Satisfied: ").append(isSatisfied()).append("\n"); - if (isPaused) { - sb.append(" │ Status: PAUSED\n"); - } - - ZonedDateTime now = getNow(); - if (getNextTriggerTimeWithPause().orElse(null) != null) { - sb.append(" │ Next Trigger: ").append(getNextTriggerTimeWithPause().orElse(getNow()).format(dateTimeFormatter)).append("\n"); - - if (!now.isAfter(getNextTriggerTimeWithPause().orElse(getNow()))) { - Duration remaining = Duration.between(now, getNextTriggerTimeWithPause().orElse(getNow())); - long seconds = remaining.getSeconds(); - sb.append(" │ Time Remaining: ") - .append(String.format("%02d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, seconds % 60)) - .append("\n"); - } else { - sb.append(" │ Ready to trigger\n"); - } - } - - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Tracking info - sb.append(" └─ Tracking ────────────────────────────────\n"); - sb.append(" Reset Count: ").append(currentValidResetCount); - if (this.getMaximumNumberOfRepeats() > 0) { - sb.append("/").append(getMaximumNumberOfRepeats()); - } else { - sb.append(" (unlimited)"); - } - sb.append("\n"); - if (lastValidResetTime != null) { - sb.append(" Last Reset: ").append(lastValidResetTime.format(dateTimeFormatter)).append("\n"); - - // Add time since last reset - Duration sinceLastReset = Duration.between(lastValidResetTime, LocalDateTime.now()); - long seconds = sinceLastReset.getSeconds(); - sb.append(" Time Since Reset: ") - .append(String.format("%02d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, seconds % 60)) - .append("\n"); - } - sb.append(" Can Trigger Again: ").append(canTriggerAgain()).append("\n"); - - return sb.toString(); - } - - @Override - public void reset(boolean randomize) { - updateValidReset(); - setNextTriggerTime(calculateNextTriggerTime()); - this.lastValidResetTime = LocalDateTime.now(); - // Reset initial delay condition if it exists - if (initialDelayCondition != null) { - initialDelayCondition.reset(false); - } - - log.debug("IntervalCondition reset, next trigger at: {}", getNextTriggerTimeWithPause().orElse(null)); - } - @Override - public void hardReset() { - // Reset the condition state - this.currentValidResetCount = 0; - this.lastValidResetTime = LocalDateTime.now(); - - // Reset initial delay condition if it exists - if (initialDelayCondition != null) { - initialDelayCondition.hardReset(); - } - setNextTriggerTime(calculateNextTriggerTime()); - } - - @Override - public double getProgressPercentage() { - - - ZonedDateTime now = getNow(); - if (getNextTriggerTimeWithPause().orElse(null) == null) { - return 0.0; - } - ZonedDateTime nextTriggerTime = getNextTriggerTimeWithPause().orElse(null); - if (now.isAfter(nextTriggerTime)) { - return 100.0; - } - - // Calculate how much time has passed since the last trigger - Duration timeUntilNextTrigger = Duration.between(now, nextTriggerTime); - - // If this is the first trigger (no lastValidResetTime), we need to use the interval - // from initialization to calculate progress - Duration lastInterval; - if (lastValidResetTime == null) { - // Use the average interval for randomized conditions - if (randomize) { - lastInterval = interval; // Average interval - } else { - lastInterval = interval; // Fixed interval - } - } else { - // If we've had a previous trigger, calculate from that time to the next trigger - Duration actualInterval = Duration.between(lastValidResetTime.atZone(getNow().getZone()), nextTriggerTime); - lastInterval = actualInterval; - if(isPaused) { - lastInterval = lastInterval.plus(getCurrentPauseDuration()); - } - } - - - // Calculate ratio of elapsed time - long remainingMillis = timeUntilNextTrigger.toMillis(); - long totalMillis = interval.toMillis(); - - double elapsedRatio = 1.0 - (remainingMillis / (double) totalMillis); - return Math.max(0, Math.min(100, elapsedRatio * 100)); - } - @Override - public Optional getCurrentTriggerTime() { - // If paused or can't trigger again, don't provide a trigger time - if ( getNextTriggerTimeWithPause().orElse(null) == null || !canTriggerAgain()) { - return Optional.empty(); // No trigger time during pause or if already triggered too often - } - - ZonedDateTime now = getNow(); - ZonedDateTime nextTriggerTime = getNextTriggerTimeWithPause().orElse(null); - - if (initialDelayCondition != null && !initialDelayCondition.isSatisfied()) { - return initialDelayCondition.getCurrentTriggerTime(); // Return the initial delay condition's trigger time - } - // If already satisfied (past the trigger time) - if (now.isAfter(nextTriggerTime)) { - return Optional.of(nextTriggerTime); // Return the passed time until reset - } - - // Otherwise return the scheduled next trigger time - return Optional.of(nextTriggerTime); - } - - /** - * Calculates the next trigger time based on the current configuration. - * - * @return The next time this condition should trigger - */ - private ZonedDateTime calculateNextTriggerTime() { - ZonedDateTime now = getNow(); - - // Skip the future interval calculation during initial creation or if can't trigger again - boolean skipFutureInterval = !canTriggerAgain() || this.currentValidResetCount == 0; - Duration nextInterval; - - // Generate a randomized interval if randomization is enabled - if (randomize) { - // Generate a random value between min and max interval - long minMillis = minInterval.toMillis(); - long maxMillis = maxInterval.toMillis(); - long randomMillis = ThreadLocalRandom.current().nextLong(minMillis, maxMillis + 1); - nextInterval = Duration.ofMillis(randomMillis); - - log.debug("Randomized interval: {}ms (between {}ms and {}ms)", - randomMillis, minMillis, maxMillis); - } - // Use fixed interval otherwise - else { - nextInterval = interval; - } - - // For initial creation or when max triggers reached, trigger immediately - if (skipFutureInterval) { - return now; - } - // Otherwise, schedule the next trigger based on the calculated interval - else { - return now.plus(nextInterval); - } - } - - @Override - protected String formatDuration(Duration duration) { - long seconds = duration.getSeconds(); - if (seconds < 60) { - return seconds + "s"; - } else if (seconds < 3600) { - return String.format("%dm %ds", seconds / 60, seconds % 60); - } else { - return String.format("%dh %dm", seconds / 3600, (seconds % 3600) / 60); - } - } - - /** - * Handles what happens when this condition is resumed. - * Shifts the next trigger time by the pause duration to maintain the same - * relative timing after a pause. - * - * @param pauseDuration The duration for which this condition was paused - */ - @Override - protected void onResume(Duration pauseDuration) { - if (isPaused()) { - return; - } - // If there's an initial delay condition, let it handle its own resume - if (initialDelayCondition != null) { - // Only shift if the initial delay hasn't been satisfied yet - if (initialDelayCondition instanceof TimeCondition) { - initialDelayCondition.onResume(pauseDuration); - // If initial delay already implements pause/resume, it will be handled by TimeCondition - } - } - // getNextTriggerTimeWithPause() provide old next trigger time -> we are resumed.. - ZonedDateTime nextTriggerTimeWithPauseDuration = getNextTriggerTimeWithPause().orElse(null); - if (nextTriggerTimeWithPauseDuration != null) { - nextTriggerTimeWithPauseDuration = nextTriggerTimeWithPauseDuration.plus(pauseDuration); - // Shift the next trigger time by the pause duration - setNextTriggerTime(nextTriggerTimeWithPauseDuration); - - if (lastValidResetTime != null) { - lastValidResetTime = lastValidResetTime.plus(pauseDuration); - } - log.debug("IntervalCondition resumed, next trigger time shifted to: {}", getNextTriggerTimeWithPause().get()); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/SingleTriggerTimeCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/SingleTriggerTimeCondition.java deleted file mode 100644 index 51741b84c1a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/SingleTriggerTimeCondition.java +++ /dev/null @@ -1,281 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.events.GameTick; -import net.runelite.client.eventbus.Subscribe; - -import java.time.Duration; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Optional; - -/** - * A time condition that triggers exactly once when the target time is reached. - * After triggering, this condition remains in a "triggered" state until explicitly reset. - * Perfect for one-time scheduled events or deadlines. - */ -@Slf4j -@EqualsAndHashCode(callSuper = false, exclude = {}) -public class SingleTriggerTimeCondition extends TimeCondition { - @Getter - private Duration definedDelay; - @Getter - private long maximumNumberOfRepeats = 1; - public static String getVersion() { - return "0.0.1"; - } - - - private static final DateTimeFormatter FORMATTER = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - public SingleTriggerTimeCondition copy(boolean reset){ - SingleTriggerTimeCondition copy = new SingleTriggerTimeCondition(getNextTriggerTimeWithPause().orElse(getNow()), this.definedDelay, this.maximumNumberOfRepeats); - if (reset) { - copy.hardReset(); - } - return copy; - } - public SingleTriggerTimeCondition copy(){ - SingleTriggerTimeCondition copy = new SingleTriggerTimeCondition(getNextTriggerTimeWithPause().orElse(getNow()), this.definedDelay, this.maximumNumberOfRepeats); - - return copy; - } - /** - * Creates a condition that triggers once at the specified time - * - * @param targetTime The time at which this condition should trigger - */ - public SingleTriggerTimeCondition(ZonedDateTime targetTime, Duration definedDelay, - long maximumNumberOfRepeats) { - super(maximumNumberOfRepeats); // Only allow one trigger - setNextTriggerTime(targetTime); - this.definedDelay = definedDelay; - } - - /** - * Creates a condition that triggers once after the specified delay - * - * @param delaySeconds Number of seconds in the future to trigger - * @return A new SingleTriggerTimeCondition - */ - public static SingleTriggerTimeCondition afterDelay(long delaySeconds) { - ZonedDateTime triggerTime = ZonedDateTime.now(ZoneId.systemDefault()) - .plusSeconds(delaySeconds); - return new SingleTriggerTimeCondition(triggerTime ,Duration.ofSeconds(delaySeconds), 1); - } - - - @Override - public boolean isSatisfied() { - return isSatisfiedAt(getNextTriggerTimeWithPause().orElse(getNow())); - } - @Override - public boolean isSatisfiedAt(ZonedDateTime triggerTime) { - if (isPaused()) { - return false; // Don't trigger if paused - } - // If already triggered, return true - if (hasTriggered()) { - if (!canTriggerAgain()) { - return true; // Only return true once after triggering - } - return false; // Return false after reset and we have triggered before - } - - // Check if current time has passed the target time - ZonedDateTime now = getNow(); - if (now.isAfter(triggerTime) || now.isEqual(triggerTime)) { - log.debug("SingleTriggerTimeCondition triggered at: {}", now.format(FORMATTER)); - return true; - } - - return false; - } - - - @Override - public String getDescription() { - String triggerStatus = hasTriggered() ? "triggered" : "not yet triggered"; - String baseDescription = super.getDescription(); - return String.format("One-time trigger at %s (%s)\n%s", - getNextTriggerTimeWithPause().orElse(getNow()).format(FORMATTER), triggerStatus, baseDescription); - } - - /** - * Returns a detailed description of the single trigger condition with additional status information - */ - @Override - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - ZonedDateTime now = getNow(); - String triggerStatus = hasTriggered() ? "triggered" : "not yet triggered"; - String pauseStatus = isPaused() ? " (PAUSED)" : ""; - - sb.append("One-time trigger at ").append(getNextTriggerTimeWithPause().orElse(getNow()).format(FORMATTER)) - .append(" (").append(triggerStatus).append(")").append(pauseStatus).append("\n"); - - if (!hasTriggered() && !isPaused()) { - if (now.isAfter(getNextTriggerTimeWithPause().orElse(getNow()))) { - sb.append("Ready to trigger now\n"); - } else { - Duration timeUntilTrigger = Duration.between(now, getNextTriggerTimeWithPause().orElse(getNow())); - long seconds = timeUntilTrigger.getSeconds(); - sb.append("Time until trigger: ") - .append(String.format("%02d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, seconds % 60)) - .append("\n"); - } - } else if (isPaused()) { - sb.append("Trigger time is paused and will be adjusted when resumed\n"); - Duration currentPauseDuration = Duration.between(pauseStartTime, now); - sb.append("Current pause duration: ").append(formatDuration(currentPauseDuration)).append("\n"); - } - - sb.append("Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - sb.append(super.getDescription()); - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - - // Basic information - sb.append("SingleTriggerTimeCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Target Time: ").append(getNextTriggerTimeWithPause().orElse(getNow()).format(dateTimeFormatter)).append("\n"); - sb.append(" │ Time Zone: ").append(getNextTriggerTimeWithPause().orElse(getNow()).getZone().getId()).append("\n"); - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - sb.append(" │ Satisfied: ").append(isSatisfied()).append("\n"); - sb.append(" │ Triggered: ").append(hasTriggered()).append("\n"); - sb.append(" │ Paused: ").append(isPaused()).append("\n"); - - ZonedDateTime now = getNow(); - // Only show trigger time info if not paused - if (!hasTriggered() && !isPaused()) { - if (now.isAfter(getNextTriggerTimeWithPause().orElse(getNow()))) { - sb.append(" │ Ready to trigger now\n"); - } else { - Duration timeUntilTrigger = Duration.between(now, getNextTriggerTimeWithPause().orElse(getNow())); - long seconds = timeUntilTrigger.getSeconds(); - sb.append(" │ Time Until Trigger: ") - .append(String.format("%02d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, seconds % 60)) - .append("\n"); - } - } else if (isPaused()) { - sb.append(" │ Trigger time paused and will be adjusted when resumed\n"); - } - - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Tracking info - sb.append(" └─ Tracking ────────────────────────────────\n"); - sb.append(" Reset Count: ").append(currentValidResetCount); - if (this.getMaximumNumberOfRepeats() > 0) { - sb.append("/").append(getMaximumNumberOfRepeats()); - } else { - sb.append(" (unlimited)"); - } - sb.append("\n"); - if (lastValidResetTime != null) { - sb.append(" Last Reset: ").append(lastValidResetTime.format(dateTimeFormatter)).append("\n"); - } - sb.append(" Can Trigger Again: ").append(canTriggerAgain()).append("\n"); - - // If paused, show pause duration - if (isPaused()) { - Duration currentPauseDuration = Duration.between(pauseStartTime, getNow()); - sb.append(" Current Pause Duration: ").append(formatDuration(currentPauseDuration)).append("\n"); - } - if (totalPauseDuration.getSeconds() > 0) { - sb.append(" Total Pause Duration: ").append(formatDuration(totalPauseDuration)).append("\n"); - } - - return sb.toString(); - } - - @Override - public void reset(boolean randomize) { - if (!isSatisfied()) { - return; - } - currentValidResetCount++; - lastValidResetTime = LocalDateTime.now(); - log.debug("SingleTriggerTimeCondition reset, will trigger again at: {}", - getNextTriggerTimeWithPause().orElse(getNow()).format(FORMATTER)); - } - @Override - public void hardReset() { - // Reset the condition state - this.currentValidResetCount = 0; - this.lastValidResetTime = LocalDateTime.now(); - setNextTriggerTime(ZonedDateTime.now(ZoneId.systemDefault()) - .plusSeconds(definedDelay.getSeconds())); - } - - - @Override - public double getProgressPercentage() { - - ZonedDateTime now = getNow(); - if (now.isAfter(getNextTriggerTimeWithPause().orElse(getNow()))) { - return 100.0; - } - - // Calculate time progress as percentage - long totalSeconds = java.time.Duration.between( - ZonedDateTime.now().withSecond(0).withNano(0), getNextTriggerTimeWithPause().orElse(getNow())).getSeconds(); - long secondsRemaining = java.time.Duration.between(now, getNextTriggerTimeWithPause().orElse(getNow())).getSeconds(); - - if (totalSeconds <= 0) { - return 0.0; - } - - double progress = 100.0 * (1.0 - (secondsRemaining / (double) totalSeconds)); - return Math.min(99.9, Math.max(0.0, progress)); // Cap between 0-99.9% - } - - - - @Subscribe - public void onGameTick(GameTick event) { - // Used to stay registered with the event bus - } - - @Override - public Optional getCurrentTriggerTime() { - // If already triggered and reset occurred, no future trigger - if (hasTriggered() && canTriggerAgain()) { - return Optional.empty(); - } - - // If already triggered but not reset, return the target time (in the past) - if (hasTriggered()) { - return Optional.of(getNextTriggerTimeWithPause().orElse(getNow())); - } - - // Not triggered yet, return future target time - return Optional.of(getNextTriggerTimeWithPause().orElse(getNow())); - } - @Override - protected void onResume(Duration pauseDuration) { - if (isPaused()) { - return; - } - // getNextTriggerTimeWithPause() provide old next trigger time -> we are resumed.. - ZonedDateTime nextTriggerTimeWithPauseDuration = getNextTriggerTimeWithPause().orElse(null); - if (nextTriggerTimeWithPauseDuration != null) { - nextTriggerTimeWithPauseDuration = nextTriggerTimeWithPauseDuration.plus(pauseDuration); - // Shift the next trigger time by the pause duration - setNextTriggerTime(nextTriggerTimeWithPauseDuration); - log.info("SingleTriggerTimeCondition resumed, next trigger time shifted to: {}", nextTriggerTimeWithPauseDuration); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/TimeCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/TimeCondition.java deleted file mode 100644 index cd31e3a13cd..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/TimeCondition.java +++ /dev/null @@ -1,487 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.events.GameTick; - -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; - -import java.time.Duration; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Optional; - -/** - * Abstract base class for all time-based conditions. - * Provides common functionality for time calculations and event handling. - */ -@Slf4j -@EqualsAndHashCode(callSuper = false) -public abstract class TimeCondition implements Condition { - @Getter - private final long maximumNumberOfRepeats; - @Getter - @Setter - protected transient long currentValidResetCount = 0; - // Last reset timestamp tracking - protected transient LocalDateTime lastValidResetTime; - protected static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss"); - - // Pause-related fields - @Getter - protected transient boolean isPaused = false; - protected transient ZonedDateTime pauseStartTime; - @Getter - protected transient Duration totalPauseDuration = Duration.ZERO; - - @Setter - private transient ZonedDateTime nextTriggerTime; - - /** - * Calculates the current pause duration without unpausing the condition. - * Provides high-resolution timing for pause duration with nanosecond precision. - * - * @return The current pause duration, or Duration.ZERO if not paused - */ - protected Duration getCurrentPauseDuration() { - if (!isPaused || pauseStartTime == null) { - return Duration.ZERO; - } - // Calculate with nanosecond precision for higher resolution - return Duration.between(pauseStartTime, getNow()); - } - - /** - * Gets the effective "now" time, adjusted for any active pause. - * This method is used to provide a consistent time reference point that - * doesn't advance during pauses. - * - * @return The current time if not paused, or the pause start time if paused - */ - protected ZonedDateTime getEffectiveNow() { - return isPaused ? pauseStartTime : getNow(); - } - - /** - * This method returns the next trigger time, adjusted for any pauses. - * If the condition is paused, the next trigger time is shifted by the duration of the pause. - * This allows the condition to account for time spent in a paused state when calculating the next trigger. - * - * Uses high-resolution pause duration tracking for more accurate calculations. - * - * @return Optional containing the adjusted next trigger time, or empty if no trigger is set - */ - public Optional getNextTriggerTimeWithPause() { - if (nextTriggerTime == null) { - return Optional.empty(); - } - - // If paused, adjust the trigger time by the current pause duration - if (isPaused) { - Duration currentPauseDuration = getCurrentPauseDuration(); - return Optional.of(nextTriggerTime.plus(currentPauseDuration)); - } - - return Optional.of(nextTriggerTime); - } - public TimeCondition() { - // Default constructor - this(0); - } - /** - * Constructor for TimeCondition with a specified repeat count - * - * @param maximumNumberOfRepeats Maximum number of times this condition can repeat, zero or negative means infinite repeats - */ - public TimeCondition(final long maximumNumberOfRepeats) { - this.maximumNumberOfRepeats = maximumNumberOfRepeats; - lastValidResetTime = LocalDateTime.now(); - } - /** - * Gets the current date and time in the system default time zone with maximum precision. - * Uses the most precise clock available in the system for consistent timing. - * - * @return The current ZonedDateTime with nanosecond precision - */ - protected ZonedDateTime getNow() { - return ZonedDateTime.now(ZoneId.systemDefault()); - } - - @Override - public ConditionType getType() { - return ConditionType.TIME; - } - - /** - * Pauses this time condition, preventing it from being satisfied until resumed. - * When paused, the condition's trigger time will be shifted by the pause duration. - * Uses high-precision time tracking to ensure accurate pause duration calculation. - */ - public void pause() { - if (!isPaused) { - isPaused = true; - pauseStartTime = getNow(); - log.debug("Time condition paused at: {}", pauseStartTime); - } - } - - /** - * resumes this time condition, allowing it to be satisfied again. - * The trigger time will be shifted by the duration of the pause with high precision. - * Uses nanosecond-level precision for duration calculations. - */ - public void resume() { - if (isPaused) { - ZonedDateTime now = getNow(); - Duration pauseDuration = Duration.between(pauseStartTime, now); - totalPauseDuration = totalPauseDuration.plus(pauseDuration); - isPaused = false; - - // Keep track of pause end time before nulling pauseStartTime - ZonedDateTime pauseEndTime = now; - pauseStartTime = null; - - // Call the subclass implementation to handle specific adjustments - onResume(pauseDuration); - - log.debug("Time condition resumed at: {}, pause duration: {}, total pause duration: {}", - pauseEndTime, formatDuration(pauseDuration), formatDuration(totalPauseDuration)); - } - } - - /** - * Called when the condition is resumed. - * Subclasses should implement this method to adjust their trigger times. - * - * @param pauseDuration The duration of the most recent pause - */ - protected abstract void onResume(Duration pauseDuration); - - /** - * Formats a duration into a human-readable string with appropriate precision. - * Shows milliseconds for durations less than 1 second for higher precision. - * - * @param duration The duration to format - * @return A human-readable string representation of the duration - */ - protected String formatDuration(Duration duration) { - long seconds = duration.getSeconds(); - int nanos = duration.getNano(); - - // For very short durations, show milliseconds - if (seconds == 0 && nanos > 0) { - return String.format("%dms", nanos / 1_000_000); - } else if (seconds < 60) { - // For durations under a minute, show seconds with decimal precision if needed - if (nanos > 0) { - return String.format("%.2fs", seconds + (nanos / 1_000_000_000.0)); - } - return seconds + "s"; - } else if (seconds < 3600) { - return String.format("%dm %ds", seconds / 60, seconds % 60); - } else { - return String.format("%dh %dm", seconds / 3600, (seconds % 3600) / 60); - } - } - - @Override - public String getDescription() { - boolean canTrigger = canTriggerAgain(); - String triggerStatus = canTrigger ? "Can trigger" : "Cannot trigger"; - String pauseStatus = isPaused ? " (PAUSED)" : ""; - String triggerCount = "Trigger Count: " + (maximumNumberOfRepeats > 0 ? - " (" + currentValidResetCount + "/" + maximumNumberOfRepeats + ")" : - String.valueOf(currentValidResetCount)); - - String lastReset = lastValidResetTime != null ? - "Last reset: " + lastValidResetTime.format(TIME_FORMATTER) : ""; - - // Enhanced pause information - StringBuilder pauseInfo = new StringBuilder(); - if (isPaused) { - Duration currentPauseDuration = getCurrentPauseDuration(); - pauseInfo.append("Current pause: ").append(formatDuration(currentPauseDuration)).append("\n"); - } - - if (totalPauseDuration.toMillis() > 0) { - pauseInfo.append("Total pause duration: ").append(formatDuration(totalPauseDuration)); - } - - return triggerStatus + pauseStatus + "\n" + triggerCount + "\n" + lastReset + - (pauseInfo.length() > 0 ? "\n" + pauseInfo.toString() : ""); - } - - - /** - * Default GameTick handler that subclasses can override - */ - public void onGameTick(GameTick gameTick) { - // Default implementation does nothing - } - - @Override - public void reset() { - this.reset(true); - } - - /** - * Resets the condition with optional randomization. - * Clears pause state and updates trigger times based on the randomize parameter. - * - * @param randomize Whether to apply randomization during reset - */ - @Override - public void reset(boolean randomize) { - // If paused, resume first - if (isPaused) { - resume(); - } - - // Reset total pause duration - totalPauseDuration = Duration.ZERO; - - // Subclasses should override this to implement specific reset behavior - } - - @Override - public void hardReset() { - // Reset the condition state completely - this.currentValidResetCount = 0; - this.lastValidResetTime = LocalDateTime.now(); - this.totalPauseDuration = Duration.ZERO; - - // Ensure not paused - if (isPaused) { - isPaused = false; - pauseStartTime = null; - } - - // Call normal reset with randomization - reset(true); - } - - void updateValidReset() { - if (isSatisfied()) { - this.currentValidResetCount++; - this.lastValidResetTime = LocalDateTime.now(); - } - - } - - /** - * Gets the next time this time condition will be satisfied, accounting for pauses. - * When paused, the trigger time is still calculated but effectively frozen until resumed. - * - * @return Optional containing the next trigger time, or empty if not applicable - */ - @Override - public Optional getCurrentTriggerTime() { - // If can't trigger again, don't provide a trigger time - if (!canTriggerAgain()) { - return Optional.empty(); - } - - // Calculate next trigger time (subclasses should override this) - // Note: We don't return empty for paused conditions anymore, instead - // we use getEffectiveNow() in calculations to freeze progress during pause - if (isSatisfied()) { - return Optional.of(getEffectiveNow()); - } - - // Default to using the next trigger time with pause handling - return getNextTriggerTimeWithPause(); - } - - /** - * Gets the duration until the next trigger time, accounting for pauses. - * When paused, the duration is calculated from the pause start time, effectively - * freezing the countdown until the condition is resumed. - * - * @return Optional containing the duration until next trigger, or empty if not applicable - */ - public Optional getDurationUntilNextTrigger() { - if(!canTriggerAgain()) { - return Optional.empty(); // No duration if already triggered too often - } - - Optional nextTrigger = getCurrentTriggerTime(); - if (nextTrigger.isPresent()) { - // Use effective now for consistent pause behavior - ZonedDateTime now = getEffectiveNow(); - 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)); - } - else { - // If trigger time is in the past, return zero duration - return Optional.of(Duration.ZERO); - } - } - return Optional.empty(); - } - - /** - * Calculates progress percentage toward next trigger. - * If the condition is paused, the progress remains frozen at its current value - * rather than resetting to zero. This provides a more accurate representation - * of the condition's state during pauses. - * - * @return Progress percentage (0-100) toward next trigger time - */ - @Override - public double getProgressPercentage() { - if (!canTriggerAgain()) { - return 0.0; // No progress if already triggered too often - } - - // If already satisfied, return 100% - if (isSatisfied()) { - return 100.0; - } - - // Calculate progress based on time until next trigger - Optional nextTrigger = getCurrentTriggerTime(); - if (nextTrigger.isPresent()) { - // When paused, use the pause start time as the reference point - // to keep progress frozen rather than resetting to 0% - ZonedDateTime now = getEffectiveNow(); - ZonedDateTime triggerTime = nextTrigger.get(); - - // If trigger is in the past, it's either 0% or 100% - if (!triggerTime.isAfter(now)) { - return isSatisfied() ? 100.0 : 0.0; - } - - // Calculate progress based on reference point and trigger time - return calculateProgressTowardTrigger(now, triggerTime); - } - - // Default behavior - return 0.0; - } - - /** - * Calculates progress percentage toward a specific trigger time. - * Base implementation for subclasses to override with specific calculations. - * This method should account for pause states by using the effective now time. - * - * When the progress calculation is requested during a paused state, - * the same progress value should be maintained rather than advancing. - * - * @param now Current time (or effective current time during pause) - * @param triggerTime Target trigger time - * @return Progress percentage (0-100) - */ - protected double calculateProgressTowardTrigger(ZonedDateTime now, ZonedDateTime triggerTime) { - // Default implementation uses a simple linear progress calculation - // Subclasses should override this to provide more specific implementations - if (now.isAfter(triggerTime)) { - return 100.0; - } - - try { - // Calculate total duration from start to trigger - Duration totalDuration = Duration.between(getNextTriggerTimeWithPause().orElse(now), triggerTime); - // Calculate elapsed duration - Duration elapsedDuration = Duration.between(now, triggerTime); - - if (totalDuration.isZero()) { - return 100.0; // Avoid division by zero - } - - // Calculate progress percentage - double progress = 100.0 * (1.0 - (elapsedDuration.toMillis() / (double) totalDuration.toMillis())); - // Ensure progress stays within 0-100 range - return Math.min(100.0, Math.max(0.0, progress)); - } catch (Exception e) { - // If any calculation errors occur, return 0% - return 0.0; - } - } - - /** - * Check if this condition uses randomization - * @return true if randomization is enabled, false otherwise - */ - public boolean isUseRandomization() { - return false; // Default implementation, subclasses should override if needed - } - /** - * Checks if this condition can trigger again (hasn't triggered yet) - * - * @return true if the condition hasn't triggered yet - */ - public boolean canTriggerAgain(){ - if (maximumNumberOfRepeats <= 0){ - return true; - } - if (currentValidResetCount < maximumNumberOfRepeats) { - return true; - } - return false; - } - abstract public boolean isSatisfiedAt(ZonedDateTime time); - - /** - * Checks if this condition has already triggered - * - * @return true if the condition has triggered at least once - */ - public boolean hasTriggered() { - return currentValidResetCount > 0; - } - - @Override - public boolean isSatisfied() { - // A condition cannot be satisfied while paused - if (isPaused) { - return false; - } - - // Default implementation defers to subclasses using isSatisfiedAt - return isSatisfiedAt(getNow()); - } - - /** - * Gets the estimated time until this time condition will be satisfied. - * This implementation leverages getCurrentTriggerTime() to provide accurate estimates - * for time-based conditions, taking into account pause adjustments. - * - * @return Optional containing the estimated duration until satisfaction, or empty if not determinable - */ - @Override - public Optional getEstimatedTimeWhenIsSatisfied() { - // If the condition is already satisfied, return zero duration - if (isSatisfied()) { - return Optional.of(Duration.ZERO); - } - - // Get the next trigger time, accounting for pauses - Optional triggerTime = getNextTriggerTimeWithPause(); - if (!triggerTime.isPresent()) { - // Try the regular getCurrentTriggerTime as fallback - triggerTime = getCurrentTriggerTime(); - } - - if (triggerTime.isPresent()) { - ZonedDateTime now = getEffectiveNow(); - 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); - } - - // If we can't determine the trigger time, return empty - return Optional.empty(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/TimeWindowCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/TimeWindowCondition.java deleted file mode 100644 index b169c04d770..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/TimeWindowCondition.java +++ /dev/null @@ -1,1292 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.enums.RepeatCycle; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.ui.TimeConditionPanelUtil; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; - -import java.time.Duration; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.temporal.ChronoUnit; -import java.util.Optional; - -import java.util.logging.Level; - -@Data -@EqualsAndHashCode(callSuper = false, exclude = { }) -@Slf4j -public class TimeWindowCondition extends TimeCondition { - - // Constants for unlimited date ranges - public static final LocalDate UNLIMITED_START_DATE = LocalDate.of(1900, 1, 1); - public static final LocalDate UNLIMITED_END_DATE = LocalDate.of(2100, 12, 31); - - public static String getVersion() { - return "0.0.3"; - } - // Time window bounds (daily start/end times) - private final LocalTime startTime; - private final LocalTime endTime; - - // Date range for validity period - private final LocalDate startDate; - private final LocalDate endDate; - - // Repeat cycle configuration - private final RepeatCycle repeatCycle; - private final int repeatIntervalUnit; //based on the repeat cycle, defines the interval unit (e.g., days, weeks, etc.) - - - // Next window tracking (for non-daily cycles) - //@Getter - //@Setter - - @Getter - @Setter - private transient LocalDateTime currentEndDateTime; - - // Randomization - private boolean useRandomization = false; - private int randomizerValue = 0; // Randomization value, depends on the repeat cycle - // randomizerValueUnit is now automatically determined based on repeatCycle - no longer stored as field - // Cached timezone for computation - not serialized - private transient ZoneId zoneId; - - - - @Getter - @Setter - private transient int transientNumberOfResetsWithinDailyInterval = 0; // Number of resets since last calculation - - /** - * Checks if the start date represents an unlimited (no restriction) start date - * @return true if start date is unlimited - */ - public boolean isUnlimitedStartDate() { - return startDate != null && startDate.equals(UNLIMITED_START_DATE); - } - - /** - * Checks if the end date represents an unlimited (no restriction) end date - * @return true if end date is unlimited - */ - public boolean isUnlimitedEndDate() { - return endDate != null && endDate.equals(UNLIMITED_END_DATE); - } - - /** - * Checks if this condition has unlimited date range (both start and end are unlimited) - * @return true if both dates are unlimited - */ - public boolean hasUnlimitedDateRange() { - return isUnlimitedStartDate() && isUnlimitedEndDate(); - } - - - /** - * Creates a time window condition with just daily start and end times. - * Uses unlimited date range (no start/end date restrictions), daily repeat cycle, and unlimited repeats. - * - * @param startTime The daily start time of the window - * @param endTime The daily end time of the window - */ - public TimeWindowCondition(LocalTime startTime, LocalTime endTime) { - this( - startTime, - endTime, - UNLIMITED_START_DATE, - UNLIMITED_END_DATE, - RepeatCycle.DAYS, - 1, - 0// 0 means infinity - ); - } - - /** - * Creates a time window condition with all parameters specified. - * This is the full constructor that allows complete configuration of the time window. - * - * @param startTime The daily start time of the window - * @param endTime The daily end time of the window - * @param startDate The earliest date the window can be active - * @param endDate The latest date the window can be active - * @param repeatCycle The cycle type for window repetition (DAYS, WEEKS, etc.) - * @param repeatIntervalUnit The interval between repetitions (e.g., 2 for every 2 days) - * @param maximumNumberOfRepeats Maximum number of times this condition can trigger (0 for unlimited) - */ - public TimeWindowCondition( - LocalTime startTime, - LocalTime endTime, - LocalDate startDate, - LocalDate endDate, - RepeatCycle repeatCycle, - int repeatIntervalUnit, - long maximumNumberOfRepeats - ) { - super(maximumNumberOfRepeats); - - this.startTime = startTime; - this.endTime = endTime; - this.startDate = startDate; - this.endDate = endDate; - this.repeatCycle = repeatCycle; - this.repeatIntervalUnit = Math.max(1, repeatIntervalUnit); // Ensure positive interval - this.zoneId = ZoneId.systemDefault(); // Initialize with system default - this.lastValidResetTime = LocalDateTime.now(); - transientNumberOfResetsWithinDailyInterval = 0; - this.randomizerValue = 0; - this.useRandomization = false; - - // Initialize next window times based on repeat cycle - calculateNextWindow(getNow().toLocalDateTime()); - } - /** - * Factory method to create a simple daily time window that repeats every day. - * Creates a window that starts and ends at the specified times each day, - * with unlimited date range (no start/end date restrictions). - * - * @param startTime The daily start time of the window - * @param endTime The daily end time of the window - * @return A configured TimeWindowCondition for daily repetition - */ - public static TimeWindowCondition createDaily(LocalTime startTime, LocalTime endTime) { - return new TimeWindowCondition( - startTime, - endTime, - UNLIMITED_START_DATE, - UNLIMITED_END_DATE, - RepeatCycle.DAYS, - 1, - 0// 0 means infinity - ); - - } - - /** - * {@inheritDoc} - * Returns the type of this condition, which is TIME. - * - * @return The condition type ConditionType.TIME - */ - @Override - public ConditionType getType() { - return ConditionType.TIME; - } - /** - * Gets the timezone used for time calculations in this condition. - * - * @return The ZoneId representing the timezone - */ - public ZoneId getZoneId() { - return zoneId; - } - - /** - * Sets the timezone to use for time calculations in this condition. - * Changes to the timezone will affect when the time window activates. - * - * @param zoneId The timezone to use for calculations - */ - public void setZoneId(ZoneId zoneId) { - this.zoneId = zoneId; - } - - /** - * Calculate the next window start and end times based on current time and reset settings - */ - private void calculateNextWindow(LocalDateTime lastValidTime) { - ZonedDateTime now = ZonedDateTime.now(getZoneId()); - LocalDateTime nowLocal = now.toLocalDateTime(); - - - LocalDateTime referenceTime = lastValidTime != null ? lastValidTime : nowLocal; - - //LocalDateTime todayStartDateTime = LocalDateTime.of(nowLocal.toLocalDate(), startTime); - //LocalDateTime todayEndDateTime = LocalDateTime.of(nowLocal.toLocalDate(), endTime); - if(Microbot.isDebug() ) log.info("Calculating next window - current: \n" + this,Level.INFO); - - switch (repeatCycle) { - case ONE_TIME: - calculateOneTimeWindow(referenceTime); - break; - case DAYS: - calculateCycleWindow(referenceTime); - - break; - case WEEKS: - calculateCycleWindow(referenceTime); - - break; - case MINUTES: - calculateCycleWindow(referenceTime); - - break; - case HOURS: - calculateCycleWindow(referenceTime); - - break; - - default: - log.warn("Unsupported repeat cycle: {}", repeatCycle); - break; - } - - if(Microbot.isDebug() ) - { - log.info(this.getDetailedDescription()); - log.info("After calculate new cycle window : \n" + this); - } - // Apply randomization if enabled - - // Only check end date bounds if not unlimited - if (!isUnlimitedEndDate()) { - LocalDateTime lastEnd = LocalDateTime.of(endDate, endTime); - if (getNextTriggerTimeWithPause().orElse(null) != null) { - LocalDateTime nextTrigger = getNextTriggerTimeWithPause().get().toLocalDateTime(); - if (nextTrigger.isAfter(lastEnd)){ - setNextTriggerTime(null); - this.currentEndDateTime = null; - } - }else{ - this.currentEndDateTime = null; - } - } - if(Microbot.isDebug() ) log.info("Calculating done - new time window: \n" + this,Level.INFO); - } - - /** - * Calculates window for ONE_TIME repeat cycle - */ - private void calculateOneTimeWindow(LocalDateTime referenceTime) { - ZonedDateTime now = ZonedDateTime.now(getZoneId()); - LocalDateTime nowLocal = now.toLocalDateTime(); - LocalDateTime todayStartDateTime = LocalDateTime.of(nowLocal.toLocalDate(), startTime); - LocalDateTime todayEndDateTime = LocalDateTime.of(nowLocal.toLocalDate(), endTime); - if (transientNumberOfResetsWithinDailyInterval == 0 ){ - if (todayEndDateTime.isBefore(todayStartDateTime)) { - todayEndDateTime = todayEndDateTime.plusDays(1); - } - if (referenceTime.isAfter(todayStartDateTime) && referenceTime.isBefore(todayEndDateTime)) { - setNextTriggerTime(todayStartDateTime.atZone(getZoneId())); - this.currentEndDateTime = todayEndDateTime; - } else { - setNextTriggerTime(todayEndDateTime.plusDays(1).atZone(getZoneId())); - this.currentEndDateTime = todayEndDateTime.plusDays(1); - - } - }else{ - // If the reset time is after the end of the window, set to null - if (lastValidResetTime.isAfter(currentEndDateTime)) { - setNextTriggerTime(null); - this.currentEndDateTime = null; - } else { - // wait until we are outside current vaild window - } - } - } - - - - /** - * Calculates window for sub-day repeat cycles (MINUTES, HOURS) - */ - private void calculateCycleWindow(LocalDateTime referenceTime) { - ZonedDateTime now = ZonedDateTime.now(getZoneId()); - // First, determine the bounds of today's overall window - LocalDate today = now.toLocalDate(); - LocalDateTime currentDayWindowStart = LocalDateTime.of(today, startTime); - LocalDateTime currentDayWindowEnd = LocalDateTime.of(today, endTime); - - // Handle cross-midnight windows - if (currentDayWindowEnd.isBefore(currentDayWindowStart)) { - currentDayWindowEnd = currentDayWindowEnd.plusDays(1); - } - - - - LocalDateTime nextTriggerTime = calculateNextStartWindow(referenceTime); - setNextTriggerTime(nextTriggerTime.atZone(getZoneId())); - if (Microbot.isDebug()) log.info("calculation of new cycle window after calculation of next start window:\n {}", this); - - - // If next interval starts after the outer window end, it's not valid today - LocalDate startday = nextTriggerTime.toLocalDate(); - this.currentEndDateTime = LocalDateTime.of(startday, endTime); - if (currentEndDateTime.isBefore(nextTriggerTime)) { - this.currentEndDateTime = calculateNextTime( this.currentEndDateTime); - LocalDateTime endDateTimeNextDay = LocalDateTime.of(startday.plusDays(1), endTime); - if (this.currentEndDateTime.isBefore(nextTriggerTime) || endDateTimeNextDay.isBefore(this.currentEndDateTime)) { - throw new IllegalStateException("Invalid end time calculation: " + this.currentEndDateTime); - } - } - - } - - private LocalDateTime calculateNextTime( LocalDateTime referenceTime) { - LocalDateTime nextStartTime; - - if (repeatIntervalUnit == 0) { - return referenceTime; - } - - // First calculate the base next time without randomization - switch (repeatCycle) { - case ONE_TIME: - nextStartTime = referenceTime; - break; - case MINUTES: - nextStartTime = referenceTime.plusMinutes(repeatIntervalUnit); - break; - case HOURS: - nextStartTime = referenceTime.plusHours(repeatIntervalUnit); - break; - case DAYS: - nextStartTime = referenceTime.plusDays(repeatIntervalUnit); - break; - case WEEKS: - nextStartTime = referenceTime.plusWeeks(repeatIntervalUnit); - break; - default: - log.warn("Unsupported repeat cycle: {}", repeatCycle); - nextStartTime = referenceTime; - break; - } - log.info("Base next start time calculated: \n\t{}\n\t{}", nextStartTime); - - // Apply user-configured randomization if enabled - if (useRandomization && randomizerValue > 0) { - // Calculate maximum allowed randomization based on interval and cycle - int maxAllowedRandomization = calculateMaxAllowedRandomization(); - - // Cap the randomizer value to the maximum allowed - int cappedRandomizerValue = Math.min(randomizerValue, maxAllowedRandomization); - - // Generate random offset between -cappedRandomizerValue and +cappedRandomizerValue - int randomOffset = Rs2Random.between(-cappedRandomizerValue, cappedRandomizerValue); - - // Store the base time before applying randomization for logging - LocalDateTime baseTime = nextStartTime; - - // Automatically determine the appropriate randomization unit based on repeat cycle - RepeatCycle randomUnit = getAutomaticRandomizerValueUnit(); - switch (randomUnit) { - case SECONDS: - nextStartTime = nextStartTime.plusSeconds(randomOffset); - break; - case MINUTES: - nextStartTime = nextStartTime.plusMinutes(randomOffset); - break; - case HOURS: - nextStartTime = nextStartTime.plusHours(randomOffset); - break; - default: - // Default to minutes if unsupported unit - nextStartTime = nextStartTime.plusMinutes(randomOffset); - break; - } - - log.info("Applied randomization: {} {} offset to next trigger time. Base: {}, Final: {} (capped from {} to {})", - randomOffset, randomUnit, baseTime, nextStartTime, - randomizerValue, cappedRandomizerValue); - } - log.info("Next start time after randomization: {}", nextStartTime); - return nextStartTime; - } - - /** - * Calculates the maximum allowed randomization value based on the repeat cycle and interval. - * This ensures randomization stays within meaningful bounds relative to the interval. - * - * @return Maximum allowed randomization value in the automatic randomization unit - */ - private int calculateMaxAllowedRandomization() { - // Convert interval to the same unit as randomization for comparison - RepeatCycle randomUnit = getAutomaticRandomizerValueUnit(); - return TimeConditionPanelUtil.calculateMaxAllowedRandomization(getRepeatCycle(), getRepeatIntervalUnit()); - // Calculate total interval in the randomization unit - } - - /** - * Converts an interval value from one unit to another for comparison purposes. - * - * @param value The interval value to convert - * @param fromUnit The original unit - * @param toUnit The target unit - * @return The converted value - */ - public static long convertToRandomizationUnit(int value, RepeatCycle fromUnit, RepeatCycle toUnit) { - // Convert to seconds first, then to target unit - long totalSeconds; - switch (fromUnit) { - case MINUTES: - totalSeconds = value * 60L; - break; - case HOURS: - totalSeconds = value * 3600L; - break; - case DAYS: - totalSeconds = value * 86400L; - break; - case WEEKS: - totalSeconds = value * 604800L; - break; - default: - totalSeconds = value; - break; - } - - // Convert from seconds to target unit - switch (toUnit) { - case SECONDS: - return totalSeconds; - case MINUTES: - return totalSeconds / 60L; - case HOURS: - return totalSeconds / 3600L; - default: - return totalSeconds / 60L; // Default to minutes - } - } - /** - * Helper method to calculate interval from a reference point - */ - private LocalDateTime calculateNextStartWindow( LocalDateTime referenceTime) { - LocalDateTime nextStartTime; - ZonedDateTime now = ZonedDateTime.now(getZoneId()); - LocalDateTime nowLocal = now.toLocalDateTime(); - LocalDate today = now.toLocalDate(); - LocalDateTime currentDayWindowStart = LocalDateTime.of(today, startTime); - LocalDateTime currentDayWindowEnd = LocalDateTime.of(today, endTime); - if (this.currentValidResetCount > 0) { - - nextStartTime = calculateNextTime(referenceTime); - - }else { - if (nowLocal.isBefore(currentDayWindowEnd)) { - nextStartTime = currentDayWindowStart; - } else { - nextStartTime = currentDayWindowStart.plusDays(1); - } - } - - if (nextStartTime.isBefore(currentDayWindowStart)) { - nextStartTime = currentDayWindowStart; - }else if (nextStartTime.isBefore(currentDayWindowEnd)) { - - }else if (nextStartTime.isAfter(currentDayWindowEnd)) { - LocalDate nextDay = now.toLocalDate().plusDays(1); - nextStartTime = LocalDateTime.of(nextDay, startTime); - } - return nextStartTime; - - - } - - - /** - * {@inheritDoc} - * Determines if the current time is within the configured time window. - * Checks if the current time is after the start time and before the end time - * of the current window, and if the condition can still trigger. - * - * @return true if the current time is within the active window and the condition can trigger, - * false otherwise - */ - @Override - public boolean isSatisfied() { - if (getNextTriggerTimeWithPause().isPresent()) { - return isSatisfied(getNextTriggerTimeWithPause().get().toLocalDateTime()); - } - return false; - } - @Override - public boolean isSatisfiedAt(ZonedDateTime triggerTime) { - return isSatisfied(triggerTime.toLocalDateTime()); - - } - - private boolean isSatisfied(LocalDateTime currentStartDateTime) { - if (isPaused()) { - return false; - } - if (!canTriggerAgain()) { - return false; - } - ZonedDateTime now = ZonedDateTime.now(getZoneId()); - LocalDateTime nowLocal = now.toLocalDateTime(); - LocalDate today = now.toLocalDate(); - LocalDate dayBefore = today.minusDays(1); - LocalDateTime currentDayWindowStart = LocalDateTime.of(today, startTime); - LocalDateTime beforeDayWindowStart = LocalDateTime.of( dayBefore, startTime); - LocalDateTime beforeDayWindowEnd = LocalDateTime.of(dayBefore, endTime); - - // For non-daily or interval > 1 day cycles, check against calculated next window - if (currentStartDateTime == null || currentEndDateTime == null) { - - return false; // No more windows in range - - } - if ((currentStartDateTime.isAfter(beforeDayWindowStart) && currentEndDateTime.isBefore(beforeDayWindowEnd))) { - lastValidResetTime = currentDayWindowStart; - this.calculateNextWindow(this.lastValidResetTime); - - } - // Check if window has passed - but don't auto-recalculate - // Let the scheduler decide when to reset the condition - if (nowLocal.isAfter(currentEndDateTime)) { - return false; - } - - // Check if within next window - return nowLocal.isAfter(currentStartDateTime) && nowLocal.isBefore(currentEndDateTime); - - } - - /** - * {@inheritDoc} - * Calculates progress through the current time window as a percentage. - * Returns 0% if outside the window or 0-100% based on how much of the window has elapsed. - * - * @return A percentage from 0-100 indicating progress through the current time window - */ - @Override - public double getProgressPercentage() { - if (!isSatisfied()) { - return 0.0; - } - - // If our window bounds aren't set, we can't calculate progress - if (getNextTriggerTimeWithPause().orElse(null) == null || currentEndDateTime == null) { - log.debug("Unable to calculate progress - window bounds are null"); - return 0.0; - } - - ZonedDateTime now = ZonedDateTime.now(getZoneId()); - LocalDateTime nowLocal = now.toLocalDateTime(); - LocalDateTime currentStartDateTime = getNextTriggerTimeWithPause().get().toLocalDateTime(); - // Calculate total window duration in seconds - long totalDuration = ChronoUnit.SECONDS.between(currentStartDateTime, currentEndDateTime); - if (totalDuration <= 0) { - log.debug("Invalid window duration: {} seconds", totalDuration); - return 0.0; - } - - // Calculate elapsed duration in seconds - long elapsedDuration = ChronoUnit.SECONDS.between(currentStartDateTime, nowLocal); - - // Calculate percentage - cap at 100% - double percentage = Math.min(100.0, (elapsedDuration * 100.0) / totalDuration); - - log.debug("Progress calculation: {}% ({}/{} seconds)", - String.format("%.1f", percentage), - elapsedDuration, - totalDuration); - - return percentage; - } - - /** - * {@inheritDoc} - * Provides a user-friendly description of this time window condition. - * Includes the time range, repeat information, and timezone. - * - * @return A human-readable string describing the time window parameters - */ - @Override - public String getDescription() { - StringBuilder description = new StringBuilder("Time Window: "); - - // Format times - DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm"); - String timeRangeStr = startTime.format(timeFormatter) + " to " + endTime.format(timeFormatter); - description.append(timeRangeStr); - // Add date range information only if not unlimited - if (!hasUnlimitedDateRange()) { - DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - description.append(" ( Between: "); - - if (isUnlimitedStartDate()) { - description.append("No start limit"); - } else { - description.append(startDate.format(dateFormatter)); - } - - description.append(" to "); - - if (isUnlimitedEndDate()) { - description.append("No end limit"); - } else { - description.append(endDate.format(dateFormatter)); - } - - description.append(")"); - } - // Add repeat information - if (repeatCycle != RepeatCycle.ONE_TIME) { - - description.append(" (") - .append(repeatCycle.getDisplayName().replace("X", Integer.toString(repeatIntervalUnit))) - .append(")"); - } - - // Add timezone information for clarity - //description.append(" [").append(getZoneId().getId()).append("]"); - description.append("\n"+super.getDescription()); - return description.toString(); - } - - - /** - * Configures time randomization for this window. - * When enabled, the start and end times will be adjusted by a random - * amount within the specified range each time the condition is reset. - * - * @param useRandomization Whether to enable time randomization - * @param randomizerValue Maximum number of minutes to randomize by (plus or minus) - */ - public void setRandomization(boolean useRandomization, int randomizerValue) { - this.useRandomization = useRandomization; - this.randomizerValue = Math.max(0, randomizerValue); - } - /** - * Configures time randomization for this window. - * When enabled, the start and end times will be adjusted by a random - * amount within the specified range each time the condition is reset. - * - * @param useRandomization Whether to enable time randomization - */ - public void setRandomization(boolean useRandomization) { - this.useRandomization = useRandomization; - this.randomizerValue = 0; - } - - /** - * Sets the randomization value without changing the enabled state. - * - * @param randomizerValue The randomization value to set - */ - public void setRandomizerValue(int randomizerValue) { - this.randomizerValue = Math.max(0, randomizerValue); - } - - /** - * Gets the randomization value. - * - * @return The current randomization value - */ - public int getRandomizerValue() { - return this.randomizerValue; - } - - /** - * Gets the randomization unit that is automatically determined based on the repeat cycle. - * - * @return The current automatically determined randomization unit - */ - public RepeatCycle getRandomizerValueUnit() { - return getAutomaticRandomizerValueUnit(); - } - - - - /** - * Custom deserialization method to initialize transient fields. - * This ensures that the timezone is properly set after deserialization. - * - * @return The properly initialized deserialized object - */ - public Object readResolve() { - // Initialize timezone if needed - if (zoneId == null) { - zoneId = ZoneId.systemDefault(); - } - return this; - } - - /** - * Resets the time window condition with default settings. - * Calculates the next time window based on current time and settings. - * This is a shorthand for reset(false). - */ - public void reset() { - reset(false); - } - - - public void hardReset() { - this.currentValidResetCount = 0; - resume(); - this.lastValidResetTime = LocalDateTime.now(); - this.setNextTriggerTime(null); - - this.currentEndDateTime = null; - this.useRandomization = true; - this.transientNumberOfResetsWithinDailyInterval = 0; - // Initialize next window times based on repeat cycle - calculateNextWindow(this.lastValidResetTime); - - } - /** - * {@inheritDoc} - * Resets the time window condition and calculates the next active window. - * Updates the reset count, applies randomization if enabled, and advances - * the window if necessary based on current time. - * - * @param randomize Whether to apply randomization to window times - */ - @Override - public void reset(boolean randomize) { - // Store current time as the reset reference - log.debug("Last reset time: {}", lastValidResetTime); - this.lastValidResetTime = LocalDateTime.now(); - ZonedDateTime now = ZonedDateTime.now(getZoneId()); - LocalDateTime nowLocal = now.toLocalDateTime(); - // If we are have a current window and we are within the window or after it, we need to force an advance - Optional currentZoneStartDateTime = getNextTriggerTimeWithPause(); - - if (currentZoneStartDateTime ==null || !currentZoneStartDateTime.isPresent()) { - return; - } - LocalDateTime currentStartDateTime =currentZoneStartDateTime.get().toLocalDateTime(); - boolean needsAdvance = currentZoneStartDateTime!=null && currentZoneStartDateTime.isPresent() && nowLocal.isAfter(currentZoneStartDateTime.get().toLocalDateTime()); - // If this the next start window that's passed or any window that needs advancing - if (needsAdvance && canTriggerAgain() ) { - this.currentValidResetCount++; - calculateNextWindow(this.lastValidResetTime); - } - if (nowLocal.isAfter(currentStartDateTime) && nowLocal.isBefore(this.currentEndDateTime)) { - transientNumberOfResetsWithinDailyInterval++; - }else { - transientNumberOfResetsWithinDailyInterval = 0; - } - // Log the new window for debugging - if (currentStartDateTime != null && this.currentEndDateTime != null) { - log.debug("Next window after reset: {} to {}", - DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(currentStartDateTime), - DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(currentEndDateTime)); - } else { - log.debug("No next window available after reset"); - } - } - - /** - * {@inheritDoc} - * Indicates whether this condition uses randomization. - * - * @return true if randomization is enabled, false otherwise - */ - @Override - public boolean isUseRandomization() { - return useRandomization; - } - - /** - * {@inheritDoc} - * Calculates the next time this condition will be satisfied (the start of the next window). - * If already within a window, returns a time slightly in the past to indicate the condition - * is currently satisfied. - * - * @return An Optional containing the time when the next window starts, - * or empty if no future windows are scheduled or the condition cannot trigger again - */ - @Override - public Optional getCurrentTriggerTime() { - if (getNextTriggerTimeWithPause().orElse(null) == null || currentEndDateTime == null || !canTriggerAgain()) { - return Optional.empty(); - } - ZonedDateTime now = ZonedDateTime.now(getZoneId()); - LocalDateTime currentStartDateTime = getNextTriggerTimeWithPause().get().toLocalDateTime(); - // If the condition is already satisfied (we're in the window), return the current time - if (isSatisfied()) { - assert(!currentStartDateTime.isAfter(now.toLocalDateTime()) && !currentEndDateTime.isBefore(now.toLocalDateTime())); - return Optional.of(currentStartDateTime.atZone(getZoneId())); // Slightly in the past to indicate "ready now" - } - - // If our window calculation failed or hasn't been done, calculate it - if (currentStartDateTime == null) { - return Optional.empty(); - }else{ - return Optional.of(currentStartDateTime.atZone(getZoneId())); - } - } - @Override - public boolean canTriggerAgain(){ - - boolean canTrigger = super.canTriggerAgain(); - - // If end date is unlimited, only check parent class logic - if (isUnlimitedEndDate()) { - return canTrigger; - } - - LocalDateTime lastDateTime = LocalDateTime.of( endDate, endTime); - if (canTrigger ) { - ZonedDateTime now = ZonedDateTime.now(getZoneId()); - LocalDateTime nowLocal = now.toLocalDateTime(); - return nowLocal.isBefore(lastDateTime); - } - return canTrigger; - - } - - /** - * {@inheritDoc} - * Generates a detailed string representation of this time window condition. - * Includes configuration, status, window times, randomization settings, - * and trigger count information formatted with visual separators. - * - * @return A multi-line string representation with detailed state information - */ - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm"); - DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - - // Basic information - sb.append("TimeWindowCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Time Range: ").append(startTime.format(timeFormatter)) - .append(" to ").append(endTime.format(timeFormatter)).append("\n"); - sb.append(" │ Date Range: "); - if (isUnlimitedStartDate()) { - sb.append("No start limit"); - } else { - sb.append(startDate.format(dateFormatter)); - } - sb.append(" to "); - if (isUnlimitedEndDate()) { - sb.append("No end limit"); - } else { - sb.append(endDate.format(dateFormatter)); - } - sb.append("\n"); - sb.append(" │ Repeat: ").append(repeatCycle) - .append(", Unit: ").append(repeatIntervalUnit).append("\n"); - sb.append(" │ Timezone: ").append(getZoneId().getId()).append("\n"); - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - sb.append(" │ Satisfied: ").append(isSatisfied()).append("\n"); - sb.append(" │ Paused: ").append(isPaused()).append("\n"); - if (getNextTriggerTimeWithPause().orElse(null) != null && currentEndDateTime != null) { - sb.append(" │ Current Window: ").append(getNextTriggerTimeWithPause().get().toLocalDateTime().format(dateTimeFormatter)) - .append("\n │ To: ").append(currentEndDateTime.format(dateTimeFormatter)).append("\n"); - } else if (getNextTriggerTimeWithPause().orElse(null) != null) { - sb.append(" │ Current Window: ").append(getNextTriggerTimeWithPause().get().toLocalDateTime().format(dateTimeFormatter)) - .append("\n │ To: Not available\n"); - } else { - sb.append(" │ Current Window: Not available\n"); - } - if (isSatisfied()) { - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - } - - // Randomization - sb.append(" ├─ Randomization ────────────────────────────\n"); - sb.append(" │ Randomization: ").append(useRandomization ? "Enabled" : "Disabled").append("\n"); - if (useRandomization) { - RepeatCycle randomUnit = getAutomaticRandomizerValueUnit(); - int maxAllowedRandomization = calculateMaxAllowedRandomization(); - int cappedRandomizerValue = Math.min(randomizerValue, maxAllowedRandomization); - - // Original configured value - sb.append(" │ Random Range: ±").append(randomizerValue).append(" ").append(randomUnit.toString().toLowerCase()).append("\n"); - - // Actual capped value used in calculations - if (cappedRandomizerValue != randomizerValue) { - sb.append(" │ Capped Range: ±").append(cappedRandomizerValue).append(" ").append(randomUnit.toString().toLowerCase()) - .append(" (limited from ").append(randomizerValue).append(")\n"); - } - - // Maximum allowed randomization for this interval - sb.append(" │ Max Allowed: ±").append(maxAllowedRandomization).append(" ").append(randomUnit.toString().toLowerCase()).append("\n"); - - // Show the automatic unit determination - sb.append(" │ Random Unit: ").append(randomUnit.toString().toLowerCase()) - .append(" (auto-determined from ").append(repeatCycle.toString().toLowerCase()).append(" cycle)\n"); - } - - // Tracking info - sb.append(" └─ Tracking ────────────────────────────────\n"); - sb.append(" Reset Count: ").append(currentValidResetCount); - if (this.getMaximumNumberOfRepeats() > 0) { - sb.append("/").append(getMaximumNumberOfRepeats()); - } else { - sb.append(" (unlimited)"); - } - sb.append("\n"); - if (lastValidResetTime != null) { - sb.append(" Last Reset: ").append(lastValidResetTime.format(dateTimeFormatter)).append("\n"); - } - sb.append(" Daily Reset Count: ").append(transientNumberOfResetsWithinDailyInterval).append("\n"); - sb.append(" Can Trigger Again: ").append(canTriggerAgain()).append("\n"); - - // Add pause information - if (isPaused()) { - sb.append(" Paused: Yes\n"); - Duration currentPauseDuration = Duration.between(pauseStartTime, getNow()); - sb.append(" Current Pause Duration: ").append(formatDuration(currentPauseDuration)).append("\n"); - } - if (totalPauseDuration.getSeconds() > 0) { - sb.append(" Total Pause Duration: ").append(formatDuration(totalPauseDuration)).append("\n"); - } - - return sb.toString(); - } - - /** - * Provides a detailed description of the time window condition with status information. - * Includes the window times, repeat cycle, current status, progress, randomization, - * and tracking information in a human-readable format. - * - * @return A detailed multi-line string with current status and configuration details - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm"); - DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - - // Basic description - sb.append("\nTime Window Condition:\ntActive from ").append(startTime.format(timeFormatter)) - .append(" to ").append(endTime.format(timeFormatter)).append("\n"); - - // Repeat cycle - if (repeatCycle == RepeatCycle.ONE_TIME) { - sb.append("Schedule: One time only"); - if (!hasUnlimitedDateRange()) { - sb.append(" ("); - if (isUnlimitedStartDate()) { - sb.append("No start limit"); - } else { - sb.append(startDate.format(dateFormatter)); - } - sb.append(" - "); - if (isUnlimitedEndDate()) { - sb.append("No end limit"); - } else { - sb.append(endDate.format(dateFormatter)); - } - sb.append(")"); - } - sb.append("\n"); - } else { - sb.append("Schedule: Repeats every ").append(repeatIntervalUnit).append(" ") - .append(repeatCycle.toString().toLowerCase()).append("\n"); - if (!hasUnlimitedDateRange()) { - sb.append("Valid period: "); - if (isUnlimitedStartDate()) { - sb.append("No start limit"); - } else { - sb.append(startDate.format(dateFormatter)); - } - sb.append(" - "); - if (isUnlimitedEndDate()) { - sb.append("No end limit"); - } else { - sb.append(endDate.format(dateFormatter)); - } - sb.append("\n"); - } - } - - // Status information - boolean satisfied = isSatisfied(); - sb.append("Status: ").append(satisfied ? "Active (in time window)" : "Inactive (outside time window)").append("\n"); - - // Current window information - ZonedDateTime now = ZonedDateTime.now(getZoneId()); - LocalDateTime nowLocal = now.toLocalDateTime(); - - if (getNextTriggerTimeWithPause() != null && currentEndDateTime != null) { - sb.append("Current window: ").append(getNextTriggerTimeWithPause().get().toLocalDateTime().format(dateTimeFormatter)) - .append(" to ").append(currentEndDateTime.format(dateTimeFormatter)).append("\n"); - - if (nowLocal.isAfter(getNextTriggerTimeWithPause().get().toLocalDateTime()) && nowLocal.isBefore(currentEndDateTime)) { - sb.append("Time remaining: ") - .append(ChronoUnit.MINUTES.between(nowLocal, currentEndDateTime)) - .append(" minutes\n"); - sb.append("Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - } else if (nowLocal.isBefore(getNextTriggerTimeWithPause().get().toLocalDateTime())) { - sb.append("Window starts in: ") - .append(ChronoUnit.MINUTES.between(nowLocal, getNextTriggerTimeWithPause().get().toLocalDateTime())) - .append(" minutes\n"); - } else { - sb.append("Window has passed\n"); - } - } else { - sb.append("No active window available\n"); - } - if (useRandomization) { - sb.append(" │ Random Range: ±").append(randomizerValue); - switch (repeatCycle) { - case MINUTES: - sb.append(" millisec\n"); - break; - case HOURS: - sb.append(" seconds\n"); - break; - case DAYS: - sb.append(" minutes\n"); - break; - case WEEKS: - sb.append(" hours\n"); - break; - default: - break; - } - }else { - sb.append("Randomization: Disabled\n"); - } - - // Reset tracking - sb.append("Reset count: ").append(currentValidResetCount); - if (getMaximumNumberOfRepeats() > 0) { - sb.append("/").append(getMaximumNumberOfRepeats()); - } else { - sb.append(" (unlimited)"); - } - sb.append("\n"); - - if (lastValidResetTime != null) { - sb.append("Last reset: ").append(lastValidResetTime.format(dateTimeFormatter)).append("\n"); - } - - // Timezone information - sb.append("Timezone: ").append(getZoneId().getId()).append("\n"); - - return sb.toString(); - } - - /** - * Called when the condition is resumed. - * Shifts time windows by the pause duration to maintain the same - * relative timing after a pause. - * - * @param pauseDuration The duration of the most recent pause - */ - @Override - protected void onResume(Duration pauseDuration) { - if (isPaused()) { - return; - } - - // Get the original trigger time (since isPaused=false at this point after resume()) - // getNextTriggerTimeWithPause() now returns the original nextTriggerTime without pause adjustments - ZonedDateTime originalTriggerTime = getNextTriggerTimeWithPause().orElse(null); - - if (originalTriggerTime != null) { - // Shift the original trigger time by the pause duration to preserve timing - ZonedDateTime shiftedTriggerTime = originalTriggerTime.plus(pauseDuration); - LocalDateTime shiftedLocalTime = shiftedTriggerTime.toLocalDateTime(); - - // Validate that the shifted time still falls within allowed bounds - boolean isValidShiftedTime = isShiftedTimeWithinBounds(shiftedLocalTime); - - if (isValidShiftedTime) { - // Shifted time is valid, use it - setNextTriggerTime(shiftedTriggerTime); - - // Also shift the current end time by the same duration to maintain window length - if (currentEndDateTime != null) { - LocalDateTime shiftedEndTime = currentEndDateTime.plus(pauseDuration); - // Validate that the shifted end time is also within bounds - if (isShiftedEndTimeWithinBounds(shiftedEndTime)) { - currentEndDateTime = shiftedEndTime; - } else { - // If shifted end time goes out of bounds, recalculate the window - log.warn("Shifted end time {} goes beyond allowed bounds, recalculating window", shiftedEndTime); - calculateNextWindow(getNow().toLocalDateTime()); - return; - } - } - - // Shift the last valid reset time if it exists - if (lastValidResetTime != null) { - lastValidResetTime = lastValidResetTime.plus(pauseDuration); - } - - log.debug("TimeWindowCondition resumed after {}, window shifted by pause duration, new trigger time: {}", - formatDuration(pauseDuration), getNextTriggerTimeWithPause().orElse(null)); - } else { - // Shifted time goes out of bounds, recalculate next valid window - log.warn("Shifted trigger time {} goes beyond allowed bounds, recalculating next valid window", shiftedLocalTime); - calculateNextWindow(getNow().toLocalDateTime()); - } - } else { - // If no trigger time was set, calculate a new window from current time - // This should only happen if the condition was never properly initialized - log.warn("TimeWindowCondition resumed but no trigger time was set, recalculating window"); - calculateNextWindow(getNow().toLocalDateTime()); - } - } - - /** - * Validates that a shifted trigger time is still within the allowed time window and date bounds. - * - * @param shiftedTime The shifted trigger time to validate - * @return true if the shifted time is within bounds, false otherwise - */ - private boolean isShiftedTimeWithinBounds(LocalDateTime shiftedTime) { - // Check date range bounds (if not unlimited) - if (!isUnlimitedStartDate() && shiftedTime.toLocalDate().isBefore(startDate)) { - log.debug("Shifted time {} is before start date {}", shiftedTime, startDate); - return false; - } - - if (!isUnlimitedEndDate()) { - LocalDateTime lastValidDateTime = LocalDateTime.of(endDate, endTime); - if (shiftedTime.isAfter(lastValidDateTime)) { - log.debug("Shifted time {} is after end date/time {}", shiftedTime, lastValidDateTime); - return false; - } - } - - // Check daily time bounds - LocalTime shiftedLocalTime = shiftedTime.toLocalTime(); - - // Handle cross-midnight windows - if (endTime.isBefore(startTime)) { - // Cross-midnight window (e.g., 22:00 to 06:00) - boolean isInFirstPart = !shiftedLocalTime.isBefore(startTime); // >= startTime - boolean isInSecondPart = !shiftedLocalTime.isAfter(endTime); // <= endTime - - if (!(isInFirstPart || isInSecondPart)) { - log.debug("Shifted time {} is outside cross-midnight window {} to {}", - shiftedLocalTime, startTime, endTime); - return false; - } - } else { - // Normal window (e.g., 09:00 to 17:00) - if (shiftedLocalTime.isBefore(startTime) || shiftedLocalTime.isAfter(endTime)) { - log.debug("Shifted time {} is outside time window {} to {}", - shiftedLocalTime, startTime, endTime); - return false; - } - } - - return true; - } - - /** - * Validates that a shifted end time is still within the allowed date bounds. - * - * @param shiftedEndTime The shifted end time to validate - * @return true if the shifted end time is within bounds, false otherwise - */ - private boolean isShiftedEndTimeWithinBounds(LocalDateTime shiftedEndTime) { - // Only need to check date bounds for end time, not daily time bounds - if (!isUnlimitedEndDate()) { - LocalDateTime lastValidDateTime = LocalDateTime.of(endDate, endTime); - if (shiftedEndTime.isAfter(lastValidDateTime)) { - log.debug("Shifted end time {} is after end date/time {}", shiftedEndTime, lastValidDateTime); - return false; - } - } - - return true; - } - - /** - * Gets the estimated time until this time window condition will be satisfied. - * This provides a more accurate estimate by considering the window start time, - * repeat cycles, and current window state. - * - * @return Optional containing the estimated duration until satisfaction, or empty if not determinable - */ - @Override - public Optional getEstimatedTimeWhenIsSatisfied() { - // If the condition is already satisfied (we're in the window), return zero - if (isSatisfied()) { - return Optional.of(Duration.ZERO); - } - - // If we can't trigger again, return empty - if (!canTriggerAgain()) { - return Optional.empty(); - } - - // Get the next trigger time with pause adjustments - Optional triggerTime = getNextTriggerTimeWithPause(); - if (!triggerTime.isPresent()) { - // Fallback to regular getCurrentTriggerTime - triggerTime = getCurrentTriggerTime(); - } - - if (triggerTime.isPresent()) { - ZonedDateTime now = getEffectiveNow(); - Duration duration = Duration.between(now, triggerTime.get()); - - // Apply randomization if enabled to provide a range estimate - if (useRandomization && randomizerValue > 0) { - // Add some uncertainty based on the randomizer value - Duration randomComponent = Duration.of(randomizerValue, - getRandomizerChronoUnit()); - duration = duration.plus(randomComponent.dividedBy(2)); // Add half the random range - } - - // Ensure we don't return negative durations - if (duration.isNegative()) { - return Optional.of(Duration.ZERO); - } - return Optional.of(duration); - } - - return Optional.empty(); - } - - /** - * Helper method to get the ChronoUnit for randomization based on the repeat cycle - */ - private java.time.temporal.ChronoUnit getRandomizerChronoUnit() { - RepeatCycle automaticUnit = getAutomaticRandomizerValueUnit(); - switch (automaticUnit) { - case SECONDS: - return java.time.temporal.ChronoUnit.SECONDS; - case MINUTES: - return java.time.temporal.ChronoUnit.MINUTES; - case HOURS: - return java.time.temporal.ChronoUnit.HOURS; - case DAYS: - return java.time.temporal.ChronoUnit.DAYS; - default: - return java.time.temporal.ChronoUnit.MINUTES; - } - } - - /** - * Automatically determines the appropriate randomization unit based on the repeat cycle. - * This ensures randomization uses sensible granularity relative to the repeat interval. - * - * @return The appropriate RepeatCycle for randomization based on the current repeatCycle - */ - private RepeatCycle getAutomaticRandomizerValueUnit() { - switch (repeatCycle) { - case MINUTES: - return RepeatCycle.SECONDS; // For minute intervals, randomize in seconds - case HOURS: - return RepeatCycle.MINUTES; // For hour intervals, randomize in minutes - case DAYS: - return RepeatCycle.MINUTES; // For day intervals, randomize in minutes - case WEEKS: - return RepeatCycle.HOURS; // For week intervals, randomize in hours - case ONE_TIME: - return RepeatCycle.MINUTES; // For one-time, use minutes as default - default: - return RepeatCycle.MINUTES; // Default fallback to minutes - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/enums/RepeatCycle.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/enums/RepeatCycle.java deleted file mode 100644 index 236559f1440..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/enums/RepeatCycle.java +++ /dev/null @@ -1,41 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.enums; - /** - * Enumeration of supported repeat cycle types - */ -public enum RepeatCycle { - MILLIS("Every X milliseconds"), - SECONDS("Every X seconds"), - MINUTES("Every X minutes"), - HOURS("Every X hours"), - DAYS("Every X days"), - WEEKS("Every X weeks"), - ONE_TIME("One time only"); - - private final String displayName; - - RepeatCycle(String displayName) { - this.displayName = displayName; - } - - public String getDisplayName() { - return displayName; - } - public String unit() { - switch (this) { - case MILLIS: - return "ms"; - case SECONDS: - return "s"; - case MINUTES: - return "min"; - case HOURS: - return "h"; - case DAYS: - return "d"; - case WEEKS: - return "w"; - default: - return ""; - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/DayOfWeekConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/DayOfWeekConditionAdapter.java deleted file mode 100644 index 21c002a3a4c..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/DayOfWeekConditionAdapter.java +++ /dev/null @@ -1,125 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.DayOfWeekCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; - -import java.lang.reflect.Type; -import java.time.DayOfWeek; -import java.time.Duration; -import java.util.EnumSet; -import java.util.Optional; -import java.util.Set; - -/** - * Serializes and deserializes DayOfWeekCondition objects - */ -@Slf4j -public class DayOfWeekConditionAdapter implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(DayOfWeekCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add type information - json.addProperty("type", DayOfWeekCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Serialize active days as an array of day values - JsonArray daysArray = new JsonArray(); - for (DayOfWeek day : src.getActiveDays()) { - daysArray.add(day.getValue()); // getValue() returns 1-7 for MON-SUN - } - data.add("activeDays", daysArray); - data.addProperty("version", src.getVersion()); - data.addProperty("maximumNumberOfRepeats", src.getMaximumNumberOfRepeats()); - data.addProperty("maxRepeatsPerDay", src.getMaxRepeatsPerDay()); - data.addProperty("maxRepeatsPerWeek", src.getMaxRepeatsPerWeek()); - - // Serialize the interval condition if it exists - Optional intervalCondition = src.getIntervalCondition(); - if (intervalCondition.isPresent()) { - // Use a separate serializer for the interval condition - JsonElement intervalJson = context.serialize(intervalCondition.get(), IntervalCondition.class); - data.add("intervalCondition", intervalJson); - } - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public DayOfWeekCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - Set activeDays = EnumSet.noneOf(DayOfWeek.class); - - - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(DayOfWeekCondition.getVersion())) { - throw new JsonParseException("Version mismatch: expected " + DayOfWeekCondition.getVersion() + - ", got " + version); - } - } - // Parse active days - if (dataObj.has("activeDays")) { - JsonArray daysArray = dataObj.getAsJsonArray("activeDays"); - for (JsonElement element : daysArray) { - int dayValue = element.getAsInt(); - // DayOfWeek.of expects 1-7 for MON-SUN - activeDays.add(DayOfWeek.of(dayValue)); - } - } - - // Get maximum number of repeats - long maximumNumberOfRepeats = 0; - if (dataObj.has("maximumNumberOfRepeats")) { - maximumNumberOfRepeats = dataObj.get("maximumNumberOfRepeats").getAsLong(); - } - - // Get maximum number of repeats per day - long maxRepeatsPerDay = 0; - if (dataObj.has("maxRepeatsPerDay")) { - maxRepeatsPerDay = dataObj.get("maxRepeatsPerDay").getAsLong(); - } - - // Get maximum number of repeats per week (new field) - long maxRepeatsPerWeek = 0; - if (dataObj.has("maxRepeatsPerWeek")) { - maxRepeatsPerWeek = dataObj.get("maxRepeatsPerWeek").getAsLong(); - } - - // Create the day of week condition with all limits - DayOfWeekCondition condition = new DayOfWeekCondition(maximumNumberOfRepeats, maxRepeatsPerDay, maxRepeatsPerWeek, activeDays); - - // If there's an interval condition, deserialize and add it - if (dataObj.has("intervalCondition")) { - JsonElement intervalJson = dataObj.get("intervalCondition"); - IntervalCondition intervalCondition = context.deserialize(intervalJson, IntervalCondition.class); - condition.setIntervalCondition(intervalCondition); - } - if (dataObj.has("currentValidResetCount")){ - condition.setCurrentValidResetCount(dataObj.get("currentValidResetCount").getAsLong()); - } - - return condition; - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/DurationAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/DurationAdapter.java deleted file mode 100644 index 18500fce53c..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/DurationAdapter.java +++ /dev/null @@ -1,37 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; - -import java.lang.reflect.Type; -import java.time.Duration; - -/** - * Custom adapter for serializing/deserializing java.time.Duration objects - * This avoids reflection issues with Java modules - */ -@Slf4j -public class DurationAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(Duration src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject jsonObject = new JsonObject(); - jsonObject.addProperty("seconds", src.getSeconds()); - jsonObject.addProperty("nanos", src.getNano()); - return jsonObject; - } - - @Override - public Duration deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - try { - JsonObject jsonObject = json.getAsJsonObject(); - long seconds = jsonObject.get("seconds").getAsLong(); - int nanos = jsonObject.has("nanos") ? jsonObject.get("nanos").getAsInt() : 0; - return Duration.ofSeconds(seconds, nanos); - } catch (Exception e) { - log.error("Error deserializing Duration", e); - return Duration.ZERO; - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/IntervalConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/IntervalConditionAdapter.java deleted file mode 100644 index ede74f5c47a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/IntervalConditionAdapter.java +++ /dev/null @@ -1,258 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; -import java.time.format.DateTimeFormatter; -import java.lang.reflect.Type; -import java.time.Duration; -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.Optional; - -/** - * Serializes and deserializes IntervalCondition objects - */ -@Slf4j -public class IntervalConditionAdapter implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(IntervalCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - json.addProperty("version", IntervalCondition.getVersion()); - // Add type information - json.addProperty("type", IntervalCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Store interval as seconds for cross-platform compatibility - data.addProperty("intervalSeconds", src.getInterval().getSeconds()); - - // Store min/max interval information if using randomized intervals - if (src.isRandomize()) { - data.addProperty("minIntervalSeconds", src.getMinInterval().getSeconds()); - data.addProperty("maxIntervalSeconds", src.getMaxInterval().getSeconds()); - data.addProperty("isMinMaxRandomized", true); - } - - // Store randomization settings - data.addProperty("randomize", src.isRandomize()); - data.addProperty("randomFactor", src.getRandomFactor()); - data.addProperty("maximumNumberOfRepeats", src.getMaximumNumberOfRepeats()); - data.addProperty("currentValidResetCount", src.getCurrentValidResetCount()); - // Store next trigger time if available - ZonedDateTime nextTrigger = src.getNextTriggerTimeWithPause().orElse(null); - - - if (nextTrigger != null) { - data.addProperty("nextTriggerTimeMillis", nextTrigger.toInstant().toEpochMilli()); - } - - // Serialize initial delay condition if it exists - if (src.getInitialDelayCondition() != null) { - SingleTriggerTimeCondition delayCondition = src.getInitialDelayCondition(); - if (delayCondition.getNextTriggerTimeWithPause().orElse(null) != null) { - data.addProperty("targetTimeMillis", delayCondition.getNextTriggerTimeWithPause().get().toInstant().toEpochMilli()); - } - data.add("initialDelayCondition", context.serialize(delayCondition)); - } - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public IntervalCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - - // Check if this is using the type/data wrapper format - if (jsonObject.has("type") && jsonObject.has("data")) { - jsonObject = jsonObject.getAsJsonObject("data"); - } - - if (jsonObject.has("version")) { - String version = jsonObject.get("version").getAsString(); - if (!IntervalCondition.getVersion().equals(version)) { - log.warn("Version mismatch: expected {}, got {}", IntervalCondition.getVersion(), version); - throw new JsonParseException("Version mismatch"); - } - } - // Parse interval - long intervalSeconds = jsonObject.get("intervalSeconds").getAsLong(); - Duration interval = Duration.ofSeconds(intervalSeconds); - - // Extract initial delay information if present - Long initialDelaySeconds = null; - SingleTriggerTimeCondition intialCondition = null; - if (jsonObject.has("initialDelayCondition")) { - JsonObject initialDelayData = jsonObject.getAsJsonObject("initialDelayCondition"); - intialCondition = context.deserialize(initialDelayData, SingleTriggerTimeCondition.class); - if (initialDelayData.has("targetTimeMillis")) { - long targetTimeMillis = initialDelayData.get("targetTimeMillis").getAsLong(); - - // Calculate the initial delay in seconds from now to target time - long nowMillis = System.currentTimeMillis(); - if (targetTimeMillis < nowMillis) { - // If the target time is in the past, set initial delay to 0 - initialDelaySeconds = 0L; - } else { - // Calculate the delay in milliseconds and convert to seconds - long delayMillis = Math.max(0, targetTimeMillis - nowMillis); - initialDelaySeconds = (Long)(delayMillis / 1000); - } - } - long intitalDelayFromCondition = intialCondition.getNextTriggerTimeWithPause().get().toInstant().toEpochMilli() - System.currentTimeMillis(); - if (intialCondition != null) { - // Format times for better readability - ZonedDateTime targetTime = intialCondition.getNextTriggerTimeWithPause().get(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - String formattedTargetTime = targetTime.format(formatter); - - // Convert milliseconds to duration for proper formatting - Duration delayFromCondition = Duration.ofMillis(Math.max(0, intitalDelayFromCondition)); - String formattedDelay = String.format("%02d:%02d:%02d", - delayFromCondition.toHours(), - delayFromCondition.toMinutesPart(), - delayFromCondition.toSecondsPart() - ); - - // Convert initialDelaySeconds to duration for formatting - Duration initialDelay = Duration.ofSeconds(initialDelaySeconds != null ? initialDelaySeconds : 0); - String formattedInitialDelay = String.format("%02d:%02d:%02d", - initialDelay.toHours(), - initialDelay.toMinutesPart(), - initialDelay.toSecondsPart() - ); - - Optional nextTriggerWithPause = intialCondition.getNextTriggerTimeWithPause(); - String formattedNextTriggerWithPause = nextTriggerWithPause - .map(time -> time.format(formatter)) - .orElse("Not set"); - - // Check if next trigger time is before current time - boolean isBeforeCurrent = nextTriggerWithPause - .map(time -> time.isBefore(ZonedDateTime.now(ZoneId.systemDefault()))) - .orElse(false); - - log.info("\nInitial delay condition: {}\n- Target time: {}\n- Initial delay: {} ({})\n- Delay from condition: {} ({})\n- Next trigger with pause: {}\n- Is before current time: {}\n", - intialCondition.toString(), - formattedTargetTime, - formattedInitialDelay, - initialDelaySeconds + " seconds", - formattedDelay, - (intitalDelayFromCondition / 1000) + " seconds", - formattedNextTriggerWithPause, - isBeforeCurrent - ); - } else { - throw new JsonParseException("Initial delay condition is null"); - } - } - - // Check if this is using min/max randomization - IntervalCondition condition = null; - if (jsonObject.has("isMinMaxRandomized") && jsonObject.get("isMinMaxRandomized").getAsBoolean()) { - long minIntervalSeconds = jsonObject.get("minIntervalSeconds").getAsLong(); - long maxIntervalSeconds = jsonObject.get("maxIntervalSeconds").getAsLong(); - Duration minInterval = Duration.ofSeconds(minIntervalSeconds); - Duration maxInterval = Duration.ofSeconds(maxIntervalSeconds); - - condition = IntervalCondition.createRandomized(minInterval, maxInterval); - - - - } - - - // Parse randomization settings for the traditional approach - boolean randomize = jsonObject.has("randomize") && jsonObject.get("randomize").getAsBoolean(); - double randomFactor = randomize && jsonObject.has("randomFactor") ? - jsonObject.get("randomFactor").getAsDouble() : 0.0; - long maximumNumberOfRepeats = jsonObject.has("maximumNumberOfRepeats") ? - jsonObject.get("maximumNumberOfRepeats").getAsLong() : 0; - if(condition == null) { - if (intialCondition != null) { - // Create a new condition with the initial delay from the initial condition - condition = new IntervalCondition(interval, randomize, randomFactor, maximumNumberOfRepeats, (Long)intialCondition.getDefinedDelay().toSeconds()); - } else if (initialDelaySeconds != null && initialDelaySeconds > 0) { - // Create a new condition with the initial delay - condition = new IntervalCondition(interval, randomize, randomFactor, maximumNumberOfRepeats, initialDelaySeconds); - }else{ - // Create a new condition without initial delay - condition = new IntervalCondition(interval, randomize, randomFactor, maximumNumberOfRepeats); - } - - - } - - - - - if (intialCondition!= null && intialCondition.getNextTriggerTimeWithPause().orElse(null) !=null && intialCondition.getNextTriggerTimeWithPause().get().isBefore(ZonedDateTime.now(ZoneId.systemDefault()))) { - Duration initialDelay = Duration.ofSeconds(intialCondition.getDefinedDelay().toSeconds()); - Duration remaingDuration = Duration.between(intialCondition.getNextTriggerTimeWithPause().get(),ZonedDateTime.now(ZoneId.systemDefault())); - Duration remaingDuration__ = Duration.between(ZonedDateTime.now(ZoneId.systemDefault()),intialCondition.getNextTriggerTimeWithPause().get()); - log.info ("\nInitial delay condition: {} \n- next targeted trigger time {} \n-remaning {} -difference: {}", intialCondition.toString(), - intialCondition.getNextTriggerTimeWithPause().get().toString(),remaingDuration.getSeconds(),remaingDuration__.getSeconds()); - condition = new IntervalCondition( - condition.getInterval(), - condition.getMinInterval(), - condition.getMaxInterval(), - condition.isRandomize(), - condition.getRandomFactor(), - condition.getMaximumNumberOfRepeats(), - remaingDuration__.getSeconds() - ); - } - else if (initialDelaySeconds != null && initialDelaySeconds > 0) { - // Create a new condition with the initial delay - log.info("\nInitial delay condition: {} \n- next targeted trigger time {} \n- initial delay seconds {}, before: {}", - condition.toString(), - condition.getNextTriggerTimeWithPause().orElse(null), - initialDelaySeconds, - condition.getNextTriggerTimeWithPause().orElse(null).isBefore(ZonedDateTime.now(ZoneId.systemDefault())) ? "yes" : "no" - ); - condition = new IntervalCondition( - condition.getInterval(), - condition.getMinInterval(), - condition.getMaxInterval(), - condition.isRandomize(), - condition.getRandomFactor(), - condition.getMaximumNumberOfRepeats(), - initialDelaySeconds - ); - } - if (jsonObject.has("currentValidResetCount")){ - if (jsonObject.get("currentValidResetCount").isJsonNull()) { - condition.setCurrentValidResetCount(0); - }else{ - try { - condition.setCurrentValidResetCount(jsonObject.get("currentValidResetCount").getAsLong()); - } catch (Exception e) { - log.warn("Invalid currentValidResetCount value: {}", jsonObject.get("currentValidResetCount").getAsString()); - } - - } - } - if (jsonObject.has("nextTriggerTimeMillis")) { - long nextTriggerMillis = jsonObject.get("nextTriggerTimeMillis").getAsLong(); - ZonedDateTime nextTrigger = ZonedDateTime.ofInstant( - Instant.ofEpochMilli(nextTriggerMillis), - ZoneId.systemDefault() - ); - ZonedDateTime currentTriggerDateTime = condition.getCurrentTriggerTime().get(); - condition.setNextTriggerTime(nextTrigger); - } - return condition; - - - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/LocalDateAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/LocalDateAdapter.java deleted file mode 100644 index 8e1acb37923..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/LocalDateAdapter.java +++ /dev/null @@ -1,32 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; - -import java.lang.reflect.Type; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; - -/** - * Serializes/deserializes LocalDate with ISO format - */ -@Slf4j -public class LocalDateAdapter implements JsonSerializer, JsonDeserializer { - private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ISO_DATE; - - @Override - public JsonElement serialize(LocalDate src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive(src.format(DATE_FORMAT)); - } - - @Override - public LocalDate deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - try { - return LocalDate.parse(json.getAsString(), DATE_FORMAT); - } catch (Exception e) { - log.warn("Error deserializing LocalDate", e); - return LocalDate.now(); // Default to today if parsing fails - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/LocalTimeAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/LocalTimeAdapter.java deleted file mode 100644 index 69a6f88bcaf..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/LocalTimeAdapter.java +++ /dev/null @@ -1,33 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; - -import java.lang.reflect.Type; -import java.time.LocalTime; -import java.time.format.DateTimeFormatter; - -/** - * Serializes LocalTime as UTC time string and deserializes back to local timezone - */ -@Slf4j -public class LocalTimeAdapter implements JsonSerializer, JsonDeserializer { - private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ISO_TIME; - - @Override - public JsonElement serialize(LocalTime src, Type typeOfSrc, JsonSerializationContext context) { - // Store the time with UTC marker for consistency - return new JsonPrimitive(src.format(TIME_FORMAT)); - } - - @Override - public LocalTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - try { - return LocalTime.parse(json.getAsString(), TIME_FORMAT); - } catch (Exception e) { - log.warn("Error deserializing LocalTime", e); - return LocalTime.of(0, 0); // Default to midnight if parsing fails - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/SingleTriggerTimeConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/SingleTriggerTimeConditionAdapter.java deleted file mode 100644 index 7b96ca09983..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/SingleTriggerTimeConditionAdapter.java +++ /dev/null @@ -1,103 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; - -import java.lang.reflect.Type; -import java.time.Duration; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; - -/** - * Custom serializer/deserializer for SingleTriggerTimeCondition - */ -@Slf4j -public class SingleTriggerTimeConditionAdapter implements JsonSerializer, JsonDeserializer { - private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ISO_TIME; - private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ISO_DATE; - - @Override - public JsonElement serialize(SingleTriggerTimeCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add type information - json.addProperty("type", SingleTriggerTimeCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Get source timezone - ZoneId sourceZone = ZoneId.systemDefault(); - - // Store times in UTC, converting from source timezone - LocalDate today = LocalDate.now(); - - // Convert start time to UTC - ZonedDateTime targetUtc = src.getNextTriggerTimeWithPause().get().withZoneSameInstant(ZoneId.of("UTC")); - data.addProperty("version", src.getVersion()); - data.addProperty("targetTime", targetUtc.toLocalTime().format(TIME_FORMAT)); - data.addProperty("targetDate", targetUtc.toLocalDate().format(DATE_FORMAT)); - - // Mark that these are UTC times for future compatibility - data.addProperty("timeFormat", "UTC"); - // Store trigger state - data.addProperty("maximumNumberOfRepeats", src.getMaximumNumberOfRepeats()); - data.addProperty("currentValidResetCount", src.getCurrentValidResetCount()); - data.addProperty("definedDelay", src.getDefinedDelay().toSeconds()); - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public SingleTriggerTimeCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Get timezone (fallback to system default) - ZoneId zoneId = ZoneId.systemDefault(); - if (dataObj.has("version")) { - if (!dataObj.get("version").getAsString().equals(SingleTriggerTimeCondition.getVersion())) { - throw new JsonParseException("Version mismatch: expected " + SingleTriggerTimeCondition.getVersion() + - ", got " + dataObj.get("version").getAsString()); - } - } - Duration definedDelay = Duration.ofSeconds(0); - if (dataObj.has("definedDelay")){ - definedDelay = Duration.ofSeconds(dataObj.get("definedDelay").getAsLong()); - } - // Parse time values - LocalTime serializedStartTime = LocalTime.parse(dataObj.get("targetTime").getAsString(), TIME_FORMAT); - // Parse date values - LocalDate serializedStartDate = LocalDate.parse(dataObj.get("targetDate").getAsString(), DATE_FORMAT); - // Convert to ZonedDateTime - ZonedDateTime targetZoned = ZonedDateTime.of(serializedStartDate, serializedStartTime, ZoneId.of("UTC")); - ZonedDateTime targetZonedSyDateTime = targetZoned.withZoneSameInstant(zoneId); - // Create condition - SingleTriggerTimeCondition condition = new SingleTriggerTimeCondition(targetZonedSyDateTime , definedDelay, - dataObj.get("maximumNumberOfRepeats").getAsInt()); - if (dataObj.has("currentValidResetCount")){ - condition.setCurrentValidResetCount(dataObj.get("currentValidResetCount").getAsLong()); - } - - return condition; - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/TimeConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/TimeConditionAdapter.java deleted file mode 100644 index 6462e8bb04f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/TimeConditionAdapter.java +++ /dev/null @@ -1,57 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization; -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.DayOfWeekCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition; - -import java.lang.reflect.Type; -import java.util.Map; - -// Then create a new TimeConditionAdapter.java class: -@Slf4j -public class TimeConditionAdapter implements JsonDeserializer { - @Override - public TimeCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - // Handle typed wrapper format - if (jsonObject.has("type") && jsonObject.has("data")) { - String type = jsonObject.get("type").getAsString(); - JsonObject data = jsonObject.getAsJsonObject("data"); - - // Create a new object with the same structure as expected by type-specific deserializers - JsonObject unwrappedJson = new JsonObject(); - for (Map.Entry entry : data.entrySet()) { - unwrappedJson.add(entry.getKey(), entry.getValue()); - } - - // Determine the concrete type from the type field - if (type.endsWith("IntervalCondition")) { - return context.deserialize(unwrappedJson, IntervalCondition.class); - } else if (type.endsWith("SingleTriggerTimeCondition")) { - return context.deserialize(unwrappedJson, SingleTriggerTimeCondition.class); - } else if (type.endsWith("TimeWindowCondition")) { - return context.deserialize(unwrappedJson, TimeWindowCondition.class); - } else if (type.endsWith("DayOfWeekCondition")) { - return context.deserialize(unwrappedJson, DayOfWeekCondition.class); - } - } - - // Legacy format - determine type from properties - if (jsonObject.has("intervalSeconds")) { - return context.deserialize(jsonObject, IntervalCondition.class); - } else if (jsonObject.has("targetTime")) { - return context.deserialize(jsonObject, SingleTriggerTimeCondition.class); - } else if (jsonObject.has("startTime") && jsonObject.has("endTime")) { - return context.deserialize(jsonObject, TimeWindowCondition.class); - } else if (jsonObject.has("activeDays")) { - return context.deserialize(jsonObject, DayOfWeekCondition.class); - } - - throw new JsonParseException("Unknown TimeCondition type"); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/TimeWindowConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/TimeWindowConditionAdapter.java deleted file mode 100644 index e37b3ab7746..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/TimeWindowConditionAdapter.java +++ /dev/null @@ -1,217 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.enums.RepeatCycle; - -import java.lang.reflect.Type; -import java.time.*; -import java.time.format.DateTimeFormatter; - -/** - * Custom serializer/deserializer for TimeWindowCondition that handles timezone conversion - */ -@Slf4j -public class TimeWindowConditionAdapter implements JsonSerializer, JsonDeserializer { - private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ISO_TIME; - private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ISO_DATE; - - @Override - public JsonElement serialize(TimeWindowCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add type information - json.addProperty("type", TimeWindowCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Get source timezone - ZoneId sourceZone = src.getZoneId() != null ? src.getZoneId() : ZoneId.systemDefault(); - data.addProperty("version", src.getVersion()); - // Store the timezone ID for deserialization - data.addProperty("zoneId", sourceZone.getId()); - - // Store times in UTC, converting from source timezone - LocalDate today = LocalDate.now(); - - // Convert start time to UTC - ZonedDateTime startZoned = ZonedDateTime.of(today, src.getStartTime(), sourceZone); - ZonedDateTime startUtc = startZoned.withZoneSameInstant(ZoneId.of("UTC")); - - // Convert end time to UTC - ZonedDateTime endZoned = ZonedDateTime.of(today, src.getEndTime(), sourceZone); - ZonedDateTime endUtc = endZoned.withZoneSameInstant(ZoneId.of("UTC")); - - // Convert dates to UTC (using noon to avoid DST issues) - ZonedDateTime startDateZoned = ZonedDateTime.of(src.getStartDate(), LocalTime.NOON, sourceZone); - ZonedDateTime startDateUtc = startDateZoned.withZoneSameInstant(ZoneId.of("UTC")); - - ZonedDateTime endDateZoned = ZonedDateTime.of(src.getEndDate(), LocalTime.NOON, sourceZone); - ZonedDateTime endDateUtc = endDateZoned.withZoneSameInstant(ZoneId.of("UTC")); - - // Store UTC times - data.addProperty("startTime", startUtc.toLocalTime().format(TIME_FORMAT)); - data.addProperty("endTime", endUtc.toLocalTime().format(TIME_FORMAT)); - data.addProperty("startDate", startDateUtc.toLocalDate().format(DATE_FORMAT)); - data.addProperty("endDate", endDateUtc.toLocalDate().format(DATE_FORMAT)); - LocalDateTime currentStartDateTime = src.getNextTriggerTimeWithPause().get().toLocalDateTime(); - LocalDateTime currentEndDateTime = src.getCurrentEndDateTime(); - if (currentStartDateTime != null) { - data.addProperty("currentStartDateTime", currentStartDateTime.format(DateTimeFormatter.ISO_DATE_TIME)); - } - if (currentEndDateTime != null) { - data.addProperty("currentEndDateTime", currentEndDateTime.format(DateTimeFormatter.ISO_DATE_TIME)); - } - data.addProperty("transientNumberOfResetsWithinDailyInterval", src.getTransientNumberOfResetsWithinDailyInterval()); - - // Mark that these are UTC times for future compatibility - data.addProperty("timeFormat", "UTC"); - - // Repeat cycle information - data.addProperty("repeatCycle", src.getRepeatCycle().name()); - data.addProperty("repeatInterval", src.getRepeatIntervalUnit()); - - // Randomization settings - data.addProperty("useRandomization", src.isUseRandomization()); - data.addProperty("randomizerValue", src.getRandomizerValue()); - data.addProperty("maximumNumberOfRepeats", src.getMaximumNumberOfRepeats()); - data.addProperty("currentValidResetCount", src.getCurrentValidResetCount()); - - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public TimeWindowCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - - - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - if (dataObj.has("version")) { - if (!dataObj.get("version").getAsString().equals(TimeWindowCondition.getVersion())) { - throw new JsonParseException("Version mismatch: expected " + TimeWindowCondition.getVersion() + - ", got " + dataObj.get("version").getAsString()); - } - } - // Get the target timezone for conversion (default to system) - ZoneId targetZone = ZoneId.systemDefault(); - if (dataObj.has("zoneId")) { - try { - targetZone = ZoneId.of(dataObj.get("zoneId").getAsString()); - } catch (Exception e) { - log.warn("Invalid zoneId in serialized TimeWindowCondition", e); - } - } - - // Check if times are stored in UTC format - boolean isUtcFormat = dataObj.has("timeFormat") && - "UTC".equals(dataObj.get("timeFormat").getAsString()); - - // Parse time values - LocalTime serializedStartTime = LocalTime.parse(dataObj.get("startTime").getAsString(), TIME_FORMAT); - LocalTime serializedEndTime = LocalTime.parse(dataObj.get("endTime").getAsString(), TIME_FORMAT); - - // Parse date values - LocalDate serializedStartDate = LocalDate.parse(dataObj.get("startDate").getAsString(), DATE_FORMAT); - LocalDate serializedEndDate = LocalDate.parse(dataObj.get("endDate").getAsString(), DATE_FORMAT); - - LocalTime startTime; - LocalTime endTime; - LocalDate startDate; - LocalDate endDate; - - if (isUtcFormat) { - // If stored in UTC format, convert back to target timezone - LocalDate today = LocalDate.now(); - - // Convert start time from UTC to target zone - ZonedDateTime startUtc = ZonedDateTime.of(today, serializedStartTime, ZoneId.of("UTC")); - ZonedDateTime startTargetZone = startUtc.withZoneSameInstant(targetZone); - startTime = startTargetZone.toLocalTime(); - - // Convert end time from UTC to target zone - ZonedDateTime endUtc = ZonedDateTime.of(today, serializedEndTime, ZoneId.of("UTC")); - ZonedDateTime endTargetZone = endUtc.withZoneSameInstant(targetZone); - endTime = endTargetZone.toLocalTime(); - - // Convert dates from UTC to target zone - ZonedDateTime startDateUtc = ZonedDateTime.of(serializedStartDate, LocalTime.NOON, ZoneId.of("UTC")); - ZonedDateTime startDateTarget = startDateUtc.withZoneSameInstant(targetZone); - startDate = startDateTarget.toLocalDate(); - - ZonedDateTime endDateUtc = ZonedDateTime.of(serializedEndDate, LocalTime.NOON, ZoneId.of("UTC")); - ZonedDateTime endDateTarget = endDateUtc.withZoneSameInstant(targetZone); - endDate = endDateTarget.toLocalDate(); - } else { - // Legacy format - use times as-is - startTime = serializedStartTime; - endTime = serializedEndTime; - startDate = serializedStartDate; - endDate = serializedEndDate; - } - - // Parse repeat cycle - RepeatCycle repeatCycle = RepeatCycle.valueOf( - dataObj.get("repeatCycle").getAsString()); - int repeatInterval = dataObj.get("repeatInterval").getAsInt(); - long maximumNumberOfRepeats = dataObj.get("maximumNumberOfRepeats").getAsLong(); - // Create the condition with the parsed values - TimeWindowCondition condition = new TimeWindowCondition( - startTime, endTime, startDate, endDate, repeatCycle, repeatInterval, maximumNumberOfRepeats); - - if (dataObj.has("currentStartDateTime") && dataObj.has("currentEndDateTime")) { - LocalDateTime lastCurrentStartDateTime = LocalDateTime.parse(dataObj.get("currentStartDateTime").getAsString()); - LocalDateTime lastCurrentEndDateTime = LocalDateTime.parse(dataObj.get("currentEndDateTime").getAsString()); - // check first if the last current start date time and end date time is in a future - // date time, if so set the current start date time and end date time to the last current start date time and end date time, otherwise set it to the current start date time and end date time - LocalDateTime currentStartDateTime = lastCurrentStartDateTime.isAfter(LocalDateTime.now()) ? lastCurrentStartDateTime : condition.getNextTriggerTimeWithPause().get().toLocalDateTime(); - LocalDateTime currentEndDateTime = lastCurrentEndDateTime.isAfter(LocalDateTime.now()) ? lastCurrentEndDateTime : condition.getCurrentEndDateTime(); - // ensure start date time is before end date time - if (currentStartDateTime.isAfter(currentEndDateTime)) { - throw new JsonParseException("Current start date time is after current end date time"); - } - condition.setNextTriggerTime(currentStartDateTime.atZone(ZoneId.systemDefault())); - condition.setCurrentEndDateTime(currentEndDateTime); - } - - - - if (dataObj.has("currentValidResetCount")){ - condition.setCurrentValidResetCount(dataObj.get("currentValidResetCount").getAsLong()); - } - if (dataObj.has("transientNumberOfResetsWithinDailyInterval")) { - condition.setTransientNumberOfResetsWithinDailyInterval(dataObj.get("transientNumberOfResetsWithinDailyInterval").getAsInt()); - } - - - - // Set timezone - condition.setZoneId(targetZone); - - // Set randomization if present - if (dataObj.has("useRandomization") && dataObj.has("randomizerValue")) { - boolean useRandomization = dataObj.get("useRandomization").getAsBoolean(); - int randomizerValue = dataObj.get("randomizerValue").getAsInt(); - condition.setRandomization(useRandomization); - condition.setRandomizerValue(randomizerValue); - } - return condition; - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/ui/TimeConditionPanelUtil.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/ui/TimeConditionPanelUtil.java deleted file mode 100644 index 11df242afa3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/ui/TimeConditionPanelUtil.java +++ /dev/null @@ -1,1492 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.ui; -import java.time.ZoneId; -import javax.swing.Box; -import javax.swing.BoxLayout; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.BorderFactory; -import javax.swing.SwingUtilities; -import javax.swing.border.TitledBorder; -import java.awt.BorderLayout; -import java.awt.Component; -import java.awt.Container; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.components.DateRangePanel; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.components.IntervalPickerPanel; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.components.SingleDateTimePickerPanel; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.components.TimeRangePanel; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; -import java.awt.Color; -import java.awt.Dimension; -import java.awt.FlowLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridLayout; -import java.time.DayOfWeek; -import java.time.Duration; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.util.EnumSet; -import java.util.Optional; -import java.util.Set; -import java.time.ZonedDateTime; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JSpinner; -import javax.swing.SpinnerNumberModel; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.DayOfWeekCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.enums.RepeatCycle; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.components.InitialDelayPanel; -@Slf4j -public class TimeConditionPanelUtil { - public static void createIntervalConfigPanel(JPanel panel, GridBagConstraints gbc) { - // Title and initial setup - JLabel titleLabel = new JLabel("Time Interval Configuration:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // Create and add interval picker component - gbc.gridy++; - IntervalPickerPanel intervalPicker = new IntervalPickerPanel(true); - panel.add(intervalPicker, gbc); - - // Add initial delay configuration - gbc.gridy++; - InitialDelayPanel initialDelayPanel = new InitialDelayPanel(); - - panel.add(initialDelayPanel, gbc); - - // Add a helpful description - gbc.gridy++; - JLabel descriptionLabel = new JLabel("Plugin will stop after specified time interval"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Add additional info about randomization - gbc.gridy++; - JLabel randomInfoLabel = new JLabel("Random intervals make your bot behavior less predictable"); - randomInfoLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - randomInfoLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(randomInfoLabel, gbc); - - // Add information about initial delay - gbc.gridy++; - JLabel initialDelayInfoLabel = new JLabel("Initial delay adds waiting time before the first interval trigger"); - initialDelayInfoLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - initialDelayInfoLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(initialDelayInfoLabel, gbc); - - // Store components for later access - panel.putClientProperty("intervalPicker", intervalPicker); - panel.putClientProperty("initialDelayPanel", initialDelayPanel); - // Removed as delayMinutesSpinner is now encapsulated in InitialDelayPanel - // Removed as delaySecondsSpinner is now encapsulated in InitialDelayPanel - } - - /** - * Helper method to validate min and max intervals ensure min <= max - */ - private static void validateMinMaxIntervals( - JSpinner minHoursSpinner, JSpinner minMinutesSpinner, - JSpinner maxHoursSpinner, JSpinner maxMinutesSpinner, - boolean isMinUpdated) { - - int minHours = (Integer) minHoursSpinner.getValue(); - int minMinutes = (Integer) minMinutesSpinner.getValue(); - int maxHours = (Integer) maxHoursSpinner.getValue(); - int maxMinutes = (Integer) maxMinutesSpinner.getValue(); - - int minTotalMinutes = minHours * 60 + minMinutes; - int maxTotalMinutes = maxHours * 60 + maxMinutes; - - if (isMinUpdated) { - // If min was updated and exceeds max, adjust max - if (minTotalMinutes > maxTotalMinutes) { - maxHoursSpinner.setValue(minHours); - maxMinutesSpinner.setValue(minMinutes); - } - } else { - // If max was updated and is less than min, adjust min - if (maxTotalMinutes < minTotalMinutes) { - minHoursSpinner.setValue(maxHours); - minMinutesSpinner.setValue(maxMinutes); - } - } - } - - /** - * Creates an IntervalCondition from the config panel. - * This replaces the createTimeCondition method. - */ - public static IntervalCondition createIntervalCondition(JPanel configPanel) { - IntervalPickerPanel intervalPicker = (IntervalPickerPanel) configPanel.getClientProperty("intervalPicker"); - InitialDelayPanel initialDelayPanel = (InitialDelayPanel) configPanel.getClientProperty("initialDelayPanel"); - if (intervalPicker == null) { - throw new IllegalStateException("Interval picker component not found"); - } - - // Get the interval condition from the picker component - IntervalCondition baseCondition = intervalPicker.createIntervalCondition(); - - // Check if initial delay should be added - if (initialDelayPanel != null && initialDelayPanel.isInitialDelayEnabled()) { - int delayHours = initialDelayPanel.getHours(); - int delayMinutes = initialDelayPanel.getMinutes(); - int delaySeconds = initialDelayPanel.getSeconds(); - int totalDelaySeconds = delayHours * 3600 + delayMinutes * 60 + delaySeconds; - - if (totalDelaySeconds > 0) { - // Create a new condition with the same parameters as the base condition plus the delay - if (baseCondition.isRandomize()) { - // For randomized intervals - return new IntervalCondition( - baseCondition.getInterval(), - baseCondition.getMinInterval(), - baseCondition.getMaxInterval(), - baseCondition.isRandomize(), - baseCondition.getRandomFactor(), - baseCondition.getMaximumNumberOfRepeats(), - (long)totalDelaySeconds - ); - } else { - // For fixed intervals - return new IntervalCondition( - baseCondition.getInterval(), - baseCondition.isRandomize(), - baseCondition.getRandomFactor(), - baseCondition.getMaximumNumberOfRepeats(), - (long)totalDelaySeconds - ); - } - } - } - - return baseCondition; - } - - public static void createTimeWindowConfigPanel(JPanel panel, GridBagConstraints gbc) { - // Section Title - JLabel titleLabel = new JLabel("Time Window Configuration:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // Date Range Configuration with Preset ComboBox - gbc.gridy++; - gbc.gridwidth = 1; - JPanel dateRangeConfigPanel = createDateRangeConfigPanel(); - gbc.gridwidth = 2; - panel.add(dateRangeConfigPanel, gbc); - - // Time Range Configuration with Preset ComboBox - gbc.gridy++; - JPanel timeRangeConfigPanel = createTimeRangeConfigPanel(); - panel.add(timeRangeConfigPanel, gbc); - - // Repeat and Randomization Panel (combined for compactness) - gbc.gridy++; - JPanel optionsPanel = createOptionsPanel(); - panel.add(optionsPanel, gbc); - - // Help text - gbc.gridy++; - JPanel helpPanel = createHelpPanel(); - panel.add(helpPanel, gbc); - - // Store components for later access - DateRangePanel dateRangePanel = (DateRangePanel) dateRangeConfigPanel.getClientProperty("dateRangePanel"); - TimeRangePanel timeRangePanel = (TimeRangePanel) timeRangeConfigPanel.getClientProperty("timeRangePanel"); - @SuppressWarnings("unchecked") - JComboBox repeatComboBox = (JComboBox) optionsPanel.getClientProperty("repeatComboBox"); - JSpinner intervalSpinner = (JSpinner) optionsPanel.getClientProperty("intervalSpinner"); - JCheckBox randomizeCheckBox = (JCheckBox) optionsPanel.getClientProperty("randomizeCheckBox"); - JSpinner randomizeSpinner = (JSpinner) optionsPanel.getClientProperty("randomizeSpinner"); - - panel.putClientProperty("dateRangePanel", dateRangePanel); - panel.putClientProperty("timeRangePanel", timeRangePanel); - panel.putClientProperty("repeatComboBox", repeatComboBox); - panel.putClientProperty("intervalSpinner", intervalSpinner); - panel.putClientProperty("randomizeCheckBox", randomizeCheckBox); - panel.putClientProperty("randomizeSpinner", randomizeSpinner); - } - - /** - * Creates a compact date range configuration panel with preset ComboBox - */ - private static JPanel createDateRangeConfigPanel() { - JPanel mainPanel = new JPanel(new BorderLayout(5, 5)); - mainPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - mainPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Date Range", - TitledBorder.LEFT, - TitledBorder.TOP, - FontManager.getRunescapeSmallFont(), - Color.WHITE)); - - // Preset selection panel - JPanel presetPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - presetPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel presetLabel = new JLabel("Preset:"); - presetLabel.setForeground(Color.WHITE); - presetLabel.setFont(FontManager.getRunescapeSmallFont()); - presetPanel.add(presetLabel); - - String[] datePresets = { - "Unlimited", "Today", "This Week", "This Month", - "Next 7 Days", "Next 30 Days", "Next 90 Days", "Custom" - }; - JComboBox datePresetCombo = new JComboBox<>(datePresets); - datePresetCombo.setSelectedItem("Unlimited"); - datePresetCombo.setFont(FontManager.getRunescapeSmallFont()); - presetPanel.add(datePresetCombo); - - mainPanel.add(presetPanel, BorderLayout.NORTH); - - // Date range panel (initially hidden for preset selections) - DateRangePanel dateRangePanel = new DateRangePanel( - net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition.UNLIMITED_START_DATE, - net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition.UNLIMITED_END_DATE - ); - dateRangePanel.setVisible(false); // Hidden by default for "Unlimited" - mainPanel.add(dateRangePanel, BorderLayout.CENTER); - - // Handle preset selection - datePresetCombo.addActionListener(e -> { - String selected = (String) datePresetCombo.getSelectedItem(); - LocalDate today = LocalDate.now(); - - switch (selected) { - case "Unlimited": - dateRangePanel.setStartDate(net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition.UNLIMITED_START_DATE); - dateRangePanel.setEndDate(net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition.UNLIMITED_END_DATE); - dateRangePanel.setVisible(false); - break; - case "Today": - dateRangePanel.setStartDate(today); - dateRangePanel.setEndDate(today); - dateRangePanel.setVisible(true); - break; - case "This Week": - dateRangePanel.setStartDate(today); - dateRangePanel.setEndDate(today.plusDays(7 - today.getDayOfWeek().getValue())); - dateRangePanel.setVisible(true); - break; - case "This Month": - dateRangePanel.setStartDate(today); - dateRangePanel.setEndDate(today.withDayOfMonth(today.lengthOfMonth())); - dateRangePanel.setVisible(true); - break; - case "Next 7 Days": - dateRangePanel.setStartDate(today); - dateRangePanel.setEndDate(today.plusDays(7)); - dateRangePanel.setVisible(true); - break; - case "Next 30 Days": - dateRangePanel.setStartDate(today); - dateRangePanel.setEndDate(today.plusDays(30)); - dateRangePanel.setVisible(true); - break; - case "Next 90 Days": - dateRangePanel.setStartDate(today); - dateRangePanel.setEndDate(today.plusDays(90)); - dateRangePanel.setVisible(true); - break; - case "Custom": - dateRangePanel.setVisible(true); - break; - } - mainPanel.revalidate(); - mainPanel.repaint(); - }); - - mainPanel.putClientProperty("dateRangePanel", dateRangePanel); - mainPanel.putClientProperty("datePresetCombo", datePresetCombo); - return mainPanel; - } - - /** - * Creates a compact time range configuration panel with preset ComboBox - */ - private static JPanel createTimeRangeConfigPanel() { - JPanel mainPanel = new JPanel(new BorderLayout(5, 5)); - mainPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - mainPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Time Range", - TitledBorder.LEFT, - TitledBorder.TOP, - FontManager.getRunescapeSmallFont(), - Color.WHITE)); - - // Preset selection panel - JPanel presetPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - presetPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel presetLabel = new JLabel("Preset:"); - presetLabel.setForeground(Color.WHITE); - presetLabel.setFont(FontManager.getRunescapeSmallFont()); - presetPanel.add(presetLabel); - - String[] timePresets = { - "All Day", "Business Hours", "Morning", "Afternoon", - "Evening", "Night", "Custom" - }; - JComboBox timePresetCombo = new JComboBox<>(timePresets); - timePresetCombo.setSelectedItem("Business Hours"); - timePresetCombo.setFont(FontManager.getRunescapeSmallFont()); - presetPanel.add(timePresetCombo); - - mainPanel.add(presetPanel, BorderLayout.NORTH); - - // Time range panel (initially hidden for preset selections) - TimeRangePanel timeRangePanel = new TimeRangePanel(LocalTime.of(9, 0), LocalTime.of(17, 0)); - timeRangePanel.setVisible(false); // Hidden by default for "Business Hours" - mainPanel.add(timeRangePanel, BorderLayout.CENTER); - - // Handle preset selection - timePresetCombo.addActionListener(e -> { - String selected = (String) timePresetCombo.getSelectedItem(); - - switch (selected) { - case "All Day": - timeRangePanel.setStartTime(LocalTime.of(0, 0)); - timeRangePanel.setEndTime(LocalTime.of(23, 59)); - timeRangePanel.setVisible(false); - break; - case "Business Hours": - timeRangePanel.setStartTime(LocalTime.of(9, 0)); - timeRangePanel.setEndTime(LocalTime.of(17, 0)); - timeRangePanel.setVisible(false); - break; - case "Morning": - timeRangePanel.setStartTime(LocalTime.of(6, 0)); - timeRangePanel.setEndTime(LocalTime.of(12, 0)); - timeRangePanel.setVisible(false); - break; - case "Afternoon": - timeRangePanel.setStartTime(LocalTime.of(12, 0)); - timeRangePanel.setEndTime(LocalTime.of(18, 0)); - timeRangePanel.setVisible(false); - break; - case "Evening": - timeRangePanel.setStartTime(LocalTime.of(18, 0)); - timeRangePanel.setEndTime(LocalTime.of(22, 0)); - timeRangePanel.setVisible(false); - break; - case "Night": - timeRangePanel.setStartTime(LocalTime.of(22, 0)); - timeRangePanel.setEndTime(LocalTime.of(6, 0)); - timeRangePanel.setVisible(false); - break; - case "Custom": - timeRangePanel.setVisible(true); - break; - } - mainPanel.revalidate(); - mainPanel.repaint(); - }); - - mainPanel.putClientProperty("timeRangePanel", timeRangePanel); - mainPanel.putClientProperty("timePresetCombo", timePresetCombo); - return mainPanel; - } - - /** - * Creates a compact options panel with repeat cycle and randomization controls - */ - private static JPanel createOptionsPanel() { - JPanel mainPanel = new JPanel(new BorderLayout(5, 5)); - mainPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - mainPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Options", - TitledBorder.LEFT, - TitledBorder.TOP, - FontManager.getRunescapeSmallFont(), - Color.WHITE)); - - // Repeat options panel - JPanel repeatPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 2)); - repeatPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel repeatLabel = new JLabel("Repeat:"); - repeatLabel.setForeground(Color.WHITE); - repeatLabel.setFont(FontManager.getRunescapeSmallFont()); - repeatPanel.add(repeatLabel); - - String[] repeatOptions = {"Every Day", "Every X Days", "Every X Hours", "Every X Minutes", "Every X Weeks", "One Time Only"}; - JComboBox repeatComboBox = new JComboBox<>(repeatOptions); - repeatComboBox.setFont(FontManager.getRunescapeSmallFont()); - repeatPanel.add(repeatComboBox); - - JLabel intervalLabel = new JLabel("Interval:"); - intervalLabel.setForeground(Color.WHITE); - intervalLabel.setFont(FontManager.getRunescapeSmallFont()); - repeatPanel.add(intervalLabel); - - SpinnerNumberModel intervalModel = new SpinnerNumberModel(1, 1, 100, 1); - JSpinner intervalSpinner = new JSpinner(intervalModel); - intervalSpinner.setPreferredSize(new Dimension(60, intervalSpinner.getPreferredSize().height)); - intervalSpinner.setEnabled(false); - repeatPanel.add(intervalSpinner); - - // Randomization options panel - JPanel randomPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 2)); - randomPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JCheckBox randomizeCheckBox = new JCheckBox("Randomize"); - randomizeCheckBox.setForeground(Color.WHITE); - randomizeCheckBox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - randomizeCheckBox.setFont(FontManager.getRunescapeSmallFont()); - randomPanel.add(randomizeCheckBox); - - JLabel randomizeAmountLabel = new JLabel("±"); - randomizeAmountLabel.setForeground(Color.WHITE); - randomizeAmountLabel.setFont(FontManager.getRunescapeSmallFont()); - randomPanel.add(randomizeAmountLabel); - - SpinnerNumberModel randomizeModel = new SpinnerNumberModel(3, 1, 15, 1); // Default to 3 minutes, max 15 for default "Every Day" - JSpinner randomizeSpinner = new JSpinner(randomizeModel); - randomizeSpinner.setPreferredSize(new Dimension(50, randomizeSpinner.getPreferredSize().height)); - randomizeSpinner.setEnabled(false); - randomizeSpinner.setToolTipText("Randomization range: ±1 to ±15 minutes
Maximum is 40% of 1 days interval"); - randomPanel.add(randomizeSpinner); - - JLabel randomizerUnitLabel = new JLabel("min"); - randomizerUnitLabel.setForeground(Color.WHITE); - randomizerUnitLabel.setFont(FontManager.getRunescapeSmallFont()); - randomPanel.add(randomizerUnitLabel); - - // Control interactions - repeatComboBox.addActionListener(e -> { - String selected = (String) repeatComboBox.getSelectedItem(); - boolean enableInterval = !selected.equals("Every Day") && !selected.equals("One Time Only"); - intervalSpinner.setEnabled(enableInterval); - - // Update randomizer limits based on selected repeat cycle and interval - updateRandomizerLimits(repeatComboBox, intervalSpinner, randomizeSpinner, randomizerUnitLabel); - }); - - // Update randomizer limits when interval changes - intervalSpinner.addChangeListener(e -> { - updateRandomizerLimits(repeatComboBox, intervalSpinner, randomizeSpinner, randomizerUnitLabel); - }); - - randomizeCheckBox.addActionListener(e -> - randomizeSpinner.setEnabled(randomizeCheckBox.isSelected()) - ); - - // Set initial randomizer limits based on default selection - SwingUtilities.invokeLater(() -> updateRandomizerLimits(repeatComboBox, intervalSpinner, randomizeSpinner, randomizerUnitLabel)); - - // Layout both panels - JPanel combinedPanel = new JPanel(new GridLayout(2, 1, 0, 2)); - combinedPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - combinedPanel.add(repeatPanel); - combinedPanel.add(randomPanel); - - mainPanel.add(combinedPanel, BorderLayout.CENTER); - - // Store components for later access - mainPanel.putClientProperty("repeatComboBox", repeatComboBox); - mainPanel.putClientProperty("intervalSpinner", intervalSpinner); - mainPanel.putClientProperty("randomizeCheckBox", randomizeCheckBox); - mainPanel.putClientProperty("randomizeSpinner", randomizeSpinner); - mainPanel.putClientProperty("randomizerUnitLabel", randomizerUnitLabel); - - return mainPanel; - } - - /** - * Updates the randomizer spinner limits based on the current repeat cycle and interval - */ - private static void updateRandomizerLimits(JComboBox repeatComboBox, JSpinner intervalSpinner, JSpinner randomizeSpinner, JLabel randomizerUnitLabel) { - String selectedOption = (String) repeatComboBox.getSelectedItem(); - int interval = (Integer) intervalSpinner.getValue(); - - // Map the selected option to RepeatCycle - RepeatCycle repeatCycle; - switch (selectedOption) { - case "Every Day": - repeatCycle = RepeatCycle.DAYS; - interval = 1; - break; - case "Every X Days": - repeatCycle = RepeatCycle.DAYS; - break; - case "Every X Hours": - repeatCycle = RepeatCycle.HOURS; - break; - case "Every X Minutes": - repeatCycle = RepeatCycle.MINUTES; - break; - case "Every X Weeks": - repeatCycle = RepeatCycle.WEEKS; - break; - case "One Time Only": - repeatCycle = RepeatCycle.ONE_TIME; - break; - default: - repeatCycle = RepeatCycle.DAYS; - interval = 1; - } - - // Get the automatic randomization unit based on repeat cycle - RepeatCycle randomUnit = getAutomaticRandomizerValueUnit(repeatCycle); - - // Calculate the maximum allowed randomizer value using the new logic - int maxRandomizer = calculateMaxAllowedRandomization(repeatCycle, interval); - - // Ensure minimum valid bounds for SpinnerNumberModel - if (maxRandomizer < 1) { - maxRandomizer = 1; - } - - // Update the spinner model with new limits - SpinnerNumberModel currentModel = (SpinnerNumberModel) randomizeSpinner.getModel(); - int currentValue = currentModel.getNumber().intValue(); - - // Ensure current value is within valid bounds - int validatedCurrentValue = Math.max(1, Math.min(currentValue, maxRandomizer)); - - // Create new model with validated values - SpinnerNumberModel newModel = new SpinnerNumberModel( - validatedCurrentValue, // current value, validated to be within bounds - 1, // minimum - maxRandomizer, // maximum (at least 1) - 1 // step - ); - - randomizeSpinner.setModel(newModel); - - // Update the unit label based on the automatic randomization unit - String unitDisplayName = getRandomizationUnitDisplayName(randomUnit); - randomizerUnitLabel.setText(unitDisplayName); - - // Update tooltip to show the reasoning with correct unit - randomizeSpinner.setToolTipText(String.format( - "Randomization range: Âą1 to Âą%d %s
" + - "Unit: %s (auto-determined from %s cycle)
" + - "Maximum is 40%% of %d %s interval", - maxRandomizer, - unitDisplayName, - randomUnit.toString().toLowerCase(), - repeatCycle.toString().toLowerCase(), - interval, - repeatCycle.toString().toLowerCase() - )); - } - - /** - * Creates a help panel with useful information - */ - private static JPanel createHelpPanel() { - JPanel helpPanel = new JPanel(); - helpPanel.setLayout(new BoxLayout(helpPanel, BoxLayout.Y_AXIS)); - helpPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel descriptionLabel = new JLabel("Plugin will only run during the specified time window"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - descriptionLabel.setAlignmentX(Component.LEFT_ALIGNMENT); - - JLabel crossDayLabel = new JLabel("Note: If start time > end time, window crosses midnight"); - crossDayLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - crossDayLabel.setFont(FontManager.getRunescapeSmallFont()); - crossDayLabel.setAlignmentX(Component.LEFT_ALIGNMENT); - - JLabel timezoneLabel = new JLabel("Timezone: " + ZoneId.systemDefault().getId()); - timezoneLabel.setForeground(Color.YELLOW); - timezoneLabel.setFont(FontManager.getRunescapeSmallFont()); - timezoneLabel.setAlignmentX(Component.LEFT_ALIGNMENT); - - helpPanel.add(descriptionLabel); - helpPanel.add(Box.createVerticalStrut(2)); - helpPanel.add(crossDayLabel); - helpPanel.add(Box.createVerticalStrut(2)); - helpPanel.add(timezoneLabel); - - return helpPanel; - } - - public static TimeWindowCondition createTimeWindowCondition(JPanel configPanel) { - DateRangePanel dateRangePanel = (DateRangePanel) configPanel.getClientProperty("dateRangePanel"); - TimeRangePanel timeRangePanel = (TimeRangePanel) configPanel.getClientProperty("timeRangePanel"); - @SuppressWarnings("unchecked") - JComboBox repeatComboBox = (JComboBox) configPanel.getClientProperty("repeatComboBox"); - JSpinner intervalSpinner = (JSpinner) configPanel.getClientProperty("intervalSpinner"); - JCheckBox randomizeCheckBox = (JCheckBox) configPanel.getClientProperty("randomizeCheckBox"); - JSpinner randomizeSpinner = (JSpinner) configPanel.getClientProperty("randomizeSpinner"); - - if (dateRangePanel == null || timeRangePanel == null) { - throw new IllegalStateException("Time window configuration components not found"); - } - - // Get date values - LocalDate startDate = dateRangePanel.getStartDate(); - LocalDate endDate = dateRangePanel.getEndDate(); - - // Get time values - LocalTime startTime = timeRangePanel.getStartTime(); - LocalTime endTime = timeRangePanel.getEndTime(); - - // Get repeat cycle configuration - String repeatOption = (String) repeatComboBox.getSelectedItem(); - RepeatCycle repeatCycle; - int interval = (Integer) intervalSpinner.getValue(); - long maximumNumberOfRepeats = 0; // Default to infinite repeats - switch (repeatOption) { - case "Every Day": - repeatCycle = RepeatCycle.DAYS; - interval = 1; - break; - case "Every X Days": - repeatCycle = RepeatCycle.DAYS; - break; - case "Every X Hours": - repeatCycle = RepeatCycle.HOURS; - break; - case "Every X Minutes": - repeatCycle = RepeatCycle.MINUTES; - break; - case "Every X Weeks": - repeatCycle = RepeatCycle.WEEKS; - break; - case "One Time Only": - repeatCycle = RepeatCycle.ONE_TIME; - interval = 1; - maximumNumberOfRepeats = 1; - break; - default: - repeatCycle = RepeatCycle.DAYS; - interval = 1; - } - - // Create the condition - TimeWindowCondition condition = new TimeWindowCondition( - startTime, - endTime, - startDate, - endDate, - repeatCycle, - interval, - maximumNumberOfRepeats - - ); - - // Apply randomization if enabled - if (randomizeCheckBox.isSelected()) { - int randomizerValue = (Integer) randomizeSpinner.getValue(); - int maxAllowedRandomizer = calculateMaxAllowedRandomization(condition.getRepeatCycle(), condition.getRepeatIntervalUnit()); - int validatedValue = Math.max(1, Math.min(randomizerValue, maxAllowedRandomizer)); - if (validatedValue != randomizerValue) { - log.warn(" - createTimeWindowCondition - Randomizer value {} is too large for interval {} {}. Capping at maxi {}", - randomizerValue, interval, repeatCycle, maxAllowedRandomizer); - randomizerValue = validatedValue; - randomizeSpinner.setValue(validatedValue); // Update UI to reflect capped value - } - - condition.setRandomization(true); - condition.setRandomizerValue(validatedValue); - // Note: randomization unit is now automatically determined based on repeat cycle - // No need to manually set it anymore - TimeWindow handles this internally - } - - return condition; - } - - - /** - * Creates a panel for configuring SingleTriggerTimeCondition - * Uses the enhanced SingleDateTimePickerPanel component - */ - public static void createSingleTriggerConfigPanel(JPanel panel, GridBagConstraints gbc) { - // Section title - JLabel titleLabel = new JLabel("One-Time Trigger Configuration:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - panel.add(titleLabel, gbc); - - // Create the date/time picker panel - gbc.gridy++; - SingleDateTimePickerPanel dateTimePicker = new SingleDateTimePickerPanel(); - panel.add(dateTimePicker, gbc); - // Description - gbc.gridy++; - JLabel descriptionLabel = new JLabel("Plugin will be triggered once at the specified date and time"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Current timezone info - gbc.gridy++; - JLabel timezoneLabel = new JLabel("Current timezone: " + ZoneId.systemDefault().getId()); - timezoneLabel.setForeground(Color.YELLOW); - timezoneLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(timezoneLabel, gbc); - - // Store components for later access - panel.putClientProperty("dateTimePicker", dateTimePicker); - - } - /** - * Creates a SingleTriggerTimeCondition from the config panel - * Uses the enhanced SingleDateTimePickerPanel component - */ - public static SingleTriggerTimeCondition createSingleTriggerCondition(JPanel configPanel) { - SingleDateTimePickerPanel dateTimePicker = (SingleDateTimePickerPanel) configPanel.getClientProperty("dateTimePicker"); - - if (dateTimePicker == null) { - log.error("Date time picker component not found in panel"); - return null; - } - - // Get the selected date and time as LocalDateTime - LocalDateTime selectedDateTime = dateTimePicker.getDateTime(); - - // Convert to ZonedDateTime using the system default timezone - ZonedDateTime triggerTime = selectedDateTime.atZone(ZoneId.systemDefault()); - - // Create and return the condition - return new SingleTriggerTimeCondition(triggerTime,Duration.ofSeconds(0),1); - } - public static void createDayOfWeekConfigPanel(JPanel panel, GridBagConstraints gbc) { - // Title and initial setup - JLabel titleLabel = new JLabel("Day of Week Configuration:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - panel.add(titleLabel, gbc); - - // Preset options - gbc.gridy++; - JPanel presetPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - presetPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JButton weekdaysButton = new JButton("Weekdays"); - weekdaysButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - weekdaysButton.setForeground(Color.WHITE); - - JButton weekendsButton = new JButton("Weekends"); - weekendsButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - weekendsButton.setForeground(Color.WHITE); - - JButton allDaysButton = new JButton("All Days"); - allDaysButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - allDaysButton.setForeground(Color.WHITE); - - presetPanel.add(weekdaysButton); - presetPanel.add(weekendsButton); - presetPanel.add(allDaysButton); - - panel.add(presetPanel, gbc); - - // Day checkboxes - gbc.gridy++; - JPanel daysPanel = new JPanel(new GridLayout(0, 3)); - daysPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - String[] dayNames = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}; - JCheckBox[] dayCheckboxes = new JCheckBox[7]; - - for (int i = 0; i < dayNames.length; i++) { - dayCheckboxes[i] = new JCheckBox(dayNames[i]); - dayCheckboxes[i].setBackground(ColorScheme.DARKER_GRAY_COLOR); - dayCheckboxes[i].setForeground(Color.WHITE); - daysPanel.add(dayCheckboxes[i]); - } - - // Set up weekdays button - weekdaysButton.addActionListener(e -> { - for (int i = 0; i < 5; i++) { - dayCheckboxes[i].setSelected(true); - } - dayCheckboxes[5].setSelected(false); - dayCheckboxes[6].setSelected(false); - }); - - // Set up weekends button - weekendsButton.addActionListener(e -> { - for (int i = 0; i < 5; i++) { - dayCheckboxes[i].setSelected(false); - } - dayCheckboxes[5].setSelected(true); - dayCheckboxes[6].setSelected(true); - }); - - // Set up all days button - allDaysButton.addActionListener(e -> { - for (JCheckBox checkbox : dayCheckboxes) { - checkbox.setSelected(true); - } - }); - - panel.add(daysPanel, gbc); - - // Add usage limits panel - gbc.gridy++; - JPanel usageLimitsPanel = new JPanel(); - usageLimitsPanel.setLayout(new BoxLayout(usageLimitsPanel, BoxLayout.Y_AXIS)); - usageLimitsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Daily limit panel - JPanel dailyLimitPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - dailyLimitPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel dailyLimitLabel = new JLabel("Max repeats per day:"); - dailyLimitLabel.setForeground(Color.WHITE); - dailyLimitPanel.add(dailyLimitLabel); - - SpinnerNumberModel dailyLimitModel = new SpinnerNumberModel(0, 0, 100, 1); - JSpinner dailyLimitSpinner = new JSpinner(dailyLimitModel); - dailyLimitSpinner.setPreferredSize(new Dimension(70, dailyLimitSpinner.getPreferredSize().height)); - dailyLimitPanel.add(dailyLimitSpinner); - - JLabel dailyUnlimitedLabel = new JLabel("(0 = unlimited)"); - dailyUnlimitedLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - dailyUnlimitedLabel.setFont(FontManager.getRunescapeSmallFont()); - dailyLimitPanel.add(dailyUnlimitedLabel); - - usageLimitsPanel.add(dailyLimitPanel); - - // Weekly limit panel - JPanel weeklyLimitPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - weeklyLimitPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel weeklyLimitLabel = new JLabel("Max repeats per week:"); - weeklyLimitLabel.setForeground(Color.WHITE); - weeklyLimitPanel.add(weeklyLimitLabel); - - SpinnerNumberModel weeklyLimitModel = new SpinnerNumberModel(0, 0, 100, 1); - JSpinner weeklyLimitSpinner = new JSpinner(weeklyLimitModel); - weeklyLimitSpinner.setPreferredSize(new Dimension(70, weeklyLimitSpinner.getPreferredSize().height)); - weeklyLimitPanel.add(weeklyLimitSpinner); - - JLabel weeklyUnlimitedLabel = new JLabel("(0 = unlimited)"); - weeklyUnlimitedLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - weeklyUnlimitedLabel.setFont(FontManager.getRunescapeSmallFont()); - weeklyLimitPanel.add(weeklyUnlimitedLabel); - - usageLimitsPanel.add(weeklyLimitPanel); - - panel.add(usageLimitsPanel, gbc); - - // Add interval configuration using the reusable IntervalPickerPanel - gbc.gridy++; - JPanel intervalOptionPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - intervalOptionPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JCheckBox useIntervalCheckBox = new JCheckBox("Use interval between triggers"); - useIntervalCheckBox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - useIntervalCheckBox.setForeground(Color.WHITE); - intervalOptionPanel.add(useIntervalCheckBox); - - panel.add(intervalOptionPanel, gbc); - - // Add the interval picker panel (initially disabled) - gbc.gridy++; - IntervalPickerPanel intervalPicker = new IntervalPickerPanel(false); // No presets needed - intervalPicker.setEnabled(false); - panel.add(intervalPicker, gbc); - - // Toggle interval picker based on checkbox - useIntervalCheckBox.addActionListener(e -> { - boolean useInterval = useIntervalCheckBox.isSelected(); - intervalPicker.setEnabled(useInterval); - }); - - // Description - gbc.gridy++; - JLabel descriptionLabel = new JLabel("Plugin will only run on selected days of the week"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Add limits description - gbc.gridy++; - JLabel limitsLabel = new JLabel("Daily/weekly limits prevent excessive usage"); - limitsLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - limitsLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(limitsLabel, gbc); - - // Add interval description - gbc.gridy++; - JLabel intervalDescLabel = new JLabel("Intervals control time between triggers on the same day"); - intervalDescLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - intervalDescLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(intervalDescLabel, gbc); - - // Store components for later access - panel.putClientProperty("dayCheckboxes", dayCheckboxes); - panel.putClientProperty("dailyLimitSpinner", dailyLimitSpinner); - panel.putClientProperty("weeklyLimitSpinner", weeklyLimitSpinner); - panel.putClientProperty("useIntervalCheckBox", useIntervalCheckBox); - panel.putClientProperty("intervalPicker", intervalPicker); -} -public static DayOfWeekCondition createDayOfWeekCondition(JPanel configPanel) { - JCheckBox[] dayCheckboxes = (JCheckBox[]) configPanel.getClientProperty("dayCheckboxes"); - JSpinner dailyLimitSpinner = (JSpinner) configPanel.getClientProperty("dailyLimitSpinner"); - JSpinner weeklyLimitSpinner = (JSpinner) configPanel.getClientProperty("weeklyLimitSpinner"); - JCheckBox useIntervalCheckBox = (JCheckBox) configPanel.getClientProperty("useIntervalCheckBox"); - IntervalPickerPanel intervalPicker = (IntervalPickerPanel) configPanel.getClientProperty("intervalPicker"); - - if (dayCheckboxes == null) { - throw new IllegalStateException("Day of week configuration components not found"); - } - - // Collect the selected days - Set activeDays = EnumSet.noneOf(DayOfWeek.class); - if (dayCheckboxes[0].isSelected()) activeDays.add(DayOfWeek.MONDAY); - if (dayCheckboxes[1].isSelected()) activeDays.add(DayOfWeek.TUESDAY); - if (dayCheckboxes[2].isSelected()) activeDays.add(DayOfWeek.WEDNESDAY); - if (dayCheckboxes[3].isSelected()) activeDays.add(DayOfWeek.THURSDAY); - if (dayCheckboxes[4].isSelected()) activeDays.add(DayOfWeek.FRIDAY); - if (dayCheckboxes[5].isSelected()) activeDays.add(DayOfWeek.SATURDAY); - if (dayCheckboxes[6].isSelected()) activeDays.add(DayOfWeek.SUNDAY); - - // If no days selected, default to all days - if (activeDays.isEmpty()) { - activeDays.add(DayOfWeek.MONDAY); - activeDays.add(DayOfWeek.TUESDAY); - activeDays.add(DayOfWeek.WEDNESDAY); - activeDays.add(DayOfWeek.THURSDAY); - activeDays.add(DayOfWeek.FRIDAY); - activeDays.add(DayOfWeek.SATURDAY); - activeDays.add(DayOfWeek.SUNDAY); - } - - // Get daily and weekly limits - long maxRepeatsPerDay = dailyLimitSpinner != null ? (Integer) dailyLimitSpinner.getValue() : 0; - long maxRepeatsPerWeek = weeklyLimitSpinner != null ? (Integer) weeklyLimitSpinner.getValue() : 0; - - // Create the base condition with appropriate limits - DayOfWeekCondition condition = new DayOfWeekCondition(0, maxRepeatsPerDay, maxRepeatsPerWeek, activeDays); - - // If using interval, add interval condition from the interval picker - if (useIntervalCheckBox != null && useIntervalCheckBox.isSelected() && intervalPicker != null) { - IntervalCondition intervalCondition = intervalPicker.createIntervalCondition(); - condition.setIntervalCondition(intervalCondition); - } - - return condition; -} - - - /** - * Sets up the panel with values from an existing time condition - * - * @param panel The panel containing the UI components - * @param condition The time condition to read values from - */ - public static void setupTimeCondition(JPanel panel, Condition condition) { - if (condition == null) { - return; - } - - if (condition instanceof IntervalCondition) { - setupIntervalCondition(panel, (IntervalCondition) condition); - } else if (condition instanceof TimeWindowCondition) { - setupTimeWindowCondition(panel, (TimeWindowCondition) condition); - } else if (condition instanceof DayOfWeekCondition) { - setupDayOfWeekCondition(panel, (DayOfWeekCondition) condition); - } else if (condition instanceof SingleTriggerTimeCondition) { - setupSingleTriggerCondition(panel, (SingleTriggerTimeCondition) condition); - } - } - - - /** - * Sets up the interval condition panel with values from an existing condition - */ - private static void setupIntervalCondition(JPanel panel, IntervalCondition condition) { - // Get the IntervalPickerPanel component which encapsulates all the interval UI controls - IntervalPickerPanel intervalPicker = (IntervalPickerPanel) panel.getClientProperty("intervalPicker"); - InitialDelayPanel initialDelayPanel = (InitialDelayPanel) panel.getClientProperty("initialDelayPanel"); - if (intervalPicker == null) { - log.error("IntervalPickerPanel component not found for interval condition setup"); - return; // Missing UI components - } - - // Use the IntervalPickerPanel's built-in method to configure itself from the condition - intervalPicker.setIntervalCondition(condition); - - // Set initial delay if it exists - if (condition.getInitialDelayCondition() != null && initialDelayPanel != null) { - Duration definedDelay = condition.getInitialDelayCondition().getDefinedDelay(); - if (definedDelay != null && definedDelay.getSeconds() > 0) { - long definedSeconds = definedDelay.getSeconds(); - - // Set the delay time in the UI - int hours = (int) (definedSeconds / 3600); - int minutes = (int) ((definedSeconds % 3600) / 60); - int seconds = (int) (definedSeconds % 60); - - initialDelayPanel.setEnabled(true); - initialDelayPanel.getHoursSpinner().setValue(hours); - initialDelayPanel.getMinutesSpinner().setValue(minutes); - initialDelayPanel.getSecondsSpinner().setValue(seconds); - } - } - } - - /** - * Sets up the time window condition panel with values from an existing condition - */ - private static void setupTimeWindowCondition(JPanel panel, TimeWindowCondition condition) { - // Get custom components from client properties - DateRangePanel dateRangePanel = (DateRangePanel) panel.getClientProperty("dateRangePanel"); - TimeRangePanel timeRangePanel = (TimeRangePanel) panel.getClientProperty("timeRangePanel"); - @SuppressWarnings("unchecked") - JComboBox repeatComboBox = (JComboBox) panel.getClientProperty("repeatComboBox"); - JSpinner intervalSpinner = (JSpinner) panel.getClientProperty("intervalSpinner"); - JCheckBox randomizeCheckBox = (JCheckBox) panel.getClientProperty("randomizeCheckBox"); - JSpinner randomizeSpinner = (JSpinner) panel.getClientProperty("randomizeSpinner"); - - // Find and update date preset ComboBox - updateDatePresetFromCondition(panel, condition); - - // Find and update time preset ComboBox - updateTimePresetFromCondition(panel, condition); - - // Set date range - if (dateRangePanel != null) { - if (condition.getStartDate() != null) { - dateRangePanel.setStartDate(condition.getStartDate()); - } - if (condition.getEndDate() != null) { - dateRangePanel.setEndDate(condition.getEndDate()); - } - } - - // Set time range - if (timeRangePanel != null) { - timeRangePanel.setStartTime(condition.getStartTime()); - timeRangePanel.setEndTime(condition.getEndTime()); - } - - // Set repeat cycle - if (repeatComboBox != null) { - RepeatCycle cycle = condition.getRepeatCycle(); - int interval = condition.getRepeatIntervalUnit(); - - // Map RepeatCycle enum to combo box options - switch (cycle) { - case DAYS: - repeatComboBox.setSelectedItem(interval == 1 ? "Every Day" : "Every X Days"); - break; - case HOURS: - repeatComboBox.setSelectedItem("Every X Hours"); - break; - case MINUTES: - repeatComboBox.setSelectedItem("Every X Minutes"); - break; - case WEEKS: - repeatComboBox.setSelectedItem("Every X Weeks"); - break; - case ONE_TIME: - repeatComboBox.setSelectedItem("One Time Only"); - break; - case SECONDS: - case MILLIS: - default: - // Fallback for unsupported cycles - repeatComboBox.setSelectedItem("Every Day"); - break; - } - - // Set interval value - if (intervalSpinner != null) { - intervalSpinner.setValue(interval); - intervalSpinner.setEnabled(!cycle.equals(RepeatCycle.DAYS) || interval != 1); - } - } - - // Set randomization - if (randomizeCheckBox != null && randomizeSpinner != null) { - randomizeCheckBox.setSelected(condition.isUseRandomization()); - randomizeSpinner.setEnabled(condition.isUseRandomization()); - - // Get the randomizer unit label for updating - JLabel randomizerUnitLabel = (JLabel) panel.getClientProperty("randomizerUnitLabel"); - - // Get the automatic randomization unit and update the label - RepeatCycle randomUnit = getAutomaticRandomizerValueUnit(condition.getRepeatCycle()); - if (randomizerUnitLabel != null) { - String unitDisplayName = getRandomizationUnitDisplayName(randomUnit); - randomizerUnitLabel.setText(unitDisplayName); - } - - // Validate and set randomizer value using the new logic - int savedRandomizerValue = condition.getRandomizerValue(); - if (savedRandomizerValue > 0) { - // Calculate max allowed for this condition's settings using new logic - int maxAllowedRandomizer = calculateMaxAllowedRandomization(condition.getRepeatCycle(), condition.getRepeatIntervalUnit()); - int validatedValue = Math.max(1, Math.min(savedRandomizerValue, maxAllowedRandomizer)); - - // Ensure maximum is at least 1 - maxAllowedRandomizer = Math.max(1, maxAllowedRandomizer); - - // Update spinner model with proper limits - SpinnerNumberModel newModel = new SpinnerNumberModel( - validatedValue, // current value, validated - 1, // minimum - maxAllowedRandomizer, // maximum - 1 // step - ); - randomizeSpinner.setModel(newModel); - - // Update tooltip with correct unit information - String unitDisplayName = getRandomizationUnitDisplayName(randomUnit); - randomizeSpinner.setToolTipText(String.format( - "Randomization range: Âą1 to Âą%d %s
" + - "Unit: %s (auto-determined from %s cycle)
" + - "Maximum is 40%% of %d %s interval", - maxAllowedRandomizer, - unitDisplayName, - randomUnit.toString().toLowerCase(), - condition.getRepeatCycle().toString().toLowerCase(), - condition.getRepeatIntervalUnit(), - condition.getRepeatCycle().toString().toLowerCase() - )); - if ( savedRandomizerValue != validatedValue) { - log.warn("Randomizer value {} was too large for {}x{} interval. Capped at {} - maximum {}", - savedRandomizerValue, condition.getRepeatIntervalUnit(), condition.getRepeatCycle(), validatedValue,maxAllowedRandomizer); - } - } else { - // Set default value if no randomization value is set - int maxAllowedRandomizer = calculateMaxAllowedRandomization(condition.getRepeatCycle(), - condition.getRepeatIntervalUnit()); - int defaultValue = Math.max(1, Math.min(3, maxAllowedRandomizer)); - - // Ensure maximum is at least 1 - maxAllowedRandomizer = Math.max(1, maxAllowedRandomizer); - - // Update spinner model with proper limits - SpinnerNumberModel newModel = new SpinnerNumberModel( - defaultValue, // default value - 1, // minimum - maxAllowedRandomizer, // maximum - 1 // step - ); - randomizeSpinner.setModel(newModel); - - // Update tooltip with correct unit information - String unitDisplayName = getRandomizationUnitDisplayName(randomUnit); - randomizeSpinner.setToolTipText(String.format( - "Randomization range: Âą1 to Âą%d %s
" + - "Unit: %s (auto-determined from %s cycle)
" + - "Maximum is 40%% of %d %s interval", - maxAllowedRandomizer, - unitDisplayName, - randomUnit.toString().toLowerCase(), - condition.getRepeatCycle().toString().toLowerCase(), - condition.getRepeatIntervalUnit(), - condition.getRepeatCycle().toString().toLowerCase() - )); - } - } - } - - /** - * Updates the date preset ComboBox based on the condition's date range - */ - private static void updateDatePresetFromCondition(JPanel panel, TimeWindowCondition condition) { - // Try to find the date preset ComboBox in the panel hierarchy - JComboBox datePresetCombo = findDatePresetComboBox(panel); - if (datePresetCombo == null) return; - - LocalDate startDate = condition.getStartDate(); - LocalDate endDate = condition.getEndDate(); - LocalDate today = LocalDate.now(); - - // Check if it matches any preset - if (condition.hasUnlimitedDateRange()) { - datePresetCombo.setSelectedItem("Unlimited"); - } else if (startDate.equals(today) && endDate.equals(today)) { - datePresetCombo.setSelectedItem("Today"); - } else if (startDate.equals(today) && endDate.equals(today.plusDays(7 - today.getDayOfWeek().getValue()))) { - datePresetCombo.setSelectedItem("This Week"); - } else if (startDate.equals(today) && endDate.equals(today.withDayOfMonth(today.lengthOfMonth()))) { - datePresetCombo.setSelectedItem("This Month"); - } else if (startDate.equals(today) && endDate.equals(today.plusDays(7))) { - datePresetCombo.setSelectedItem("Next 7 Days"); - } else if (startDate.equals(today) && endDate.equals(today.plusDays(30))) { - datePresetCombo.setSelectedItem("Next 30 Days"); - } else if (startDate.equals(today) && endDate.equals(today.plusDays(90))) { - datePresetCombo.setSelectedItem("Next 90 Days"); - } else { - datePresetCombo.setSelectedItem("Custom"); - } - } - - /** - * Updates the time preset ComboBox based on the condition's time range - */ - private static void updateTimePresetFromCondition(JPanel panel, TimeWindowCondition condition) { - // Try to find the time preset ComboBox in the panel hierarchy - JComboBox timePresetCombo = findTimePresetComboBox(panel); - if (timePresetCombo == null) return; - - LocalTime startTime = condition.getStartTime(); - LocalTime endTime = condition.getEndTime(); - - // Check if it matches any preset - if (startTime.equals(LocalTime.of(0, 0)) && endTime.equals(LocalTime.of(23, 59))) { - timePresetCombo.setSelectedItem("All Day"); - } else if (startTime.equals(LocalTime.of(9, 0)) && endTime.equals(LocalTime.of(17, 0))) { - timePresetCombo.setSelectedItem("Business Hours"); - } else if (startTime.equals(LocalTime.of(6, 0)) && endTime.equals(LocalTime.of(12, 0))) { - timePresetCombo.setSelectedItem("Morning"); - } else if (startTime.equals(LocalTime.of(12, 0)) && endTime.equals(LocalTime.of(18, 0))) { - timePresetCombo.setSelectedItem("Afternoon"); - } else if (startTime.equals(LocalTime.of(18, 0)) && endTime.equals(LocalTime.of(22, 0))) { - timePresetCombo.setSelectedItem("Evening"); - } else if (startTime.equals(LocalTime.of(22, 0)) && endTime.equals(LocalTime.of(6, 0))) { - timePresetCombo.setSelectedItem("Night"); - } else { - timePresetCombo.setSelectedItem("Custom"); - } - } - - /** - * Recursively searches for the date preset ComboBox in the panel hierarchy - */ - private static JComboBox findDatePresetComboBox(Container container) { - for (Component component : container.getComponents()) { - if (component instanceof JPanel) { - JPanel panel = (JPanel) component; - Object datePresetCombo = panel.getClientProperty("datePresetCombo"); - if (datePresetCombo instanceof JComboBox) { - @SuppressWarnings("unchecked") - JComboBox comboBox = (JComboBox) datePresetCombo; - return comboBox; - } - // Recursively search in child panels - JComboBox found = findDatePresetComboBox(panel); - if (found != null) return found; - } - } - return null; - } - - /** - * Recursively searches for the time preset ComboBox in the panel hierarchy - */ - private static JComboBox findTimePresetComboBox(Container container) { - for (Component component : container.getComponents()) { - if (component instanceof JPanel) { - JPanel panel = (JPanel) component; - Object timePresetCombo = panel.getClientProperty("timePresetCombo"); - if (timePresetCombo instanceof JComboBox) { - @SuppressWarnings("unchecked") - JComboBox comboBox = (JComboBox) timePresetCombo; - return comboBox; - } - // Recursively search in child panels - JComboBox found = findTimePresetComboBox(panel); - if (found != null) return found; - } - } - return null; - } - - /** - * Sets up the day of week condition panel with values from an existing condition - */ - private static void setupDayOfWeekCondition(JPanel panel, DayOfWeekCondition condition) { - JCheckBox[] dayCheckboxes = (JCheckBox[]) panel.getClientProperty("dayCheckboxes"); - JSpinner dailyLimitSpinner = (JSpinner) panel.getClientProperty("dailyLimitSpinner"); - JSpinner weeklyLimitSpinner = (JSpinner) panel.getClientProperty("weeklyLimitSpinner"); - JCheckBox useIntervalCheckBox = (JCheckBox) panel.getClientProperty("useIntervalCheckBox"); - IntervalPickerPanel intervalPicker = (IntervalPickerPanel) panel.getClientProperty("intervalPicker"); - - if (dayCheckboxes != null) { - Set activeDays = condition.getActiveDays(); - - // Map DayOfWeek enum values to checkbox indices (0 = Monday) - if (activeDays.contains(DayOfWeek.MONDAY)) dayCheckboxes[0].setSelected(true); - if (activeDays.contains(DayOfWeek.TUESDAY)) dayCheckboxes[1].setSelected(true); - if (activeDays.contains(DayOfWeek.WEDNESDAY)) dayCheckboxes[2].setSelected(true); - if (activeDays.contains(DayOfWeek.THURSDAY)) dayCheckboxes[3].setSelected(true); - if (activeDays.contains(DayOfWeek.FRIDAY)) dayCheckboxes[4].setSelected(true); - if (activeDays.contains(DayOfWeek.SATURDAY)) dayCheckboxes[5].setSelected(true); - if (activeDays.contains(DayOfWeek.SUNDAY)) dayCheckboxes[6].setSelected(true); - } - - // Set daily and weekly limits - if (dailyLimitSpinner != null) { - dailyLimitSpinner.setValue((int)condition.getMaxRepeatsPerDay()); - } - - if (weeklyLimitSpinner != null) { - weeklyLimitSpinner.setValue((int)condition.getMaxRepeatsPerWeek()); - } - - // Handle interval condition if present - Optional intervalConditionOpt = condition.getIntervalCondition(); - if (intervalConditionOpt.isPresent() && useIntervalCheckBox != null && intervalPicker != null) { - // Enable the interval checkbox - useIntervalCheckBox.setSelected(true); - intervalPicker.setEnabled(true); - - // Configure the interval picker with the condition - intervalPicker.setIntervalCondition(intervalConditionOpt.get()); - } - - // Refresh panel layout - panel.revalidate(); - panel.repaint(); - } - - /** - * Sets up the single trigger condition panel with values from an existing condition - */ - private static void setupSingleTriggerCondition(JPanel panel, SingleTriggerTimeCondition condition) { - SingleDateTimePickerPanel dateTimePicker = (SingleDateTimePickerPanel) panel.getClientProperty("dateTimePicker"); - if (dateTimePicker != null) { - // Convert ZonedDateTime to LocalDateTime - dateTimePicker.setDateTime(condition.getNextTriggerTimeWithPause().get().toLocalDateTime()); - } - - } - - - - /** - * Gets the automatic randomization unit based on repeat cycle (mirrors TimeWindowCondition logic) - */ - private static RepeatCycle getAutomaticRandomizerValueUnit(RepeatCycle repeatCycle) { - switch (repeatCycle) { - case MINUTES: - return RepeatCycle.SECONDS; // For minute intervals, randomize in seconds - case HOURS: - return RepeatCycle.MINUTES; // For hour intervals, randomize in minutes - case DAYS: - return RepeatCycle.MINUTES; // For day intervals, randomize in minutes - case WEEKS: - return RepeatCycle.HOURS; // For week intervals, randomize in hours - case ONE_TIME: - return RepeatCycle.MINUTES; // For one-time, use minutes as default - default: - return RepeatCycle.MINUTES; // Default fallback to minutes - } - } - - /** - * Converts an interval value from one unit to another (mirrors TimeWindowCondition logic) - */ - public static long convertToRandomizationUnit(int value, RepeatCycle fromUnit, RepeatCycle toUnit) { - // Convert to seconds first, then to target unit - long totalSeconds; - switch (fromUnit) { - case MINUTES: - totalSeconds = value * 60L; - break; - case HOURS: - totalSeconds = value * 3600L; - break; - case DAYS: - totalSeconds = value * 86400L; - break; - case WEEKS: - totalSeconds = value * 604800L; - break; - default: - totalSeconds = value; - break; - } - - // Convert from seconds to target unit - switch (toUnit) { - case SECONDS: - return totalSeconds; - case MINUTES: - return totalSeconds / 60L; - case HOURS: - return totalSeconds / 3600L; - default: - return totalSeconds / 60L; // Default to minutes - } - } - - /** - * Calculates the maximum allowed randomization value (mirrors TimeWindowCondition logic) - */ - public static int calculateMaxAllowedRandomization(RepeatCycle repeatCycle, int interval) { - RepeatCycle randomUnit = getAutomaticRandomizerValueUnit(repeatCycle); - - // Calculate total interval in the randomization unit - long totalIntervalInRandomUnit; - switch (repeatCycle) { - case MINUTES: - totalIntervalInRandomUnit = convertToRandomizationUnit(interval, RepeatCycle.MINUTES, randomUnit); - break; - case HOURS: - totalIntervalInRandomUnit = convertToRandomizationUnit(interval, RepeatCycle.HOURS, randomUnit); - break; - case DAYS: - totalIntervalInRandomUnit = convertToRandomizationUnit(interval, RepeatCycle.DAYS, randomUnit); - break; - case WEEKS: - totalIntervalInRandomUnit = convertToRandomizationUnit(interval, RepeatCycle.WEEKS, randomUnit); - break; - case ONE_TIME: - // For one-time, allow up to 1 hour of randomization - return randomUnit == RepeatCycle.HOURS ? 1 : - randomUnit == RepeatCycle.MINUTES ? 60 : - randomUnit == RepeatCycle.SECONDS ? 3600 : 15; - default: - return 15; // Default fallback - } - // Allow randomization up to 40% of the total interval, but apply sensible caps - int maxRandomization = (int) Math.min(totalIntervalInRandomUnit * 0.4, totalIntervalInRandomUnit / 2); - - // Apply caps based on randomization unit to prevent excessive randomization - switch (randomUnit) { - case SECONDS: - return Math.min(maxRandomization, 3600); // Max 1 hour in seconds - case MINUTES: - return Math.min(maxRandomization, 720); // Max 12 hours in minutes - case HOURS: - return Math.min(maxRandomization, 48); // Max 2 days in hours - default: - return Math.min(maxRandomization, 60); // Default to 1 hour equivalent - } - } - - /** - * Gets the display name for the randomization unit - */ - private static String getRandomizationUnitDisplayName(RepeatCycle randomUnit) { - switch (randomUnit) { - case SECONDS: - return "sec"; - case MINUTES: - return "min"; - case HOURS: - return "hr"; - default: - return "min"; - } - } - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/util/TimeConditionUtil.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/util/TimeConditionUtil.java deleted file mode 100644 index 9d7e7fbfe08..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/util/TimeConditionUtil.java +++ /dev/null @@ -1,730 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.util; - -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.OrCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.DayOfWeekCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.enums.RepeatCycle; - -import java.time.DayOfWeek; -import java.time.Duration; -import java.time.LocalDate; -import java.time.LocalTime; -import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; -import java.util.EnumSet; - -import lombok.Getter; -import lombok.Setter; - - -/** - * Utility class providing factory methods for creating time-based conditions - * for the plugin scheduler system. These methods create start conditions with - * configurable parameters and appropriate caps. - */ -public final class TimeConditionUtil { - - // Maximum durations to prevent excessive sessions - private static final Duration MAX_WEEKDAY_SESSION = Duration.ofHours(4); - private static final Duration MAX_WEEKEND_SESSION = Duration.ofHours(6); - private static final int MAX_DAILY_REPEATS = 5; - private static final int MAX_WEEKLY_REPEATS = 20; - - // Private constructor to prevent instantiation - private TimeConditionUtil() {} - - /** - * Creates a day of week condition with configurable daily limits - * - * @param maxRepeatsPerDay Maximum number of times to trigger per day (capped at 5) - * @param days The days on which the condition should be active - * @return A DayOfWeekCondition with the specified settings - */ - public static DayOfWeekCondition createDailyLimitedCondition(int maxRepeatsPerDay, DayOfWeek... days) { - // Cap daily repeats at the maximum value - int cappedRepeats = Math.min(maxRepeatsPerDay, MAX_DAILY_REPEATS); - return new DayOfWeekCondition(0, cappedRepeats, days); - } - - /** - * Creates a day of week condition with configurable weekly limits - * - * @param maxRepeatsPerWeek Maximum number of times to trigger per week (capped at 20) - * @param days The days on which the condition should be active - * @return A DayOfWeekCondition with the specified settings - */ - public static DayOfWeekCondition createWeeklyLimitedCondition(int maxRepeatsPerWeek, DayOfWeek... days) { - // Cap weekly repeats at the maximum value - int cappedRepeats = Math.min(maxRepeatsPerWeek, MAX_WEEKLY_REPEATS); - return new DayOfWeekCondition(0, 0, cappedRepeats, days); - } - - /** - * Creates a day of week condition with both daily and weekly limits - * - * @param maxRepeatsPerDay Maximum number of times to trigger per day (capped at 5) - * @param maxRepeatsPerWeek Maximum number of times to trigger per week (capped at 20) - * @param days The days on which the condition should be active - * @return A DayOfWeekCondition with the specified settings - */ - public static DayOfWeekCondition createDailyAndWeeklyLimitedCondition( - int maxRepeatsPerDay, int maxRepeatsPerWeek, DayOfWeek... days) { - // Cap daily and weekly repeats at the maximum values - int cappedDailyRepeats = Math.min(maxRepeatsPerDay, MAX_DAILY_REPEATS); - int cappedWeeklyRepeats = Math.min(maxRepeatsPerWeek, MAX_WEEKLY_REPEATS); - return new DayOfWeekCondition(0, cappedDailyRepeats, cappedWeeklyRepeats, days); - } - - /** - * Creates a combined condition for running on weekdays with a specified session duration - * - * @param sessionHours Duration of each session in hours (capped at 4 hours for weekdays) - * @param maxRepeatsPerDay Maximum repeats per day (optional, defaults to 1) - * @return A combined condition for weekday play - */ - public static DayOfWeekCondition createWeekdaySessionCondition(float sessionHours, int maxRepeatsPerDay) { - // Cap the session duration - float cappedHours = Math.min(sessionHours, MAX_WEEKDAY_SESSION.toHours()); - int cappedRepeats = Math.min(maxRepeatsPerDay, MAX_DAILY_REPEATS); - - // Calculate minutes portion for partial hours - int hours = (int)cappedHours; - int minutes = (int)((cappedHours - hours) * 60); - - // Create the interval condition - IntervalCondition sessionDuration = new IntervalCondition( - Duration.ofHours(hours).plusMinutes(minutes)); - - // Create day of week condition for weekdays - DayOfWeekCondition weekdays = new DayOfWeekCondition( - 0, cappedRepeats, - DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, DayOfWeek.FRIDAY); - weekdays.setIntervalCondition(sessionDuration); - return weekdays; - } - - /** - * Creates a combined condition for running on weekdays with a specified session duration - * Uses default of 1 repeat per day - * - * @param sessionHours Duration of each session in hours (capped at 4 hours for weekdays) - * @return A combined condition for weekday play - */ - public static DayOfWeekCondition createWeekdaySessionCondition(float sessionHours) { - return createWeekdaySessionCondition(sessionHours, 1); - } - - /** - * Creates a combined condition for running on weekends with a specified session duration - * - * @param sessionHours Duration of each session in hours (capped at 6 hours for weekends) - * @param maxRepeatsPerDay Maximum repeats per day (optional, defaults to 2) - * @return A combined condition for weekend play - */ - public static DayOfWeekCondition createWeekendSessionCondition(float sessionHours, int maxRepeatsPerDay) { - // Cap the session duration - float cappedHours = Math.min(sessionHours, MAX_WEEKEND_SESSION.toHours()); - int cappedRepeats = Math.min(maxRepeatsPerDay, MAX_DAILY_REPEATS); - - // Calculate minutes portion for partial hours - int hours = (int)cappedHours; - int minutes = (int)((cappedHours - hours) * 60); - - // Create the interval condition - IntervalCondition sessionDuration = new IntervalCondition( - Duration.ofHours(hours).plusMinutes(minutes)); - - // Create day of week condition for weekends - DayOfWeekCondition weekends = new DayOfWeekCondition( - 0, cappedRepeats, - DayOfWeek.SATURDAY, DayOfWeek.SUNDAY); - weekends.setIntervalCondition(sessionDuration); - - - return weekends; - } - - /** - * Creates a combined condition for running on weekends with a specified session duration - * Uses default of 2 repeats per day - * - * @param sessionHours Duration of each session in hours (capped at 6 hours for weekends) - * @return A combined condition for weekend play - */ - public static DayOfWeekCondition createWeekendSessionCondition(float sessionHours) { - return createWeekendSessionCondition(sessionHours, 2); - } - - /** - * Creates a condition for randomized session durations on specified days - * - * @param minSessionHours Minimum session hours - * @param maxSessionHours Maximum session hours (capped based on weekday/weekend) - * @param maxRepeatsPerDay Maximum repeats per day (capped at 5) - * @param days The days on which the condition should be active - * @return A combined condition with randomized session length - */ - public static DayOfWeekCondition createRandomizedSessionCondition( - float minSessionHours, float maxSessionHours, int maxRepeatsPerDay, DayOfWeek... days) { - - // Determine if the days contain only weekends, only weekdays, or mixed - boolean hasWeekend = false; - boolean hasWeekday = false; - - for (DayOfWeek day : days) { - if (day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY) { - hasWeekend = true; - } else { - hasWeekday = true; - } - } - - // Apply appropriate caps based on day type - float cappedMaxHours; - if (hasWeekend && !hasWeekday) { - // Weekend-only cap - cappedMaxHours = Math.min(maxSessionHours, MAX_WEEKEND_SESSION.toHours()); - } else { - // Apply weekday cap (more restrictive) if any weekday is included - cappedMaxHours = Math.min(maxSessionHours, MAX_WEEKDAY_SESSION.toHours()); - } - - // Ensure min <= max - float cappedMinHours = Math.min(minSessionHours, cappedMaxHours); - - // Create min/max durations - int minHours = (int)cappedMinHours; - int minMinutes = (int)((cappedMinHours - minHours) * 60); - Duration minDuration = Duration.ofHours(minHours).plusMinutes(minMinutes); - - int maxHours = (int)cappedMaxHours; - int maxMinutes = (int)((cappedMaxHours - maxHours) * 60); - Duration maxDuration = Duration.ofHours(maxHours).plusMinutes(maxMinutes); - - // Create randomized interval condition - IntervalCondition intervalCondition = IntervalCondition.createRandomized( - minDuration, maxDuration); - - // Create day of week condition - int cappedRepeats = Math.min(maxRepeatsPerDay, MAX_DAILY_REPEATS); - DayOfWeekCondition dayCondition = new DayOfWeekCondition(0, cappedRepeats, days); - dayCondition.setIntervalCondition(intervalCondition); - - - return dayCondition; - } - - /** - * Creates a balanced weekly schedule with different session durations for different days - * and an overall weekly limit. - * - * @param weekdaySessionHours Session duration for weekdays (capped at 4 hours) - * @param weekendSessionHours Session duration for weekends (capped at 6 hours) - * @param weekdayRepeatsPerDay Maximum repeats per weekday (capped at 3) - * @param weekendRepeatsPerDay Maximum repeats per weekend day (capped at 4) - * @param weeklyLimit Overall weekly limit (capped at 20) - * @return A condition that combines all these restrictions - */ - public static AndCondition createBalancedWeeklySchedule( - float weekdaySessionHours, float weekendSessionHours, - int weekdayRepeatsPerDay, int weekendRepeatsPerDay, int weeklyLimit) { - - // Cap input values - float cappedWeekdayHours = Math.min(weekdaySessionHours, MAX_WEEKDAY_SESSION.toHours()); - float cappedWeekendHours = Math.min(weekendSessionHours, MAX_WEEKEND_SESSION.toHours()); - int cappedWeekdayRepeats = Math.min(weekdayRepeatsPerDay, 3); // Stricter cap for weekdays - int cappedWeekendRepeats = Math.min(weekendRepeatsPerDay, 4); // Looser cap for weekends - int cappedWeeklyLimit = Math.min(weeklyLimit, MAX_WEEKLY_REPEATS); - - // Convert hours to durations - int weekdayHours = (int)cappedWeekdayHours; - int weekdayMinutes = (int)((cappedWeekdayHours - weekdayHours) * 60); - Duration weekdayDuration = Duration.ofHours(weekdayHours).plusMinutes(weekdayMinutes); - - int weekendHours = (int)cappedWeekendHours; - int weekendMinutes = (int)((cappedWeekendHours - weekendHours) * 60); - Duration weekendDuration = Duration.ofHours(weekendHours).plusMinutes(weekendMinutes); - - // Create weekday condition - DayOfWeekCondition weekdays = new DayOfWeekCondition( - 0, cappedWeekdayRepeats, 0, - DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, DayOfWeek.FRIDAY); - - IntervalCondition weekdayInterval = new IntervalCondition(weekdayDuration); - weekdays.setIntervalCondition(weekdayInterval); - - - // Create weekend condition - DayOfWeekCondition weekends = new DayOfWeekCondition( - 0, cappedWeekendRepeats, 0, - DayOfWeek.SATURDAY, DayOfWeek.SUNDAY); - - IntervalCondition weekendInterval = new IntervalCondition(weekendDuration); - weekends.setIntervalCondition(weekendInterval); - - - - - // Create OR condition to combine weekday and weekend options - OrCondition dayOptions = new OrCondition(); - dayOptions.addCondition(weekdays); - dayOptions.addCondition(weekends); - - // Create weekly limit condition that applies to all days - DayOfWeekCondition weeklyLimitCondition = new DayOfWeekCondition( - 0, 0, cappedWeeklyLimit, - DayOfWeek.values()); - - // Combine day options with weekly limit - AndCondition finalCondition = new AndCondition(); - finalCondition.addCondition(weeklyLimitCondition); - finalCondition.addCondition(dayOptions); - - return finalCondition; - } - - /** - * Creates a scheduled time window condition based on time of day. - * - * @param startHour Starting hour (0-23) - * @param startMinute Starting minute (0-59) - * @param endHour Ending hour (0-23) - * @param endMinute Ending minute (0-59) - * @param days Days of week to apply this schedule - * @return A combined time window and day of week condition - */ - public static AndCondition createTimeWindowSchedule( - int startHour, int startMinute, int endHour, int endMinute, DayOfWeek... days) { - - // Validate and cap time values - startHour = Math.min(Math.max(startHour, 0), 23); - startMinute = Math.min(Math.max(startMinute, 0), 59); - endHour = Math.min(Math.max(endHour, 0), 23); - endMinute = Math.min(Math.max(endMinute, 0), 59); - - // Create time window condition - TimeWindowCondition timeWindow = new TimeWindowCondition( - LocalTime.of(startHour, startMinute), - LocalTime.of(endHour, endMinute), - LocalDate.now(), - LocalDate.now().plus(1, ChronoUnit.YEARS), - null, 1, 0); - - // Create day of week condition - DayOfWeekCondition dayCondition = new DayOfWeekCondition(0, days); - - // Combine conditions - AndCondition condition = new AndCondition(); - condition.addCondition(dayCondition); - condition.addCondition(timeWindow); - - return condition; - } - - /** - * Creates a randomized time window schedule. - * - * @param baseStartHour Base starting hour (0-23) - * @param baseStartMinute Base starting minute (0-59) - * @param baseEndHour Base ending hour (0-23) - * @param baseEndMinute Base ending minute (0-59) - * @param randomizerValue Amount to randomize times by (±minutes) - * @param days Days of week to apply this schedule - * @return A combined randomized time window and day of week condition - */ - public static AndCondition createRandomizedTimeWindowSchedule( - int baseStartHour, int baseStartMinute, int baseEndHour, int baseEndMinute, - int randomizerValue, DayOfWeek... days) { - - // Validate and cap time values - baseStartHour = Math.min(Math.max(baseStartHour, 0), 23); - baseStartMinute = Math.min(Math.max(baseStartMinute, 0), 59); - baseEndHour = Math.min(Math.max(baseEndHour, 0), 23); - baseEndMinute = Math.min(Math.max(baseEndMinute, 0), 59); - randomizerValue = Math.min(Math.max(randomizerValue, 0), 60); - - // Create time window condition - TimeWindowCondition timeWindow = new TimeWindowCondition( - LocalTime.of(baseStartHour, baseStartMinute), - LocalTime.of(baseEndHour, baseEndMinute), - LocalDate.now(), - LocalDate.now().plus(1, ChronoUnit.YEARS), - null, 1, 0); - - // Set randomization if requested - if (randomizerValue > 0) { - timeWindow.setRandomization(true); - } - - // Create day of week condition - DayOfWeekCondition dayCondition = new DayOfWeekCondition(0, days); - - // Combine conditions - AndCondition condition = new AndCondition(); - condition.addCondition(dayCondition); - condition.addCondition(timeWindow); - - return condition; - } - - /** - * Creates a humanized play schedule that mimics natural human gaming patterns - * with appropriate limits. - * - * @param weekdayMaxHours Maximum session hours for weekdays - * @param weekendMaxHours Maximum session hours for weekends - * @param weeklyMaxRepeats Maximum weekly repeats overall - * @return A realistic human-like play schedule - */ - public static AndCondition createHumanizedPlaySchedule( - float weekdayMaxHours, float weekendMaxHours, int weeklyMaxRepeats) { - - // Cap input values - float cappedWeekdayHours = Math.min(weekdayMaxHours, MAX_WEEKDAY_SESSION.toHours()); - float cappedWeekendHours = Math.min(weekendMaxHours, MAX_WEEKEND_SESSION.toHours()); - int cappedWeeklyRepeats = Math.min(weeklyMaxRepeats, MAX_WEEKLY_REPEATS); - - // Calculate min duration as ~60% of max - float weekdayMinHours = cappedWeekdayHours * 0.6f; - float weekendMinHours = cappedWeekendHours * 0.6f; - - // Convert to Duration objects - int weekdayMinH = (int)weekdayMinHours; - int weekdayMinM = (int)((weekdayMinHours - weekdayMinH) * 60); - Duration weekdayMinDuration = Duration.ofHours(weekdayMinH).plusMinutes(weekdayMinM); - - int weekdayMaxH = (int)cappedWeekdayHours; - int weekdayMaxM = (int)((cappedWeekdayHours - weekdayMaxH) * 60); - Duration weekdayMaxDuration = Duration.ofHours(weekdayMaxH).plusMinutes(weekdayMaxM); - - int weekendMinH = (int)weekendMinHours; - int weekendMinM = (int)((weekendMinHours - weekendMinH) * 60); - Duration weekendMinDuration = Duration.ofHours(weekendMinH).plusMinutes(weekendMinM); - - int weekendMaxH = (int)cappedWeekendHours; - int weekendMaxM = (int)((cappedWeekendHours - weekendMaxH) * 60); - Duration weekendMaxDuration = Duration.ofHours(weekendMaxH).plusMinutes(weekendMaxM); - - // Monday/Wednesday/Friday: 1 session per day, shorter - DayOfWeekCondition mwfDays = new DayOfWeekCondition( - 0, 1, 0, // 1 per day, no specific weekly limit - DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY - ); - - IntervalCondition mwfSession = IntervalCondition.createRandomized( - weekdayMinDuration, weekdayMaxDuration - ); - mwfDays.setIntervalCondition(mwfSession); - - - - - // Tuesday/Thursday: 2 sessions per day, even shorter - DayOfWeekCondition ttDays = new DayOfWeekCondition( - 0, 2, 0, // 2 per day, no specific weekly limit - DayOfWeek.TUESDAY, DayOfWeek.THURSDAY - ); - - // Make TT sessions ~75% of MWF sessions - Duration ttMinDuration = Duration.ofMillis((long)(weekdayMinDuration.toMillis() * 0.75)); - Duration ttMaxDuration = Duration.ofMillis((long)(weekdayMaxDuration.toMillis() * 0.75)); - - IntervalCondition ttSession = IntervalCondition.createRandomized( - ttMinDuration, ttMaxDuration - ); - ttDays .setIntervalCondition(ttSession); - - - - - // Weekend: 2-3 sessions per day, longer - DayOfWeekCondition weekendDays = new DayOfWeekCondition( - 0, 3, 0, // Up to 3 per day - DayOfWeek.SATURDAY, DayOfWeek.SUNDAY - ); - - IntervalCondition weekendSession = IntervalCondition.createRandomized( - weekendMinDuration, weekendMaxDuration - ); - - weekendDays.setIntervalCondition(weekendSession); - - - // Create an OR condition for the day schedules - OrCondition daySchedules = new OrCondition(); - daySchedules.addCondition(mwfDays); - daySchedules.addCondition(ttDays); - daySchedules.addCondition(weekendDays); - - // Apply weekly limit - DayOfWeekCondition weeklyLimit = new DayOfWeekCondition( - 0, 0, cappedWeeklyRepeats, EnumSet.allOf(DayOfWeek.class) - ); - - // Combine with the weekly limit - AndCondition finalSchedule = new AndCondition(); - finalSchedule.addCondition(weeklyLimit); - finalSchedule.addCondition(daySchedules); - - return finalSchedule; - } - - /** - * Creates a "work-life balance" schedule that simulates a player with a daytime job, - * playing evenings on weekdays and more on weekends. - * - * @param weekdayMaxHours Maximum session hours for weekdays - * @param weekendMaxHours Maximum session hours for weekends - * @param weeklyMaxRepeats Maximum weekly repeats overall - * @return A realistic work-life balance schedule - */ - public static Condition createWorkLifeBalanceSchedule( - float weekdayMaxHours, float weekendMaxHours, int weeklyMaxRepeats) { - - // Cap input values - float cappedWeekdayHours = Math.min(weekdayMaxHours, MAX_WEEKDAY_SESSION.toHours()); - float cappedWeekendHours = Math.min(weekendMaxHours, MAX_WEEKEND_SESSION.toHours()); - int cappedWeeklyRepeats = Math.min(weeklyMaxRepeats, MAX_WEEKLY_REPEATS); - - // Calculate min duration as ~70% of max for more predictable evening sessions - float weekdayMinHours = cappedWeekdayHours * 0.7f; - float weekendMinHours = cappedWeekendHours * 0.6f; // More variation on weekends - - // Convert to Duration objects - int weekdayMinH = (int)weekdayMinHours; - int weekdayMinM = (int)((weekdayMinHours - weekdayMinH) * 60); - Duration weekdayMinDuration = Duration.ofHours(weekdayMinH).plusMinutes(weekdayMinM); - - int weekdayMaxH = (int)cappedWeekdayHours; - int weekdayMaxM = (int)((cappedWeekdayHours - weekdayMaxH) * 60); - Duration weekdayMaxDuration = Duration.ofHours(weekdayMaxH).plusMinutes(weekdayMaxM); - - int weekendMinH = (int)weekendMinHours; - int weekendMinM = (int)((weekendMinHours - weekendMinH) * 60); - Duration weekendMinDuration = Duration.ofHours(weekendMinH).plusMinutes(weekendMinM); - - int weekendMaxH = (int)cappedWeekendHours; - int weekendMaxM = (int)((cappedWeekendHours - weekendMaxH) * 60); - Duration weekendMaxDuration = Duration.ofHours(weekendMaxH).plusMinutes(weekendMaxM); - - // Weekday schedule (Mon-Fri) with evening hours - TimeWindowCondition eveningHours = new TimeWindowCondition( - LocalTime.of(18, 0), // 6:00 PM - LocalTime.of(23, 0), // 11:00 PM - LocalDate.now(), - LocalDate.now().plusYears(1), - null, 1, 0 - ); - eveningHours.setRandomization(true, 30); // Randomize by ±30 minutes - - // MWF - lighter play (1 session) - DayOfWeekCondition mwfDays = new DayOfWeekCondition(0, 1, 0, - DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY - ); - - // TT - slightly more play (2 sessions) - DayOfWeekCondition ttDays = new DayOfWeekCondition(0, 2, 0, - DayOfWeek.TUESDAY, DayOfWeek.THURSDAY - ); - - // Weekday session duration - IntervalCondition weekdaySession = IntervalCondition.createRandomized( - weekdayMinDuration, weekdayMaxDuration - ); - - // Combine MWF schedule - AndCondition mwfSchedule = new AndCondition(); - mwfSchedule.addCondition(mwfDays); - mwfSchedule.addCondition(eveningHours); - mwfSchedule.addCondition(weekdaySession); - - // Combine TT schedule - AndCondition ttSchedule = new AndCondition(); - ttSchedule.addCondition(ttDays); - ttSchedule.addCondition(eveningHours); - ttSchedule.addCondition(weekdaySession); - - // Weekend schedule (Sat-Sun) - DayOfWeekCondition weekendDays = new DayOfWeekCondition(0, 3, 0, - DayOfWeek.SATURDAY, DayOfWeek.SUNDAY - ); - - // Weekend has more flexible hours - TimeWindowCondition flexibleHours = new TimeWindowCondition( - LocalTime.of(10, 0), // 10:00 AM - LocalTime.of(23, 59), // 11:59 PM - LocalDate.now(), - LocalDate.now().plusYears(1), - null, 1, 0 - ); - flexibleHours.setRandomization(true, 60); // Randomize by ±60 minutes - - // Weekend session duration - IntervalCondition weekendSession = IntervalCondition.createRandomized( - weekendMinDuration, weekendMaxDuration - ); - - // Combine weekend schedule - AndCondition weekendSchedule = new AndCondition(); - weekendSchedule.addCondition(weekendDays); - weekendSchedule.addCondition(flexibleHours); - weekendSchedule.addCondition(weekendSession); - - // Combine all daily schedules with OR - OrCondition allDaySchedules = new OrCondition(); - allDaySchedules.addCondition(mwfSchedule); - allDaySchedules.addCondition(ttSchedule); - allDaySchedules.addCondition(weekendSchedule); - - // Apply weekly limit - DayOfWeekCondition weeklyLimit = new DayOfWeekCondition( - 0, 0, cappedWeeklyRepeats, EnumSet.allOf(DayOfWeek.class) - ); - - // Apply weekly limit to the combined schedule - AndCondition finalSchedule = new AndCondition(); - finalSchedule.addCondition(weeklyLimit); - finalSchedule.addCondition(allDaySchedules); - - return finalSchedule; - } - - /** - * Creates a condition that runs all day (from midnight to midnight) - * - * @param startDate The start date of the condition - * @param endDate The end date of the condition - * @param repeatCycle The repeat cycle type - * @param repeatIntervalUnit The interval between repetitions - * @return A TimeWindowCondition configured to run all day - */ - public static TimeWindowCondition createAllDayTimeWindow( - LocalDate startDate, - LocalDate endDate, - RepeatCycle repeatCycle, - int repeatIntervalUnit) { - return new TimeWindowCondition( - LocalTime.of(0, 0), - LocalTime.of(23, 59), - startDate, - endDate, - repeatCycle, - repeatIntervalUnit, - 0 // unlimited - ); - } - - /** - * Creates a condition that runs from the start of the day until a specific time - * - * @param endTime The time when the window should end - * @param startDate The start date of the condition - * @param endDate The end date of the condition - * @param repeatCycle The repeat cycle type - * @param repeatIntervalUnit The interval between repetitions - * @return A TimeWindowCondition configured to run from midnight to the specified end time - */ - public static TimeWindowCondition createStartOfDayTimeWindow( - LocalTime endTime, - LocalDate startDate, - LocalDate endDate, - RepeatCycle repeatCycle, - int repeatIntervalUnit) { - return new TimeWindowCondition( - LocalTime.of(0, 0), - endTime, - startDate, - endDate, - repeatCycle, - repeatIntervalUnit, - 0 // unlimited - ); - } - - /** - * Creates a condition that runs from a specific time until the end of the day - * - * @param startTime The time when the window should start - * @param startDate The start date of the condition - * @param endDate The end date of the condition - * @param repeatCycle The repeat cycle type - * @param repeatIntervalUnit The interval between repetitions - * @return A TimeWindowCondition configured to run from the specified start time until midnight - */ - public static TimeWindowCondition createEndOfDayTimeWindow( - LocalTime startTime, - LocalDate startDate, - LocalDate endDate, - RepeatCycle repeatCycle, - int repeatIntervalUnit) { - return new TimeWindowCondition( - startTime, - LocalTime.of(23, 59), - startDate, - endDate, - repeatCycle, - repeatIntervalUnit, - 0 // unlimited - ); - } - - /** - * Provides diagnostic information about an AndCondition, explaining whether - * each component is satisfied and why. - * - * @param condition The AndCondition to diagnose - * @return A detailed diagnostic report - */ - public static String diagnoseCombinedCondition(AndCondition condition) { - StringBuilder sb = new StringBuilder(); - sb.append("Combined condition status: ").append(condition.isSatisfied() ? "SATISFIED" : "NOT SATISFIED").append("\n"); - sb.append("Analyzing individual conditions:\n"); - - for (int i = 0; i < condition.getConditions().size(); i++) { - Condition subCondition = condition.getConditions().get(i); - boolean satisfied = subCondition.isSatisfied(); - - sb.append(i + 1).append(". "); - sb.append(subCondition.getClass().getSimpleName()).append(": "); - sb.append(satisfied ? "SATISFIED" : "NOT SATISFIED").append("\n"); - sb.append(" - ").append(subCondition.getDescription().replace("\n", "\n - ")).append("\n"); - - if (!satisfied) { - // Special handling for different condition types - if (subCondition instanceof DayOfWeekCondition) { - DayOfWeekCondition dayCondition = (DayOfWeekCondition) subCondition; - sb.append(" - Today is not an active day or has reached daily/weekly limit\n"); - sb.append(" - Current day usage: ").append( - dayCondition.getResetCountForDate(LocalDate.now())).append("\n"); - sb.append(" - Daily limit reached: ").append(dayCondition.isDailyLimitReached()).append("\n"); - sb.append(" - Current week usage: ").append(dayCondition.getCurrentWeekResetCount()).append("\n"); - sb.append(" - Weekly limit reached: ").append(dayCondition.isWeeklyLimitReached()).append("\n"); - - // Show next trigger day - dayCondition.getCurrentTriggerTime().ifPresent(time -> - sb.append(" - Next active day: ").append(time.toLocalDate()).append("\n")); - } - else if (subCondition instanceof IntervalCondition) { - IntervalCondition intervalCondition = (IntervalCondition) subCondition; - sb.append(" - Interval not yet elapsed\n"); - intervalCondition.getCurrentTriggerTime().ifPresent(time -> - sb.append(" - Next trigger time: ").append(time).append("\n")); - } - else if (subCondition instanceof TimeWindowCondition) { - TimeWindowCondition timeWindow = (TimeWindowCondition) subCondition; - sb.append(" - Outside of configured time window\n"); - sb.append(" - Current time window: ") - .append(timeWindow.getStartTime()).append(" - ") - .append(timeWindow.getEndTime()).append("\n"); - } - } - } - - return sb.toString(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/ConditionConfigPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/ConditionConfigPanel.java deleted file mode 100644 index dffb133cb81..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/ConditionConfigPanel.java +++ /dev/null @@ -1,2883 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.ui; - -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.FlowLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.GridLayout; -import java.awt.Insets; - -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Consumer; - -import javax.swing.BorderFactory; -import javax.swing.Box; -import javax.swing.BoxLayout; -import javax.swing.DefaultListCellRenderer; -import javax.swing.DefaultListModel; -import javax.swing.JButton; - -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JMenuItem; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JPopupMenu; -import javax.swing.JScrollPane; -import javax.swing.JSeparator; - -import javax.swing.JSplitPane; -import javax.swing.JTabbedPane; - -import javax.swing.JTree; -import javax.swing.ListSelectionModel; - -import javax.swing.SwingConstants; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import javax.swing.event.PopupMenuEvent; -import javax.swing.event.PopupMenuListener; -import javax.swing.tree.DefaultMutableTreeNode; -import javax.swing.tree.DefaultTreeModel; -import javax.swing.tree.TreeNode; -import javax.swing.tree.TreePath; -import javax.swing.tree.TreeSelectionModel; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionManager; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.AreaCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.LocationCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.PositionCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.RegionCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.ui.LocationConditionUtil; -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.resource.BankItemCountCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.GatheredResourceCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.InventoryItemCountCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.LootItemCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ProcessItemCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ui.ResourceConditionPanelUtil; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.SkillLevelCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.SkillXpCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.ui.SkillConditionPanelUtil; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.DayOfWeekCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.ui.TimeConditionPanelUtil; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ui.callback.ConditionUpdateCallback; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.util.SchedulerUIUtils; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ui.renderer.ConditionTreeCellRenderer; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -// Import the utility class -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ui.util.ConditionConfigPanelUtil; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.VarbitCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.ui.VarbitConditionPanelUtil; - -@Slf4j -public class ConditionConfigPanel extends JPanel { - public static final Color BRAND_BLUE = new Color(25, 130, 196); - private final JComboBox conditionCategoryComboBox; - private final JComboBox conditionTypeComboBox; - private JPanel configPanel; - - private ConditionTreeCellRenderer conditionTreeCellRenderer; - - // Tree visualization components - private DefaultMutableTreeNode rootNode; - private DefaultTreeModel treeModel; - private JTree conditionTree; - private JSplitPane splitPane; - - // Condition list components - private DefaultListModel conditionListModel; - private JList conditionList; - - - // callback system - private ConditionUpdateCallback conditionUpdateCallback; - - - - - private PluginScheduleEntry selectScheduledPlugin; - // UI Controls - private JButton resetButton; - private JButton resetUserConditionsButton; - - private JButton editButton; - private JButton addButton; - private JButton removeButton; - private JButton negateButton; - private JButton convertToAndButton; - private JButton convertToOrButton; - private JButton ungroupButton; - private JPanel titlePanel; - private JLabel titleLabel; - private final boolean stopConditionPanel; - private boolean[] updatingSelectionFlag = new boolean[1]; - List lastRefreshConditions = new CopyOnWriteArrayList<>(); - - public ConditionConfigPanel(boolean stopConditionPanel) { - this.stopConditionPanel = stopConditionPanel; - setLayout(new BorderLayout()); - setBorder(BorderFactory.createTitledBorder( - BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(1, 1, 1, 1, ColorScheme.DARK_GRAY_COLOR), - BorderFactory.createEmptyBorder(5, 5, 5, 5) - ), - stopConditionPanel ? "Stop Conditions" : "Start Conditions", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Initialize title panel - titlePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - titlePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - titlePanel.setName("titlePanel"); - - titleLabel = new JLabel("No plugin selected"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeBoldFont()); - titlePanel.add(titleLabel); - - // Initialize reset buttons - initializeResetButton(); - initializeResetUserConditionsButton(); - - // Create a panel for the top buttons, aligned to the right - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - buttonPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - buttonPanel.add(resetUserConditionsButton); - buttonPanel.add(Box.createHorizontalStrut(5)); - buttonPanel.add(resetButton); - - // Add the title and buttons to the top panel - JPanel topPanel = new JPanel(new BorderLayout()); - topPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - topPanel.add(titlePanel, BorderLayout.WEST); - topPanel.add(buttonPanel, BorderLayout.EAST); - - add(topPanel, BorderLayout.NORTH); - - // Define condition categories based on ConditionType enum - String[] conditionCategories = new String[]{ - "Time", - "Skill", - "Resource", - "Location", - "Varbit" // Added Varbit condition category - }; - - conditionCategoryComboBox = new JComboBox<>(conditionCategories); - - // Initialize with empty condition types - will be populated based on category - conditionTypeComboBox = new JComboBox<>(); - - // Set initial condition types based on first category - updateConditionTypes(conditionCategories[0]); - - // Create split pane for main content - splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); - splitPane.setResizeWeight(0.6); // Give more space to the top components - splitPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Initialize condition list - JPanel listPanel = createConditionListPanel(); - - // Initialize condition tree - JPanel treePanel = createLogicalTreePanel(); - - // Create a panel for the list and tree components - JPanel conditionsPanel = new JPanel(new BorderLayout()); - conditionsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Remove split pane and just use the tree panel directly - conditionsPanel.add(treePanel, BorderLayout.CENTER); - - // If you want to keep the list panel in the code but hidden for now, - // you can add this commented code: - // JSplitPane conditionsSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); - // conditionsSplitPane.setTopComponent(treePanel); - // conditionsSplitPane.setBottomComponent(listPanel); - // conditionsSplitPane.setResizeWeight(0.9); // Give almost all space to the tree - // conditionsSplitPane.setBorder(null); - // conditionsSplitPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - // conditionsPanel.add(conditionsSplitPane, BorderLayout.CENTER); - - // Create the condition config panel - JPanel addConditionPanel = createAddConditionPanel(); - - // Add both panels to the main split pane - splitPane.setTopComponent(conditionsPanel); - splitPane.setBottomComponent(addConditionPanel); - - add(splitPane, BorderLayout.CENTER); - - // Initialize the config panel - updateConfigPanel(); - - // Set up tree and list selection synchronization - fixSelectionPersistence(false); - } - - /** - * Updates the condition types dropdown based on the selected category - */ - private void updateConditionTypes(String category) { - conditionTypeComboBox.removeAllItems(); - - if ("Time".equals(category)) { - if (stopConditionPanel) { - // Stop conditions - conditionTypeComboBox.addItem("Time Duration"); - conditionTypeComboBox.addItem("Time Window"); - conditionTypeComboBox.addItem("Not In Time Window"); - conditionTypeComboBox.addItem("Day of Week"); - conditionTypeComboBox.addItem("Specific Time"); - } else { - // Start conditions - conditionTypeComboBox.addItem("Time Interval"); - conditionTypeComboBox.addItem("Time Window"); - conditionTypeComboBox.addItem("Outside Time Window"); - conditionTypeComboBox.addItem("Day of Week"); - conditionTypeComboBox.addItem("Specific Time"); - } - } else if ("Skill".equals(category)) { - if (stopConditionPanel) { - // Stop conditions - conditionTypeComboBox.addItem("Skill Level"); - conditionTypeComboBox.addItem("Skill XP Goal"); - } else { - // Start conditions - conditionTypeComboBox.addItem("Skill Level Required"); - } - } else if ("Resource".equals(category)) { - if (stopConditionPanel) { - // Stop conditions - conditionTypeComboBox.addItem("Item Collection"); - conditionTypeComboBox.addItem("Process Items"); - conditionTypeComboBox.addItem("Gather Resources"); - } else { - // Start conditions - conditionTypeComboBox.addItem("Item Required"); - conditionTypeComboBox.addItem("Inventory Item Count"); - } - } else if ("Location".equals(category)) { - conditionTypeComboBox.addItem("Position"); - conditionTypeComboBox.addItem("Area"); - conditionTypeComboBox.addItem("Region"); - } else if ("Varbit".equals(category)) { - // Varbit conditions with improved naming - conditionTypeComboBox.addItem("Collection Log - Bosses"); - conditionTypeComboBox.addItem("Collection Log - Minigames"); - //conditionTypeComboBox.addItem("General Varbit Condition"); Not yet implemented - } - } - /** - * Fixes selection persistence in the tree and list view with improved event blocking - */ - private void fixSelectionPersistence( boolean syncWithList) { - if(! syncWithList){ - // Create a tree selection listener that only updates button states - conditionTree.addTreeSelectionListener(e -> { - if (!updatingSelectionFlag[0]) { - updateLogicalButtonStates(); - // Update the condition editor when a condition is selected - updateConditionPanelForSelectedNode(); - } - }); - return; - } - // Store in a class field to allow other methods to access it - // Create a tree selection listener that doesn't trigger when programmatically updating - conditionTree.addTreeSelectionListener(e -> { - if (updatingSelectionFlag[0]) return; - - updateLogicalButtonStates(); - // Update the condition editor when a condition is selected - updateConditionPanelForSelectedNode(); - - // Sync with list - only if there's a valid selection - DefaultMutableTreeNode node = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (node != null && node.getUserObject() instanceof Condition) { - Condition condition = (Condition) node.getUserObject(); - int index = getCurrentConditions().indexOf(condition); - if (index >= 0) { - try { - updatingSelectionFlag[0] = true; - conditionList.setSelectedIndex(index); - } finally { - updatingSelectionFlag[0] = false; - } - } - } - }); - - // Create a list selection listener that doesn't trigger when programmatically updating - conditionList.addListSelectionListener(e -> { - if (e.getValueIsAdjusting() || updatingSelectionFlag[0]) return; - - int index = conditionList.getSelectedIndex(); - if (index >= 0 && index < getCurrentConditions().size()) { - try { - updatingSelectionFlag[0] = true; - selectNodeForCondition(getCurrentConditions().get(index)); - } finally { - updatingSelectionFlag[0] = false; - } - } - }); - } - /** - * Gets the current conditions from the selected plugin - * @return List of conditions, or empty list if no plugin selected - */ - private List getCurrentConditions() { - if (selectScheduledPlugin == null) { - return new ArrayList<>(); - }else if (this.stopConditionPanel && selectScheduledPlugin.getStopConditionManager() != null) { - return selectScheduledPlugin.getStopConditions(); - }else if (!this.stopConditionPanel && selectScheduledPlugin.getStartConditionManager() != null) { - return selectScheduledPlugin.getStartConditions(); - } - return new ArrayList<>(); - } - - /** - * Checks if the current plugin has user-defined conditions - * @return true if there are user-defined conditions, false otherwise - */ - private boolean hasUserDefinedConditions() { - if (selectScheduledPlugin == null) { - return false; - } - - ConditionManager manager = getConditionManger(); - if (manager == null) { - return false; - } - - return !manager.getUserConditions().isEmpty(); - } - - /** - * Checks if conditions can be edited based on plugin state - * @return true if conditions can be edited, false if editing should be disabled - */ - private boolean canEditConditions() { - if (selectScheduledPlugin == null) { - return false; - } - - // For stop conditions, disable editing when plugin is running - if (stopConditionPanel && selectScheduledPlugin.isRunning()) { - JOptionPane.showMessageDialog(this, - "Cannot edit stop conditions while the plugin is running.\n" + - "Please wait for the plugin to finish or stop it manually.", - "Plugin Running", - JOptionPane.WARNING_MESSAGE); - return false; - } - - return true; - } - // Side-effect-free variant for enablement checks - private boolean isEditAllowedNoDialog() { - if (selectScheduledPlugin == null) { - return false; - } - return !(stopConditionPanel && selectScheduledPlugin.isRunning()); - } - - - - /** - * Refreshes the UI to display the current plugin conditions - * while preserving selection and expansion state - */ - private void refreshDisplay() { - if (selectScheduledPlugin == null) { - log.debug("refreshDisplay: No plugin selected, skipping refresh"); - return; - } - - List currentConditions = getCurrentConditions(); - log.debug("refreshDisplay: Found {} conditions in plugin", currentConditions.size()); - - // Store both list and tree selection states with better debugging - int selectedListIndex = conditionList.getSelectedIndex(); - log.debug("refreshDisplay: Current list selection index: {}", selectedListIndex); - - // list selection tracking - Condition selectedListCondition = null; - if (selectedListIndex >= 0 && selectedListIndex < currentConditions.size()) { - selectedListCondition = currentConditions.get(selectedListIndex); - log.debug("refreshDisplay: List selection mapped to condition: {}", - selectedListCondition.getDescription()); - } - - // Remember tree selection with better logging - Set selectedTreeConditions = new HashSet<>(); - TreePath[] selectedTreePaths = conditionTree.getSelectionPaths(); - if (selectedTreePaths != null && selectedTreePaths.length > 0) { - log.debug("refreshDisplay: Found {} selected tree paths", selectedTreePaths.length); - for (TreePath path : selectedTreePaths) { - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - if (node != null && node.getUserObject() instanceof Condition) { - Condition condition = (Condition) node.getUserObject(); - selectedTreeConditions.add(condition); - log.debug("refreshDisplay: Added selected tree condition: {}", - condition.getDescription()); - } - } - } else { - log.debug("refreshDisplay: No tree paths selected"); - } - - // robust expansion state tracking with better debugging - Set expandedConditions = new HashSet<>(); - Map expandedPathMap = new HashMap<>(); // Store path for easier restoration - - // First check if root node exists - if (rootNode != null) { - TreePath rootPath = new TreePath(rootNode.getPath()); - log.debug("refreshDisplay: Getting expanded nodes from root path: {}", rootPath); - - Enumeration expandedPaths = conditionTree.getExpandedDescendants(rootPath); - if (expandedPaths != null && expandedPaths.hasMoreElements()) { - log.debug("refreshDisplay: Found expanded paths"); - int expandedCount = 0; - while (expandedPaths.hasMoreElements()) { - TreePath path = expandedPaths.nextElement(); - expandedCount++; - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - if (node != null && node.getUserObject() instanceof Condition) { - Condition condition = (Condition) node.getUserObject(); - expandedConditions.add(condition); - expandedPathMap.put(condition, path); - log.debug("refreshDisplay: Added expanded condition: {}", - condition.getDescription()); - } - } - log.debug("refreshDisplay: Found {} expanded paths, {} are conditions", - expandedCount, expandedConditions.size()); - } else { - log.debug("refreshDisplay: No expanded paths found"); - } - } else { - log.debug("refreshDisplay: Root node is null, can't get expanded paths"); - } - - // Flag to track update types needed - boolean needsStructureUpdate = false; // Complete rebuild needed - boolean needsTextUpdate = false; // Just text needs refreshing - - // Check if structure has changed - if (lastRefreshConditions.size() != currentConditions.size()) { - log.debug("refreshDisplay: Condition count changed from {} to {}, structure update needed", - lastRefreshConditions.size(), currentConditions.size()); - needsStructureUpdate = true; - } else { - // Check if conditions have changed or reordered - for (int i = 0; i < lastRefreshConditions.size(); i++) { - if (!lastRefreshConditions.get(i).equals(currentConditions.get(i))) { - log.debug("refreshDisplay: Condition at index {} changed, structure update needed", i); - needsStructureUpdate = true; - break; - } - } - - // If structure unchanged, check if descriptions need updating - if (!needsStructureUpdate) { - for (int i = 0; i < currentConditions.size(); i++) { - String existingDesc = conditionListModel.getElementAt(i); - String newDesc = descriptionForCondition(currentConditions.get(i)); - if (!existingDesc.equals(newDesc)) { - log.debug("refreshDisplay: Description at index {} changed from '{}' to '{}', text update needed", - i, existingDesc, newDesc); - needsTextUpdate = true; - break; - } - } - } - } - - // Use a flag to prevent selection events during refresh - updatingSelectionFlag[0] = true; - log.debug("refreshDisplay: Setting updatingSelectionFlag to prevent event feedback"); - - try { - // Case 1: Full structure update needed - if (needsStructureUpdate) { - log.debug("refreshDisplay: Performing full structure update"); - lastRefreshConditions = new CopyOnWriteArrayList<>(currentConditions); - - // Update list model - conditionListModel.clear(); - for (Condition condition : currentConditions) { - conditionListModel.addElement(descriptionForCondition(condition)); - } - - // Update tree - updateTreeFromConditions(); - - // Expand all category nodes by default - for (int i = 0; i < conditionTree.getRowCount(); i++) { - TreePath path = conditionTree.getPathForRow(i); - if (path == null) continue; - - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - if (node.getUserObject() instanceof String) { - log.debug("refreshDisplay: Auto-expanding category node: {}", node.getUserObject()); - conditionTree.expandPath(path); - } - } - - // Restore expansion state for condition nodes - if (!expandedConditions.isEmpty()) { - log.debug("refreshDisplay: Restoring {} expanded conditions", expandedConditions.size()); - - for (int i = 0; i < conditionTree.getRowCount(); i++) { - TreePath path = conditionTree.getPathForRow(i); - if (path == null) continue; - - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - if (node.getUserObject() instanceof Condition) { - Condition condition = (Condition) node.getUserObject(); - if (expandedConditions.contains(condition)) { - log.debug("refreshDisplay: Expanding condition node: {}", condition.getDescription()); - conditionTree.expandPath(path); - } - } - } - } else { - log.debug("refreshDisplay: No expanded conditions to restore"); - } - - // Restore selection state - if (!selectedTreeConditions.isEmpty()) { - log.debug("refreshDisplay: Restoring {} selected tree conditions", selectedTreeConditions.size()); - List pathsToSelect = new ArrayList<>(); - - // Find paths to all selected conditions - for (int i = 0; i < conditionTree.getRowCount(); i++) { - TreePath path = conditionTree.getPathForRow(i); - if (path == null) continue; - - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - if (node.getUserObject() instanceof Condition) { - Condition condition = (Condition) node.getUserObject(); - if (selectedTreeConditions.contains(condition)) { - pathsToSelect.add(path); - log.debug("refreshDisplay: Found path for selected condition: {}", - condition.getDescription()); - } - } - } - - if (!pathsToSelect.isEmpty()) { - log.debug("refreshDisplay: Setting {} tree selection paths", pathsToSelect.size()); - conditionTree.setSelectionPaths(pathsToSelect.toArray(new TreePath[0])); - } else { - log.debug("refreshDisplay: Could not find any paths for selected conditions"); - } - } else { - log.debug("refreshDisplay: No tree selections to restore"); - } - - // Restore list selection - if (selectedListCondition != null) { - int newIndex = currentConditions.indexOf(selectedListCondition); - if (newIndex >= 0) { - log.debug("refreshDisplay: Restoring list selection to index {}", newIndex); - conditionList.setSelectedIndex(newIndex); - } else { - log.debug("refreshDisplay: Could not find list selection in current conditions"); - } - } - } - // Case 2: Only text descriptions need updating - else if (needsTextUpdate) { - log.debug("refreshDisplay: Performing text-only update"); - - // Update just the text in the list model without rebuilding - for (int i = 0; i < currentConditions.size(); i++) { - String newDesc = descriptionForCondition(currentConditions.get(i)); - if (!conditionListModel.getElementAt(i).equals(newDesc)) { - log.debug("refreshDisplay: Updating description at index {} to '{}'", i, newDesc); - conditionListModel.setElementAt(newDesc, i); - } - } - - // Update tree nodes' text by forcing renderer refresh without rebuilding - log.debug("refreshDisplay: Repainting tree to refresh node text"); - conditionTree.repaint(); - } else { - log.debug("refreshDisplay: No updates needed"); - } - } finally { - // Re-enable selection events - updatingSelectionFlag[0] = false; - log.debug("refreshDisplay: Resetting updatingSelectionFlag to allow events"); - } - if (this.conditionTreeCellRenderer != null ){ - this.conditionTreeCellRenderer.setIsActive(selectScheduledPlugin.isRunning()); - } - } - - /** - * Helper method to get consistent description for a condition - */ - private String descriptionForCondition(Condition condition) { - // Check if this is a plugin-defined condition - boolean isPluginDefined = false; - - if (this.selectScheduledPlugin!= null && getConditionManger() != null) { - - isPluginDefined = getConditionManger().isPluginDefinedCondition(condition); - } - - - // Add with appropriate tag for plugin-defined conditions - String description = condition.getDescription(); - if (isPluginDefined) { - description = "[Plugin] " + description; - } - - return description; - } - /** - * Updates the panel when a new plugin is selected - * - * @param selectedPlugin The newly selected plugin, or null if selection cleared - */ - public void setSelectScheduledPlugin(PluginScheduleEntry selectedPlugin) { - if (selectedPlugin == this.selectScheduledPlugin) { - return; - }else{ - if (Microbot.isDebug()){ - log.info("setSelectScheduledPlugin: Changing selected plugin from {} to {} - reload list and tree", - this.selectScheduledPlugin==null ? "null": this.selectScheduledPlugin.getCleanName() , selectedPlugin==null ? "null" : selectedPlugin.getCleanName()); - } - } - - // Store the selected plugin - this.selectScheduledPlugin = selectedPlugin; - - // Enable/disable controls based on whether a plugin is selected - boolean hasPlugin = (selectedPlugin != null); - boolean pluginRunning = hasPlugin && selectedPlugin.isRunning(); - boolean isEditingStopConditions = stopConditionPanel; - boolean shouldDisableStopConditionEditing = isEditingStopConditions && pluginRunning; - - resetButton.setEnabled(hasPlugin); - resetUserConditionsButton.setEnabled(hasPlugin && hasUserDefinedConditions()); - - // Disable stop condition editing when plugin is running - editButton.setEnabled(hasPlugin && !shouldDisableStopConditionEditing); - if(addButton != null) { - // Only enable add button if conditions can be edited - addButton.setEnabled(hasPlugin && !shouldDisableStopConditionEditing); - } - if (removeButton != null) { - removeButton.setEnabled(hasPlugin && !shouldDisableStopConditionEditing); - } - conditionTypeComboBox.setEnabled(hasPlugin && !shouldDisableStopConditionEditing); - conditionList.setEnabled(hasPlugin); - conditionTree.setEnabled(hasPlugin); - - // Update logical operation buttons based on edit restrictions - updateLogicalButtonStates(); - - // Update the plugin name display - setScheduledPluginNameLabel(); - - // If a plugin is selected, load its conditions - if (hasPlugin) { - // Set the logic type combo box based on the plugin's condition manager - // Safely obtain the plugin's stop-condition manager - ConditionManager conditionManager; - if (stopConditionPanel){ - conditionManager = selectedPlugin.getStopConditionManager();// has sPlugin checks if null already - }else{ - conditionManager = selectedPlugin.getStartConditionManager(); - } - boolean requireAll = conditionManager != null && conditionManager.requiresAll(); - // Load conditions using the guarded manager - if (conditionManager != null) { - loadConditions(conditionManager.getConditions(), requireAll); - } else { - // Fallback to empty if no manager - loadConditions(new ArrayList<>(), requireAll); - } - } else { - // Clear conditions if no plugin selected - loadConditions(new ArrayList<>(), true); - } - - // Update the tree and list displays - refreshDisplay(); - } - - - private JPanel createConditionListPanel() { - JPanel panel = new JPanel(new BorderLayout()); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - panel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Condition List", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), // Changed to bold font - Color.WHITE - )); - - conditionListModel = new DefaultListModel<>(); - conditionList = new JList<>(conditionListModel); - conditionList.setCellRenderer(new DefaultListCellRenderer() { - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, - boolean isSelected, boolean cellHasFocus) { - Component c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - - List currentConditions = getCurrentConditions(); - if (index >= 0 && index < currentConditions.size()) { - Condition condition = currentConditions.get(index); - - boolean isConditionRelevant = false; - - // Check if this condition is relevant based on plugin state - if (selectScheduledPlugin != null) { - if (stopConditionPanel) { - // Stop conditions are relevant when plugin is running - isConditionRelevant = selectScheduledPlugin.isRunning(); - } else { - // Start conditions are relevant when plugin is enabled but not started - isConditionRelevant = selectScheduledPlugin.isEnabled() && !selectScheduledPlugin.isRunning(); - } - } - - // Check if condition is satisfied - boolean isConditionSatisfied = condition.isSatisfied(); - - // Apply appropriate styling - if (!isSelected) { - // Plugin-defined conditions get blue color - if (selectScheduledPlugin != null && - getConditionManger() != null && - getConditionManger().isPluginDefinedCondition(condition)) { - - setForeground(new Color(0, 128, 255)); // Blue for plugin conditions - setFont(getFont().deriveFont(Font.ITALIC)); // Italic for plugin conditions - } else if (isConditionRelevant) { - // Relevant conditions - color based on satisfied status - if (isConditionSatisfied) { - setForeground(new Color(0, 180, 0)); // Green for satisfied - } else { - setForeground(new Color(220, 60, 60)); // Red for unsatisfied - } - } else { - // Non-relevant conditions shown in gray - setForeground(new Color(150, 150, 150)); - } - } - - // Add visual indicators for condition state - String text = (String) value; - String prefix = ""; - - // Relevance indicator - if (isConditionRelevant) { - prefix += "⚡ "; - } - - // Status indicator for relevant conditions - if (isConditionRelevant) { - if (isConditionSatisfied) { - prefix += "✓ "; - } else { - prefix += "✗ "; - } - } - - setText(prefix + text); - - // Enhanced tooltip - StringBuilder tooltip = new StringBuilder(""); - tooltip.append(text).append("
"); - - if (isConditionRelevant) { - if (isConditionSatisfied) { - tooltip.append("Condition is satisfied"); - } else { - tooltip.append("Condition is not satisfied"); - } - - if (condition instanceof TimeCondition) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - ZonedDateTime triggerTime = ((TimeCondition)condition).getCurrentTriggerTime().orElse(null); - if (triggerTime != null) { - tooltip.append("
Trigger time: ").append(triggerTime.format(formatter)); - } - } - } - - tooltip.append(""); - setToolTipText(tooltip.toString()); - } - - return c; - } - }); - conditionList.setBackground(ColorScheme.DARKER_GRAY_COLOR); - conditionList.setForeground(Color.WHITE); - conditionList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - /*conditionList.addListSelectionListener(e -> { - if (!e.getValueIsAdjusting()) { - int index = conditionList.getSelectedIndex(); - if (index >= 0 && index < getCurrentConditions().size()) { - // Select corresponding node in tree - selectNodeForCondition(getCurrentConditions().get(index)); - } - } - });*/ - - JScrollPane scrollPane = new JScrollPane(conditionList); - scrollPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - scrollPane.setBorder(new EmptyBorder(5, 5, 5, 5)); - scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); - scrollPane.getVerticalScrollBar().setUnitIncrement(16); // For smoother scrolling - - panel.add(scrollPane, BorderLayout.CENTER); - return panel; - } - /** - * Creates the add condition panel with condition type selector and controls - */ - private JPanel createAddConditionPanel() { - // Create a panel with border to clearly separate this section - JPanel panel = new JPanel(new BorderLayout()); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - panel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Condition Editor", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - - // Create a main content panel that will be scrollable - JPanel contentPanel = new JPanel(new BorderLayout()); - contentPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Add condition type selector with a more descriptive label - JPanel selectorPanel = new JPanel(new BorderLayout(5, 0)); - selectorPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - selectorPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); - - // Create a panel for both dropdowns - JPanel dropdownsPanel = new JPanel(new GridLayout(2, 2, 5, 5)); - dropdownsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Add category selector - JLabel categoryLabel = new JLabel("Condition Category:"); - categoryLabel.setForeground(Color.WHITE); - categoryLabel.setFont(FontManager.getRunescapeSmallFont()); - dropdownsPanel.add(categoryLabel); - - // Style the category combobox - SchedulerUIUtils.styleComboBox(conditionCategoryComboBox); - conditionCategoryComboBox.addActionListener(e -> { - String selectedCategory = (String) conditionCategoryComboBox.getSelectedItem(); - if (selectedCategory != null) { - updateConditionTypes(selectedCategory); - updateConfigPanel(); - } - }); - dropdownsPanel.add(conditionCategoryComboBox); - - // Add type selector - JLabel typeLabel = new JLabel("Condition Type:"); - typeLabel.setForeground(Color.WHITE); - typeLabel.setFont(FontManager.getRunescapeSmallFont()); - dropdownsPanel.add(typeLabel); - - // Style the type combobox - SchedulerUIUtils.styleComboBox(conditionTypeComboBox); - conditionTypeComboBox.addActionListener(e -> updateConfigPanel()); - dropdownsPanel.add(conditionTypeComboBox); - - selectorPanel.add(dropdownsPanel, BorderLayout.CENTER); - - // Config panel with scroll pane for better visibility - configPanel = new JPanel(new BorderLayout()); - configPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - configPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); - - JScrollPane configScrollPane = new JScrollPane(configPanel); - configScrollPane.setBorder(BorderFactory.createEmptyBorder()); - configScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - configScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - configScrollPane.getVerticalScrollBar().setUnitIncrement(16); - - // Button panel with improved spacing - JPanel buttonPanel = new JPanel(new GridLayout(1, 2, 5, 0)); - buttonPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - buttonPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); - - this.editButton = ConditionConfigPanelUtil.createButton("Add", ColorScheme.PROGRESS_COMPLETE_COLOR); - editButton.addActionListener(e -> { - if ("Apply Changes".equals(editButton.getText())) { - editSelectedCondition(); - } else { - addCurrentCondition(); - } - }); - buttonPanel.add(editButton); - - this.removeButton = ConditionConfigPanelUtil.createButton("Remove", ColorScheme.PROGRESS_ERROR_COLOR); - removeButton.addActionListener(e -> removeSelectedCondition()); - buttonPanel.add(removeButton); - - // Add components to content panel - contentPanel.add(selectorPanel, BorderLayout.NORTH); - contentPanel.add(configScrollPane, BorderLayout.CENTER); - contentPanel.add(buttonPanel, BorderLayout.SOUTH); - - // Add content panel to main panel - panel.add(contentPanel, BorderLayout.CENTER); - - return panel; - } - /** - * Creates the logical condition tree panel with controls - */ - private JPanel createLogicalTreePanel() { - // Use the utility method instead of duplicating code - JPanel panel = ConditionConfigPanelUtil.createTitledPanel("Condition Structure"); - panel.setLayout(new BorderLayout()); - - // Initialize tree - initializeConditionTree(panel); - - // Add logical operations toolbar - JPanel logicalOpPanel = createLogicalOperationsToolbar(); - panel.add(logicalOpPanel, BorderLayout.SOUTH); - - return panel; -} - - private JPanel createLogicalOperationsToolbar() { - // Create the panel - JPanel logicalOpPanel = new JPanel(); - logicalOpPanel.setLayout(new BoxLayout(logicalOpPanel, BoxLayout.X_AXIS)); - logicalOpPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); - logicalOpPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Group operations section - JButton createAndButton = ConditionConfigPanelUtil.createButton("Group as AND", ColorScheme.BRAND_ORANGE); - createAndButton.setToolTipText("Group selected conditions with AND logic"); - createAndButton.addActionListener(e -> { - if (canEditConditions()) { - createLogicalGroup(true); - } - }); - - JButton createOrButton = ConditionConfigPanelUtil.createButton("Group as OR", BRAND_BLUE); - createOrButton.setToolTipText("Group selected conditions with OR logic"); - createOrButton.addActionListener(e -> { - if (canEditConditions()) { - createLogicalGroup(false); - } - }); - - // Negation button - JButton negateButton = ConditionConfigPanelUtil.createButton("Negate", new Color(220, 50, 50)); - negateButton.setToolTipText("Negate the selected condition (toggle NOT)"); - negateButton.addActionListener(e -> { - if (canEditConditions()) { - negateSelectedCondition(); - } - }); - - - // Convert operation buttons - JButton convertToAndButton = ConditionConfigPanelUtil.createButton("Convert to AND", ColorScheme.BRAND_ORANGE); - convertToAndButton.setToolTipText("Convert selected logical group to AND type"); - convertToAndButton.addActionListener(e -> { - if (canEditConditions()) { - convertLogicalType(true); - } - }); - - - JButton convertToOrButton = ConditionConfigPanelUtil.createButton("Convert to OR", BRAND_BLUE); - convertToOrButton.setToolTipText("Convert selected logical group to OR type"); - convertToOrButton.addActionListener(e -> { - if (canEditConditions()) { - convertLogicalType(false); - } - }); - - - // Ungroup button - JButton ungroupButton = ConditionConfigPanelUtil.createButton("Ungroup", ColorScheme.LIGHT_GRAY_COLOR); - ungroupButton.setToolTipText("Remove the logical group but keep its conditions"); - ungroupButton.addActionListener(e -> { - if (canEditConditions()) { - ungroupSelectedLogical(); - } - }); - - - // Add buttons to panel with separators - logicalOpPanel.add(createAndButton); - logicalOpPanel.add(Box.createHorizontalStrut(5)); - logicalOpPanel.add(createOrButton); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(new JSeparator(SwingConstants.VERTICAL)); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(negateButton); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(new JSeparator(SwingConstants.VERTICAL)); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(convertToAndButton); - logicalOpPanel.add(Box.createHorizontalStrut(5)); - logicalOpPanel.add(convertToOrButton); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(new JSeparator(SwingConstants.VERTICAL)); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(ungroupButton); - - // Store references to buttons that need context-sensitive enabling - this.negateButton = negateButton; - this.convertToAndButton = convertToAndButton; - this.convertToOrButton = convertToOrButton; - this.ungroupButton = ungroupButton; - - // Initial state - disable all by default - negateButton.setEnabled(false); - convertToAndButton.setEnabled(false); - convertToOrButton.setEnabled(false); - ungroupButton.setEnabled(false); - - return logicalOpPanel; - } - - private void initializeConditionTree(JPanel panel) { - rootNode = new DefaultMutableTreeNode("Conditions"); - treeModel = new DefaultTreeModel(rootNode); - conditionTree = new JTree(treeModel); - conditionTree.setRootVisible(false); - conditionTree.setShowsRootHandles(true); - this.conditionTreeCellRenderer = new ConditionTreeCellRenderer(getConditionManger(),this.stopConditionPanel); - conditionTree.setCellRenderer(this.conditionTreeCellRenderer); - - conditionTree.getSelectionModel().setSelectionMode( - TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); // Enable multi-select - - // Add popup menu for right-click operations - JPopupMenu popupMenu = createTreePopupMenu(); - conditionTree.setComponentPopupMenu(popupMenu); - - // Add selection listener to update button states and handle selection synchronization - fixSelectionPersistence(false); - - // Create scroll pane with the utility method - JScrollPane treeScrollPane = ConditionConfigPanelUtil.createScrollPane(conditionTree); - treeScrollPane.setPreferredSize(new Dimension(400, 300)); - - panel.add(treeScrollPane, BorderLayout.CENTER); - } - - - - private void updateConfigPanel() { - configPanel.removeAll(); - - // Create a main panel with GridBagLayout for flexibility - JPanel panel = new JPanel(new GridBagLayout()); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 0; - gbc.anchor = GridBagConstraints.WEST; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.weightx = 1.0; - gbc.insets = new Insets(5, 5, 5, 5); - - // Get selected category and type - String selectedCategory = (String) conditionCategoryComboBox.getSelectedItem(); - String selectedType = (String) conditionTypeComboBox.getSelectedItem(); - - if (selectedCategory == null || selectedType == null) { - // No selection, show empty panel - configPanel.add(panel, BorderLayout.NORTH); - return; - } - - // Add condition type header - JLabel typeHeaderLabel = new JLabel("Configure " + selectedType + " Condition"); - typeHeaderLabel.setForeground(Color.WHITE); - typeHeaderLabel.setFont(FontManager.getRunescapeBoldFont()); - panel.add(typeHeaderLabel, gbc); - gbc.gridy++; - - // Add a separator for visual clarity - JSeparator separator = new JSeparator(); - separator.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - panel.add(separator, gbc); - gbc.gridy++; - - // Create the appropriate config panel based on the selected category and type - if ("Location".equals(selectedCategory)) { - // Use LocationConditionUtil for location-based conditions - LocationConditionUtil.createLocationConditionPanel(panel, gbc); - - // After creating the location panel, copy the location-specific client properties from the inner panel to configPanel for later access - JTabbedPane locationTabbedPane = (JTabbedPane) panel.getClientProperty("locationTabbedPane"); - if (locationTabbedPane != null) { - configPanel.putClientProperty("locationTabbedPane", locationTabbedPane); - - // Auto-select the appropriate tab based on the selected condition type - switch (selectedType) { - case "Position": - locationTabbedPane.setSelectedIndex(0); // Position tab - break; - case "Area": - locationTabbedPane.setSelectedIndex(1); // Area tab - break; - case "Region": - locationTabbedPane.setSelectedIndex(2); // Region tab - break; - } - // Copy all location-related properties from the tabbed pane panels - for (int i = 0; i < locationTabbedPane.getTabCount(); i++) { - JPanel tabPanel = (JPanel) locationTabbedPane.getComponentAt(i); - // Copy client properties from tab panels to configPanel for universal access - if (tabPanel.getClientProperty("positionXSpinner") != null) { - configPanel.putClientProperty("positionXSpinner", tabPanel.getClientProperty("positionXSpinner")); - configPanel.putClientProperty("positionYSpinner", tabPanel.getClientProperty("positionYSpinner")); - configPanel.putClientProperty("positionPlaneSpinner", tabPanel.getClientProperty("positionPlaneSpinner")); - configPanel.putClientProperty("positionDistanceSpinner", tabPanel.getClientProperty("positionDistanceSpinner")); - configPanel.putClientProperty("positionNameField", tabPanel.getClientProperty("positionNameField")); - } - if (tabPanel.getClientProperty("areaX1Spinner") != null) { - configPanel.putClientProperty("areaX1Spinner", tabPanel.getClientProperty("areaX1Spinner")); - configPanel.putClientProperty("areaY1Spinner", tabPanel.getClientProperty("areaY1Spinner")); - configPanel.putClientProperty("areaX2Spinner", tabPanel.getClientProperty("areaX2Spinner")); - configPanel.putClientProperty("areaY2Spinner", tabPanel.getClientProperty("areaY2Spinner")); - configPanel.putClientProperty("areaPlaneSpinner", tabPanel.getClientProperty("areaPlaneSpinner")); - configPanel.putClientProperty("areaNameField", tabPanel.getClientProperty("areaNameField")); - } - if (tabPanel.getClientProperty("regionIdsField") != null) { - configPanel.putClientProperty("regionIdsField", tabPanel.getClientProperty("regionIdsField")); - configPanel.putClientProperty("regionNameField", tabPanel.getClientProperty("regionNameField")); - } - } - } - } else if ("Varbit".equals(selectedCategory)) { - // Use VarbitConditionPanelUtil for varbit-based conditions - switch (selectedType) { - case "Collection Log - Minigames": - VarbitConditionPanelUtil.createMinigameVarbitPanel(panel, gbc); - break; - case "Collection Log - Bosses": - VarbitConditionPanelUtil.createBossVarbitPanel(panel, gbc); - break; - } - } else if (stopConditionPanel) { - switch (selectedType) { - case "Time Duration": - TimeConditionPanelUtil.createIntervalConfigPanel(panel, gbc); - break; - case "Time Window": - TimeConditionPanelUtil.createTimeWindowConfigPanel(panel, gbc); - break; - case "Not In Time Window": - TimeConditionPanelUtil.createTimeWindowConfigPanel(panel, gbc); - // Store whether we want inside or outside the window - configPanel.putClientProperty("withInWindow", false); - break; - case "Day of Week": - TimeConditionPanelUtil.createDayOfWeekConfigPanel(panel, gbc); - break; - case "Specific Time": - TimeConditionPanelUtil.createSingleTriggerConfigPanel(panel, gbc); - break; - case "Skill Level": - SkillConditionPanelUtil.createSkillLevelConfigPanel(panel, gbc, true); - break; - case "Skill XP Goal": - SkillConditionPanelUtil.createSkillXpConfigPanel(panel, gbc, panel); - break; - case "Item Collection": - ResourceConditionPanelUtil.createItemConfigPanel(panel, gbc, panel, true); - break; - case "Process Items": - //ResourceConditionPanelUtil.createProcessItemConfigPanel(panel, gbc, panel); - break; - case "Gather Resources": - //ResourceConditionPanelUtil.createGatheredResourceConditionPanel(panel, gbc, panel); - break; - default: - JLabel notImplementedLabel = new JLabel("This condition type is not yet implemented: " + selectedType); - notImplementedLabel.setForeground(Color.RED); - panel.add(notImplementedLabel, gbc); - break; - } - } else { - switch (selectedType) { - case "Time Interval": - TimeConditionPanelUtil.createIntervalConfigPanel(panel, gbc); - break; - case "Time Window": - TimeConditionPanelUtil.createTimeWindowConfigPanel(panel, gbc); - break; - case "Outside Time Window": - TimeConditionPanelUtil.createTimeWindowConfigPanel(panel, gbc); - break; - case "Day of Week": - TimeConditionPanelUtil.createDayOfWeekConfigPanel(panel, gbc); - break; - case "Specific Time": - TimeConditionPanelUtil.createSingleTriggerConfigPanel(panel, gbc); - break; - case "Skill Level Required": - SkillConditionPanelUtil.createSkillLevelConfigPanel(panel, gbc, false); - break; - case "Item Required": - ResourceConditionPanelUtil.createInventoryItemCountPanel(panel, gbc); - break; - default: - JLabel notImplementedLabel = new JLabel("This Start condition type is not yet implemented"); - notImplementedLabel.setForeground(Color.RED); - panel.add(notImplementedLabel, gbc); - break; - } - } - - // Add the panel to the config panel - configPanel.add(panel, BorderLayout.NORTH); - - // Add a filler panel to push everything to the top - JPanel fillerPanel = new JPanel(); - fillerPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - configPanel.add(fillerPanel, BorderLayout.CENTER); - - configPanel.revalidate(); - configPanel.repaint(); - configPanel.putClientProperty("localConditionPanel", panel); - } - - /** - * Sets the condition update callback interface - * This is the preferred way to handle condition updates - * - * @param callback The callback interface to be called when conditions are updated - */ - public void setConditionUpdateCallback(ConditionUpdateCallback callback) { - this.conditionUpdateCallback = callback; - } - - - - - - private void editSelectedCondition() { - // Get the selected node from the tree (same as updateConditionPanelForSelectedNode) - DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (selectedNode == null || !(selectedNode.getUserObject() instanceof Condition)) { - log.warn("editSelectedCondition: No condition selected or invalid selection"); - return; - } - - // Check if conditions can be edited - if (!canEditConditions()) { - return; - } - - // Get the selected condition from the tree node - Condition oldCondition = (Condition) selectedNode.getUserObject(); - - // Skip logical conditions as they don't have direct UI editors - if (oldCondition instanceof LogicalCondition || oldCondition instanceof NotCondition) { - log.warn("editSelectedCondition: Cannot edit logical condition: {}", oldCondition.getDescription()); - JOptionPane.showMessageDialog(this, - "Logical conditions (AND/OR/NOT) cannot be edited directly. Edit their child conditions instead.", - "Cannot Edit Logical Condition", - JOptionPane.INFORMATION_MESSAGE); - return; - } - - // Check if this is a plugin-defined condition - ConditionManager manager = getConditionManger(); - if (manager != null && manager.isPluginDefinedCondition(oldCondition)) { - JOptionPane.showMessageDialog(this, - "This condition is defined by the plugin and cannot be edited.", - "Plugin Condition", - JOptionPane.WARNING_MESSAGE); - return; - } - - // Find the logical condition that contains our target condition - LogicalCondition parentLogical = manager.findContainingLogical(oldCondition); - if (parentLogical == null) { - log.warn("editSelectedCondition: Could not find parent logical for condition: {}", oldCondition.getDescription()); - parentLogical = manager.getUserLogicalCondition(); - } - - log.info("editSelectedCondition: Editing condition: {} from parent: {}", - oldCondition.getDescription(), parentLogical.getDescription()); - - // Create the new condition from the current UI state - - try { - Condition newCondition = getCurrentComboboxCondition(); - - if (newCondition == null) { - log.warn("editSelectedCondition: Failed to create new condition"); - return; - } - - log.info("editSelectedCondition: Created new condition: {}", newCondition.getDescription()); - - // Remove the old condition - boolean removed = manager.removeFromLogicalStructure(parentLogical, oldCondition); - - if (!removed) { - log.warn("editSelectedCondition: Failed to remove old condition from parent"); - return; - } - - // Add the new condition to the parent logical - manager.addConditionToLogical(newCondition, parentLogical); - - // Update UI - updateTreeFromConditions(); - refreshDisplay(); - - // Notify listeners - notifyConditionUpdate(); - - // Select the newly edited condition - selectNodeForCondition(newCondition); - - // Save changes - saveConditionsToScheduledPlugin(); - - } catch (Exception e) { - log.error("editSelectedCondition: Error editing condition", e); - JOptionPane.showMessageDialog(this, - "Error editing condition: " + e.getMessage(), - "Edit Failed", - JOptionPane.ERROR_MESSAGE); - } - } - - - private void loadConditions(List conditionList, boolean requireAll) { - boolean needsUpdate = false; - for (Condition condition : conditionList) { - if (! lastRefreshConditions.contains(condition)){ - needsUpdate = true; - } - } - conditionListModel.clear(); - - if (conditionList != null) { - for (Condition condition : conditionList) { - // Check if this is a plugin-defined condition - boolean isPluginDefined = false; - - if (selectScheduledPlugin != null && getConditionManger() != null) { - isPluginDefined = getConditionManger().isPluginDefinedCondition(condition); - } - - - // Add with appropriate tag for plugin-defined conditions - String description = condition.getDescription(); - if (isPluginDefined) { - description = "[Plugin] " + description; - } - - conditionListModel.addElement(description); - } - } - - updateTreeFromConditions(); - } - /** - * Updates the tree from conditions while preserving selection and expansion state - */ - private void updateTreeFromConditions() { - // Store selected conditions and expanded state before rebuilding - Set selectedConditions = new HashSet<>(); - Set expandedConditions = new HashSet<>(); - - // Remember selected conditions - TreePath[] selectedPaths = conditionTree.getSelectionPaths(); - if (selectedPaths != null) { - for (TreePath path : selectedPaths) { - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - if (node.getUserObject() instanceof Condition) { - selectedConditions.add((Condition) node.getUserObject()); - } - } - } - - // Remember expanded nodes - Enumeration expandedPaths = conditionTree.getExpandedDescendants(new TreePath(rootNode.getPath())); - if (expandedPaths != null) { - while (expandedPaths.hasMoreElements()) { - TreePath path = expandedPaths.nextElement(); - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - if (node.getUserObject() instanceof Condition) { - expandedConditions.add((Condition) node.getUserObject()); - } - } - } - - // Clear and rebuild tree - rootNode.removeAllChildren(); - - if (selectScheduledPlugin == null) { - treeModel.nodeStructureChanged(rootNode); - return; - } - - // Build the tree from the condition manager - ConditionManager manager = getConditionManger(); - if (Microbot.isDebug()){log.info ("updateTreeFromConditions: Building tree for plugin {} with {} conditions", - selectScheduledPlugin.getCleanName(), manager.getConditions().size());} - // If there is a plugin condition, show plugin and user sections separately - if (manager.getPluginCondition() != null && !manager.getPluginCondition().getConditions().isEmpty()) { - // Add plugin section - DefaultMutableTreeNode pluginNode = new DefaultMutableTreeNode("Plugin Conditions"); - rootNode.add(pluginNode); - buildConditionTree(pluginNode, manager.getPluginCondition()); - - // Add user section if it has conditions - if (manager.getUserLogicalCondition() != null && - !manager.getUserLogicalCondition().getConditions().isEmpty()) { - - DefaultMutableTreeNode userNode = new DefaultMutableTreeNode("User Conditions"); - rootNode.add(userNode); - buildConditionTree(userNode, manager.getUserLogicalCondition()); - } - } - // Otherwise just build from the root logical or flat conditions - else if (manager.getUserLogicalCondition() != null) { - LogicalCondition rootLogical = manager.getUserLogicalCondition(); - - // For the root logical, show its children directly if it matches the selected type - buildConditionTree(rootNode, rootLogical); - - } - else { - // This handles the case where we have flat conditions without logical structure - for (Condition condition : getCurrentConditions()) { - buildConditionTree(rootNode, condition); - } - } - - // Update tree model - treeModel.nodeStructureChanged(rootNode); - - // First expand all nodes that were previously expanded - for (int i = 0; i < conditionTree.getRowCount(); i++) { - TreePath path = conditionTree.getPathForRow(i); - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - - if (node.getUserObject() instanceof Condition) { - Condition condition = (Condition) node.getUserObject(); - if (expandedConditions.contains(condition)) { - conditionTree.expandPath(path); - } - } else if (node.getUserObject() instanceof String) { - // Always expand category headers - conditionTree.expandPath(path); - } - } - - // Then restore selection - List pathsToSelect = new ArrayList<>(); - for (int i = 0; i < conditionTree.getRowCount(); i++) { - TreePath path = conditionTree.getPathForRow(i); - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - - if (node.getUserObject() instanceof Condition) { - Condition condition = (Condition) node.getUserObject(); - if (selectedConditions.contains(condition)) { - pathsToSelect.add(path); - } - } - } - - if (!pathsToSelect.isEmpty()) { - conditionTree.setSelectionPaths(pathsToSelect.toArray(new TreePath[0])); - } - this.conditionTreeCellRenderer.setIsActive(selectScheduledPlugin.isEnabled() && selectScheduledPlugin.isRunning()); - } - - private void buildConditionTree(DefaultMutableTreeNode parent, Condition condition) { - if (condition instanceof AndCondition) { - AndCondition andCondition = (AndCondition) condition; - DefaultMutableTreeNode andNode = new DefaultMutableTreeNode(andCondition); - parent.add(andNode); - - for (Condition child : andCondition.getConditions()) { - buildConditionTree(andNode, child); - } - } else if (condition instanceof OrCondition) { - OrCondition orCondition = (OrCondition) condition; - DefaultMutableTreeNode orNode = new DefaultMutableTreeNode(orCondition); - parent.add(orNode); - - for (Condition child : orCondition.getConditions()) { - buildConditionTree(orNode, child); - } - } else if (condition instanceof NotCondition) { - NotCondition notCondition = (NotCondition) condition; - DefaultMutableTreeNode notNode = new DefaultMutableTreeNode(notCondition); - parent.add(notNode); - - buildConditionTree(notNode, notCondition.getCondition()); - } else { - // Add leaf condition - parent.add(new DefaultMutableTreeNode(condition)); - } - } - /** - * Recursively searches for a tree node containing the specified condition - */ - private DefaultMutableTreeNode findTreeNodeForCondition(DefaultMutableTreeNode parent, Condition target) { - // Check if this node contains our target - if (parent.getUserObject() == target) { - return parent; - } - - // Check all children - for (int i = 0; i < parent.getChildCount(); i++) { - DefaultMutableTreeNode child = (DefaultMutableTreeNode) parent.getChildAt(i); - DefaultMutableTreeNode result = findTreeNodeForCondition(child, target); - if (result != null) { - return result; - } - } - - return null; - } - private void groupSelectedWithLogical(LogicalCondition logicalCondition) { - DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (selectedNode == null || selectedNode == rootNode) { - return; - } - - // Get the parent node - DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) selectedNode.getParent(); - if (parentNode == null) { - return; - } - - // Get the selected condition - Object userObject = selectedNode.getUserObject(); - if (!(userObject instanceof Condition)) { - return; - } - - Condition condition = (Condition) userObject; - - // Remove the condition from its current position - int index = getCurrentConditions().indexOf(condition); - if (index >= 0) { - getCurrentConditions().remove(index); - conditionListModel.remove(index); - } - - // Add it to the new logical condition - logicalCondition.addCondition(condition); - - // Add the logical condition to the list - getCurrentConditions().add(logicalCondition); - conditionListModel.addElement(logicalCondition.getDescription()); - - updateTreeFromConditions(); - notifyConditionUpdate(); - } - private void negateSelected() { - DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (selectedNode == null || selectedNode == rootNode) { - return; - } - - // Get the selected condition - Object userObject = selectedNode.getUserObject(); - if (!(userObject instanceof Condition)) { - return; - } - - Condition condition = (Condition) userObject; - - // Remove the condition from its current position - int index = getCurrentConditions().indexOf(condition); - if (index >= 0) { - getCurrentConditions().remove(index); - conditionListModel.remove(index); - } - - // Create a NOT condition - NotCondition notCondition = new NotCondition(condition); - - // Add the NOT condition to the list - getCurrentConditions().add(notCondition); - conditionListModel.addElement(notCondition.getDescription()); - - updateTreeFromConditions(); - notifyConditionUpdate(); - } - /** - * Removes the selected condition, properly handling nested logic conditions - */ - private void removeSelectedFromTree() { - DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (selectedNode == null || selectedNode == rootNode) { - return; - } - - // Get the selected condition - Object userObject = selectedNode.getUserObject(); - if (!(userObject instanceof Condition)) { - return; - } - - Condition condition = (Condition) userObject; - - // Check if this is a plugin-defined condition that shouldn't be removed - if (selectScheduledPlugin != null && - getConditionManger().isPluginDefinedCondition(condition)) { - JOptionPane.showMessageDialog(this, - "This condition is defined by the plugin and cannot be removed.", - "Plugin Condition", - JOptionPane.WARNING_MESSAGE); - return; - } - - - - // Get parent logical condition if any - DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) selectedNode.getParent(); - if (parentNode != rootNode && parentNode.getUserObject() instanceof LogicalCondition) { - LogicalCondition parentLogical = (LogicalCondition) parentNode.getUserObject(); - parentLogical.removeCondition(condition); - - // If logical condition is now empty and not the root, remove it too - if (parentLogical.getConditions().isEmpty() && - parentNode.getParent() != rootNode && - parentNode.getParent() instanceof DefaultMutableTreeNode) { - - DefaultMutableTreeNode grandparentNode = (DefaultMutableTreeNode) parentNode.getParent(); - if (grandparentNode.getUserObject() instanceof LogicalCondition) { - LogicalCondition grandparentLogical = (LogicalCondition) grandparentNode.getUserObject(); - grandparentLogical.removeCondition(parentLogical); - } - } - } else { - // Direct removal from condition manager - getConditionManger().removeCondition(condition); - } - - updateTreeFromConditions(); - notifyConditionUpdate(); - } - /** - * Selects a tree node corresponding to the condition, preserving expansion state - */ - private void selectNodeForCondition(Condition condition) { - if (condition == null) { - log.debug("selectNodeForCondition: Cannot select null condition"); - return; - } - - log.debug("selectNodeForCondition: Attempting to select condition: {}", condition.getDescription()); - - // Store current expansion state - Set expandedPaths = new HashSet<>(); - Enumeration expanded = conditionTree.getExpandedDescendants(new TreePath(rootNode.getPath())); - if (expanded != null) { - while (expanded.hasMoreElements()) { - expandedPaths.add(expanded.nextElement()); - } - log.debug("selectNodeForCondition: Saved {} expanded paths", expandedPaths.size()); - } else { - log.debug("selectNodeForCondition: No expanded paths to save"); - } - - // Find the node corresponding to the condition - DefaultMutableTreeNode targetNode = null; - Enumeration e = rootNode.breadthFirstEnumeration(); - while (e.hasMoreElements()) { - DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.nextElement(); - if (node.getUserObject() == condition) { - targetNode = node; - break; - } - } - - if (targetNode != null) { - log.debug("selectNodeForCondition: Found node for condition: {}", condition.getDescription()); - TreePath path = new TreePath(targetNode.getPath()); - - // Make all parent nodes visible - expanding the path as needed - TreePath parentPath = path.getParentPath(); - if (parentPath != null) { - log.debug("selectNodeForCondition: Expanding parent path"); - conditionTree.expandPath(parentPath); - } - - // Set selection - log.debug("selectNodeForCondition: Setting selection path"); - conditionTree.setSelectionPath(path); - - // Ensure the selected node is visible - log.debug("selectNodeForCondition: Scrolling to make path visible"); - conditionTree.scrollPathToVisible(path); - - // Restore previously expanded paths - log.debug("selectNodeForCondition: Restoring {} expanded paths", expandedPaths.size()); - for (TreePath expandedPath : expandedPaths) { - conditionTree.expandPath(expandedPath); - } - } else { - log.debug("selectNodeForCondition: Could not find node for condition: {}", condition.getDescription()); - } - } - - - private void initializeResetButton() { - resetButton = ConditionConfigPanelUtil.createButton("Reset All Conditions", ColorScheme.PROGRESS_ERROR_COLOR); - - resetButton.addActionListener(e -> { - if (selectScheduledPlugin == null) return; - - int option = JOptionPane.showConfirmDialog(this, - "Are you sure you want to reset all " + (stopConditionPanel ? "stop" : "start") + " conditions?\n" + - "This will remove both user-defined and plugin-defined conditions.", - "Reset All Conditions", - JOptionPane.YES_NO_OPTION, - JOptionPane.WARNING_MESSAGE); - - if (option == JOptionPane.YES_OPTION) { - // Clear all conditions from the condition manager - if (conditionUpdateCallback != null) { - conditionUpdateCallback.onConditionsReset(selectScheduledPlugin, stopConditionPanel); - } - - // Refresh the display - refreshDisplay(); - } - }); - } - - private void initializeResetUserConditionsButton() { - resetUserConditionsButton = ConditionConfigPanelUtil.createButton("Reset User Conditions", new Color(180, 120, 0)); - resetUserConditionsButton.setToolTipText("Reset only user-defined conditions (preserves plugin conditions)"); - - resetUserConditionsButton.addActionListener(e -> { - if (selectScheduledPlugin == null) return; - - if (!hasUserDefinedConditions()) { - JOptionPane.showMessageDialog(this, - "No user-defined conditions to reset.", - "No User Conditions", - JOptionPane.INFORMATION_MESSAGE); - return; - } - - int option = JOptionPane.showConfirmDialog(this, - "Are you sure you want to reset only user-defined " + (stopConditionPanel ? "stop" : "start") + " conditions?\n" + - "Plugin-defined conditions will be preserved.", - "Reset User Conditions", - JOptionPane.YES_NO_OPTION, - JOptionPane.WARNING_MESSAGE); - - if (option == JOptionPane.YES_OPTION) { - // Clear only user conditions from the condition manager - ConditionManager manager = getConditionManger(); - if (manager != null) { - manager.clearUserConditions(); - - // Save changes - if (conditionUpdateCallback != null) { - conditionUpdateCallback.onConditionsUpdated( - manager.getUserLogicalCondition(), - selectScheduledPlugin, - stopConditionPanel - ); - } - } - - // Refresh the display - refreshDisplay(); - - JOptionPane.showMessageDialog(this, - "User-defined conditions have been reset.\n" + - "Plugin-defined conditions remain unchanged.", - "Reset Complete", - JOptionPane.INFORMATION_MESSAGE); - } - }); - } - - private void saveConditionsToScheduledPlugin() { - if (selectScheduledPlugin == null) return; - // Save to config - setScheduledPluginNameLabel(); // Update label - } - /** - * Updates the title label with the selected plugin name using color for better visibility - */ - private void setScheduledPluginNameLabel() { - if (selectScheduledPlugin != null) { - String pluginName = selectScheduledPlugin.getCleanName(); - boolean isRunning = selectScheduledPlugin.isRunning(); - boolean isEnabled = selectScheduledPlugin.isEnabled(); - - titleLabel.setText(ConditionConfigPanelUtil.formatPluginTitle(isRunning, isEnabled, pluginName)); - } else { - titleLabel.setText(ConditionConfigPanelUtil.formatPluginTitle(false, false, null)); - } - } - - /** - * Notifies any external components of condition changes - * and ensures changes are saved to the config. - */ - private void notifyConditionUpdate() { - if (selectScheduledPlugin == null) { - return; - } - - // Get the logical condition structure - LogicalCondition logicalCondition = getConditionManger().getUserLogicalCondition(); - - // Call the new callback if registered - if (conditionUpdateCallback != null) { - conditionUpdateCallback.onConditionsUpdated( - logicalCondition, - selectScheduledPlugin, - stopConditionPanel - ); - } - - - } - /** - * Refreshes the condition list and tree if conditions have changed in the selected plugin. - * This should be called periodically to keep the UI in sync with the plugin state. - * - * @return true if conditions were refreshed, false if no changes were detected - */ - public boolean refreshConditions() { - if (selectScheduledPlugin == null) { - return false; - } - - refreshDisplay(); - return true; - } - private Condition getCurrentComboboxCondition( ) { - JPanel localConfigPanel = (JPanel) configPanel.getClientProperty("localConditionPanel"); - if (localConfigPanel == null) { - log.debug("No config panel found"); - return null; - } - - String selectedCategory = (String) conditionCategoryComboBox.getSelectedItem(); - String selectedType = (String) conditionTypeComboBox.getSelectedItem(); - - if (selectedCategory == null || selectedType == null) { - log.debug("Selected category or type is null"); - return null; - } - - Condition condition = null; - try { - // Create appropriate condition based on the type - if ("Location".equals(selectedCategory)) { - condition = LocationConditionUtil.createLocationCondition(localConfigPanel); - } else if ("Varbit".equals(selectedCategory)) { - - condition = VarbitConditionPanelUtil.createVarbitCondition(localConfigPanel); - } else if (stopConditionPanel) { - // Stop conditions - switch (selectedType) { - case "Time Duration": - condition = TimeConditionPanelUtil.createIntervalCondition(localConfigPanel); - break; - case "Time Window": - condition = TimeConditionPanelUtil.createTimeWindowCondition(localConfigPanel); - break; - case "Not In Time Window": - condition = TimeConditionPanelUtil.createTimeWindowCondition(localConfigPanel); - break; - case "Day of Week": - condition = TimeConditionPanelUtil.createDayOfWeekCondition(localConfigPanel); - break; - case "Specific Time": - condition = TimeConditionPanelUtil.createSingleTriggerCondition(localConfigPanel); - break; - case "Skill Level": - condition = SkillConditionPanelUtil.createSkillLevelCondition(localConfigPanel); - break; - case "Skill XP Goal": - condition = SkillConditionPanelUtil.createSkillXpCondition(localConfigPanel); - break; - - case "Item Collection": - condition = ResourceConditionPanelUtil.createItemCondition(localConfigPanel); - break; - case "Process Items": - condition = ResourceConditionPanelUtil.createProcessItemCondition(localConfigPanel); - break; - case "Gather Resources": - condition = ResourceConditionPanelUtil.createGatheredResourceCondition(localConfigPanel); - break; - case "Inventory Item Count"://TODO these are not working right now. have to update the logic-> change these here to gaathered items - condition = ResourceConditionPanelUtil.createInventoryItemCountCondition(localConfigPanel); - break; - default: - JOptionPane.showMessageDialog(this, "Condition type not implemented", "Error", JOptionPane.ERROR_MESSAGE); - return null; - } - - } else { - switch (selectedType) { - case "Time Interval": - condition = TimeConditionPanelUtil.createIntervalCondition(localConfigPanel); - break; - case "Time Window": - condition = TimeConditionPanelUtil.createTimeWindowCondition(localConfigPanel); - break; - case "Outside Time Window": - condition = TimeConditionPanelUtil.createTimeWindowCondition(localConfigPanel); - break; - case "Day of Week": - condition = TimeConditionPanelUtil.createDayOfWeekCondition(localConfigPanel); - break; - case "Specific Time": - condition = TimeConditionPanelUtil.createSingleTriggerCondition(localConfigPanel); - break; - case "Skill Level Required": - condition = SkillConditionPanelUtil.createSkillLevelCondition(localConfigPanel); - break; - case "Item Required": - //condition = ResourceConditionPanelUtil.createItemRequiredCondition(localConfigPanel); - condition = ResourceConditionPanelUtil.createInventoryItemCountCondition(localConfigPanel); - break; - default: - JOptionPane.showMessageDialog(this, "Condition type not implemented", "Error", JOptionPane.ERROR_MESSAGE); - return null; - } - } - } catch (Exception e) { - JOptionPane.showMessageDialog(this, "Error creating condition: " + e.getMessage()); - } - return condition; - } - - private void addCurrentCondition() { - if (selectScheduledPlugin == null) { - JOptionPane.showMessageDialog(this, "No plugin selected", "Error", JOptionPane.ERROR_MESSAGE); - return; - } - - // Check if conditions can be edited - if (!canEditConditions()) { - return; - } - - Condition condition = getCurrentComboboxCondition(); - - if (condition != null) { - addConditionToPlugin(condition); - refreshDisplay(); - } else { - JOptionPane.showMessageDialog(this, - "Failed to create condition. Check your inputs.", - "Error", JOptionPane.ERROR_MESSAGE); - } - - - } - private ConditionManager getConditionManger(){ - if (selectScheduledPlugin == null) { - return null; - } - if (stopConditionPanel){ - return selectScheduledPlugin.getStopConditionManager(); - }else{ - return selectScheduledPlugin.getStartConditionManager(); - } - } - /** - * Finds the logical condition that should be the target for adding a new condition - * based on the current tree selection - */ - private LogicalCondition findTargetLogicalForAddition() { - if (stopConditionPanel){ - ConditionManager manager = getConditionManger(); - } - if (selectScheduledPlugin == null) { - return null; - } - - DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (selectedNode == null) { - // No selection, use root logical - return getConditionManger().getUserLogicalCondition(); - } - - Object userObject = selectedNode.getUserObject(); - - // If selected node is a logical condition, use it directly - if (userObject instanceof LogicalCondition) { - // Check if this is a plugin-defined condition - if (getConditionManger().isPluginDefinedCondition((LogicalCondition)userObject)) { - return getConditionManger().getUserLogicalCondition(); - } - return (LogicalCondition) userObject; - } - - // If selected node is a regular condition, find its parent logical - if (userObject instanceof Condition && - selectedNode.getParent() != null && - selectedNode.getParent() != rootNode) { - - DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) selectedNode.getParent(); - if (parentNode.getUserObject() instanceof LogicalCondition) { - // Check if this is a plugin-defined condition - - if (getConditionManger().isPluginDefinedCondition((LogicalCondition)parentNode.getUserObject())) { - return getConditionManger().getUserLogicalCondition(); - } - return (LogicalCondition) parentNode.getUserObject(); - } - } - - // Default to user logical condition - return getConditionManger().getUserLogicalCondition(); - } - - /** - * Adds a condition to the appropriate logical structure based on selection - */ - private void addConditionToPlugin(Condition condition) { - if (selectScheduledPlugin == null || condition == null) { - log.error("addConditionToPlugin: Cannot add condition - plugin or condition is null"); - return; - } - - log.info("addConditionToPlugin: Adding condition: {}", condition.getDescription()); - - ConditionManager manager = getConditionManger(); - if (manager == null) { - log.error("addConditionToPlugin: Condition manager is null"); - return; - } - - // Find target logical condition based on selection - LogicalCondition targetLogical = findTargetLogicalForAddition(); - if (targetLogical == null) { - log.error("addConditionToPlugin: Target logical is null"); - return; - } - - log.info("addConditionToPlugin: Using target logical: {}", targetLogical.getDescription()); - - // Add the condition - manager.addConditionToLogical(condition, targetLogical); - - log.info("addConditionToPlugin: Condition added to manager"); - - // Update UI - updateTreeFromConditions(); - - // Notify listeners - notifyConditionUpdate(); - - // Select the newly added condition - selectNodeForCondition(condition); - } - - /** - * Removes the selected condition from the logical structure - */ - private void removeSelectedCondition() { - DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (selectedNode == null || selectedNode == rootNode) { - log.warn("No condition selected for removal"); - return; - } - - // Check if conditions can be edited - if (!canEditConditions()) { - return; - } - - Object userObject = selectedNode.getUserObject(); - if (!(userObject instanceof Condition)) { - log.warn("Selected node is not a condition"); - return; - } - - Condition condition = (Condition) userObject; - ConditionManager manager = getConditionManger(); - - // Check if this is a plugin-defined condition - if (manager.isPluginDefinedCondition(condition)) { - JOptionPane.showMessageDialog(this, - "This condition is defined by the plugin and cannot be removed.", - "Plugin Condition", - JOptionPane.WARNING_MESSAGE); - return; - } - - // Remove the condition from its logical structure - boolean removed = manager.removeCondition(condition); - - if (!removed) { - log.warn("Failed to remove condition: {}", condition.getDescription()); - } - - // Update UI - updateTreeFromConditions(); - notifyConditionUpdate(); - } - - - - /** - * Finds the common parent logical condition for a set of tree nodes - */ - private LogicalCondition findCommonParent(DefaultMutableTreeNode[] nodes) { - if (nodes.length == 0) { - return null; - } - - // Get the parent of the first node - DefaultMutableTreeNode firstParent = (DefaultMutableTreeNode) nodes[0].getParent(); - if (firstParent == null || firstParent == rootNode) { - return getConditionManger().getUserLogicalCondition(); - } - - if (!(firstParent.getUserObject() instanceof LogicalCondition)) { - return null; - } - - LogicalCondition parentLogical = (LogicalCondition) firstParent.getUserObject(); - - // Check if all nodes have the same parent - for (int i = 1; i < nodes.length; i++) { - DefaultMutableTreeNode parent = (DefaultMutableTreeNode) nodes[i].getParent(); - if (parent != firstParent) { - return null; - } - } - - return parentLogical; - } - - /** - * Negates the selected condition - */ - private void negateSelectedCondition() { - DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (selectedNode == null || !(selectedNode.getUserObject() instanceof Condition)) { - return; - } - - Condition selectedCondition = (Condition) selectedNode.getUserObject(); - - // Use the utility method for negation - boolean success = ConditionConfigPanelUtil.negateCondition( - selectedCondition, - getConditionManger(), - this - ); - - if (success) { - // Update UI - updateTreeFromConditions(); - notifyConditionUpdate(); - } - } - - - - /** - * Updates button states based on current selection - */ - private void updateLogicalButtonStates() { - TreePath[] selectionPaths = conditionTree.getSelectionPaths(); - - // Check if we should disable editing for stop conditions when plugin is running - boolean pluginRunning = selectScheduledPlugin != null && selectScheduledPlugin.isRunning(); - boolean isEditingStopConditions = stopConditionPanel; - boolean shouldDisableStopConditionEditing = isEditingStopConditions && pluginRunning; - - // Default state - all operations disabled - boolean canNegate = false; - boolean canConvertToAnd = false; - boolean canConvertToOr = false; - boolean canUngroup = false; - - // If we have a single selection - if (selectionPaths != null && selectionPaths.length == 1) { - DefaultMutableTreeNode node = (DefaultMutableTreeNode) selectionPaths[0].getLastPathComponent(); - Object userObject = node.getUserObject(); - - if (userObject instanceof Condition) { - Condition condition = (Condition) userObject; - - // Check if it's a plugin-defined condition (can't modify these) - boolean isPluginDefined = selectScheduledPlugin != null && - getConditionManger() != null && - getConditionManger().isPluginDefinedCondition(condition); - - // Can negate any condition that isn't plugin-defined and editing is allowed - canNegate = !isPluginDefined && !shouldDisableStopConditionEditing; - - // Can convert logical conditions (AND, OR) if they're not plugin-defined and editing is allowed - if (condition instanceof LogicalCondition && !isPluginDefined && !shouldDisableStopConditionEditing) { - // Can convert AND to OR - canConvertToOr = condition instanceof AndCondition; - - // Can convert OR to AND - canConvertToAnd = condition instanceof OrCondition; - - // Can ungroup any logical that has a parent and isn't plugin-defined - canUngroup = node.getParent() != null && node.getParent() != rootNode; - } - } - } - // If we have multiple selections, only enable group operations if editing is allowed - else if (selectionPaths != null && selectionPaths.length > 1 && !shouldDisableStopConditionEditing) { - // Group operations are handled separately - don't enable other operations - } - - // Update button states - if (negateButton != null) negateButton.setEnabled(canNegate); - if (convertToAndButton != null) convertToAndButton.setEnabled(canConvertToAnd); - if (convertToOrButton != null) convertToOrButton.setEnabled(canConvertToOr); - if (ungroupButton != null) ungroupButton.setEnabled(canUngroup); - } - - /** - * Creates a popup menu for the condition tree with improved visibility - */ - private JPopupMenu createTreePopupMenu() { - JPopupMenu menu = new JPopupMenu(); - menu.setBackground(new Color(30, 30, 35)); - menu.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(new Color(80, 80, 90), 1), - BorderFactory.createEmptyBorder(2, 2, 2, 2) - )); - - // Negate option - JMenuItem negateItem = new JMenuItem("Negate"); - negateItem.setIcon(ConditionConfigPanelUtil.getResourceIcon("not-equal.png", 16, 16)); - negateItem.addActionListener(e -> negateSelectedCondition()); - styleMenuItem(negateItem, new Color(220, 50, 50)); - - // Group options with icons and improved styling - JMenuItem groupAndItem = new JMenuItem("Group as AND"); - groupAndItem.setIcon(ConditionConfigPanelUtil.getResourceIcon("logic-gate-and.png", 16, 16)); - groupAndItem.addActionListener(e -> createLogicalGroup(true)); - styleMenuItem(groupAndItem, ColorScheme.BRAND_ORANGE); - - JMenuItem groupOrItem = new JMenuItem("Group as OR"); - groupOrItem.setIcon(ConditionConfigPanelUtil.getResourceIcon("logic-gate-or.png", 16, 16)); - groupOrItem.addActionListener(e -> createLogicalGroup(false)); - styleMenuItem(groupOrItem, BRAND_BLUE); - - // Convert options with visual distinction - JMenuItem convertToAndItem = new JMenuItem("Convert to AND"); - convertToAndItem.addActionListener(e -> convertLogicalType(true)); - styleMenuItem(convertToAndItem, ColorScheme.BRAND_ORANGE); - - JMenuItem convertToOrItem = new JMenuItem("Convert to OR"); - convertToOrItem.addActionListener(e -> convertLogicalType(false)); - styleMenuItem(convertToOrItem, BRAND_BLUE); - - // Ungroup option - JMenuItem ungroupItem = new JMenuItem("Ungroup"); - ungroupItem.setIcon(ConditionConfigPanelUtil.getResourceIcon("ungroup.png", 16, 16)); - ungroupItem.addActionListener(e -> ungroupSelectedLogical()); - styleMenuItem(ungroupItem, Color.LIGHT_GRAY); - - // Remove option with warning color - JMenuItem removeItem = new JMenuItem("Remove"); - removeItem.setIcon(ConditionConfigPanelUtil.getResourceIcon("delete.png", 16, 16)); - removeItem.addActionListener(e -> removeSelectedCondition()); - styleMenuItem(removeItem, new Color(220, 50, 50)); - - // Add all items with clear separators and sections - menu.add(negateItem); - menu.addSeparator(); - - // Group operations section with header - JLabel groupHeader = new JLabel("Group Operations"); - groupHeader.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - groupHeader.setBorder(BorderFactory.createEmptyBorder(3, 5, 3, 0)); - groupHeader.setForeground(Color.LIGHT_GRAY); - menu.add(groupHeader); - menu.add(groupAndItem); - menu.add(groupOrItem); - - // Conversion operations section - menu.addSeparator(); - JLabel convertHeader = new JLabel("Convert Operations"); - convertHeader.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - convertHeader.setBorder(BorderFactory.createEmptyBorder(3, 5, 3, 0)); - convertHeader.setForeground(Color.LIGHT_GRAY); - menu.add(convertHeader); - menu.add(convertToAndItem); - menu.add(convertToOrItem); - menu.add(ungroupItem); - - // Remove operation section - menu.addSeparator(); - menu.add(removeItem); - - // Use a custom PopupMenuListener to add visual cues for available operations - menu.addPopupMenuListener(new PopupMenuListener() { - @Override - public void popupMenuWillBecomeVisible(PopupMenuEvent e) { - DefaultMutableTreeNode[] selectedNodes = getSelectedConditionNodes(); - DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - - if (selectedNode == null) { - return; - } - - boolean isLogical = selectedNode != null && selectedNode.getUserObject() instanceof LogicalCondition; - boolean isAnd = isLogical && selectedNode.getUserObject() instanceof AndCondition; - boolean isPluginDefined = selectedNode != null && - selectedNode.getUserObject() instanceof Condition && - getConditionManger() != null && - getConditionManger() - .isPluginDefinedCondition((Condition)selectedNode.getUserObject()); - - // Check if editing is allowed for the current plugin state - boolean canEdit = isEditAllowedNoDialog(); - - // Enable/disable with visual indicators for context awareness - configureMenuItem(negateItem, selectedNode != null && !isLogical && !isPluginDefined && canEdit); - configureMenuItem(groupAndItem, selectedNodes.length >= 2 && !isPluginDefined && canEdit); - configureMenuItem(groupOrItem, selectedNodes.length >= 2 && !isPluginDefined && canEdit); - configureMenuItem(convertToAndItem, isLogical && !isAnd && !isPluginDefined && canEdit); - configureMenuItem(convertToOrItem, isLogical && isAnd && !isPluginDefined && canEdit); - configureMenuItem(ungroupItem, isLogical && selectedNode.getParent() != rootNode && !isPluginDefined && canEdit); - configureMenuItem(removeItem, selectedNode != null && !isPluginDefined && canEdit); - - // Set headers visible only if their sections have enabled items - boolean hasGroupOperations = groupAndItem.isEnabled() || groupOrItem.isEnabled(); - groupHeader.setVisible(hasGroupOperations); - - boolean hasConvertOperations = convertToAndItem.isEnabled() || - convertToOrItem.isEnabled() || - ungroupItem.isEnabled(); - convertHeader.setVisible(hasConvertOperations); - } - - @Override - public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {} - - @Override - public void popupMenuCanceled(PopupMenuEvent e) {} - }); - - return menu; - } - - /** - * Helper method to configure menu items with visual indicators - */ - private void configureMenuItem(JMenuItem item, boolean enabled) { - // Get the accent color that was stored during styling - Color accentColor = (Color) item.getClientProperty("accentColor"); - - // Set the enabled state - item.setEnabled(enabled); - - // Apply different visual styling based on availability - if (enabled) { - // For enabled items: dark background, accent-colored text and border - item.setBackground(new Color(45, 45, 45)); - item.setForeground(accentColor != null ? accentColor : Color.WHITE); - item.setOpaque(true); - item.setBorderPainted(true); - - // Add a subtle left border to indicate availability - item.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(0, 3, 0, 0, accentColor != null ? accentColor : Color.LIGHT_GRAY), - BorderFactory.createEmptyBorder(6, 8, 6, 8) - )); - - // Add bold font for available options - item.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - } else { - // For disabled items: transparent background, dimmed text - item.setBackground(new Color(35, 35, 35)); - item.setForeground(new Color(150, 150, 150, 160)); - item.setOpaque(true); - item.setBorderPainted(false); - - // Regular padding for disabled items - item.setBorder(BorderFactory.createEmptyBorder(6, 11, 6, 8)); - - // Normal font for disabled options - item.setFont(FontManager.getRunescapeSmallFont()); - } - } - - /** - * Gets an array of tree nodes representing the selected conditions - */ - private DefaultMutableTreeNode[] getSelectedConditionNodes() { - TreePath[] paths = conditionTree.getSelectionPaths(); - if (paths == null) { - return new DefaultMutableTreeNode[0]; - } - - List nodes = new ArrayList<>(); - for (TreePath path : paths) { - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - if (node != rootNode && node.getUserObject() instanceof Condition) { - nodes.add(node); - } - } - - return nodes.toArray(new DefaultMutableTreeNode[0]); - } - - /** - * Converts a logical group from one type to another (AND <-> OR) - */ - private void convertLogicalType(boolean toAnd) { - DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (selectedNode == null || !(selectedNode.getUserObject() instanceof LogicalCondition)) { - return; - } - - LogicalCondition logicalCondition = (LogicalCondition) selectedNode.getUserObject(); - - // Use the utility method for conversion - boolean success = ConditionConfigPanelUtil.convertLogicalType( - logicalCondition, - toAnd, - getConditionManger(), - this - ); - - if (success) { - // Update UI - updateTreeFromConditions(); - selectNodeForCondition(toAnd ? - getConditionManger().getUserLogicalCondition() : - getConditionManger().getUserLogicalCondition()); - notifyConditionUpdate(); - } -} - - /** - * Removes a logical group but keeps its conditions - */ - private void ungroupSelectedLogical() { - DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (selectedNode == null || !(selectedNode.getUserObject() instanceof LogicalCondition)) { - return; - } - - // Don't allow ungrouping the root logical - if (selectedNode.getParent() == rootNode) { - JOptionPane.showMessageDialog(this, - "Cannot ungroup the root logical condition.", - "Operation Not Allowed", - JOptionPane.WARNING_MESSAGE); - return; - } - - LogicalCondition logicalToUngroup = (LogicalCondition) selectedNode.getUserObject(); - - // Check if this is a plugin-defined logical group - if (selectScheduledPlugin != null && - selectScheduledPlugin.getStopConditionManager().isPluginDefinedCondition(logicalToUngroup)) { - - JOptionPane.showMessageDialog(this, - "Cannot ungroup plugin-defined condition groups. These conditions are protected.", - "Protected Conditions", - JOptionPane.WARNING_MESSAGE); - return; - } - DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) selectedNode.getParent(); - - // Only proceed if parent is a logical condition - if (!(parentNode.getUserObject() instanceof LogicalCondition)) { - return; - } - - LogicalCondition parentLogical = (LogicalCondition) parentNode.getUserObject(); - - // Find position in parent - int index = parentLogical.getConditions().indexOf(logicalToUngroup); - if (index < 0) { - return; - } - - // Remove the logical from its parent - parentLogical.getConditions().remove(index); - - // Add all of its conditions to the parent at the same position - int currentIndex = index; - for (Condition condition : new ArrayList<>(logicalToUngroup.getConditions())) { - parentLogical.addConditionAt(currentIndex++, condition); - } - - // Update UI - updateTreeFromConditions(); - notifyConditionUpdate(); - } - - /** - * Creates a new logical group from the selected conditions - */ - private void createLogicalGroup(boolean isAnd) { - DefaultMutableTreeNode[] selectedNodes = getSelectedConditionNodes(); - if (selectedNodes.length < 2) { - JOptionPane.showMessageDialog(this, - "Please select at least two conditions to group", - "Selection Required", - JOptionPane.INFORMATION_MESSAGE); - return; - } - - // Verify all nodes have the same parent - DefaultMutableTreeNode firstParent = (DefaultMutableTreeNode) selectedNodes[0].getParent(); - for (int i = 1; i < selectedNodes.length; i++) { - if (selectedNodes[i].getParent() != firstParent) { - JOptionPane.showMessageDialog(this, - "All conditions must have the same parent to group them", - "Invalid Selection", - JOptionPane.WARNING_MESSAGE); - return; - } - } - // Check for plugin-defined conditions - for (DefaultMutableTreeNode node : selectedNodes) { - if (node.getUserObject() instanceof Condition) { - Condition condition = (Condition) node.getUserObject(); - // Don't allow modifying plugin-defined conditions - if (selectScheduledPlugin != null && - getConditionManger().isPluginDefinedCondition(condition)) { - - JOptionPane.showMessageDialog(this, - "Cannot group plugin-defined conditions. These conditions are protected.", - "Protected Conditions", - JOptionPane.WARNING_MESSAGE); - return; - } - } - } - // Create new logical condition - LogicalCondition newLogical = isAnd ? new AndCondition() : new OrCondition(); - - // Determine parent logical - LogicalCondition parentLogical; - if (firstParent == rootNode) { - parentLogical = getConditionManger().getUserLogicalCondition(); - } else if (firstParent.getUserObject() instanceof LogicalCondition) { - parentLogical = (LogicalCondition) firstParent.getUserObject(); - } else { - JOptionPane.showMessageDialog(this, - "Cannot determine parent logical group", - "Operation Failed", - JOptionPane.WARNING_MESSAGE); - return; - } - - // Collect all selected conditions - List conditionsToGroup = new ArrayList<>(); - for (DefaultMutableTreeNode node : selectedNodes) { - if (node.getUserObject() instanceof Condition) { - conditionsToGroup.add((Condition) node.getUserObject()); - } - } - - // Remove conditions from parent - for (Condition condition : conditionsToGroup) { - parentLogical.getConditions().remove(condition); - } - - // Add conditions to new logical - for (Condition condition : conditionsToGroup) { - newLogical.addCondition(condition); - } - - // Add new logical to parent - parentLogical.addCondition(newLogical); - - // Update UI - updateTreeFromConditions(); - selectNodeForCondition(newLogical); - notifyConditionUpdate(); - } - - /** - * Updates the condition editor panel when a condition is selected in the tree - */ - private void updateConditionPanelForSelectedNode() { - DefaultMutableTreeNode node = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (node == null || !(node.getUserObject() instanceof Condition)) { - // Reset edit button text when no valid condition is selected - editButton.setText("Add"); - return; - } - - Condition condition = (Condition) node.getUserObject(); - if (condition instanceof LogicalCondition || condition instanceof NotCondition) { - // Reset edit button text for logical conditions since they can't be edited - editButton.setText("Add"); - return; // Skip logical conditions as they don't have direct UI editors - } - - try { - updatingSelectionFlag[0] = true; - - // Determine condition type and setup appropriate UI - if (condition instanceof VarbitCondition) { - conditionCategoryComboBox.setSelectedItem("Varbit"); - updateConditionTypes("Varbit"); - // Select a valid Varbit UI type present in the model - boolean varbitTypeSet = false; - for (int i = 0; i < conditionTypeComboBox.getItemCount(); i++) { - String item = conditionTypeComboBox.getItemAt(i); - if ("Collection Log - Bosses".equals(item) || "Collection Log - Minigames".equals(item)) { - conditionTypeComboBox.setSelectedIndex(i); - varbitTypeSet = true; - break; - } - } - if (!varbitTypeSet && conditionTypeComboBox.getItemCount() > 0) { - conditionTypeComboBox.setSelectedIndex(0); - } - - updateConfigPanel(); - JPanel localConfigPanel = (JPanel) configPanel.getClientProperty("localConditionPanel"); - if (localConfigPanel != null) { - VarbitConditionPanelUtil.setupVarbitCondition(localConfigPanel, (VarbitCondition) condition); - } - } - else if (condition instanceof LocationCondition) { - conditionCategoryComboBox.setSelectedItem("Location"); - updateConditionTypes("Location"); - - if (condition instanceof PositionCondition) { - conditionTypeComboBox.setSelectedItem("Position"); - } else if (condition instanceof AreaCondition) { - conditionTypeComboBox.setSelectedItem("Area"); - } else if (condition instanceof RegionCondition) { - conditionTypeComboBox.setSelectedItem("Region"); - } - - updateConfigPanel(); - JPanel localConfigPanel = (JPanel) configPanel.getClientProperty("localConditionPanel"); - if (localConfigPanel != null) { - LocationConditionUtil.setupLocationCondition(localConfigPanel, condition); - } - } - else if (condition instanceof SkillLevelCondition || condition instanceof SkillXpCondition) { - conditionCategoryComboBox.setSelectedItem("Skill"); - updateConditionTypes("Skill"); - - if (condition instanceof SkillLevelCondition) { - if (stopConditionPanel) { - conditionTypeComboBox.setSelectedItem("Skill Level"); - } else { - conditionTypeComboBox.setSelectedItem("Skill Level Required"); - } - } else if (condition instanceof SkillXpCondition) { - conditionTypeComboBox.setSelectedItem("Skill XP Goal"); - } - - updateConfigPanel(); - JPanel localConfigPanel = (JPanel) configPanel.getClientProperty("localConditionPanel"); - if (localConfigPanel != null) { - SkillConditionPanelUtil.setupSkillCondition(localConfigPanel, condition); - } - } - else if (condition instanceof TimeCondition) { - conditionCategoryComboBox.setSelectedItem("Time"); - updateConditionTypes("Time"); - - if (condition instanceof IntervalCondition) { - conditionTypeComboBox.setSelectedItem(stopConditionPanel ? "Time Duration" : "Time Interval"); - } else if (condition instanceof TimeWindowCondition) { - TimeWindowCondition windowCondition = (TimeWindowCondition) condition; - conditionTypeComboBox.setSelectedItem("Time Window"); - } else if (condition instanceof NotCondition && - ((NotCondition) condition).getCondition() instanceof TimeWindowCondition) { - // This is a negated time window condition - conditionTypeComboBox.setSelectedItem(stopConditionPanel ? "Not In Time Window" : "Outside Time Window"); - } else if (condition instanceof SingleTriggerTimeCondition) { - conditionTypeComboBox.setSelectedItem("Specific Time"); - } else if (condition instanceof DayOfWeekCondition) { - conditionTypeComboBox.setSelectedItem("Day of Week"); - } - - updateConfigPanel(); - JPanel localConfigPanel = (JPanel) configPanel.getClientProperty("localConditionPanel"); - if (localConfigPanel != null) { - TimeConditionPanelUtil.setupTimeCondition(localConfigPanel, condition); - } - } - else if (condition instanceof InventoryItemCountCondition || - condition instanceof BankItemCountCondition || - condition instanceof LootItemCondition || - condition instanceof ProcessItemCondition || - condition instanceof GatheredResourceCondition - || condition instanceof AndCondition|| condition instanceof OrCondition) { - Condition baseResourceCondition = condition; - if (condition instanceof AndCondition || condition instanceof OrCondition) { - //check if all conditions are resource conditions, and from the same type - baseResourceCondition = (Condition) ((LogicalCondition)condition).getConditions().get(0); - for (Condition c : ((LogicalCondition) condition).getConditions()) { - //check if c is of the same typ as first condition - if (!c.getClass().equals(baseResourceCondition.getClass())) { - //not all from the same type ? - return; - } - } - } - conditionCategoryComboBox.setSelectedItem("Resource"); - updateConditionTypes("Resource"); - - if (baseResourceCondition instanceof InventoryItemCountCondition) { - conditionTypeComboBox.setSelectedItem(stopConditionPanel ? "Item Collection" : "Item Required"); - } else if (baseResourceCondition instanceof ProcessItemCondition) { - conditionTypeComboBox.setSelectedItem("Process Items"); - } else if (baseResourceCondition instanceof GatheredResourceCondition) { - conditionTypeComboBox.setSelectedItem("Gather Resources"); - } else if (baseResourceCondition instanceof LootItemCondition) { - conditionTypeComboBox.setSelectedItem("Loot Items"); - } - - updateConfigPanel(); - JPanel localConfigPanel = (JPanel) configPanel.getClientProperty("localConditionPanel"); - if (localConfigPanel != null) { - ResourceConditionPanelUtil.setupResourceCondition(localConfigPanel, condition); - } - } - - // Update edit button state - editButton.setText("Apply Changes"); - } finally { - updatingSelectionFlag[0] = false; - } - } - /** - * Styles a menu item with consistent fonts, borders and hover effects - */ - private void styleMenuItem(JMenuItem item, Color accentColor) { - item.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - item.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8)); - item.setBackground(new Color(35, 35, 35)); - - // Store the accent color as a client property for use in configureMenuItem - item.putClientProperty("accentColor", accentColor); - - // Add hover effect using MouseListener - item.addMouseListener(new MouseAdapter() { - @Override - public void mouseEntered(MouseEvent e) { - if (item.isEnabled()) { - // Highlight with slightly lighter background on hover - item.setBackground(new Color(55, 55, 55)); - - // Make text brighter on hover - Color currentColor = item.getForeground(); - item.putClientProperty("originalForeground", currentColor); - - // Create a brighter version of the accent color - int r = Math.min(255, (int)(currentColor.getRed() * 1.2)); - int g = Math.min(255, (int)(currentColor.getGreen() * 1.2)); - int b = Math.min(255, (int)(currentColor.getBlue() * 1.2)); - item.setForeground(new Color(r, g, b)); - - // Add a stronger border on hover - if (accentColor != null) { - item.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(0, 4, 0, 0, accentColor), - BorderFactory.createEmptyBorder(6, 7, 6, 8) - )); - } - } - } - - @Override - public void mouseExited(MouseEvent e) { - if (item.isEnabled()) { - // Restore original background - item.setBackground(new Color(45, 45, 45)); - - // Restore original foreground - Color originalColor = (Color)item.getClientProperty("originalForeground"); - if (originalColor != null) { - item.setForeground(originalColor); - } - - // Restore original border - if (accentColor != null) { - item.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(0, 3, 0, 0, accentColor), - BorderFactory.createEmptyBorder(6, 8, 6, 8) - )); - } - } - } - }); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/callback/ConditionUpdateCallback.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/callback/ConditionUpdateCallback.java deleted file mode 100644 index 186eef3b41b..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/callback/ConditionUpdateCallback.java +++ /dev/null @@ -1,53 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.ui.callback; - -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LogicalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; - -import java.io.File; - -/** - * Callback interface for condition updates in the scheduler system. - *

- * This interface defines methods that are called when conditions are updated or reset - * in the UI. Implementing classes can respond to these events by saving the updated - * conditions to the appropriate location (config or file). - *

- * This approach provides a flexible way for different components of the system to be - * notified about condition changes without needing to know the details of how and - * where conditions are saved. - */ -public interface ConditionUpdateCallback { - - /** - * Called when conditions are updated in the UI. - * This version saves to the default configuration. - * - * @param logicalCondition The updated logical condition structure - * @param plugin The plugin entry whose conditions are being updated - * @param isStopCondition True if these are stop conditions, false for start conditions - */ - void onConditionsUpdated(LogicalCondition logicalCondition, - PluginScheduleEntry plugin, - boolean isStopCondition); - - /** - * Called when conditions are updated in the UI with a specific file destination. - * - * @param logicalCondition The updated logical condition structure - * @param plugin The plugin entry whose conditions are being updated - * @param isStopCondition True if these are stop conditions, false for start conditions - * @param saveFile The file to save the conditions to, or null to use default config - */ - void onConditionsUpdated(LogicalCondition logicalCondition, - PluginScheduleEntry plugin, - boolean isStopCondition, - File saveFile); - - /** - * Called when conditions are reset in the UI. - * - * @param plugin The plugin entry whose conditions are being reset - * @param isStopCondition True if these are stop conditions, false for start conditions - */ - void onConditionsReset(PluginScheduleEntry plugin, boolean isStopCondition); -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/renderer/ConditionTreeCellRenderer.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/renderer/ConditionTreeCellRenderer.java deleted file mode 100644 index 39a0feb5333..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/renderer/ConditionTreeCellRenderer.java +++ /dev/null @@ -1,290 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.ui.renderer; - -import javax.swing.ImageIcon; -import javax.swing.JTree; -import javax.swing.UIManager; -import javax.swing.tree.DefaultMutableTreeNode; -import javax.swing.tree.DefaultTreeCellRenderer; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionManager; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.AreaCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.PositionCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.RegionCondition; -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.condition.logical.NotCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.OrCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.LootItemCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.SkillCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.DayOfWeekCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ui.util.ConditionConfigPanelUtil; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; - -import java.awt.Component; -import java.awt.Color; -import java.awt.Font; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; - -import javax.swing.Icon; - -// Condition tree cell renderer -public class ConditionTreeCellRenderer extends DefaultTreeCellRenderer { - - private static final Color PLUGIN_CONDITION_COLOR = new Color(0, 128, 255); // Blue for plugin conditions - private static final Color USER_CONDITION_COLOR = Color.WHITE; // White for user conditions - private static final Color SATISFIED_COLOR = new Color(0, 180, 0); // Bright green for satisfied conditions - private static final Color NOT_SATISFIED_COLOR = new Color(220, 60, 60); // Bright red for unsatisfied conditions - private static final Color RELEVANT_CONDITION_COLOR = new Color(255, 215, 0); // Gold for relevant conditions - private static final Color INACTIVE_CONDITION_COLOR = new Color(150, 150, 150); // Gray for inactive conditions - - private final ConditionManager conditionManager; - private final boolean isStopConditionRenderer; - private boolean isActive = true; - - public ConditionTreeCellRenderer(ConditionManager conditionManager, boolean isStopConditionRenderer) { - this.conditionManager = conditionManager; - this.isStopConditionRenderer = isStopConditionRenderer; - } - public void setIsActive(boolean isActive) { - this.isActive = isActive; - } - - @Override - public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, - boolean leaf, int row, boolean hasFocus) { - super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); - // Check if node is null first - if (value == null) { - setText("null"); - return this; - } - - // Safely cast to DefaultMutableTreeNode - if (!(value instanceof DefaultMutableTreeNode)) { - return this; - } - DefaultMutableTreeNode node = (DefaultMutableTreeNode) value; - Object userObject = node.getUserObject(); - - // Default styling - setFont(getFont().deriveFont(Font.PLAIN)); - - - - // Determine relevance: - // - For start conditions: relevant when plugin is enabled but not started - // - For stop conditions: relevant when plugin is running - boolean conditionsAreRelevant = isStopConditionRenderer ? isActive : !isActive; - - - if (userObject instanceof LogicalCondition) { - LogicalCondition logicalCondition = (LogicalCondition) userObject; - - // Show condition counts and progress percentage - int total = logicalCondition.getTotalConditionCount(); - int met = logicalCondition.getMetConditionCount(); - double progress = logicalCondition.getProgressPercentage(); - - // Check if this is a plugin-defined logical condition - boolean isPluginDefined = conditionManager != null && - conditionManager.isPluginDefinedCondition(logicalCondition); - - // Determine icon based on logical type - if (logicalCondition instanceof AndCondition) { - setIcon(getConditionTypeIcon(logicalCondition)); - setFont(getFont().deriveFont(isPluginDefined ? Font.BOLD | Font.ITALIC : Font.BOLD)); - } else if (logicalCondition instanceof OrCondition) { - setIcon(getConditionTypeIcon(logicalCondition)); - setFont(getFont().deriveFont(isPluginDefined ? Font.BOLD | Font.ITALIC : Font.BOLD)); - } else { - // Use appropriate icon based on condition type - setIcon(getConditionTypeIcon(logicalCondition)); - } - - // Color based on condition status and relevance - if (!conditionsAreRelevant) { - // If conditions aren't relevant, show in gray - setForeground(INACTIVE_CONDITION_COLOR); - } else if (logicalCondition.isSatisfied()) { - setForeground(SATISFIED_COLOR); // Green for satisfied conditions - } else { - setForeground(NOT_SATISFIED_COLOR); // Red for unsatisfied conditions - } - - // Show progress info with more detailed formatting - String text = logicalCondition.getDescription(); - if (total > 0) { - text += String.format(" [%d/%d met, %.0f%%]", met, total, progress); - } - - if (isPluginDefined) { - text = "📌 " + text; - } - - // Add a visual indicator for relevance - if (conditionsAreRelevant) { - text = "⚡ " + text; - } - - // Handle newlines in text by replacing them with spaces for tree display - text = text.replace('\n', ' ').replace('\r', ' '); - // Collapse multiple spaces into single spaces - text = text.replaceAll("\\s+", " ").trim(); - - setText(text); - - // For tooltips, use the new HTML formatting - setToolTipText(logicalCondition.getHtmlDescription(200)); - } else if (userObject instanceof NotCondition) { - NotCondition notCondition = (NotCondition) userObject; - setIcon(ConditionConfigPanelUtil.getResourceIcon("not-equal.png")); - - // Adjust color based on relevance - if (!conditionsAreRelevant) { - setForeground(INACTIVE_CONDITION_COLOR); - } else { - setForeground(new Color(210, 40, 40)); // Red for NOT - } - - String text = notCondition.getDescription(); - if (conditionsAreRelevant) { - text = "⚡ " + text; - } - - // Handle newlines in text by replacing them with spaces for tree display - text = text.replace('\n', ' ').replace('\r', ' '); - // Collapse multiple spaces into single spaces - text = text.replaceAll("\\s+", " ").trim(); - - setText(text); - } else if (userObject instanceof Condition) { - Condition condition = (Condition) userObject; - - // Show progress for the condition - String text = condition.getDescription(); - double progress = condition.getProgressPercentage(); - - if (progress > 0 && progress < 100) { - text += String.format(" (%.0f%%)", progress); - } - - // Use appropriate icon based on condition type - setIcon(getConditionTypeIcon(condition)); - - // Color based on condition status and relevance - if (!conditionsAreRelevant) { - // If conditions aren't relevant, show in gray - setForeground(INACTIVE_CONDITION_COLOR); - } else if (condition.isSatisfied()) { - setForeground(SATISFIED_COLOR); // Green for satisfied conditions - } else { - setForeground(NOT_SATISFIED_COLOR); // Red for unsatisfied conditions - } - - // Visual indicator for plugin-defined conditions - if (conditionManager != null && conditionManager.isPluginDefinedCondition(condition)) { - setFont(getFont().deriveFont(Font.ITALIC)); - text = "📌 " + text; - } - - // Add a visual indicator for relevance - if (conditionsAreRelevant) { - text = "⚡ " + text; - } - - // Handle newlines in text by replacing them with spaces for tree display - text = text.replace('\n', ' ').replace('\r', ' '); - // Collapse multiple spaces into single spaces - text = text.replaceAll("\\s+", " ").trim(); - - setText(text); - - // Enhanced tooltip with status information - StringBuilder tooltip = new StringBuilder(); - tooltip.append("").append(condition.getDescription()).append("
"); - tooltip.append(condition.isSatisfied() ? - "Satisfied" : - "Not satisfied"); - - if (condition instanceof TimeCondition) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - ZonedDateTime triggerTime = ((TimeCondition)condition).getCurrentTriggerTime().orElse(null); - if (triggerTime != null) { - tooltip.append("
Trigger time: ").append(triggerTime.format(formatter)); - } - } - - tooltip.append(""); - setToolTipText(tooltip.toString()); - } else if (userObject instanceof String) { - // Section headers (Plugin/User Conditions) - setFont(getFont().deriveFont(Font.BOLD)); - setIcon(null); - setForeground(Color.YELLOW); - } - - // If selected, keep our custom foreground color but change background - if (sel) { - // Keep the foreground color we've set above, but use a selection background that works with it - setBackground(new Color(60, 60, 60)); // Dark gray selection background - setBorderSelectionColor(new Color(100, 100, 100)); // Darker border for selection - } - - return this; - } - - /** - * Gets an appropriate icon for the condition type - */ - private Icon getConditionTypeIcon(Condition condition) { - if (condition instanceof SkillCondition) { - SkillCondition skillCondition = (SkillCondition) condition; - Icon skillIcon = skillCondition.getSkillIcon(); - - if (skillIcon != null) { - // Ensure icon is properly sized to 24x24 - if (skillIcon instanceof ImageIcon) { - ImageIcon imageIcon = (ImageIcon) skillIcon; - if (imageIcon.getIconWidth() != 24 || imageIcon.getIconHeight() != 24) { - // Rescale if not already the right size - return ConditionConfigPanelUtil.getResourceIcon("skill_icon.png"); - } - } - return skillIcon; - } - return ConditionConfigPanelUtil.getResourceIcon("skill_icon.png"); - } else if (condition instanceof TimeCondition) { - if (condition instanceof IntervalCondition) { - return ConditionConfigPanelUtil.getResourceIcon("clock.png"); - }else if (condition instanceof TimeWindowCondition) { - return ConditionConfigPanelUtil.getResourceIcon("calendar-icon.png"); - } - return ConditionConfigPanelUtil.getResourceIcon("clock.png"); - } else if (condition instanceof TimeWindowCondition) { - return ConditionConfigPanelUtil.getResourceIcon("calendar_icon.png"); - } else if (condition instanceof DayOfWeekCondition) { - return ConditionConfigPanelUtil.getResourceIcon("day_icon.png"); - } else if (condition instanceof AndCondition) { - return ConditionConfigPanelUtil.getResourceIcon("logic-gate-and.png"); - } else if (condition instanceof OrCondition) { - return ConditionConfigPanelUtil.getResourceIcon("logic-gate-or.png"); - }else if (condition instanceof LootItemCondition) { - return ConditionConfigPanelUtil.getResourceIcon("loot_icon.png"); - }else if (condition instanceof AreaCondition) { - return ConditionConfigPanelUtil.getResourceIcon("area_map.png"); - }else if (condition instanceof RegionCondition) { - return ConditionConfigPanelUtil.getResourceIcon("region.png"); - }else if (condition instanceof PositionCondition) { - return ConditionConfigPanelUtil.getResourceIcon("position.png"); - }else if (condition instanceof LockCondition) { - return ConditionConfigPanelUtil.getResourceIcon("padlock.png"); - } - - return UIManager.getIcon("Tree.leafIcon"); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/util/ConditionConfigPanelUtil.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/util/ConditionConfigPanelUtil.java deleted file mode 100644 index eed7183897f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/util/ConditionConfigPanelUtil.java +++ /dev/null @@ -1,868 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.ui.util; - -import java.awt.Color; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.Graphics2D; -import java.awt.GridBagConstraints; -import java.awt.Image; -import java.awt.Insets; -import java.awt.RenderingHints; -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.net.URL; - -import javax.imageio.ImageIO; -import javax.swing.BorderFactory; -import javax.swing.Box; -import javax.swing.BoxLayout; -import javax.swing.Icon; -import javax.swing.ImageIcon; -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JComponent; -import javax.swing.JDialog; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JSeparator; -import javax.swing.SwingConstants; -import javax.swing.UIManager; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; - -import java.awt.BorderLayout; -import java.awt.FlowLayout; -import javax.swing.JTree; -import javax.swing.tree.DefaultMutableTreeNode; -import javax.swing.tree.DefaultTreeModel; -import javax.swing.tree.TreeSelectionModel; - -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ui.renderer.ConditionTreeCellRenderer; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionManager; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionManager; -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.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -/** - * Utility class for common methods used in the ConditionConfigPanel - */ -@Slf4j -public class ConditionConfigPanelUtil { - - /** - * Creates a styled button with consistent appearance - * - * @param text The button text - * @param color The background color - * @return A styled JButton - */ - public static JButton createButton(String text, Color color) { - JButton button = new JButton(text); - button.setFont(FontManager.getRunescapeSmallFont()); - button.setBackground(color); - button.setForeground(Color.WHITE); - button.setFocusPainted(false); - button.setBorderPainted(false); - button.setFont(button.getFont().deriveFont(Font.BOLD)); - button.setMargin(new Insets(5, 10, 5, 10)); - button.setBorder(new EmptyBorder(5, 10, 5, 10)); - return button; - } - - /** - * Creates a button with neutral appearance for common actions - * - * @param text The button text - * @return A styled JButton with neutral appearance - */ - public static JButton createNeutralButton(String text) { - return createButton(text, new Color(60, 60, 60)); - } - - /** - * Creates a button with positive/success appearance - * - * @param text The button text - * @return A styled JButton with positive appearance - */ - public static JButton createPositiveButton(String text) { - return createButton(text, new Color(0, 160, 0)); - } - - /** - * Creates a button with negative/danger appearance - * - * @param text The button text - * @return A styled JButton with negative appearance - */ - public static JButton createNegativeButton(String text) { - return createButton(text, new Color(180, 60, 60)); - } - - /** - * Creates a standard titled section panel for consistent visual style - * - * @param title The title for the panel - * @return A JPanel with titled border - */ - public static JPanel createTitledPanel(String title) { - JPanel panel = new JPanel(); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - panel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - title, - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - return panel; - } - - /** - * Creates a scrollable panel with consistent styling - * - * @param component The component to make scrollable - * @return A JScrollPane containing the component - */ - public static JScrollPane createScrollPane(Component component) { - JScrollPane scrollPane = new JScrollPane(component); - scrollPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - scrollPane.setBorder(new EmptyBorder(5, 5, 5, 5)); - scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); - scrollPane.getVerticalScrollBar().setUnitIncrement(16); // For smoother scrolling - return scrollPane; - } - - /** - * Styles a JComboBox for consistent appearance - * - * @param comboBox The JComboBox to style - */ - public static void styleComboBox(JComboBox comboBox) { - comboBox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - comboBox.setForeground(Color.WHITE); - comboBox.setFocusable(false); - comboBox.setFont(FontManager.getRunescapeSmallFont()); - } - - /** - * Creates a styled label with consistent appearance - * - * @param text The label text - * @return A styled JLabel - */ - public static JLabel createLabel(String text) { - JLabel label = new JLabel(text); - label.setForeground(Color.WHITE); - label.setFont(FontManager.getRunescapeSmallFont()); - return label; - } - - /** - * Creates a styled header label with larger, bold font - * - * @param text The header text - * @return A styled header JLabel - */ - public static JLabel createHeaderLabel(String text) { - JLabel label = new JLabel(text); - label.setForeground(Color.WHITE); - label.setFont(FontManager.getRunescapeBoldFont()); - return label; - } - - /** - * Adds a labeled separator to a panel at the given grid position - * - * @param panel The panel to add the separator to - * @param gbc The GridBagConstraints to use - * @param text The text for the separator (can be null for plain separator) - */ - public static void addSeparator(JPanel panel, GridBagConstraints gbc, String text) { - int originalGridy = gbc.gridy; - - if (text != null && !text.isEmpty()) { - JLabel label = createHeaderLabel(text); - gbc.anchor = GridBagConstraints.WEST; - gbc.fill = GridBagConstraints.NONE; - panel.add(label, gbc); - gbc.gridy++; - } - - JSeparator separator = new JSeparator(SwingConstants.HORIZONTAL); - separator.setForeground(ColorScheme.MEDIUM_GRAY_COLOR); - separator.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.anchor = GridBagConstraints.CENTER; - gbc.insets = new Insets(text != null ? 2 : 10, 5, 10, 5); - panel.add(separator, gbc); - - // Restore original insets - gbc.insets = new Insets(5, 5, 5, 5); - gbc.gridy++; - } - - /** - * Sets consistent padding for a component - * - * @param component The component to pad - * @param padding The padding size in pixels - */ - public static void setPadding(JComponent component, int padding) { - component.setBorder(BorderFactory.createEmptyBorder(padding, padding, padding, padding)); - } - - /** - * Sets consistent maximum width for a component while keeping its preferred height - * - * @param component The component to constrain - * @param maxWidth The maximum width in pixels - */ - public static void setMaxWidth(JComponent component, int maxWidth) { - Dimension prefSize = component.getPreferredSize(); - component.setMaximumSize(new Dimension(maxWidth, prefSize.height)); - } - - /** - * Loads and scales an icon from resources - * - * @param name The resource name/path - * @param width The desired width - * @param height The desired height - * @return An Icon, or the default leaf icon if loading fails - */ - public static Icon getResourceIcon(String name, int width, int height) { - try { - URL resourceUrl = ConditionConfigPanelUtil.class.getResource( - "/net/runelite/client/plugins/microbot/pluginscheduler/" + name); - - if (resourceUrl == null) { - log.warn("Resource not found: /net/runelite/client/plugins/microbot/pluginscheduler/" + name); - return UIManager.getIcon("Tree.leafIcon"); - } - - BufferedImage originalImage = ImageIO.read(resourceUrl); - - if (originalImage == null) { - log.warn("Could not load resource: " + name); - return UIManager.getIcon("Tree.leafIcon"); - } - - if (originalImage.getWidth() == width && originalImage.getHeight() == height) { - return new ImageIcon(originalImage); - } - - // Scale the image to desired size - BufferedImage scaledImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2d = scaledImage.createGraphics(); - - // Use high quality scaling - g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - g2d.drawImage(originalImage, 0, 0, width, height, null); - g2d.dispose(); - - return new ImageIcon(scaledImage); - } catch (IOException e) { - log.warn("Failed to load icon: " + name, e); - // Fallback to default icon - return UIManager.getIcon("Tree.leafIcon"); - } - } - - /** - * Convenience method for default 24x24 icons - * - * @param name The resource name - * @return An Icon sized 24x24 - */ - public static Icon getResourceIcon(String name) { - return getResourceIcon(name, 24, 24); - } - - /** - * Shows a confirmation dialog with consistent styling - * - * @param parentComponent The parent component for the dialog - * @param message The message to display - * @param title The dialog title - * @return true if user confirms, false otherwise - */ - public static boolean showConfirmDialog(Component parentComponent, String message, String title) { - // Create custom button text - Object[] options = {"Yes", "No"}; - - // Create styled option pane - JOptionPane optionPane = new JOptionPane( - message, - JOptionPane.QUESTION_MESSAGE, - JOptionPane.YES_NO_OPTION, - null, // No custom icon - options, - options[1] // Default to "No" for safety - ); - - // Create and configure dialog - JDialog dialog = optionPane.createDialog(parentComponent, title); - dialog.setVisible(true); - - // Get the result (returns the selected value or null if closed) - Object selectedValue = optionPane.getValue(); - - // Check if user selected "Yes" - return selectedValue != null && selectedValue.equals(options[0]); - } - - /** - * Shows an error dialog with consistent styling - * - * @param parentComponent The parent component for the dialog - * @param message The error message to display - * @param title The dialog title - */ - public static void showErrorDialog(Component parentComponent, String message, String title) { - JOptionPane.showMessageDialog( - parentComponent, - message, - title, - JOptionPane.ERROR_MESSAGE - ); - } - - /** - * Shows a warning dialog with consistent styling - * - * @param parentComponent The parent component for the dialog - * @param message The warning message to display - * @param title The dialog title - */ - public static void showWarningDialog(Component parentComponent, String message, String title) { - JOptionPane.showMessageDialog( - parentComponent, - message, - title, - JOptionPane.WARNING_MESSAGE - ); - } - - /** - * Shows an information dialog with consistent styling - * - * @param parentComponent The parent component for the dialog - * @param message The information message to display - * @param title The dialog title - */ - public static void showInfoDialog(Component parentComponent, String message, String title) { - JOptionPane.showMessageDialog( - parentComponent, - message, - title, - JOptionPane.INFORMATION_MESSAGE - ); - } - - /** - * Creates a logical operations toolbar with buttons for manipulating logical conditions - * - * @param createAndAction The action to perform when creating an AND group - * @param createOrAction The action to perform when creating an OR group - * @param negateAction The action to perform when negating a condition - * @param convertToAndAction The action to perform when converting to AND - * @param convertToOrAction The action to perform when converting to OR - * @param ungroupAction The action to perform when ungrouping a logical condition - * @return A panel containing the logical operations buttons - */ - public static JPanel createLogicalOperationsToolbar( - Runnable createAndAction, - Runnable createOrAction, - Runnable negateAction, - Runnable convertToAndAction, - Runnable convertToOrAction, - Runnable ungroupAction) { - - JPanel logicalOpPanel = new JPanel(); - logicalOpPanel.setLayout(new BoxLayout(logicalOpPanel, BoxLayout.X_AXIS)); - logicalOpPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); - logicalOpPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Group operations section - JButton createAndButton = createButton("Group as AND", ColorScheme.BRAND_ORANGE); - createAndButton.setToolTipText("Group selected conditions with AND logic"); - createAndButton.addActionListener(e -> createAndAction.run()); - - JButton createOrButton = createButton("Group as OR", new Color(25, 130, 196)); - createOrButton.setToolTipText("Group selected conditions with OR logic"); - createOrButton.addActionListener(e -> createOrAction.run()); - - // Negation button - JButton negateButton = createButton("Negate", new Color(220, 50, 50)); - negateButton.setToolTipText("Negate the selected condition (toggle NOT)"); - negateButton.addActionListener(e -> negateAction.run()); - - // Convert operation buttons - JButton convertToAndButton = createButton("Convert to AND", ColorScheme.BRAND_ORANGE); - convertToAndButton.setToolTipText("Convert selected logical group to AND type"); - convertToAndButton.addActionListener(e -> convertToAndAction.run()); - - JButton convertToOrButton = createButton("Convert to OR", new Color(25, 130, 196)); - convertToOrButton.setToolTipText("Convert selected logical group to OR type"); - convertToOrButton.addActionListener(e -> convertToOrAction.run()); - - // Ungroup button - JButton ungroupButton = createButton("Ungroup", ColorScheme.LIGHT_GRAY_COLOR); - ungroupButton.setToolTipText("Remove the logical group but keep its conditions"); - ungroupButton.addActionListener(e -> ungroupAction.run()); - - // Add buttons to panel with separators - logicalOpPanel.add(createAndButton); - logicalOpPanel.add(Box.createHorizontalStrut(5)); - logicalOpPanel.add(createOrButton); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(new JSeparator(SwingConstants.VERTICAL)); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(negateButton); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(new JSeparator(SwingConstants.VERTICAL)); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(convertToAndButton); - logicalOpPanel.add(Box.createHorizontalStrut(5)); - logicalOpPanel.add(convertToOrButton); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(new JSeparator(SwingConstants.VERTICAL)); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(ungroupButton); - - return logicalOpPanel; - } - - /** - * Creates a logical condition operations panel that handles button state management - * - * @param panel The panel to add the toolbar to - * @param negateButtonRef Reference to store the negate button - * @param convertToAndButtonRef Reference to store the convert to AND button - * @param convertToOrButtonRef Reference to store the convert to OR button - * @param ungroupButtonRef Reference to store the ungroup button - * @param createAndAction The action to perform when creating an AND group - * @param createOrAction The action to perform when creating an OR group - * @param negateAction The action to perform when negating a condition - * @param convertToAndAction The action to perform when converting to AND - * @param convertToOrAction The action to perform when converting to OR - * @param ungroupAction The action to perform when ungrouping a logical condition - */ - public static void addLogicalOperationsToolbar( - JPanel panel, - JButton[] negateButtonRef, - JButton[] convertToAndButtonRef, - JButton[] convertToOrButtonRef, - JButton[] ungroupButtonRef, - Runnable createAndAction, - Runnable createOrAction, - Runnable negateAction, - Runnable convertToAndAction, - Runnable convertToOrAction, - Runnable ungroupAction) { - - JPanel logicalOpPanel = new JPanel(); - logicalOpPanel.setLayout(new BoxLayout(logicalOpPanel, BoxLayout.X_AXIS)); - logicalOpPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); - logicalOpPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Group operations section - JButton createAndButton = createButton("Group as AND", ColorScheme.BRAND_ORANGE); - createAndButton.setToolTipText("Group selected conditions with AND logic"); - createAndButton.addActionListener(e -> createAndAction.run()); - - JButton createOrButton = createButton("Group as OR", new Color(25, 130, 196)); - createOrButton.setToolTipText("Group selected conditions with OR logic"); - createOrButton.addActionListener(e -> createOrAction.run()); - - // Negation button - JButton negateButton = createButton("Negate", new Color(220, 50, 50)); - negateButton.setToolTipText("Negate the selected condition (toggle NOT)"); - negateButton.addActionListener(e -> negateAction.run()); - if (negateButtonRef != null && negateButtonRef.length > 0) { - negateButtonRef[0] = negateButton; - } - - // Convert operation buttons - JButton convertToAndButton = createButton("Convert to AND", ColorScheme.BRAND_ORANGE); - convertToAndButton.setToolTipText("Convert selected logical group to AND type"); - convertToAndButton.addActionListener(e -> convertToAndAction.run()); - if (convertToAndButtonRef != null && convertToAndButtonRef.length > 0) { - convertToAndButtonRef[0] = convertToAndButton; - } - - JButton convertToOrButton = createButton("Convert to OR", new Color(25, 130, 196)); - convertToOrButton.setToolTipText("Convert selected logical group to OR type"); - convertToOrButton.addActionListener(e -> convertToOrAction.run()); - if (convertToOrButtonRef != null && convertToOrButtonRef.length > 0) { - convertToOrButtonRef[0] = convertToOrButton; - } - - // Ungroup button - JButton ungroupButton = createButton("Ungroup", ColorScheme.LIGHT_GRAY_COLOR); - ungroupButton.setToolTipText("Remove the logical group but keep its conditions"); - ungroupButton.addActionListener(e -> ungroupAction.run()); - if (ungroupButtonRef != null && ungroupButtonRef.length > 0) { - ungroupButtonRef[0] = ungroupButton; - } - - // Add buttons to panel with separators - logicalOpPanel.add(createAndButton); - logicalOpPanel.add(Box.createHorizontalStrut(5)); - logicalOpPanel.add(createOrButton); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(new JSeparator(SwingConstants.VERTICAL)); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(negateButton); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(new JSeparator(SwingConstants.VERTICAL)); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(convertToAndButton); - logicalOpPanel.add(Box.createHorizontalStrut(5)); - logicalOpPanel.add(convertToOrButton); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(new JSeparator(SwingConstants.VERTICAL)); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(ungroupButton); - - // Initialize button states - negateButton.setEnabled(false); - convertToAndButton.setEnabled(false); - convertToOrButton.setEnabled(false); - ungroupButton.setEnabled(false); - - panel.add(logicalOpPanel, "North"); - } - - /** - * Creates a condition manipulation panel with add, edit, and remove buttons - * - * @param addAction The action to perform when adding a condition - * @param editAction The action to perform when editing a condition - * @param removeAction The action to perform when removing a condition - * @return A panel containing the condition manipulation buttons - */ - public static JPanel createConditionManipulationPanel( - Runnable addAction, - Runnable editAction, - Runnable removeAction) { - - JPanel buttonPanel = new JPanel(); - buttonPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - buttonPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); - - JButton addButton = createButton("Add", ColorScheme.PROGRESS_COMPLETE_COLOR); - addButton.addActionListener(e -> addAction.run()); - buttonPanel.add(addButton); - - JButton editButton = createButton("Edit", ColorScheme.BRAND_ORANGE); - editButton.addActionListener(e -> editAction.run()); - buttonPanel.add(editButton); - - JButton removeButton = createButton("Remove", ColorScheme.PROGRESS_ERROR_COLOR); - removeButton.addActionListener(e -> removeAction.run()); - buttonPanel.add(removeButton); - - return buttonPanel; - } - - /** - * Safely negates a condition within its parent logical condition - * - * @param condition The condition to negate - * @param conditionManager The condition manager - * @param parentComponent The parent component for error dialogs - * @return true if negation was successful, false otherwise - */ - public static boolean negateCondition(Condition condition, ConditionManager conditionManager, Component parentComponent) { - // Don't allow negating null conditions - if (condition == null) { - return false; - } - - // Check if this is a plugin-defined condition that shouldn't be modified - if (conditionManager != null && conditionManager.isPluginDefinedCondition(condition)) { - showWarningDialog( - parentComponent, - "Cannot negate plugin-defined conditions. These conditions are protected.", - "Protected Conditions" - ); - return false; - } - - // Find parent logical condition - LogicalCondition parentLogical = conditionManager.findContainingLogical(condition); - if (parentLogical == null) { - showWarningDialog( - parentComponent, - "Could not determine which logical group contains this condition", - "Operation Failed" - ); - return false; - } - - // Toggle NOT status - int index = parentLogical.getConditions().indexOf(condition); - if (index < 0) { - return false; - } - - // If condition is already a NOT, unwrap it - if (condition instanceof NotCondition) { - NotCondition notCondition = (NotCondition) condition; - Condition innerCondition = notCondition.getCondition(); - - // Replace NOT with its inner condition - parentLogical.getConditions().remove(index); - parentLogical.addConditionAt(index, innerCondition); - } - // Otherwise, wrap it in a NOT - else { - // Create NOT condition - NotCondition notCondition = new NotCondition(condition); - - // Replace original with NOT version - parentLogical.getConditions().remove(index); - parentLogical.addConditionAt(index, notCondition); - } - - return true; - } - - /** - * Converts a logical condition from one type to another - * - * @param logical The logical condition to convert - * @param toAnd true to convert to AND, false to convert to OR - * @param conditionManager The condition manager - * @param parentComponent The parent component for error dialogs - * @return true if conversion was successful, false otherwise - */ - public static boolean convertLogicalType( - LogicalCondition logical, - boolean toAnd, - ConditionManager conditionManager, - Component parentComponent) { - - // Skip if already the desired type - if ((toAnd && logical instanceof AndCondition) || - (!toAnd && logical instanceof OrCondition)) { - return false; - } - - // Check if this is a plugin-defined logical group - if (conditionManager != null && conditionManager.isPluginDefinedCondition(logical)) { - showWarningDialog( - parentComponent, - "Cannot modify plugin-defined condition groups. These conditions are protected.", - "Protected Conditions" - ); - return false; - } - - // Create new logical of the desired type - LogicalCondition newLogical = toAnd ? new AndCondition() : new OrCondition(); - - // Transfer all conditions to the new logical - for (Condition condition : logical.getConditions()) { - newLogical.addCondition(condition); - } - - // Find parent logical - LogicalCondition parentLogical = conditionManager.findContainingLogical(logical); - - if (parentLogical == logical) { - // This is the root logical - replace in condition manager - if (toAnd) { - conditionManager.setUserLogicalCondition((AndCondition) newLogical); - } else { - conditionManager.setUserLogicalCondition((OrCondition) newLogical); - } - } else if (parentLogical != null) { - // Replace in parent - int index = parentLogical.getConditions().indexOf(logical); - if (index >= 0) { - parentLogical.getConditions().remove(index); - parentLogical.addConditionAt(index, newLogical); - } - } else { - // Couldn't find parent - showWarningDialog( - parentComponent, - "Couldn't find the parent logical condition for this group.", - "Operation Failed" - ); - return false; - } - - return true; - } - - /** - * Creates a condition tree panel with full functionality - * - * @param rootNode The root node for the tree - * @param treeModel The tree model - * @param conditionTree The tree component - * @param conditionManager The condition manager for rendering - * @return A panel containing the condition tree with scrolling - */ - public static JPanel createConditionTreePanel( - DefaultMutableTreeNode rootNode, - DefaultTreeModel treeModel, - JTree conditionTree, - ConditionManager conditionManager,boolean isStopConditionRenderer) { - - // Create the panel with border - JPanel panel = createTitledPanel("Condition Structure"); - panel.setLayout(new BorderLayout()); - - // Initialize tree if not already done - if (rootNode == null) { - rootNode = new DefaultMutableTreeNode("Conditions"); - } - - if (treeModel == null) { - treeModel = new DefaultTreeModel(rootNode); - } - - if (conditionTree == null) { - conditionTree = new JTree(treeModel); - conditionTree.setRootVisible(false); - conditionTree.setShowsRootHandles(true); - - // Set up tree cell renderer - conditionTree.setCellRenderer(new ConditionTreeCellRenderer(conditionManager,isStopConditionRenderer)); - - // Set up tree selection mode - conditionTree.getSelectionModel().setSelectionMode( - TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); // Enable multi-select - } - - // Create scroll pane for the tree - JScrollPane treeScrollPane = createScrollPane(conditionTree); - treeScrollPane.setPreferredSize(new Dimension(400, 300)); - - // Add to panel - panel.add(treeScrollPane, BorderLayout.CENTER); - - return panel; - } - - /** - * Creates a title panel with plugin name display - * - * @param isRunning Whether the plugin is running - * @param isEnabled Whether the plugin is enabled - * @param pluginName The name of the plugin - * @return A panel with the title display - */ - public static JPanel createTitlePanel(boolean isRunning, boolean isEnabled, String pluginName) { - JPanel titlePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - titlePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - titlePanel.setName("titlePanel"); - - // Create and format the title label - JLabel titleLabel = new JLabel(formatPluginTitle(isRunning, isEnabled, pluginName)); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeBoldFont()); - - titlePanel.add(titleLabel); - - return titlePanel; - } - - /** - * Formats the plugin title with appropriate HTML styling - * - * @param isRunning Whether the plugin is running - * @param isEnabled Whether the plugin is enabled - * @param pluginName The name of the plugin - * @return Formatted HTML title string - */ - public static String formatPluginTitle(boolean isRunning, boolean isEnabled, String pluginName) { - if (pluginName == null || pluginName.isEmpty()) { - return "No plugin selected"; - } - - // Apply color based on plugin state - String colorHex; - if (isEnabled) { - if (isRunning) { - // Running plugin - bright green - colorHex = "#4CAF50"; - } else { - // Enabled but not running - blue - colorHex = "#2196F3"; - } - } else { - // Disabled plugin - orange/amber - colorHex = "#FFC107"; - } - - // Format with HTML for color and bold styling - return "" + - pluginName + ""; - } - - /** - * Creates a top control panel with title and buttons - * - * @param titlePanel The title panel to include - * @param saveAction The action to perform when saving - * @param loadAction The action to perform when loading - * @param resetAction The action to perform when resetting - * @return A panel with title and control buttons - */ - public static JPanel createTopControlPanel( - JPanel titlePanel, - Runnable saveAction, - Runnable loadAction, - Runnable resetAction) { - - // Create button panel with right alignment - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - buttonPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Create control buttons - JButton loadButton = createButton("Load Current Conditions", ColorScheme.PROGRESS_COMPLETE_COLOR); - loadButton.addActionListener(e -> loadAction.run()); - - JButton saveButton = createButton("Save Conditions", ColorScheme.PROGRESS_COMPLETE_COLOR); - saveButton.addActionListener(e -> saveAction.run()); - - JButton resetButton = createButton("Reset Conditions", ColorScheme.PROGRESS_ERROR_COLOR); - resetButton.addActionListener(e -> resetAction.run()); - - // Add buttons to panel - buttonPanel.add(loadButton); - buttonPanel.add(saveButton); - buttonPanel.add(resetButton); - - // Create main top panel - JPanel topPanel = new JPanel(new BorderLayout()); - topPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - topPanel.add(titlePanel, BorderLayout.WEST); - topPanel.add(buttonPanel, BorderLayout.EAST); - - return topPanel; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/VarbitCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/VarbitCondition.java deleted file mode 100644 index 96601eb41d9..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/VarbitCondition.java +++ /dev/null @@ -1,559 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit; - -import java.time.ZonedDateTime; -import java.util.Optional; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.events.VarbitChanged; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; - -/** - * A condition that checks the current value of a Varbit or VarPlayer. - * This can be used to track game state like quest progress, minigame scores, - * collection log completions, etc. - */ -@Slf4j -@EqualsAndHashCode(callSuper = false) -public class VarbitCondition implements Condition { - - public static String getVersion() { - return "0.0.1"; - } - /** - * Defines the different types of variables that can be tracked - */ - public enum VarType { - VARBIT, - VARPLAYER - } - - /** - * Comparison operators for the varbit value - */ - public enum ComparisonOperator { - EQUALS("equals"), - NOT_EQUALS("not equals"), - GREATER_THAN("greater than"), - GREATER_THAN_OR_EQUALS("greater than or equals"), - LESS_THAN("less than"), - LESS_THAN_OR_EQUALS("less than or equals"); - - private final String displayName; - - ComparisonOperator(String displayName) { - this.displayName = displayName; - } - - public String getDisplayName() { - return displayName; - } - - @Override - public String toString() { - return displayName; - } - } - - @Getter private final String name; - @Getter private final VarType varType; - @Getter private final int varId; - @Getter private final int targetValue; - @Getter private final ComparisonOperator operator; - - - @Getter private final boolean relative; - @Getter private final boolean randomized; - @Getter private final int targetValueMin; - @Getter private final int targetValueMax; - - @Getter @Setter private transient int currentValue; - @Getter private transient volatile boolean satisfied; - @Getter private transient int startValue; - @Getter private transient int effectiveTargetValue; - - /** - * Creates a new VarbitCondition with absolute target value - * - * @param name A human-readable name for this condition - * @param varType Whether this is tracking a Varbit or VarPlayer variable - * @param varId The ID of the variable to track - * @param targetValue The target value to compare against - * @param operator The comparison operator to use - */ - public VarbitCondition(String name, VarType varType, int varId, int targetValue, ComparisonOperator operator) { - this.name = name; - this.varType = varType; - this.varId = varId; - this.targetValue = targetValue; - this.operator = operator; - this.relative = false; - this.randomized = false; - this.targetValueMin = targetValue; - this.targetValueMax = targetValue; - this.effectiveTargetValue = targetValue; - - // Initialize current value and starting value - updateCurrentValue(); - this.startValue = this.currentValue; - this.satisfied = checkSatisfied(); - } - - /** - * Creates a new VarbitCondition with absolute target value and randomization range - * - * @param name A human-readable name for this condition - * @param varType Whether this is tracking a Varbit or VarPlayer variable - * @param varId The ID of the variable to track - * @param targetValueMin The minimum target value to compare against - * @param targetValueMax The maximum target value to compare against - * @param operator The comparison operator to use - */ - public VarbitCondition(String name, VarType varType, int varId, int targetValueMin, int targetValueMax, ComparisonOperator operator) { - this.name = name; - this.varType = varType; - this.varId = varId; - this.targetValueMin = targetValueMin; - this.targetValueMax = targetValueMax; - this.targetValue = Rs2Random.between(targetValueMin, targetValueMax); - this.operator = operator; - this.relative = false; - this.randomized = true; - this.effectiveTargetValue = this.targetValue; - - // Initialize current value and starting value - updateCurrentValue(); - this.startValue = this.currentValue; - this.satisfied = checkSatisfied(); - } - - /** - * Creates a new VarbitCondition with relative target value - * - * @param name A human-readable name for this condition - * @param varType Whether this is tracking a Varbit or VarPlayer variable - * @param varId The ID of the variable to track - * @param targetValue The target value delta to compare against - * @param operator The comparison operator to use - * @param relative Whether this is a relative target value - */ - public VarbitCondition(String name, VarType varType, int varId, int targetValue, ComparisonOperator operator, boolean relative) { - this.name = name; - this.varType = varType; - this.varId = varId; - this.targetValue = targetValue; - this.operator = operator; - this.relative = relative; - this.randomized = false; - this.targetValueMin = targetValue; - this.targetValueMax = targetValue; - - // Initialize current value and starting value - updateCurrentValue(); - this.startValue = this.currentValue; - - // Calculate effective target value for relative mode - if (relative) { - calculateEffectiveTargetValue(); - } else { - this.effectiveTargetValue = targetValue; - } - - this.satisfied = checkSatisfied(); - } - - /** - * Creates a new VarbitCondition with relative target value and randomization range - * - * @param name A human-readable name for this condition - * @param varType Whether this is tracking a Varbit or VarPlayer variable - * @param varId The ID of the variable to track - * @param targetValueMin The minimum target value delta to compare against - * @param targetValueMax The maximum target value delta to compare against - * @param operator The comparison operator to use - * @param relative Whether this is a relative target value - */ - public VarbitCondition(String name, VarType varType, int varId, int targetValueMin, int targetValueMax, ComparisonOperator operator, boolean relative) { - this.name = name; - this.varType = varType; - this.varId = varId; - this.targetValueMin = targetValueMin; - this.targetValueMax = targetValueMax; - this.targetValue = Rs2Random.between(targetValueMin, targetValueMax); - this.operator = operator; - this.relative = relative; - this.randomized = true; - - // Initialize current value and starting value - updateCurrentValue(); - this.startValue = this.currentValue; - - // Calculate effective target value for relative mode - - calculateEffectiveTargetValue(); - - - - - this.satisfied = checkSatisfied(); - } - - /** - * Calculate the effective target value based on the starting value and target delta - */ - private void calculateEffectiveTargetValue() { - if (relative) { - switch (operator) { - case EQUALS: - case GREATER_THAN: - case GREATER_THAN_OR_EQUALS: - this.effectiveTargetValue = startValue + targetValue; - break; - case LESS_THAN: - case LESS_THAN_OR_EQUALS: - this.effectiveTargetValue = Math.max(0, startValue - targetValue); - break; - case NOT_EQUALS: - this.effectiveTargetValue = startValue; // Not straightforward for NOT_EQUALS, use start value - break; - default: - this.effectiveTargetValue = targetValue; - } - } else { - this.effectiveTargetValue = targetValue; - } - } - - /** - * Create a VarbitCondition with relative target value - */ - public static VarbitCondition createRelative(String name, VarType varType, int varId, int targetValue, ComparisonOperator operator) { - return new VarbitCondition(name, varType, varId, targetValue, operator, true); - } - - /** - * Create a VarbitCondition with randomized relative target value - */ - public static VarbitCondition createRelativeRandomized(String name, VarType varType, int varId, int targetValueMin, int targetValueMax, ComparisonOperator operator) { - return new VarbitCondition(name, varType, varId, targetValueMin, targetValueMax, operator, true); - } - - /** - * Create a VarbitCondition with randomized absolute target value - */ - public static VarbitCondition createRandomized(String name, VarType varType, int varId, int targetValueMin, int targetValueMax, ComparisonOperator operator) { - return new VarbitCondition(name, varType, varId, targetValueMin, targetValueMax, operator); - } - - /** - * Updates the current value from the game - */ - private void updateCurrentValue() { - try { - if (Microbot.isLoggedIn()){ - if (varType == VarType.VARBIT) { - this.currentValue = Microbot.getVarbitValue(varId); - } else { - this.currentValue = Microbot.getVarbitPlayerValue(varId); - } - }else{ - this.currentValue = -1; - } - } catch (Exception e) { - log.error("Error getting current value for " + varType + " " + varId, e); - this.currentValue = -1; - } - } - - /** - * Checks if the condition is satisfied based on the current value and operator - */ - private boolean checkSatisfied() { - if ( this.startValue ==-1) { - updateCurrentValue(); - this.startValue = this.currentValue; - if (relative) { - calculateEffectiveTargetValue(); - } else { - this.effectiveTargetValue = targetValue; - } - if (this.startValue == -1) { - return false; // Not logged in or error getting value - } - } - int compareValue = relative ? effectiveTargetValue : targetValue; - - switch (operator) { - case EQUALS: - return currentValue == compareValue; - case NOT_EQUALS: - return currentValue != compareValue; - case GREATER_THAN: - return currentValue > compareValue; - case GREATER_THAN_OR_EQUALS: - return currentValue >= compareValue; - case LESS_THAN: - return currentValue < compareValue; - case LESS_THAN_OR_EQUALS: - return currentValue <= compareValue; - default: - return false; - } - } - - /** - * Get the value change since the condition was created - */ - public int getValueChange() { - return currentValue - startValue; - } - - /** - * Get the value needed to reach the target - */ - public int getValueNeeded() { - if (!relative) { - return 0; - } - - switch (operator) { - case EQUALS: - return effectiveTargetValue - currentValue; - case GREATER_THAN: - case GREATER_THAN_OR_EQUALS: - return Math.max(0, effectiveTargetValue - currentValue); - case LESS_THAN: - case LESS_THAN_OR_EQUALS: - return Math.max(0, currentValue - effectiveTargetValue); - case NOT_EQUALS: - default: - return 0; - } - } - - /** - * Called when a varbit changes - */ - @Override - public void onVarbitChanged(VarbitChanged event) { - boolean oldSatisfied = this.satisfied; - updateCurrentValue(); - this.satisfied = checkSatisfied(); - - // Log when the condition changes state - if (oldSatisfied != this.satisfied) { - log.debug("VarbitCondition '{}' changed state: {} -> {}", - name, oldSatisfied, this.satisfied); - } - } - - @Override - public boolean isSatisfied() { - updateCurrentValue(); - this.satisfied = checkSatisfied(); - return this.satisfied; - } - - @Override - public String getDescription() { - String varTypeDisplay = varType.toString().toLowerCase(); - updateCurrentValue(); - - StringBuilder description = new StringBuilder(name); - description.append(" (").append(varTypeDisplay).append(" ID: ").append(varId); - description.append(", Name: ").append(this.name); - description.append(", Operate: ").append(this.operator.getDisplayName()); - // Show randomization range if applicable - if (randomized) { - description.append(", random "); - } - - // Show relative or absolute mode - if (relative) { - if (operator == ComparisonOperator.EQUALS) { - description.append(", change by ").append(targetValue); - } else { - description.append(", ").append(operator.getDisplayName()) - .append(" change of ").append(targetValue); - } - if (this.startValue ==-1) { - description.append(", starting value unknown"); - } else { - description.append(", starting ").append(startValue); - } - // Add current progress for relative mode - description.append(", changed ").append(getValueChange()); - - int valueNeeded = getValueNeeded(); - if (valueNeeded > 0) { - description.append(", need ").append(valueNeeded).append(" more"); - } - } else { - description.append(", ").append(operator.getDisplayName()) - .append(" ").append(targetValue); - } - if (currentValue != -1) { - description.append(", current ").append(currentValue); - }else{ - description.append(", current value unknown"); - } - description.append(")"); - return description.toString(); - } - - @Override - public String getDetailedDescription() { - updateCurrentValue(); - StringBuilder desc = new StringBuilder(); - - desc.append("VarbitCondition: ").append(name).append("\n") - .append("Type: ").append(varType.toString()).append("\n") - .append("ID: ").append(varId).append("\n") - .append("Mode: ").append(relative ? "Relative" : "Absolute").append("\n"); - - if (randomized) { - desc.append("Target range: ").append(targetValueMin).append("-").append(targetValueMax).append("\n"); - } - - if (relative) { - desc.append("Target change: ").append(targetValue).append("\n") - .append("Starting value: ").append(startValue).append("\n") - .append("Current value: ").append(currentValue).append("\n") - .append("Value change: ").append(getValueChange()).append("\n") - .append("Effective target: ").append(effectiveTargetValue).append("\n"); - } else { - desc.append("Target value: ").append(targetValue).append("\n") - .append("Current value: ").append(currentValue).append("\n"); - } - - desc.append("Operator: ").append(operator.getDisplayName()).append("\n") - .append("Satisfied: ").append(isSatisfied() ? "Yes" : "No").append("\n") - .append("Progress: ").append(String.format("%.1f%%", getProgressPercentage())); - - return desc.toString(); - } - - @Override - public ConditionType getType() { - return ConditionType.VARBIT; - } - - @Override - public void reset(boolean randomize) { - updateCurrentValue(); - - // Reset starting value - this.startValue = this.currentValue; - - // Randomize target if needed - if (randomize && randomized) { - int newTarget = Rs2Random.between(targetValueMin, targetValueMax); - - // Use reflection to update the final field (not ideal but necessary for this design) - try { - java.lang.reflect.Field targetField = VarbitCondition.class.getDeclaredField("targetValue"); - targetField.setAccessible(true); - - // Remove final modifier - java.lang.reflect.Field modifiersField = java.lang.reflect.Field.class.getDeclaredField("modifiers"); - modifiersField.setAccessible(true); - modifiersField.setInt(targetField, targetField.getModifiers() & ~java.lang.reflect.Modifier.FINAL); - - // Set new value - targetField.set(this, newTarget); - } catch (Exception e) { - log.error("Error updating target value", e); - } - } - - // Recalculate effective target for relative mode - if (relative) { - calculateEffectiveTargetValue(); - } - - this.satisfied = checkSatisfied(); - } - - @Override - public Optional getCurrentTriggerTime() { - return Condition.super.getCurrentTriggerTime(); - } - - @Override - public double getProgressPercentage() { - updateCurrentValue(); - - // For binary conditions (equals/not equals), return either 0 or 100 - if (operator == ComparisonOperator.EQUALS || operator == ComparisonOperator.NOT_EQUALS) { - return isSatisfied() ? 100.0 : 0.0; - } - - // For relative mode with increase operators - if (relative && (operator == ComparisonOperator.GREATER_THAN || - operator == ComparisonOperator.GREATER_THAN_OR_EQUALS)) { - int change = getValueChange(); - if (change >= targetValue) { - return 100.0; - } - return targetValue > 0 ? Math.min(100.0, (change * 100.0) / targetValue) : 0.0; - } - - // For relative mode with decrease operators - if (relative && (operator == ComparisonOperator.LESS_THAN || - operator == ComparisonOperator.LESS_THAN_OR_EQUALS)) { - int change = startValue - currentValue; - if (change >= targetValue) { - return 100.0; - } - return targetValue > 0 ? Math.min(100.0, (change * 100.0) / targetValue) : 0.0; - } - - // For absolute mode comparisons - int compareValue = relative ? effectiveTargetValue : targetValue; - double progress = 0.0; - - switch (operator) { - case GREATER_THAN: - case GREATER_THAN_OR_EQUALS: - if (currentValue >= compareValue) { - progress = 100.0; - } else if (compareValue > 0) { - progress = Math.min(100.0, (currentValue * 100.0) / compareValue); - } - break; - - case LESS_THAN: - case LESS_THAN_OR_EQUALS: - // For "less than" we show progress if we're below the target - if (currentValue <= compareValue) { - progress = 100.0; - } else if (currentValue > 0) { - // Inverse progress - as we get closer to the target - progress = Math.min(100.0, (compareValue * 100.0) / currentValue); - } - break; - - default: - progress = isSatisfied() ? 100.0 : 0.0; - } - - return progress; - } - - @Override - public void pause() { - // Default implementation for VarbitCondition - no specific pause behavior needed - // Varbit conditions are event-based and don't track timing or accumulative state - } - - @Override - public void resume() { - // Default implementation for VarbitCondition - no specific resume behavior needed - // Varbit conditions are event-based and don't track timing or accumulative state - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/VarbitUtil.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/VarbitUtil.java deleted file mode 100644 index 1898569bb82..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/VarbitUtil.java +++ /dev/null @@ -1,249 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.gameval.VarbitID; -import net.runelite.api.gameval.VarPlayerID; - -import java.lang.reflect.Field; -import java.util.*; -import java.util.stream.Collectors; - -/** - * Utility class for game variables (Varbits and VarPlayers) - */ -@Slf4j -public class VarbitUtil { - // Cache of constant names and values - private static Map varbitConstantMap = null; - private static Map varPlayerConstantMap = null; - - // Categories for organizing varbits - private static final Map> varbitCategories = new HashMap<>(); - private static final String[] CATEGORY_NAMES = { - "Quests", - "Skills", - "Minigames", - "Bosses", - "Diaries", - "Combat Achievements", - "Features", - "Items", - "Other" - }; - - /** - * Initializes the constant maps for Varbits and VarPlayer if not already initialized - */ - public static synchronized void initConstantMaps() { - if (varbitConstantMap == null) { - varbitConstantMap = new HashMap<>(); - for (Field field : VarbitID.class.getDeclaredFields()) { - if (field.getType() == int.class && java.lang.reflect.Modifier.isStatic(field.getModifiers())) { - try { - int value = field.getInt(null); - varbitConstantMap.put(value, field.getName()); - } catch (IllegalAccessException e) { - log.error("Error accessing field", e); - } - } - } - } - - if (varPlayerConstantMap == null) { - varPlayerConstantMap = new HashMap<>(); - - // Process VarPlayerID class - for (Field field : VarPlayerID.class.getDeclaredFields()) { - if (field.getType() == int.class && java.lang.reflect.Modifier.isStatic(field.getModifiers())) { - try { - int value = field.getInt(null); - varPlayerConstantMap.put(value, field.getName()); - } catch (IllegalAccessException e) { - log.error("Error accessing field", e); - } - } - } - } - } - - /** - * Gets the constant name for a given varbit/varplayer ID - */ - public static String getConstantNameForId(boolean isVarbit, int id) { - initConstantMaps(); - Map map = isVarbit ? varbitConstantMap : varPlayerConstantMap; - return map.get(id); - } - - /** - * Helper class to hold a varbit/varplayer entry - */ - public static class VarEntry { - public final int id; - public final String name; - - public VarEntry(int id, String name) { - this.id = id; - this.name = name; - } - } - - /** - * Gets a list of predefined Varbit options from the Varbits enum - * - * @return List of formatted Varbit options - */ - public static List getVarbitOptions() { - List options = new ArrayList<>(); - options.add("Select Varbit"); - - try { - // Initialize constant maps if needed - initConstantMaps(); - - // Add all varbit options - for (Map.Entry entry : varbitConstantMap.entrySet()) { - int id = entry.getKey(); - String name = formatConstantName(entry.getValue()); - - // Add formatted option: "Name (ID)" - options.add(name + " (" + id + ")"); - } - - // Sort options alphabetically after the first "Select Varbit" item - if (options.size() > 1) { - List sortedOptions = new ArrayList<>(options.subList(1, options.size())); - Collections.sort(sortedOptions); - options = new ArrayList<>(); - options.add("Select Varbit"); - options.addAll(sortedOptions); - } - - } catch (Exception e) { - log.error("Error getting Varbit options", e); - } - - return options; - } - - /** - * Formats a constant name for better readability - * - * @param name The raw constant name - * @return Formatted name - */ - public static String formatConstantName(String name) { - name = name.replace('_', ' ').toLowerCase(); - - // Capitalize words for better readability - StringBuilder sb = new StringBuilder(); - boolean capitalizeNext = true; - for (char c : name.toCharArray()) { - if (Character.isWhitespace(c)) { - capitalizeNext = true; - sb.append(c); - } else if (capitalizeNext) { - sb.append(Character.toUpperCase(c)); - capitalizeNext = false; - } else { - sb.append(c); - } - } - - return sb.toString(); - } - - /** - * Organizes varbits into meaningful categories - */ - public static void initializeVarbitCategories() { - // Initialize the varbit constant map if needed - initConstantMaps(); - - // Create category lists - for (String category : CATEGORY_NAMES) { - varbitCategories.put(category, new ArrayList<>()); - } - - // Populate categories based on name matching - for (Map.Entry entry : varbitConstantMap.entrySet()) { - int id = entry.getKey(); - String name = entry.getValue(); - - String formattedName = formatConstantName(name); - String lowerName = name.toLowerCase(); - VarEntry varEntry = new VarEntry(id, formattedName); - - // First handle COLLECTION prefixed varbits as they have clear categorization - if (name.contains("COLLECTION")) { - if (name.contains("_BOSSES_") || name.contains("_RAIDS_")) { - varbitCategories.get("Bosses").add(varEntry); - continue; - } else if (name.contains("_MINIGAMES_")) { - varbitCategories.get("Minigames").add(varEntry); - continue; - } - } - - // Check for specific other collections that might fit in our categories - if (name.contains("SLAYER_") && (name.contains("_TASKS_COMPLETED") || name.contains("_POINTS"))) { - varbitCategories.get("Skills").add(varEntry); - continue; - } - if (name.contains("CA_") && (name.contains("_TOTAL_TASKS"))) { - varbitCategories.get("Combat Achievements").add(varEntry); - continue; - } - - // Achievement name - if (name.contains("_DIARY_") && name.contains("_COMPLETE")) { - varbitCategories.get("Diaries").add(varEntry); - continue; - } - } - } - - /** - * Gets the category names - * - * @return Array of category names - */ - public static String[] getCategoryNames() { - return CATEGORY_NAMES; - } - - /** - * Gets the varbit entries for a specific category - * - * @param category The category name - * @return List of VarEntry objects for the category - */ - public static List getVarbitEntriesByCategory(String category) { - // Initialize categories if needed - if (varbitCategories.isEmpty()) { - initializeVarbitCategories(); - } - - return varbitCategories.getOrDefault(category, new ArrayList<>()); - } - - /** - * Gets all varbit entries - * - * @return Map of all varbit entries - */ - public static Map getAllVarbitEntries() { - initConstantMaps(); - return new HashMap<>(varbitConstantMap); - } - - /** - * Gets all varplayer entries - * - * @return Map of all varplayer entries - */ - public static Map getAllVarPlayerEntries() { - initConstantMaps(); - return new HashMap<>(varPlayerConstantMap); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/serialization/VarbitConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/serialization/VarbitConditionAdapter.java deleted file mode 100644 index fe9a543c3e3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/serialization/VarbitConditionAdapter.java +++ /dev/null @@ -1,135 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.VarbitCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.VarbitCondition.VarType; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.VarbitCondition.ComparisonOperator; - -import java.lang.reflect.Type; - -/** - * Serializes and deserializes VarbitCondition objects - */ -@Slf4j -public class VarbitConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(VarbitCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add type information - json.addProperty("type", VarbitCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Store basic properties - data.addProperty("name", src.getName()); - data.addProperty("version", src.getVersion()); - data.addProperty("varType", src.getVarType().toString()); - data.addProperty("varId", src.getVarId()); - data.addProperty("operator", src.getOperator().name()); // Use name() instead of toString() - - // Store target value information - data.addProperty("targetValue", src.getTargetValue()); - data.addProperty("relative", src.isRelative()); - data.addProperty("randomized", src.isRandomized()); - - // Store randomization range if using randomization - if (src.isRandomized()) { - data.addProperty("targetValueMin", src.getTargetValueMin()); - data.addProperty("targetValueMax", src.getTargetValueMax()); - } - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public VarbitCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is using the type/data wrapper format - if (jsonObject.has("type") && jsonObject.has("data")) { - jsonObject = jsonObject.getAsJsonObject("data"); - } - - if (jsonObject.has("version")) { - // Parse basic properties - String version = jsonObject.get("version").getAsString(); - if (!version.equals(VarbitCondition.getVersion())) { - - throw new JsonParseException("Version mismatch: expected " + VarbitCondition.getVersion() + - ", got " + version); - } - } - - String name = jsonObject.get("name").getAsString(); - VarType varType = VarType.valueOf(jsonObject.get("varType").getAsString()); - int varId = jsonObject.get("varId").getAsInt(); - - // Get operator - handle both name and display name formats for backward compatibility - String operatorStr = jsonObject.get("operator").getAsString(); - ComparisonOperator operator; - - try { - // Try parsing as enum name first (new format) - operator = ComparisonOperator.valueOf(operatorStr); - } catch (IllegalArgumentException e) { - // If that fails, try matching by display name (old format) - operator = getOperatorByDisplayName(operatorStr); - if (operator == null) { - // If all parsing fails, default to EQUALS - log.warn("Unknown operator '{}', defaulting to EQUALS", operatorStr); - operator = ComparisonOperator.EQUALS; - } - } - - boolean relative = jsonObject.has("relative") && jsonObject.get("relative").getAsBoolean(); - - // Check if this is using randomization - boolean randomized = jsonObject.has("randomized") && jsonObject.get("randomized").getAsBoolean(); - - if (randomized) { - int targetValueMin = jsonObject.get("targetValueMin").getAsInt(); - int targetValueMax = jsonObject.get("targetValueMax").getAsInt(); - - // Create with randomization - if (relative) { - return VarbitCondition.createRelativeRandomized(name, varType, varId, - targetValueMin, targetValueMax, operator); - } else { - return VarbitCondition.createRandomized(name, varType, varId, - targetValueMin, targetValueMax, operator); - } - } else { - // Regular non-randomized condition - int targetValue = jsonObject.get("targetValue").getAsInt(); - - if (relative) { - return VarbitCondition.createRelative(name, varType, varId, targetValue, operator); - } else { - return new VarbitCondition(name, varType, varId, targetValue, operator); - } - } - - } - - /** - * Helper method to get an operator by its display name - * Used for backward compatibility with old serialized data - */ - private ComparisonOperator getOperatorByDisplayName(String displayName) { - for (ComparisonOperator op : ComparisonOperator.values()) { - if (op.getDisplayName().equalsIgnoreCase(displayName)) { - return op; - } - } - return null; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/ui/VarbitConditionPanelUtil.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/ui/VarbitConditionPanelUtil.java deleted file mode 100644 index 55cbe16c44f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/ui/VarbitConditionPanelUtil.java +++ /dev/null @@ -1,940 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.ui; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.VarbitCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.VarbitUtil; - -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import java.awt.*; - -import java.util.*; -import java.util.List; - - -/** - * Utility class for creating VarbitCondition configuration UI panels. - */ -@Slf4j -public class VarbitConditionPanelUtil { - - /** - * Callback interface for variable selection - */ - private interface VarSelectCallback { - void onVarSelected(int id); - } - - /** - * Creates a panel for configuring VarbitCondition - * - * @param panel The panel to add components to - * @param gbc GridBagConstraints for layout - * @param specificCategory Optional specific category to restrict selection to (null for general panel) - */ - public static void createVarbitConditionPanel(JPanel panel, GridBagConstraints gbc, String specificCategory) { - // Main label - String titleText = specificCategory == null - ? "Varbit Condition:" - : "Collection Log - " + specificCategory + " Condition:"; - - JLabel titleLabel = new JLabel(titleText); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // Create var type selector (disabled if category is specified) - gbc.gridy++; - JPanel varTypePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - varTypePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel varTypeLabel = new JLabel("Variable Type:"); - varTypeLabel.setForeground(Color.WHITE); - varTypePanel.add(varTypeLabel); - - String[] varTypes = specificCategory != null ? new String[]{"Varbit"} : new String[]{"Varbit", "VarPlayer"}; - JComboBox varTypeComboBox = new JComboBox<>(varTypes); - varTypeComboBox.setPreferredSize(new Dimension(120, varTypeComboBox.getPreferredSize().height)); - varTypeComboBox.setEnabled(specificCategory == null); // Only enable if no specific category - varTypePanel.add(varTypeComboBox); - - panel.add(varTypePanel, gbc); - - // Add mode selection (relative vs absolute) - gbc.gridy++; - JPanel modePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - modePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel modeLabel = new JLabel("Mode:"); - modeLabel.setForeground(Color.WHITE); - modePanel.add(modeLabel); - - JRadioButton absoluteButton = new JRadioButton("Absolute Value"); - absoluteButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - absoluteButton.setForeground(Color.WHITE); - absoluteButton.setSelected(true); // Default to absolute mode - absoluteButton.setToolTipText("Track a specific target value"); - - JRadioButton relativeButton = new JRadioButton("Relative Change"); - relativeButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - relativeButton.setForeground(Color.WHITE); - relativeButton.setToolTipText("Track changes from the current value"); - - ButtonGroup modeGroup = new ButtonGroup(); - modeGroup.add(absoluteButton); - modeGroup.add(relativeButton); - - modePanel.add(absoluteButton); - modePanel.add(relativeButton); - panel.add(modePanel, gbc); - - // Create ID input based on if we have a specific category - JTextField idField = new JTextField(8); - - // Initialize variables that will hold references to the category comboboxes - JComboBox generalCategoryComboBox = null; - JComboBox entriesComboBox = null; - - if (specificCategory == null) { - // For general panel, show category selector and direct ID input - gbc.gridy++; - JPanel categoryPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - categoryPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel categoryLabel = new JLabel("Category:"); - categoryLabel.setForeground(Color.WHITE); - categoryPanel.add(categoryLabel); - - // Create the category selection combobox - generalCategoryComboBox = new JComboBox<>(VarbitUtil.getCategoryNames()); - generalCategoryComboBox.setPreferredSize(new Dimension(150, generalCategoryComboBox.getPreferredSize().height)); - categoryPanel.add(generalCategoryComboBox); - - panel.add(categoryPanel, gbc); - - // Create ID input panel with dropdown for common Varbits - gbc.gridy++; - JPanel idPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - idPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel idLabel = new JLabel("Variable ID:"); - idLabel.setForeground(Color.WHITE); - idPanel.add(idLabel); - - // Numeric field for direct ID input - idField.setToolTipText("Enter the Varbit ID or VarPlayer ID directly"); - idPanel.add(idField); - - // Add lookup button - JButton lookupButton = new JButton("Lookup"); - lookupButton.setBackground(ColorScheme.BRAND_ORANGE); - lookupButton.setForeground(Color.WHITE); - lookupButton.addActionListener(e -> { - showVarLookupDialog(panel, varTypeComboBox.getSelectedItem().equals("Varbit"), id -> { - idField.setText(String.valueOf(id)); - }); - }); - idPanel.add(lookupButton); - - // Add dropdown for entries from selected category - List varbitOptions = new ArrayList<>(); - varbitOptions.add("Select Varbit"); - entriesComboBox = new JComboBox<>(varbitOptions.toArray(new String[0])); - entriesComboBox.setPreferredSize(new Dimension(200, entriesComboBox.getPreferredSize().height)); - entriesComboBox.setToolTipText("Select a predefined Varbit"); - - // Create a final reference to the entries combobox for use in the lambda - final JComboBox finalEntriesComboBox = entriesComboBox; - - entriesComboBox.addActionListener(e -> { - if (finalEntriesComboBox.getSelectedIndex() > 0) { - String selected = (String) finalEntriesComboBox.getSelectedItem(); - if (selected != null && !selected.isEmpty()) { - int openParen = selected.indexOf('('); - int closeParen = selected.indexOf(')'); - if (openParen >= 0 && closeParen > openParen) { - String idStr = selected.substring(openParen + 1, closeParen).trim(); - idField.setText(idStr); - } - } - } - }); - - // Create a final reference to the category combobox for use in the lambda - final JComboBox finalCategoryComboBox = generalCategoryComboBox; - final JComboBox finalEntriesComboBox2 = entriesComboBox; - - // Update the varbit combobox when category changes - generalCategoryComboBox.addActionListener(e -> { - String category = (String) finalCategoryComboBox.getSelectedItem(); - updateVarbitComboBoxByCategory(finalEntriesComboBox2, category); - }); - - idPanel.add(entriesComboBox); - - // Add a name label that shows the constant name if available - JLabel constantNameLabel = new JLabel(""); - constantNameLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - idPanel.add(constantNameLabel); - - // Update constant name when varId changes - idField.getDocument().addDocumentListener(new DocumentListener() { - private void update() { - try { - int id = Integer.parseInt(idField.getText().trim()); - boolean isVarbit = varTypeComboBox.getSelectedItem().equals("Varbit"); - String name = VarbitUtil.getConstantNameForId(isVarbit, id); - if (name != null && !name.isEmpty()) { - constantNameLabel.setText(name); - } else { - constantNameLabel.setText("(Unknown ID)"); - } - } catch (NumberFormatException ex) { - constantNameLabel.setText(""); - } - } - - @Override - public void insertUpdate(DocumentEvent e) { update(); } - - @Override - public void removeUpdate(DocumentEvent e) { update(); } - - @Override - public void changedUpdate(DocumentEvent e) { update(); } - }); - - panel.add(idPanel, gbc); - - // Store components for later - panel.putClientProperty("varbitConstantNameLabel", constantNameLabel); - } else { - // For category-specific panel (Boss/Minigame), show only dropdown - gbc.gridy++; - JPanel idPanel = new JPanel(new BorderLayout()); - idPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - String labelText = "Select " + (specificCategory.equals("Bosses") ? "Boss:" : "Minigame:"); - JLabel idLabel = new JLabel(labelText); - idLabel.setForeground(Color.WHITE); - idPanel.add(idLabel, BorderLayout.WEST); - - // Create a hidden field to store the selected ID - idField.setVisible(false); - - // Initialize the varbit categories if needed - VarbitUtil.initializeVarbitCategories(); - - // Populate the combobox with entries from the specific category - DefaultComboBoxModel entriesModel = new DefaultComboBoxModel<>(); - entriesModel.addElement("Select a " + (specificCategory.equals("Bosses") ? "Boss" : "Minigame") + "..."); - - List categoryEntries = new ArrayList<>(); - for (Map.Entry entry : VarbitUtil.getAllVarbitEntries().entrySet()) { - if (specificCategory.equals("Bosses")) { - if (entry.getValue().startsWith("COLLECTION_BOSSES_") || - entry.getValue().startsWith("COLLECTION_RAIDS_")) { - String formattedName = VarbitUtil.formatConstantName(entry.getValue()); - categoryEntries.add(new VarbitUtil.VarEntry(entry.getKey(), formattedName)); - } - } else if (specificCategory.equals("Minigames")) { - if (entry.getValue().startsWith("COLLECTION_MINIGAMES_")) { - String formattedName = VarbitUtil.formatConstantName(entry.getValue()); - categoryEntries.add(new VarbitUtil.VarEntry(entry.getKey(), formattedName)); - } - } - } - - // Sort entries by name - categoryEntries.sort(Comparator.comparing(e -> e.name)); - - // Add each entry to the combo box model - for (VarbitUtil.VarEntry entry : categoryEntries) { - entriesModel.addElement(entry.name + " (" + entry.id + ")"); - } - - entriesComboBox = new JComboBox<>(entriesModel); - entriesComboBox.setPreferredSize(new Dimension(350, entriesComboBox.getPreferredSize().height)); - entriesComboBox.setToolTipText("Select a " + specificCategory.toLowerCase() + " from the Collection Log"); - - // Create a final reference to the combobox for use in the lambda - final JComboBox finalEntriesComboBox = entriesComboBox; - - // Update the hidden field when selection changes - entriesComboBox.addActionListener(e -> { - if (finalEntriesComboBox.getSelectedIndex() > 0) { - String selected = (String) finalEntriesComboBox.getSelectedItem(); - if (selected != null && !selected.isEmpty()) { - int openParen = selected.indexOf('('); - int closeParen = selected.indexOf(')'); - if (openParen >= 0 && closeParen > openParen) { - String idStr = selected.substring(openParen + 1, closeParen); - idField.setText(idStr); - } - } - } - }); - - idPanel.add(entriesComboBox, BorderLayout.CENTER); - panel.add(idPanel, gbc); - } - - // Create comparison operator selector - gbc.gridy++; - JPanel operatorPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - operatorPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel operatorLabel = new JLabel("Comparison:"); - operatorLabel.setForeground(Color.WHITE); - operatorPanel.add(operatorLabel); - - // Get operator names from enum - String[] operators = new String[VarbitCondition.ComparisonOperator.values().length]; - for (int i = 0; i < VarbitCondition.ComparisonOperator.values().length; i++) { - operators[i] = VarbitCondition.ComparisonOperator.values()[i].getDisplayName(); - } - - JComboBox operatorComboBox = new JComboBox<>(operators); - operatorComboBox.setPreferredSize(new Dimension(150, operatorComboBox.getPreferredSize().height)); - operatorPanel.add(operatorComboBox); - - panel.add(operatorPanel, gbc); - - // Create target value input with randomization option - gbc.gridy++; - JPanel targetValuePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - targetValuePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel targetValueLabel = new JLabel("Target Value:"); - targetValueLabel.setForeground(Color.WHITE); - targetValuePanel.add(targetValueLabel); - - SpinnerNumberModel targetValueModel = new SpinnerNumberModel(1, 0, Integer.MAX_VALUE, 1); - JSpinner targetValueSpinner = new JSpinner(targetValueModel); - targetValuePanel.add(targetValueSpinner); - - JCheckBox randomizeCheckBox = new JCheckBox("Randomize"); - randomizeCheckBox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - randomizeCheckBox.setForeground(Color.WHITE); - targetValuePanel.add(randomizeCheckBox); - - panel.add(targetValuePanel, gbc); - - // Min/Max panel for randomization - gbc.gridy++; - JPanel minMaxPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - minMaxPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel minValueLabel = new JLabel("Min Value:"); - minValueLabel.setForeground(Color.WHITE); - minMaxPanel.add(minValueLabel); - - SpinnerNumberModel minValueModel = new SpinnerNumberModel(0, 0, Integer.MAX_VALUE, 1); - JSpinner minValueSpinner = new JSpinner(minValueModel); - minMaxPanel.add(minValueSpinner); - - JLabel maxValueLabel = new JLabel("Max Value:"); - maxValueLabel.setForeground(Color.WHITE); - minMaxPanel.add(maxValueLabel); - - SpinnerNumberModel maxValueModel = new SpinnerNumberModel(10, 0, Integer.MAX_VALUE, 1); - JSpinner maxValueSpinner = new JSpinner(maxValueModel); - minMaxPanel.add(maxValueSpinner); - - minMaxPanel.setVisible(false); // Initially hidden - panel.add(minMaxPanel, gbc); - - // Set up randomize checkbox behavior - randomizeCheckBox.addChangeListener(e -> { - minMaxPanel.setVisible(randomizeCheckBox.isSelected()); - targetValueSpinner.setEnabled(!randomizeCheckBox.isSelected()); - - if (randomizeCheckBox.isSelected()) { - int value = (Integer) targetValueSpinner.getValue(); - - // Set reasonable min/max values based on current target - minValueSpinner.setValue(Math.max(0, value - 5)); - maxValueSpinner.setValue(value + 5); - } - - panel.revalidate(); - panel.repaint(); - }); - - // Set up min/max validation - minValueSpinner.addChangeListener(e -> { - int min = (Integer) minValueSpinner.getValue(); - int max = (Integer) maxValueSpinner.getValue(); - - if (min > max) { - maxValueSpinner.setValue(min); - } - }); - - maxValueSpinner.addChangeListener(e -> { - int min = (Integer) minValueSpinner.getValue(); - int max = (Integer) maxValueSpinner.getValue(); - - if (max < min) { - minValueSpinner.setValue(max); - } - }); - - // Current value display to help the user - gbc.gridy++; - JPanel currentValuePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - currentValuePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel currentValueLabel = new JLabel("Current Value:"); - currentValueLabel.setForeground(Color.WHITE); - currentValuePanel.add(currentValueLabel); - - JLabel currentValueDisplay = new JLabel("--"); - currentValueDisplay.setForeground(Color.YELLOW); - currentValuePanel.add(currentValueDisplay); - - JButton checkValueButton = new JButton("Check Current Value"); - checkValueButton.addActionListener(e -> { - try { - String idText = idField.getText().trim(); - if (idText.isEmpty()) { - if (specificCategory != null) { - currentValueDisplay.setText("Please select a " + - (specificCategory.equals("Bosses") ? "boss" : "minigame") + " first"); - } else { - currentValueDisplay.setText("Please enter a valid ID"); - } - return; - } - - int varId = Integer.parseInt(idText); - boolean isVarbit = varTypeComboBox.getSelectedItem().equals("Varbit"); - int value; - - if (isVarbit) { - value = Microbot.getVarbitValue(varId); - } else { - value = Microbot.getVarbitPlayerValue(varId); - } - - currentValueDisplay.setText(String.valueOf(value)); - - // If relative mode is enabled, update the description to show the potential target - if (relativeButton.isSelected()) { - int targetValue = (Integer) targetValueSpinner.getValue(); - String operator = (String) operatorComboBox.getSelectedItem(); - - if (operator.contains("greater")) { - currentValueDisplay.setText(value + " (target would be " + (value + targetValue) + ")"); - } else if (operator.contains("less")) { - currentValueDisplay.setText(value + " (target would be " + (value - targetValue) + ")"); - } else { - currentValueDisplay.setText(value + " (target would be " + (value + targetValue) + ")"); - } - } - } catch (NumberFormatException ex) { - currentValueDisplay.setText("Invalid ID"); - } catch (Exception ex) { - currentValueDisplay.setText("Error: " + ex.getMessage()); - } - }); - currentValuePanel.add(checkValueButton); - - panel.add(currentValuePanel, gbc); - - // Update target value label based on selected mode - relativeButton.addActionListener(e -> { - targetValueLabel.setText("Value Change:"); - }); - - absoluteButton.addActionListener(e -> { - targetValueLabel.setText("Target Value:"); - }); - - // Add a helpful description - gbc.gridy++; - String descriptionText; - if (specificCategory == null) { - descriptionText = "Varbits and VarPlayers are game values that track states like quest progress,
minigame scores, etc. Great for tracking game completion objectives."; - } else if (specificCategory.equals("Bosses")) { - descriptionText = "Collection Log Boss varbits track your boss kills and achievements.
Generally, values of 1 indicate completion or a kill count achievement."; - } else { - descriptionText = "Collection Log Minigame varbits track your progress in minigames.
Generally, values of 1 indicate completion or a kill count achievement."; - } - - JLabel descriptionLabel = new JLabel(descriptionText); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Store components for later access using unified naming scheme - panel.putClientProperty("varbitTypeComboBox", varTypeComboBox); - panel.putClientProperty("varbitIdField", idField); - panel.putClientProperty("varbitOperatorComboBox", operatorComboBox); - panel.putClientProperty("varbitTargetValueSpinner", targetValueSpinner); - panel.putClientProperty("varbitCategoryEntriesComboBox", entriesComboBox); - - // Store the category selector only if we're in the general panel - if (specificCategory == null && generalCategoryComboBox != null) { - panel.putClientProperty("varbitCategoryComboBox", generalCategoryComboBox); - } else if (specificCategory != null) { - // Store the specific category as a string property - panel.putClientProperty("varbitSpecificCategory", specificCategory); - } - - panel.putClientProperty("varbitRelativeMode", relativeButton); - panel.putClientProperty("varbitAbsoluteMode", absoluteButton); - panel.putClientProperty("varbitCurrentValueDisplay", currentValueDisplay); - panel.putClientProperty("varbitRandomize", randomizeCheckBox); - panel.putClientProperty("varbitMinValueSpinner", minValueSpinner); - panel.putClientProperty("varbitMaxValueSpinner", maxValueSpinner); - panel.putClientProperty("varbitMinMaxPanel", minMaxPanel); - } - - /** - * Creates a panel for configuring general VarbitCondition - * - * @param panel The panel to add components to - * @param gbc GridBagConstraints for layout - */ - public static void createVarbitConditionPanel(JPanel panel, GridBagConstraints gbc) { - createVarbitConditionPanel(panel, gbc, null); - } - - /** - * Creates a panel specifically for minigame-related varbit conditions - * - * @param panel The panel to add components to - * @param gbc GridBagConstraints for layout - */ - public static void createMinigameVarbitPanel(JPanel panel, GridBagConstraints gbc) { - createVarbitConditionPanel(panel, gbc, "Minigames"); - } - - /** - * Creates a panel specifically for boss-related varbit conditions - * - * @param panel The panel to add components to - * @param gbc GridBagConstraints for layout - */ - public static void createBossVarbitPanel(JPanel panel, GridBagConstraints gbc) { - createVarbitConditionPanel(panel, gbc, "Bosses"); - } - - // ... rest of the code remains unchanged ... - - /** - * Updates the varbit combo box to show only items from a specific category - * - * @param comboBox The combo box to update - * @param category The category to filter by - */ - private static void updateVarbitComboBoxByCategory(JComboBox comboBox, String category) { - comboBox.removeAllItems(); - comboBox.addItem("Select Varbit"); - - List entries = VarbitUtil.getVarbitEntriesByCategory(category); - - if (category.equals("Other") && entries.isEmpty()) { - // For "Other" category, include all entries if not specifically categorized - Map varbitConstantMap = VarbitUtil.getAllVarbitEntries(); - for (Map.Entry entry : varbitConstantMap.entrySet()) { - String formattedName = VarbitUtil.formatConstantName(entry.getValue()); - comboBox.addItem(formattedName + " (" + entry.getKey() + ")"); - } - } else { - // Sort entries by name - entries.sort(Comparator.comparing(e -> e.name)); - - // Add all entries from the selected category - for (VarbitUtil.VarEntry entry : entries) { - comboBox.addItem(entry.name + " (" + entry.id + ")"); - } - } - } - - - /** - * Shows a dialog to select from known varbits or varplayers - */ - private static void showVarLookupDialog(Component parent, boolean isVarbit, VarSelectCallback callback) { - // Create a dialog for variable selection - JDialog dialog = new JDialog(SwingUtilities.getWindowAncestor(parent), - (isVarbit ? "Select Varbit" : "Select VarPlayer"), Dialog.ModalityType.APPLICATION_MODAL); - dialog.setLayout(new BorderLayout()); - dialog.setSize(600, 500); - dialog.setLocationRelativeTo(parent); - - JPanel contentPanel = new JPanel(new BorderLayout()); - contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - contentPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Add category filter dropdown - JPanel filterPanel = new JPanel(new BorderLayout()); - filterPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - filterPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0)); - - JPanel categoryPanel = new JPanel(new BorderLayout()); - categoryPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - categoryPanel.add(new JLabel("Category:"), BorderLayout.WEST); - - JComboBox categoryComboBox = new JComboBox<>( - new String[]{"All Categories", "Collection Log", "Bosses", "Minigames", "Quests", "Skills", "Diaries", "Features", "Items", "Other"} - ); - categoryPanel.add(categoryComboBox, BorderLayout.CENTER); - - // Search field - JPanel searchPanel = new JPanel(new BorderLayout()); - searchPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - searchPanel.setBorder(BorderFactory.createEmptyBorder(5, 0, 0, 0)); - searchPanel.add(new JLabel("Search:"), BorderLayout.WEST); - - JTextField searchField = new JTextField(); - searchField.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - searchField.setForeground(Color.WHITE); - searchPanel.add(searchField, BorderLayout.CENTER); - - filterPanel.add(categoryPanel, BorderLayout.NORTH); - filterPanel.add(searchPanel, BorderLayout.SOUTH); - - // List model and list - DefaultListModel varListModel = new DefaultListModel<>(); - - // Get entries from VarbitUtil - Map constantMap = isVarbit ? VarbitUtil.getAllVarbitEntries() : VarbitUtil.getAllVarPlayerEntries(); - for (Map.Entry entry : constantMap.entrySet()) { - varListModel.addElement(new VarbitUtil.VarEntry(entry.getKey(), VarbitUtil.formatConstantName(entry.getValue()))); - } - - JList varList = new JList<>(varListModel); - varList.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - varList.setForeground(Color.WHITE); - varList.setCellRenderer(new DefaultListCellRenderer() { - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, - boolean isSelected, boolean cellHasFocus) { - super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - if (value instanceof VarbitUtil.VarEntry) { - VarbitUtil.VarEntry entry = (VarbitUtil.VarEntry) value; - setText(entry.name + " (" + entry.id + ")"); - } - return this; - } - }); - - JScrollPane scrollPane = new JScrollPane(varList); - scrollPane.getVerticalScrollBar().setUnitIncrement(16); - - // Filter the list when search text changes or category changes - DocumentListener searchListener = new DocumentListener() { - @Override - public void insertUpdate(DocumentEvent e) { filterList(); } - - @Override - public void removeUpdate(DocumentEvent e) { filterList(); } - - @Override - public void changedUpdate(DocumentEvent e) { filterList(); } - - private void filterList() { - String searchText = searchField.getText().toLowerCase(); - String category = (String) categoryComboBox.getSelectedItem(); - DefaultListModel filteredModel = new DefaultListModel<>(); - - Map constantMap = isVarbit ? VarbitUtil.getAllVarbitEntries() : VarbitUtil.getAllVarPlayerEntries(); - List filteredEntries = new ArrayList<>(); - - if ("Collection Log".equals(category)) { - // For collection log category - for (Map.Entry entry : constantMap.entrySet()) { - if (entry.getValue().contains("COLLECTION")) { - String formatted = VarbitUtil.formatConstantName(entry.getValue()); - if (formatted.toLowerCase().contains(searchText)) { - filteredEntries.add(new VarbitUtil.VarEntry(entry.getKey(), formatted)); - } - } - } - } else if (!"All Categories".equals(category)) { - // For other specific categories, use the VarbitUtil's categorization - List categoryEntries = VarbitUtil.getVarbitEntriesByCategory(category); - for (VarbitUtil.VarEntry entry : categoryEntries) { - if (entry.name.toLowerCase().contains(searchText)) { - filteredEntries.add(entry); - } - } - } else { - // For "All Categories", search all entries - for (Map.Entry entry : constantMap.entrySet()) { - String formatted = VarbitUtil.formatConstantName(entry.getValue()); - if (formatted.toLowerCase().contains(searchText) || - entry.getValue().toLowerCase().contains(searchText) || - String.valueOf(entry.getKey()).contains(searchText)) { - filteredEntries.add(new VarbitUtil.VarEntry(entry.getKey(), formatted)); - } - } - } - - // Sort entries by name - filteredEntries.sort(Comparator.comparing(e -> e.name)); - - for (VarbitUtil.VarEntry entry : filteredEntries) { - filteredModel.addElement(entry); - } - - varList.setModel(filteredModel); - } - }; - - searchField.getDocument().addDocumentListener(searchListener); - categoryComboBox.addActionListener(e -> searchListener.changedUpdate(null)); - - // Buttons panel - JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - buttonsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JButton selectButton = new JButton("Select"); - selectButton.setBackground(ColorScheme.BRAND_ORANGE); - selectButton.setForeground(Color.WHITE); - selectButton.addActionListener(e -> { - VarbitUtil.VarEntry selected = varList.getSelectedValue(); - if (selected != null) { - callback.onVarSelected(selected.id); - dialog.dispose(); - } - }); - - JButton cancelButton = new JButton("Cancel"); - cancelButton.setBackground(ColorScheme.LIGHT_GRAY_COLOR); - cancelButton.setForeground(Color.BLACK); - cancelButton.addActionListener(e -> dialog.dispose()); - - buttonsPanel.add(cancelButton); - buttonsPanel.add(selectButton); - - // Double-click to select - varList.addMouseListener(new java.awt.event.MouseAdapter() { - @Override - public void mouseClicked(java.awt.event.MouseEvent e) { - if (e.getClickCount() == 2) { - VarbitUtil.VarEntry selected = varList.getSelectedValue(); - if (selected != null) { - callback.onVarSelected(selected.id); - dialog.dispose(); - } - } - } - }); - - // Add everything to dialog - contentPanel.add(filterPanel, BorderLayout.NORTH); - contentPanel.add(scrollPane, BorderLayout.CENTER); - contentPanel.add(buttonsPanel, BorderLayout.SOUTH); - - dialog.add(contentPanel); - dialog.setVisible(true); - } - - - - /** - * Custom DocumentListener that can be toggled between auto-updating and manual modes - */ - private static class EditableDocumentListener implements DocumentListener { - private boolean userEditing = false; - private final Runnable updateAction; - - public EditableDocumentListener(Runnable updateAction) { - this.updateAction = updateAction; - } - - public void setUserEditing(boolean editing) { - this.userEditing = editing; - } - - @Override - public void insertUpdate(DocumentEvent e) { - if (!userEditing) { - // Use invokeLater to avoid mutating during notification - SwingUtilities.invokeLater(updateAction); - } - } - - @Override - public void removeUpdate(DocumentEvent e) { - if (!userEditing) { - // Use invokeLater to avoid mutating during notification - SwingUtilities.invokeLater(updateAction); - } - } - - @Override - public void changedUpdate(DocumentEvent e) { - if (!userEditing) { - // Use invokeLater to avoid mutating during notification - SwingUtilities.invokeLater(updateAction); - } - } - } - - /** - * Sets up the varbit condition panel with values from an existing condition - * - * @param panel The panel containing the UI components - * @param condition The varbit condition to read values from - */ - public static void setupVarbitCondition(JPanel panel, VarbitCondition condition) { - if (condition == null) return; - - // Get UI components - JComboBox varTypeComboBox = (JComboBox) panel.getClientProperty("varbitTypeComboBox"); - JTextField idField = (JTextField) panel.getClientProperty("varbitIdField"); - JComboBox operatorComboBox = (JComboBox) panel.getClientProperty("varbitOperatorComboBox"); - JSpinner targetValueSpinner = (JSpinner) panel.getClientProperty("varbitTargetValueSpinner"); - - JRadioButton relativeMode = (JRadioButton) panel.getClientProperty("varbitRelativeMode"); - JRadioButton absoluteMode = (JRadioButton) panel.getClientProperty("varbitAbsoluteMode"); - JLabel currentValueDisplay = (JLabel) panel.getClientProperty("varbitCurrentValueDisplay"); - JCheckBox randomizeCheckBox = (JCheckBox) panel.getClientProperty("varbitRandomize"); - JSpinner minValueSpinner = (JSpinner) panel.getClientProperty("varbitMinValueSpinner"); - JSpinner maxValueSpinner = (JSpinner) panel.getClientProperty("varbitMaxValueSpinner"); - JPanel minMaxPanel = (JPanel) panel.getClientProperty("varbitMinMaxPanel"); - JComboBox categoryEntriesComboBox = (JComboBox) panel.getClientProperty("varbitCategoryEntriesComboBox"); - - if (varTypeComboBox == null || idField == null || operatorComboBox == null || targetValueSpinner == null || - relativeMode == null || absoluteMode == null) { - return; - } - - int varId = condition.getVarId(); - idField.setText(String.valueOf(varId)); - - // Set var type - varTypeComboBox.setSelectedItem(condition.getVarType() == VarbitCondition.VarType.VARBIT ? "Varbit" : "VarPlayer"); - - // Set operator - operatorComboBox.setSelectedItem(condition.getOperator().getDisplayName()); - - // Set mode - relativeMode.setSelected(condition.isRelative()); - absoluteMode.setSelected(!condition.isRelative()); - - // Set randomization - randomizeCheckBox.setSelected(condition.isRandomized()); - if (condition.isRandomized()) { - minValueSpinner.setValue(condition.getTargetValueMin()); - maxValueSpinner.setValue(condition.getTargetValueMax()); - minMaxPanel.setVisible(true); - targetValueSpinner.setEnabled(false); - } else { - targetValueSpinner.setValue(condition.getTargetValue()); - minMaxPanel.setVisible(false); - } - - // Set current value - currentValueDisplay.setText(String.valueOf(condition.getCurrentValue())); - - // If we have a category-specific panel, try to select the matching entry - String specificCategory = (String) panel.getClientProperty("varbitSpecificCategory"); - if (specificCategory != null && categoryEntriesComboBox != null) { - // Get the varbit name if available - String varbitName = VarbitUtil.getConstantNameForId( - condition.getVarType() == VarbitCondition.VarType.VARBIT, varId); - - // Try to find and select the matching entry in the dropdown - if (varbitName != null) { - for (int i = 0; i < categoryEntriesComboBox.getItemCount(); i++) { - String item = categoryEntriesComboBox.getItemAt(i); - if (item.contains("(" + varId + ")")) { - categoryEntriesComboBox.setSelectedIndex(i); - break; - } - } - } - } - } - - /** - * Creates a VarbitCondition from the panel configuration - * - * @param panel The panel containing the configuration - * @return A new VarbitCondition - */ - public static VarbitCondition createVarbitCondition(JPanel panel) { - // Get all required UI components using getClientProperty - JComboBox varTypeComboBox = (JComboBox) panel.getClientProperty("varbitTypeComboBox"); - JTextField idField = (JTextField) panel.getClientProperty("varbitIdField"); - JComboBox operatorComboBox = (JComboBox) panel.getClientProperty("varbitOperatorComboBox"); - JSpinner targetValueSpinner = (JSpinner) panel.getClientProperty("varbitTargetValueSpinner"); - JRadioButton relativeMode = (JRadioButton) panel.getClientProperty("varbitRelativeMode"); - JCheckBox randomizeCheckBox = (JCheckBox) panel.getClientProperty("varbitRandomize"); - JSpinner minValueSpinner = (JSpinner) panel.getClientProperty("varbitMinValueSpinner"); - JSpinner maxValueSpinner = (JSpinner) panel.getClientProperty("varbitMaxValueSpinner"); - - // Get the category entries dropdown (exists in both general and specific panels) - JComboBox categoryEntriesComboBox = (JComboBox) panel.getClientProperty("varbitCategoryEntriesComboBox"); - - // Check if we have the core required components - if (varTypeComboBox == null || idField == null || operatorComboBox == null || - targetValueSpinner == null || relativeMode == null || randomizeCheckBox == null) { - throw new IllegalStateException("Missing required UI components for Varbit condition"); - } - - // Check for category-specific panels - String specificCategory = (String) panel.getClientProperty("varbitSpecificCategory"); - if (specificCategory != null && categoryEntriesComboBox != null) { - // For category-specific panels, ensure we have a selection in the dropdown - if (categoryEntriesComboBox.getSelectedIndex() <= 0) { - throw new IllegalArgumentException("Please select a " + - (specificCategory.equals("Bosses") ? "boss" : "minigame")); - } - } - - // Get values from UI components - String name = varTypeComboBox.getSelectedItem().toString(); - VarbitCondition.VarType varType = varTypeComboBox.getSelectedItem().equals("Varbit") - ? VarbitCondition.VarType.VARBIT - : VarbitCondition.VarType.VARPLAYER; - - // Parse var ID - int varId; - try { - varId = Integer.parseInt(idField.getText().trim()); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid variable ID: " + idField.getText()); - } - - // Get comparison operator - get enum value by ordinal - int operatorIndex = operatorComboBox.getSelectedIndex(); - VarbitCondition.ComparisonOperator operator = VarbitCondition.ComparisonOperator.values()[operatorIndex]; - - // Get target value(s) - int targetValue = (Integer) targetValueSpinner.getValue(); - boolean isRelative = relativeMode.isSelected(); - - // Create condition based on configuration - if (randomizeCheckBox.isSelected()) { - int minValue = (Integer) minValueSpinner.getValue(); - int maxValue = (Integer) maxValueSpinner.getValue(); - - if (isRelative) { - return VarbitCondition.createRelativeRandomized(name, varType, varId, minValue, maxValue, operator); - } else { - return VarbitCondition.createRandomized(name, varType, varId, minValue, maxValue, operator); - } - } else { - if (isRelative) { - return VarbitCondition.createRelative(name, varType, varId, targetValue, operator); - } else { - return new VarbitCondition(name, varType, varId, targetValue, operator); - } - } - } - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ScheduleEntryConfigManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ScheduleEntryConfigManager.java deleted file mode 100644 index 626a6557b40..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ScheduleEntryConfigManager.java +++ /dev/null @@ -1,612 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.config; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.config.ConfigDescriptor; -import net.runelite.client.config.ConfigGroup; -import net.runelite.client.config.ConfigItemDescriptor; -import net.runelite.client.config.ConfigSectionDescriptor; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.api.SchedulablePlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.config.ui.ScheduleEntryConfigManagerPanel; -import java.util.Collection; -import java.util.Optional; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import javax.swing.JPanel; - - -/** - * Utility class for managing plugin configuration interactions from PluginScheduleEntry - */ -@Slf4j -public class ScheduleEntryConfigManager { - private ConfigDescriptor initialConfigPluginDescriptor; // Initial configuration from plugin (now final) - private ConfigDescriptor configScheduleEntryDescriptor; - private Supplier configPluginDescriptorProvider; - private JPanel configPanel; - - - public void setConfigPluginDescriptor(ConfigDescriptor configPluginDescriptor) { - if (configScheduleEntryDescriptor == null) { - log.info("Setting configScheduleEntryDescriptor to configPluginDescriptor"); - this.configScheduleEntryDescriptor = configPluginDescriptor; - } - // Cannot modify initialConfigPluginDescriptor as it's final now - } - - /** - * Sets a provider function that returns the current ConfigDescriptor for a plugin - * This is more flexible than directly storing the descriptor as it can get updated values - * - * @param provider A supplier function that returns the current ConfigDescriptor - */ - public void setConfigPluginDescriptorProvider(Supplier provider) { - this.configPluginDescriptorProvider = provider; - } - - /** - * Sets a provider function that returns the current ConfigDescriptor for a SchedulablePlugin - * - * @param plugin The SchedulablePlugin that provides the ConfigDescriptor - */ - public void setConfigPluginDescriptorProvider(SchedulablePlugin plugin) { - if (plugin != null) { - this.configPluginDescriptorProvider = plugin::getConfigDescriptor; - this.initialConfigPluginDescriptor = plugin.getConfigDescriptor(); - // Initialize with the current value - ConfigDescriptor descriptor = plugin.getConfigDescriptor(); - if (descriptor != null) { - // Only update schedule entry descriptor, not the initial descriptor - this.configScheduleEntryDescriptor = descriptor; - } - } - } - - /** - * Gets the current ConfigDescriptor from the provider if available, - * otherwise returns the stored descriptor - * - * @return The current ConfigDescriptor from the provider or stored value - */ - public ConfigDescriptor getCurrentPluginConfigDescriptor() { - if (configPluginDescriptorProvider != null) { - ConfigDescriptor current = configPluginDescriptorProvider.get(); - if (current != null) { - return current; - } - } - return initialConfigPluginDescriptor; - } - - //private final JPanel configPanel; - /** - * Constructs a new configuration manager for a specific plugin - * - * @param configPluginDescriptor The config descriptor from the plugin - */ - public ScheduleEntryConfigManager() { - this.initialConfigPluginDescriptor = null; - } - /** - * Constructs a new configuration manager for a specific plugin - * - * @param configPluginDescriptor The config descriptor from the plugin - */ - public ScheduleEntryConfigManager(ConfigDescriptor configPluginDescriptor) { - this.initialConfigPluginDescriptor = configPluginDescriptor; - //this.configGroup = configPluginDescriptor != null ? configPluginDescriptor.getGroup().value() : null; - // Initialize with the plugin's config descriptor - this.configScheduleEntryDescriptor = configPluginDescriptor; - } - - /** - * Constructs a new configuration manager with a provider function - * - * @param provider A supplier function that returns the current ConfigDescriptor - */ - public ScheduleEntryConfigManager(Supplier provider) { - this.configPluginDescriptorProvider = provider; - ConfigDescriptor initialDescriptor = provider.get(); - this.initialConfigPluginDescriptor = initialDescriptor; - if (initialDescriptor != null) { - this.configScheduleEntryDescriptor = initialDescriptor; - } - } - - /** - * Constructs a new configuration manager for a specific SchedulablePlugin - * - * @param plugin The SchedulablePlugin that provides the ConfigDescriptor - */ - public ScheduleEntryConfigManager(SchedulablePlugin plugin) { - if (plugin != null) { - this.configPluginDescriptorProvider = plugin::getConfigDescriptor; - ConfigDescriptor initialDescriptor = plugin.getConfigDescriptor(); - this.initialConfigPluginDescriptor = initialDescriptor; - if (initialDescriptor != null) { - this.configScheduleEntryDescriptor = initialDescriptor; - } - } else { - this.initialConfigPluginDescriptor = null; - } - } - - /** - * Finds a config item descriptor by its name - * - * @param name The display name of the config item - * @return Optional containing the config item descriptor if found - */ - public Optional findConfigItemByName(String name) { - if (configScheduleEntryDescriptor == null) { - return Optional.empty(); - } - - Collection items = configScheduleEntryDescriptor.getItems(); - return items.stream() - .filter(item -> item.name().equals(name)) - .findFirst(); - } - - /** - * Finds a config item descriptor by its key name - * - * @param keyName The key name of the config item - * @return Optional containing the config item descriptor if found - */ - public Optional findConfigItemByKeyName(String keyName) { - if (configScheduleEntryDescriptor == null) { - return Optional.empty(); - } - - Collection items = configScheduleEntryDescriptor.getItems(); - return items.stream() - .filter(item -> item.key().equals(keyName)) - .findFirst(); - } - - /** - * Gets the configuration value for a specific key - * - * @param keyName The key name of the config item - * @param type The type of the config value - * @return The configuration value, or null if not found - */ - public T getConfiguration(String keyName, Class type) { - if (getConfigGroup() == null) { - return null; - } - return Microbot.getConfigManager().getConfiguration(getConfigGroup(), keyName, type); - } - - /** - * Sets the configuration value for a specific key - * - * @param keyName The key name of the config item - * @param value The value to set - */ - public void setConfiguration(String keyName, T value) { - if (getConfigGroup() == null) { - return; - } - - Microbot.getConfigManager().setConfiguration(getConfigGroup(), keyName, value); - } - - /** - * Gets the boolean value of a config item - * - * @param keyName The key name of the config item - * @return The boolean value, or false if the item doesn't exist or is not a boolean - */ - public boolean getBooleanValue(String keyName) { - Boolean value = getConfiguration(keyName, Boolean.class); - return value != null && value; - } - - /** - * Gets the string value of a config item - * - * @param keyName The key name of the config item - * @return The string value, or null if not found - */ - public String getStringValue(String keyName) { - return getConfiguration(keyName, String.class); - } - - /** - * Gets the integer value of a config item - * - * @param keyName The key name of the config item - * @return The integer value, or null if not found - */ - public Integer getIntegerValue(String keyName) { - return getConfiguration(keyName, Integer.class); - } - - /** - * Sets the schedule mode of the plugin. - * This is a utility function to indicate whether a plugin is currently being managed - * by the scheduler. - * - * @param isActive Whether the plugin is actively being scheduled - */ - public void setScheduleMode(boolean isActive) { - setConfiguration("scheduleMode", isActive); - } - - /** - * Gets the current schedule mode of the plugin - * - * @return Whether the plugin is in schedule mode - */ - public boolean isInScheduleMode() { - return getBooleanValue("scheduleMode"); - } - - /** - * Gets the configuration group name - * - * @return The configuration group name - */ - public String getConfigGroup() { - - - if (initialConfigPluginDescriptor == null) { - return configScheduleEntryDescriptor != null ? configScheduleEntryDescriptor.getGroup().value() : null; - } - return initialConfigPluginDescriptor != null ? initialConfigPluginDescriptor.getGroup().value() : null; - } - - - /** - * Gets the current schedule entry's configuration descriptor - * - * @return The current schedule entry's configuration descriptor - */ - public ConfigDescriptor getConfigScheduleEntryDescriptor() { - return configScheduleEntryDescriptor; - } - - /** - * Sets the current schedule entry's configuration descriptor - * - * @param configScheduleEntryDescriptor The configuration descriptor to set - */ - public void setConfigScheduleEntryDescriptor(ConfigDescriptor configScheduleEntryDescriptor) { - this.configScheduleEntryDescriptor = configScheduleEntryDescriptor; - } - - /** - * Gets all configuration items from the current schedule entry's config descriptor - * - * @return Collection of config item descriptors - */ - public Collection getAllConfigItems() { - if (configScheduleEntryDescriptor == null) { - return java.util.Collections.emptyList(); - } - - return configScheduleEntryDescriptor.getItems(); - } - - /** - * Applies the saved schedule entry configuration to the current plugin - * This should be called when starting a scheduled plugin to use its custom config - */ - public void applyScheduleEntryConfig() { - if (configScheduleEntryDescriptor == null || initialConfigPluginDescriptor == null) { - return; - } - - // Apply configuration values from the schedule entry config - for (ConfigItemDescriptor item : configScheduleEntryDescriptor.getItems()) { - // Find corresponding item in plugin config - initialConfigPluginDescriptor.getItems().stream() - .filter(pluginItem -> pluginItem.key().equals(item.key())) - .findFirst() - .ifPresent(pluginItem -> { - // Copy configuration value from schedule entry to plugin - try { - Object value = Microbot.getConfigManager().getConfiguration( - configScheduleEntryDescriptor.getGroup().value(), - item.key(), - item.getType() - ); - - if (value != null) { - Microbot.getConfigManager().setConfiguration( - initialConfigPluginDescriptor.getGroup().value(), - pluginItem.key(), - value - ); - } - } catch (Exception e) { - log.error("Failed to apply config item " + item.key(), e); - } - }); - } - } - - - - /** - * Applies the original plugin configuration to reset the plugin to default settings - * This should be called when stopping a scheduled plugin to reset its configuration - * Renamed from applyPluginConfig to applyInitialPluginConfig - */ - public void applyInitialPluginConfig() { - // Use the initial config descriptor directly instead of getting from provider - ConfigDescriptor descriptorToApply = initialConfigPluginDescriptor; - - if (descriptorToApply == null) { - log.warn("No initial config descriptor available to apply plugin config"); - return; - } - - // Apply configuration values from the original plugin config descriptor - for (ConfigItemDescriptor item : descriptorToApply.getItems()) { - try { - Object value = Microbot.getConfigManager().getConfiguration( - descriptorToApply.getGroup().value(), - item.key(), - item.getType() - ); - - if (value != null) { - Microbot.getConfigManager().setConfiguration( - descriptorToApply.getGroup().value(), - item.key(), - value - ); - } - } catch (Exception e) { - log.error("Failed to apply original config item " + item.key(), e); - } - } - - log.debug("Applied initial plugin configuration"); - } - - /** - * Gets or creates a configuration panel for the current schedule entry config - * - * @return JPanel containing configuration controls, or null if no configuration is available - */ - public JPanel getConfigPanel() { - if (configScheduleEntryDescriptor == null) { - return null; - } - - // Return cached panel if it exists - if (configPanel != null) { - return configPanel; - } - - try { - // Create a new panel using the ScheduleEntryConfigManagerPanel class - configPanel = new ScheduleEntryConfigManagerPanel(Microbot.getConfigManager(), configScheduleEntryDescriptor); - return configPanel; - } catch (Exception e) { - log.error("Error creating config panel", e); - return null; - } - } - - /** - * Refreshes the configuration panel if it exists. - * Call this when the configScheduleEntryDescriptor has been updated. - */ - public void refreshConfigPanel() { - if (configPanel != null) { - configPanel = new ScheduleEntryConfigManagerPanel(Microbot.getConfigManager(), configScheduleEntryDescriptor); - } - } - - /** - * Returns whether there is configuration available for this plugin - * - * @return true if configuration is available, false otherwise - */ - public boolean hasConfiguration() { - return configScheduleEntryDescriptor != null && - !configScheduleEntryDescriptor.getItems().isEmpty(); - } - - /** - * Logs the current ConfigDescriptor for debugging purposes - */ - public void logConfigDescriptor() { - log.debug("Plugin ConfigDescriptor: \n{}", getPluginConfigDescriptorString()); - log.debug("Schedule Entry ConfigDescriptor: \n{}", getScheduleEntryConfigDescriptorString()); - } - - /** - * Returns a string representation of the plugin ConfigDescriptor - * - * @return A readable string representation of the plugin ConfigDescriptor - */ - public String getPluginConfigDescriptorString() { - return configDescriptorToString(initialConfigPluginDescriptor); - } - - /** - * Returns a string representation of the schedule entry ConfigDescriptor - * - * @return A readable string representation of the schedule entry ConfigDescriptor - */ - public String getScheduleEntryConfigDescriptorString() { - return configDescriptorToString(configScheduleEntryDescriptor); - } - - /** - * Converts a ConfigDescriptor to a readable string representation - * - * @param descriptor The ConfigDescriptor to convert - * @return A string representation of the ConfigDescriptor - */ - private String configDescriptorToString(ConfigDescriptor descriptor) { - if (descriptor == null) { - return "null"; - } - - StringBuilder sb = new StringBuilder(); - sb.append("ConfigDescriptor{"); - - // Add group info - sb.append("group=").append(configGroupToString(descriptor.getGroup())); - - // Add sections - sb.append(", sections=["); - if (descriptor.getSections() != null) { - sb.append(descriptor.getSections().stream() - .map(this::configSectionDescriptorToString) - .collect(Collectors.joining(", "))); - } - sb.append("]"); - - // Add items - sb.append(", items=["); - if (descriptor.getItems() != null) { - sb.append(descriptor.getItems().stream() - .map(this::configItemDescriptorToString) - .collect(Collectors.joining(", "))); - } - sb.append("]"); - - // Add information if present - if (descriptor.getInformation() != null) { - sb.append(", information='").append(descriptor.getInformation().value()).append("'"); - } - - sb.append("}"); - return sb.toString(); - } - - /** - * Converts a ConfigGroup to a readable string representation - * - * @param group The ConfigGroup to convert - * @return A string representation of the ConfigGroup - */ - private String configGroupToString(ConfigGroup group) { - if (group == null) { - return "null"; - } - - return "'" + group.value() + "'"; - } - - /** - * Converts a ConfigSectionDescriptor to a readable string representation - * - * @param section The ConfigSectionDescriptor to convert - * @return A string representation of the ConfigSectionDescriptor - */ - private String configSectionDescriptorToString(ConfigSectionDescriptor section) { - if (section == null) { - return "null"; - } - - StringBuilder sb = new StringBuilder(); - sb.append("Section{"); - sb.append("key='").append(section.key()).append("'"); - sb.append(", name='").append(section.name()).append("'"); - sb.append(", position=").append(section.position()); - - if (section.getSection() != null) { - sb.append(", description='").append(section.getSection().description()).append("'"); - sb.append(", closedByDefault=").append(section.getSection().closedByDefault()); - } - - sb.append("}"); - return sb.toString(); - } - - /** - * Converts a ConfigItemDescriptor to a readable string representation - * - * @param item The ConfigItemDescriptor to convert - * @return A string representation of the ConfigItemDescriptor - */ - private String configItemDescriptorToString(ConfigItemDescriptor item) { - if (item == null) { - return "null"; - } - - StringBuilder sb = new StringBuilder(); - sb.append("Item{"); - sb.append("key='").append(item.key()).append("'"); - - if (item.getItem() != null) { - sb.append(", name='").append(item.getItem().name()).append("'"); - sb.append(", description='").append(item.getItem().description()).append("'"); - sb.append(", position=").append(item.getItem().position()); - - if (!item.getItem().section().isEmpty()) { - sb.append(", section='").append(item.getItem().section()).append("'"); - } - - if (item.getItem().hidden()) { - sb.append(", hidden=true"); - } - - if (item.getItem().secret()) { - sb.append(", secret=true"); - } - - if (!item.getItem().warning().isEmpty()) { - sb.append(", warning='").append(item.getItem().warning()).append("'"); - } - } - - if (item.getType() != null) { - sb.append(", type=").append(item.getType().getTypeName()); - } - - if (item.getRange() != null) { - sb.append(", range=[min=").append(item.getRange().min()); - sb.append(", max=").append(item.getRange().max()); - sb.append("]"); - } - - if (item.getAlpha() != null) { - sb.append(", hasAlpha=true"); - } - - if (item.getUnits() != null) { - sb.append(", units='").append(item.getUnits().value()).append("'"); - } - - sb.append("}"); - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("ScheduleEntryConfigManager{"); - - sb.append("configGroup='").append(getConfigGroup()).append("'"); - - if (configPluginDescriptorProvider != null) { - sb.append(", hasConfigProvider=true"); - } else { - sb.append(", hasConfigProvider=false"); - } - - if (initialConfigPluginDescriptor != null) { - sb.append(", pluginConfig=").append(getPluginConfigDescriptorString()); - } else { - sb.append(", pluginConfig=null"); - } - - if (configScheduleEntryDescriptor != null) { - sb.append(", scheduleEntryConfig=").append(getScheduleEntryConfigDescriptorString()); - } else { - sb.append(", scheduleEntryConfig=null"); - } - - sb.append("}"); - return sb.toString(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ui/HotkeyButton.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ui/HotkeyButton.java deleted file mode 100644 index a8f7240c1f0..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ui/HotkeyButton.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2018 Abex - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package net.runelite.client.plugins.microbot.pluginscheduler.config.ui; - -import java.awt.event.KeyAdapter; -import java.awt.event.KeyEvent; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import javax.swing.JButton; -import lombok.Getter; -import net.runelite.client.config.Keybind; -import net.runelite.client.config.ModifierlessKeybind; -import net.runelite.client.ui.FontManager; - -class HotkeyButton extends JButton -{ - @Getter - private Keybind value; - - public HotkeyButton(Keybind value, boolean modifierless) - { - // Disable focus traversal keys such as tab to allow tab key to be bound - setFocusTraversalKeysEnabled(false); - setFont(FontManager.getDefaultFont().deriveFont(12.f)); - setValue(value); - addMouseListener(new MouseAdapter() - { - @Override - public void mouseReleased(MouseEvent e) - { - // Mouse buttons other than button1 don't give focus - if (e.getButton() == MouseEvent.BUTTON1) - { - // We have to use a mouse adapter instead of an action listener so the press action key (space) can be bound - setValue(Keybind.NOT_SET); - } - } - }); - - addKeyListener(new KeyAdapter() - { - @Override - public void keyPressed(KeyEvent e) - { - if (modifierless) - { - setValue(new ModifierlessKeybind(e)); - } - else - { - setValue(new Keybind(e)); - } - } - }); - } - - public void setValue(Keybind value) - { - if (value == null) - { - value = Keybind.NOT_SET; - } - - this.value = value; - setText(value.toString()); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ui/ScheduleEntryConfigManagerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ui/ScheduleEntryConfigManagerPanel.java deleted file mode 100644 index d38708f47b3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ui/ScheduleEntryConfigManagerPanel.java +++ /dev/null @@ -1,619 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.config.ui; - -import com.google.common.base.MoreObjects; -import com.google.common.base.Strings; -import com.google.common.collect.ComparisonChain; -import com.google.common.collect.Sets; -import com.google.common.primitives.Ints; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.config.*; -import net.runelite.client.plugins.microbot.Microbot; - -import net.runelite.client.plugins.microbot.MicrobotPlugin; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.DynamicGridLayout; -import net.runelite.client.ui.FontManager; - -import net.runelite.client.ui.UnitFormatterFactory; -import net.runelite.client.ui.components.ColorJButton; -import net.runelite.client.ui.components.TitleCaseListCellRenderer; -import net.runelite.client.ui.components.colorpicker.ColorPickerManager; -import net.runelite.client.ui.components.colorpicker.RuneliteColorPicker; -import net.runelite.client.util.ColorUtil; -import net.runelite.client.util.ImageUtil; -import net.runelite.client.util.SwingUtil; -import net.runelite.client.util.Text; -import org.apache.commons.lang3.ArrayUtils; - -import javax.inject.Inject; - -import javax.swing.*; -import javax.swing.border.CompoundBorder; -import javax.swing.border.EmptyBorder; -import javax.swing.border.LineBorder; -import javax.swing.border.MatteBorder; -import javax.swing.event.ChangeListener; -import javax.swing.text.JTextComponent; -import java.awt.*; -import java.awt.event.*; -import java.awt.image.BufferedImage; -import java.lang.reflect.ParameterizedType; -import java.util.List; -import java.util.*; - - -@Slf4j -public class ScheduleEntryConfigManagerPanel extends JPanel { - private static final int BORDER_OFFSET = 5; - private static final int PANEL_WIDTH = 220; - private static final int SPINNER_FIELD_WIDTH = 6; - private final JPanel mainPanel; - @Getter - private final ConfigDescriptor configDescriptor; - private final ConfigManager configManager; - private static final Map sectionExpandStates = new HashMap<>(); - private static final ImageIcon SECTION_EXPAND_ICON; - private static final ImageIcon SECTION_RETRACT_ICON; - static final ImageIcon CONFIG_ICON; - static final ImageIcon BACK_ICON; - private final TitleCaseListCellRenderer listCellRenderer = new TitleCaseListCellRenderer(); - @Inject - private ColorPickerManager colorPickerManager; - //@Inject - //private final Provider notificationPanelProvider; - //private final JPanel configPanel; - static - { - final BufferedImage backIcon = ImageUtil.loadImageResource(MicrobotPlugin.class, "config_back_icon.png"); - BACK_ICON = new ImageIcon(backIcon); - - BufferedImage sectionRetractIcon = ImageUtil.loadImageResource(MicrobotPlugin.class, "/util/arrow_right.png"); - sectionRetractIcon = ImageUtil.luminanceOffset(sectionRetractIcon, -121); - SECTION_EXPAND_ICON = new ImageIcon(sectionRetractIcon); - final BufferedImage sectionExpandIcon = ImageUtil.rotateImage(sectionRetractIcon, Math.PI / 2); - SECTION_RETRACT_ICON = new ImageIcon(sectionExpandIcon); - BufferedImage configIcon = ImageUtil.loadImageResource(MicrobotPlugin.class, "config_edit_icon.png"); - CONFIG_ICON = new ImageIcon(configIcon); - } - /** - * Constructs a new configuration manager for a specific plugin - * - * @param configDescriptor The config descriptor from the plugin - */ - public ScheduleEntryConfigManagerPanel(ConfigManager configManager, ConfigDescriptor configDescriptor) { - this.configManager = configManager; - this.configDescriptor = configDescriptor; - mainPanel = new JPanel(); - mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); - rebuild(); - //mainPanel.setBorder(new EmptyBorder(8, 10, 10, 10)); - //mainPanel.setLayout(new DynamicGridLayout(0, 1, 0, 5)); - //mainPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - } - - private void toggleSection(ConfigSectionDescriptor csd, JButton button, JPanel contents) - { - boolean newState = !contents.isVisible(); - contents.setVisible(newState); - button.setIcon(newState ? SECTION_RETRACT_ICON : SECTION_EXPAND_ICON); - button.setToolTipText(newState ? "Retract" : "Expand"); - sectionExpandStates.put(csd, newState); - SwingUtilities.invokeLater(contents::revalidate); - } - private void rebuild() - { - mainPanel.removeAll(); - - - ConfigDescriptor cd = getConfigDescriptor(); - - final Map sectionWidgets = new HashMap<>(); - final Map topLevelPanels = new TreeMap<>((a, b) -> - ComparisonChain.start() - .compare(a.position(), b.position()) - .compare(a.name(), b.name()) - .result()); - - if (cd.getInformation() != null) { - buildInformationPanel(cd.getInformation()); - } - - for (ConfigSectionDescriptor csd : cd.getSections()) - { - ConfigSection cs = csd.getSection(); - final boolean isOpen = sectionExpandStates.getOrDefault(csd, !cs.closedByDefault()); - - final JPanel section = new JPanel(); - section.setLayout(new BoxLayout(section, BoxLayout.Y_AXIS)); - section.setMinimumSize(new Dimension(PANEL_WIDTH, 0)); - - final JPanel sectionHeader = new JPanel(); - sectionHeader.setLayout(new BorderLayout()); - sectionHeader.setMinimumSize(new Dimension(PANEL_WIDTH, 0)); - // For whatever reason, the header extends out by a single pixel when closed. Adding a single pixel of - // border on the right only affects the width when closed, fixing the issue. - sectionHeader.setBorder(new CompoundBorder( - new MatteBorder(0, 0, 1, 0, ColorScheme.MEDIUM_GRAY_COLOR), - new EmptyBorder(0, 0, 3, 1))); - section.add(sectionHeader, BorderLayout.NORTH); - - final JButton sectionToggle = new JButton(isOpen ? SECTION_RETRACT_ICON : SECTION_EXPAND_ICON); - sectionToggle.setPreferredSize(new Dimension(18, 0)); - sectionToggle.setBorder(new EmptyBorder(0, 0, 0, 5)); - sectionToggle.setToolTipText(isOpen ? "Retract" : "Expand"); - SwingUtil.removeButtonDecorations(sectionToggle); - sectionHeader.add(sectionToggle, BorderLayout.WEST); - - String name = cs.name(); - final JLabel sectionName = new JLabel(name); - sectionName.setForeground(ColorScheme.BRAND_ORANGE); - sectionName.setFont(FontManager.getRunescapeBoldFont()); - sectionName.setToolTipText("" + name + ":
" + cs.description() + ""); - sectionHeader.add(sectionName, BorderLayout.CENTER); - - final JPanel sectionContents = new JPanel(); - sectionContents.setLayout(new DynamicGridLayout(0, 1, 0, 5)); - sectionContents.setMinimumSize(new Dimension(PANEL_WIDTH, 0)); - sectionContents.setBorder(new CompoundBorder( - new MatteBorder(0, 0, 1, 0, ColorScheme.MEDIUM_GRAY_COLOR), - new EmptyBorder(BORDER_OFFSET, 0, BORDER_OFFSET, 0))); - sectionContents.setVisible(isOpen); - section.add(sectionContents, BorderLayout.SOUTH); - - // Add listeners to each part of the header so that it's easier to toggle them - final MouseAdapter adapter = new MouseAdapter() - { - @Override - public void mouseClicked(MouseEvent e) - { - toggleSection(csd, sectionToggle, sectionContents); - } - }; - sectionToggle.addActionListener(actionEvent -> toggleSection(csd, sectionToggle, sectionContents)); - sectionName.addMouseListener(adapter); - sectionHeader.addMouseListener(adapter); - - sectionWidgets.put(csd.getKey(), sectionContents); - - topLevelPanels.put(csd, section); - } - - for (ConfigItemDescriptor cid : cd.getItems()) - { - if (cid.getItem().hidden()) - { - continue; - } - - JPanel item = new JPanel(); - item.setLayout(new BorderLayout()); - item.setMinimumSize(new Dimension(PANEL_WIDTH, 0)); - String name = cid.getItem().name(); - JLabel configEntryName = new JLabel(name); - configEntryName.setForeground(Color.WHITE); - String description = cid.getItem().description(); - if (!"".equals(description)) - { - configEntryName.setToolTipText("" + name + ":
" + description + ""); - } - - item.add(configEntryName, BorderLayout.CENTER); - - if (cid.getType() == boolean.class) - { - item.add(createCheckbox(cd, cid), BorderLayout.EAST); - } - else if (cid.getType() == int.class) - { - item.add(createIntSpinner(cd, cid), BorderLayout.EAST); - } - else if (cid.getType() == double.class) - { - item.add(createDoubleSpinner(cd, cid), BorderLayout.EAST); - } - else if (cid.getType() == String.class) - { - item.add(createTextField(cd, cid), BorderLayout.SOUTH); - } - else if (cid.getType() == Color.class) - { - item.add(createColorPicker(cd, cid), BorderLayout.EAST); - } - else if (cid.getType() == Dimension.class) - { - item.add(createDimension(cd, cid), BorderLayout.EAST); - } - else if (cid.getType() instanceof Class && ((Class) cid.getType()).isEnum()) - { - item.add(createComboBox(cd, cid), BorderLayout.EAST); - } - else if (cid.getType() == Keybind.class || cid.getType() == ModifierlessKeybind.class) - { - item.add(createKeybind(cd, cid), BorderLayout.EAST); - } - else if (cid.getType() == Notification.class) - { - //item.add(createNotification(cd, cid), BorderLayout.EAST); - } - else if (cid.getType() instanceof ParameterizedType) - { - ParameterizedType parameterizedType = (ParameterizedType) cid.getType(); - if (parameterizedType.getRawType() == Set.class) - { - item.add(createList(cd, cid), BorderLayout.EAST); - } - } - - JPanel section = sectionWidgets.get(cid.getItem().section()); - if (section == null) - { - topLevelPanels.put(cid, item); - } - else - { - section.add(item); - } - } - - topLevelPanels.values().forEach(mainPanel::add); - - - revalidate(); - } - - - - private void buildInformationPanel(ConfigInformation ci) { - // Create the main panel (similar to a Bootstrap panel) - JPanel panel = new JPanel(); - panel.setLayout(new BorderLayout()); - panel.setBorder(new CompoundBorder( - new EmptyBorder(10, 10, 10, 10), // Outer padding - new LineBorder(Color.GRAY, 1) // Border around the panel - )); - - // Create the body/content panel - JPanel bodyPanel = new JPanel(); - bodyPanel.setLayout(new BoxLayout(bodyPanel, BoxLayout.Y_AXIS)); // Vertical alignment - bodyPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); // Padding inside the body - bodyPanel.setBackground(new Color(0, 142, 255, 50)); - JLabel bodyLabel1 = new JLabel("" + ci.value() + ""); - bodyLabel1.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); - bodyPanel.add(bodyLabel1); - bodyPanel.add(Box.createRigidArea(new Dimension(0, 5))); // Spacer between components - - panel.add(bodyPanel, BorderLayout.CENTER); - - mainPanel.add(panel); - } - - private JCheckBox createCheckbox(ConfigDescriptor cd, ConfigItemDescriptor cid) - { - ConfigManager configManager = Microbot.getConfigManager(); - if (configManager == null) - { - return new JCheckBox(); - } - JCheckBox checkbox = new JCheckBox(); - checkbox.setSelected(Boolean.parseBoolean(configManager.getConfiguration(cd.getGroup().value(), cid.getItem().keyName()))); - checkbox.addActionListener(ae -> changeConfiguration(checkbox, cd, cid)); - return checkbox; - } - - private JSpinner createIntSpinner(ConfigDescriptor cd, ConfigItemDescriptor cid) - { - ConfigManager configManager = Microbot.getConfigManager(); - if (configManager == null) - { - return new JSpinner(); - } - int value = MoreObjects.firstNonNull(configManager.getConfiguration(cd.getGroup().value(), cid.getItem().keyName(), int.class), 0); - - Range range = cid.getRange(); - int min = 0, max = Integer.MAX_VALUE; - if (range != null) - { - min = range.min(); - max = range.max(); - } - - // Config may previously have been out of range - value = Ints.constrainToRange(value, min, max); - - SpinnerModel model = new SpinnerNumberModel(value, min, max, 1); - JSpinner spinner = new JSpinner(model); - Component editor = spinner.getEditor(); - JFormattedTextField spinnerTextField = ((JSpinner.DefaultEditor) editor).getTextField(); - spinnerTextField.setColumns(SPINNER_FIELD_WIDTH); - spinner.addChangeListener(ce -> changeConfiguration(spinner, cd, cid)); - - Units units = cid.getUnits(); - if (units != null) - { - // The existing DefaultFormatterFactory with a NumberEditorFormatter. Its model is the same SpinnerModel above. - JFormattedTextField.AbstractFormatterFactory delegate = spinnerTextField.getFormatterFactory(); - spinnerTextField.setFormatterFactory(new UnitFormatterFactory(delegate, units.value())); - } - - return spinner; - } - - private JSpinner createDoubleSpinner(ConfigDescriptor cd, ConfigItemDescriptor cid) - { - double value = MoreObjects.firstNonNull(configManager.getConfiguration(cd.getGroup().value(), cid.getItem().keyName(), double.class), 0d); - - SpinnerModel model = new SpinnerNumberModel(value, 0, Double.MAX_VALUE, 0.1); - JSpinner spinner = new JSpinner(model); - Component editor = spinner.getEditor(); - JFormattedTextField spinnerTextField = ((JSpinner.DefaultEditor) editor).getTextField(); - spinnerTextField.setColumns(SPINNER_FIELD_WIDTH); - spinner.addChangeListener(ce -> changeConfiguration(spinner, cd, cid)); - - Units units = cid.getUnits(); - if (units != null) - { - // The existing DefaultFormatterFactory with a NumberEditorFormatter. Its model is the same SpinnerModel above. - JFormattedTextField.AbstractFormatterFactory delegate = spinnerTextField.getFormatterFactory(); - spinnerTextField.setFormatterFactory(new UnitFormatterFactory(delegate, units.value())); - } - - return spinner; - } - - private JTextComponent createTextField(ConfigDescriptor cd, ConfigItemDescriptor cid) - { - JTextComponent textField; - - if (cid.getItem().secret()) - { - textField = new JPasswordField(); - } - else - { - final JTextArea textArea = new JTextArea(); - textArea.setLineWrap(true); - textArea.setWrapStyleWord(true); - textField = textArea; - } - - textField.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); - textField.setText(configManager.getConfiguration(cd.getGroup().value(), cid.getItem().keyName())); - - textField.addFocusListener(new FocusAdapter() - { - @Override - public void focusLost(FocusEvent e) - { - changeConfiguration(textField, cd, cid); - } - }); - - return textField; - } - - private ColorJButton createColorPicker(ConfigDescriptor cd, ConfigItemDescriptor cid) - { - Color existing = configManager.getConfiguration(cd.getGroup().value(), cid.getItem().keyName(), Color.class); - - ColorJButton colorPickerBtn; - - boolean alphaHidden = cid.getAlpha() == null; - - if (existing == null) - { - colorPickerBtn = new ColorJButton("Pick a color", Color.BLACK); - } - else - { - String colorHex = "#" + (alphaHidden ? ColorUtil.colorToHexCode(existing) : ColorUtil.colorToAlphaHexCode(existing)).toUpperCase(); - colorPickerBtn = new ColorJButton(colorHex, existing); - } - - colorPickerBtn.setFocusable(false); - colorPickerBtn.addMouseListener(new MouseAdapter() - { - @Override - public void mouseClicked(MouseEvent e) - { - RuneliteColorPicker colorPicker = colorPickerManager.create( - ScheduleEntryConfigManagerPanel.this, - colorPickerBtn.getColor(), - cid.getItem().name(), - alphaHidden); - colorPicker.setLocationRelativeTo(colorPickerBtn); - colorPicker.setOnColorChange(c -> - { - colorPickerBtn.setColor(c); - colorPickerBtn.setText("#" + (alphaHidden ? ColorUtil.colorToHexCode(c) : ColorUtil.colorToAlphaHexCode(c)).toUpperCase()); - }); - colorPicker.setOnClose(c -> changeConfiguration(colorPicker, cd, cid)); - colorPicker.setVisible(true); - } - }); - - return colorPickerBtn; - } - - private JPanel createDimension(ConfigDescriptor cd, ConfigItemDescriptor cid) - { - - - JPanel dimensionPanel = new JPanel(); - dimensionPanel.setLayout(new BorderLayout()); - - Dimension dimension = MoreObjects.firstNonNull(configManager.getConfiguration(cd.getGroup().value(), cid.getItem().keyName(), Dimension.class), new Dimension()); - int width = dimension.width; - int height = dimension.height; - - SpinnerModel widthModel = new SpinnerNumberModel(width, 0, Integer.MAX_VALUE, 1); - JSpinner widthSpinner = new JSpinner(widthModel); - Component widthEditor = widthSpinner.getEditor(); - JFormattedTextField widthSpinnerTextField = ((JSpinner.DefaultEditor) widthEditor).getTextField(); - widthSpinnerTextField.setColumns(4); - - SpinnerModel heightModel = new SpinnerNumberModel(height, 0, Integer.MAX_VALUE, 1); - JSpinner heightSpinner = new JSpinner(heightModel); - Component heightEditor = heightSpinner.getEditor(); - JFormattedTextField heightSpinnerTextField = ((JSpinner.DefaultEditor) heightEditor).getTextField(); - heightSpinnerTextField.setColumns(4); - - ChangeListener listener = e -> - configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), widthSpinner.getValue() + "x" + heightSpinner.getValue()); - - widthSpinner.addChangeListener(listener); - heightSpinner.addChangeListener(listener); - - dimensionPanel.add(widthSpinner, BorderLayout.WEST); - dimensionPanel.add(new JLabel(" x "), BorderLayout.CENTER); - dimensionPanel.add(heightSpinner, BorderLayout.EAST); - - return dimensionPanel; - } - - private JComboBox> createComboBox(ConfigDescriptor cd, ConfigItemDescriptor cid) - { - Class type = (Class) cid.getType(); - - JComboBox> box = new JComboBox>(type.getEnumConstants()); // NOPMD: UseDiamondOperator - // set renderer prior to calling box.getPreferredSize(), since it will invoke the renderer - // to build components for each combobox element in order to compute the display size of the - // combobox - box.setRenderer(listCellRenderer); - box.setPreferredSize(new Dimension(box.getPreferredSize().width, 22)); - - try - { - Enum selectedItem = Enum.valueOf(type, configManager.getConfiguration(cd.getGroup().value(), cid.getItem().keyName())); - box.setSelectedItem(selectedItem); - box.setToolTipText(Text.titleCase(selectedItem)); - } - catch (IllegalArgumentException ex) - { - log.debug("invalid selected item", ex); - } - box.addItemListener(e -> - { - if (e.getStateChange() == ItemEvent.SELECTED) - { - changeConfiguration(box, cd, cid); - box.setToolTipText(Text.titleCase((Enum) box.getSelectedItem())); - } - }); - - return box; - } - - private HotkeyButton createKeybind(ConfigDescriptor cd, ConfigItemDescriptor cid) - { - Keybind startingValue = configManager.getConfiguration(cd.getGroup().value(), - cid.getItem().keyName(), - (Class) cid.getType()); - - HotkeyButton button = new HotkeyButton(startingValue, cid.getType() == ModifierlessKeybind.class); - - button.addFocusListener(new FocusAdapter() - { - @Override - public void focusLost(FocusEvent e) - { - changeConfiguration(button, cd, cid); - } - }); - - return button; - } - - - - private JList> createList(ConfigDescriptor cd, ConfigItemDescriptor cid) - { - ParameterizedType parameterizedType = (ParameterizedType) cid.getType(); - Class type = (Class) parameterizedType.getActualTypeArguments()[0]; - Set set = configManager.getConfiguration(cd.getGroup().value(), null, - cid.getItem().keyName(), parameterizedType); - - JList> list = new JList>(type.getEnumConstants()); // NOPMD: UseDiamondOperator - list.setCellRenderer(listCellRenderer); - list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); - list.setLayoutOrientation(JList.VERTICAL); - list.setSelectedIndices( - MoreObjects.firstNonNull(set, Collections.emptySet()) - .stream() - .mapToInt(e -> ArrayUtils.indexOf(type.getEnumConstants(), e)) - .toArray()); - list.addFocusListener(new FocusAdapter() - { - @Override - public void focusLost(FocusEvent e) - { - changeConfiguration(list, cd, cid); - } - }); - - return list; - } - - private void changeConfiguration(Component component, ConfigDescriptor cd, ConfigItemDescriptor cid) - { - ConfigManager configManager = Microbot.getConfigManager(); - if (configManager == null) - { - return; - } - - final ConfigItem configItem = cid.getItem(); - - if (!Strings.isNullOrEmpty(configItem.warning())) - { - final int result = JOptionPane.showOptionDialog(component, configItem.warning(), - "Are you sure?", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, - null, new String[]{"Yes", "No"}, "No"); - - if (result != JOptionPane.YES_OPTION) - { - rebuild(); - return; - } - } - - if (component instanceof JCheckBox) - { - JCheckBox checkbox = (JCheckBox) component; - configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), "" + checkbox.isSelected()); - } - else if (component instanceof JSpinner) - { - JSpinner spinner = (JSpinner) component; - configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), "" + spinner.getValue()); - } - else if (component instanceof JTextComponent) - { - JTextComponent textField = (JTextComponent) component; - configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), textField.getText()); - } - else if (component instanceof RuneliteColorPicker) - { - RuneliteColorPicker colorPicker = (RuneliteColorPicker) component; - configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), colorPicker.getSelectedColor().getRGB() + ""); - } - else if (component instanceof JComboBox) - { - JComboBox jComboBox = (JComboBox) component; - configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), ((Enum) jComboBox.getSelectedItem()).name()); - } - else if (component instanceof HotkeyButton) - { - HotkeyButton hotkeyButton = (HotkeyButton) component; - configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), hotkeyButton.getValue()); - } - else if (component instanceof JList) - { - JList list = (JList) component; - List selectedValues = list.getSelectedValuesList(); - - configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), Sets.newHashSet(selectedValues)); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/ExecutionResult.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/ExecutionResult.java deleted file mode 100644 index 6b6d5c20b4a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/ExecutionResult.java +++ /dev/null @@ -1,93 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.event; - -/** - * Enum representing the granular success states for plugin execution events. - * This provides more detailed reporting of execution outcomes beyond simple success/failure. - */ -public enum ExecutionResult { - /** - * Plugin execution completed successfully without any issues. - * The plugin accomplished its intended task and is ready for normal scheduling. - */ - SUCCESS("Success", "Plugin completed successfully", true, false), - - /** - * Plugin encountered a recoverable issue but can potentially run again. - * Examples: temporary resource unavailability, network timeouts, minor game state issues. - * The plugin schedule entry remains enabled but the failure is tracked. - */ - SOFT_FAILURE("Soft Failure", "Plugin failed but can retry", false, true), - - /** - * Plugin encountered a critical failure that prevents future execution. - * Examples: invalid configuration, missing dependencies, critical errors. - * The plugin schedule entry should be disabled after this result. - */ - HARD_FAILURE("Hard Failure", "Plugin failed critically", false, false); - - private final String displayName; - private final String description; - private final boolean isSuccess; - private final boolean canRetry; - - ExecutionResult(String displayName, String description, boolean isSuccess, boolean canRetry) { - this.displayName = displayName; - this.description = description; - this.isSuccess = isSuccess; - this.canRetry = canRetry; - } - - /** - * @return Human-readable display name for this result - */ - public String getDisplayName() { - return displayName; - } - - /** - * @return Detailed description of what this result means - */ - public String getDescription() { - return description; - } - - /** - * @return true if this represents a successful execution - */ - public boolean isSuccess() { - return isSuccess; - } - - /** - * @return true if the plugin can be retried after this result - */ - public boolean canRetry() { - return canRetry; - } - - /** - * @return true if this represents any kind of failure (soft or hard) - */ - public boolean isFailure() { - return !isSuccess; - } - - /** - * @return true if this is a soft failure that allows retries - */ - public boolean isSoftFailure() { - return this == SOFT_FAILURE; - } - - /** - * @return true if this is a hard failure that prevents retries - */ - public boolean isHardFailure() { - return this == HARD_FAILURE; - } - - @Override - public String toString() { - return displayName; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryMainTaskFinishedEvent.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryMainTaskFinishedEvent.java deleted file mode 100644 index 4e642a1c9dd..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryMainTaskFinishedEvent.java +++ /dev/null @@ -1,55 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.event; - -import java.time.ZonedDateTime; - -import lombok.Getter; -import net.runelite.client.plugins.Plugin; - -/** - * Event fired when a plugin has completed its work and is ready to be stopped - * This is different from ScheduledStopEvent as it represents a plugin self-reporting - * that it has finished its task rather than the scheduler determining it should be stopped - * due to conditions. - */ -@Getter -public class PluginScheduleEntryMainTaskFinishedEvent { - private final Plugin plugin; - private final ZonedDateTime finishDateTime; - private final String reason; - private final ExecutionResult result; - - /** - * Creates a new plugin finished event - * - * @param plugin The plugin that has finished - * @param finishDateTime The time when the plugin finished - * @param reason A description of why the plugin finished - * @param result The execution result (SUCCESS, SOFT_FAILURE, or HARD_FAILURE) - */ - public PluginScheduleEntryMainTaskFinishedEvent(Plugin plugin, ZonedDateTime finishDateTime, String reason, ExecutionResult result) { - this.plugin = plugin; - this.finishDateTime = finishDateTime; - this.reason = reason; - this.result = result; - } - - /** - * Creates a new plugin finished event with current time - * - * @param plugin The plugin that has finished - * @param reason A description of why the plugin finished - * @param result The execution result (SUCCESS, SOFT_FAILURE, or HARD_FAILURE) - */ - public PluginScheduleEntryMainTaskFinishedEvent(Plugin plugin, String reason, ExecutionResult result) { - this(plugin, ZonedDateTime.now(), reason, result); - } - - /** - * @deprecated Use {@link #getResult()} instead for more granular result information - * @return true if the result indicates success - */ - @Deprecated - public boolean isSuccess() { - return result.isSuccess(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPostScheduleTaskEvent.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPostScheduleTaskEvent.java deleted file mode 100644 index 70c1d4fad69..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPostScheduleTaskEvent.java +++ /dev/null @@ -1,38 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.event; - -import java.time.ZonedDateTime; - -import lombok.Getter; -import net.runelite.client.plugins.Plugin; - -/** - * Event fired when a plugin should start its post-schedule tasks. - * This is sent by the scheduler to plugins that implement SchedulablePlugin to initiate - * their post-schedule task execution after the plugin has been stopped. - */ -@Getter -public class PluginScheduleEntryPostScheduleTaskEvent { - private final Plugin plugin; - private final ZonedDateTime stopDateTime; - - /** - * Creates a new plugin post-schedule task event - * - * @param plugin The plugin that should start post-schedule tasks - * @param stopDateTime The time when the plugin was stopped - */ - public PluginScheduleEntryPostScheduleTaskEvent(Plugin plugin, ZonedDateTime stopDateTime) { - this.plugin = plugin; - this.stopDateTime = stopDateTime; - } - - /** - * Creates a new plugin post-schedule task event with current time - * - * @param plugin The plugin that should start post-schedule tasks - * @param wasSuccessful Whether the plugin run was successful - */ - public PluginScheduleEntryPostScheduleTaskEvent(Plugin plugin) { - this(plugin, ZonedDateTime.now()); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPostScheduleTaskFinishedEvent.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPostScheduleTaskFinishedEvent.java deleted file mode 100644 index 9ef4e79c4fc..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPostScheduleTaskFinishedEvent.java +++ /dev/null @@ -1,53 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.event; - -import java.time.ZonedDateTime; - -import lombok.Getter; -import net.runelite.client.plugins.Plugin; - -/** - * Event fired when a plugin's post-schedule tasks have finished. - * This is sent by plugins back to the scheduler to indicate completion of post-schedule tasks. - */ -@Getter -public class PluginScheduleEntryPostScheduleTaskFinishedEvent { - private final Plugin plugin; - private final ZonedDateTime finishDateTime; - private final ExecutionResult result; - private final String message; - - /** - * Creates a new plugin post-schedule task finished event - * - * @param plugin The plugin that finished post-schedule tasks - * @param finishDateTime The time when the plugin finished - * @param result The execution result (SUCCESS, SOFT_FAILURE, or HARD_FAILURE) - * @param message Optional message describing the completion - */ - public PluginScheduleEntryPostScheduleTaskFinishedEvent(Plugin plugin, ZonedDateTime finishDateTime, ExecutionResult result, String message) { - this.plugin = plugin; - this.finishDateTime = finishDateTime; - this.result = result; - this.message = message; - } - - /** - * Creates a new plugin post-schedule task finished event with current time - * - * @param plugin The plugin that finished post-schedule tasks - * @param result The execution result (SUCCESS, SOFT_FAILURE, or HARD_FAILURE) - * @param message Optional message describing the completion - */ - public PluginScheduleEntryPostScheduleTaskFinishedEvent(Plugin plugin, ExecutionResult result, String message) { - this(plugin, ZonedDateTime.now(), result, message); - } - - /** - * @deprecated Use {@link #getResult()} instead for more granular result information - * @return true if the result indicates success - */ - @Deprecated - public boolean isSuccess() { - return result.isSuccess(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPreScheduleTaskEvent.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPreScheduleTaskEvent.java deleted file mode 100644 index ad73c7aa5fd..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPreScheduleTaskEvent.java +++ /dev/null @@ -1,41 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.event; - -import java.time.ZonedDateTime; - -import lombok.Getter; -import net.runelite.client.plugins.Plugin; - -/** - * Event fired when a plugin should start its pre-schedule tasks. - * This is sent by the scheduler to plugins that implement SchedulablePlugin to initiate - * their pre-schedule task execution. - */ -@Getter -public class PluginScheduleEntryPreScheduleTaskEvent { - private final Plugin plugin; - private final ZonedDateTime startDateTime; - private final boolean isSchedulerControlled; - - /** - * Creates a new plugin pre-schedule task event - * - * @param plugin The plugin that should start pre-schedule tasks - * @param startDateTime The time when the plugin should start - * @param isSchedulerControlled Whether this plugin is under scheduler control - */ - public PluginScheduleEntryPreScheduleTaskEvent(Plugin plugin, ZonedDateTime startDateTime, boolean isSchedulerControlled) { - this.plugin = plugin; - this.startDateTime = startDateTime; - this.isSchedulerControlled = isSchedulerControlled; - } - - /** - * Creates a new plugin pre-schedule task event with current time - * - * @param plugin The plugin that should start pre-schedule tasks - * @param isSchedulerControlled Whether this plugin is under scheduler control - */ - public PluginScheduleEntryPreScheduleTaskEvent(Plugin plugin, boolean isSchedulerControlled) { - this(plugin, ZonedDateTime.now(), isSchedulerControlled); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPreScheduleTaskFinishedEvent.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPreScheduleTaskFinishedEvent.java deleted file mode 100644 index cc1bc3f66a3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPreScheduleTaskFinishedEvent.java +++ /dev/null @@ -1,53 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.event; - -import java.time.ZonedDateTime; - -import lombok.Getter; -import net.runelite.client.plugins.Plugin; - -/** - * Event fired when a plugin's pre-schedule tasks have finished. - * This is sent by plugins back to the scheduler to indicate completion of pre-schedule tasks. - */ -@Getter -public class PluginScheduleEntryPreScheduleTaskFinishedEvent { - private final Plugin plugin; - private final ZonedDateTime finishDateTime; - private final ExecutionResult result; - private final String message; - - /** - * Creates a new plugin pre-schedule task finished event - * - * @param plugin The plugin that finished pre-schedule tasks - * @param finishDateTime The time when the plugin finished - * @param result The execution result (SUCCESS, SOFT_FAILURE, or HARD_FAILURE) - * @param message Optional message describing the completion - */ - public PluginScheduleEntryPreScheduleTaskFinishedEvent(Plugin plugin, ZonedDateTime finishDateTime, ExecutionResult result, String message) { - this.plugin = plugin; - this.finishDateTime = finishDateTime; - this.result = result; - this.message = message; - } - - /** - * Creates a new plugin pre-schedule task finished event with current time - * - * @param plugin The plugin that finished pre-schedule tasks - * @param result The execution result (SUCCESS, SOFT_FAILURE, or HARD_FAILURE) - * @param message Optional message describing the completion - */ - public PluginScheduleEntryPreScheduleTaskFinishedEvent(Plugin plugin, ExecutionResult result, String message) { - this(plugin, ZonedDateTime.now(), result, message); - } - - /** - * @deprecated Use {@link #getResult()} instead for more granular result information - * @return true if the result indicates success - */ - @Deprecated - public boolean isSuccess() { - return result.isSuccess(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntrySoftStopEvent.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntrySoftStopEvent.java deleted file mode 100644 index f5becec3780..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntrySoftStopEvent.java +++ /dev/null @@ -1,45 +0,0 @@ - -// DEPRECATED: This event class is deprecated and will be removed in a future release. -// Use PluginScheduleEntryPostScheduleTaskEvent instead for post-schedule task signaling. -// (See PluginScheduleEntryPostScheduleTaskEvent.java) -// -// This class remains only for backward compatibility and migration purposes. -// Please update all usages to the new event class as soon as possible. - -// TODO: Remove this class after migration to PluginScheduleEntryPostScheduleTaskEvent is complete. -// (Scheduled for removal in the next major version.) -// -// Replacement: PluginScheduleEntryPostScheduleTaskEvent -// -// --- -// Original Javadoc below: -// -// Event fired when a plugin should start its post-schedule tasks. -// This is sent by the scheduler to plugins that implement SchedulablePlugin to initiate -// their post-schedule task execution after the plugin has been stopped. - -package net.runelite.client.plugins.microbot.pluginscheduler.event; - -import java.time.ZoneOffset; -import java.time.ZonedDateTime; - -import lombok.Getter; -import net.runelite.client.plugins.Plugin; - -/** - * Event fired when a scheduled plugin should be stopped - */ -/** -* @deprecated Use {@link net.runelite.client.plugins.microbot.pluginscheduler.event.PluginScheduleEntryPostScheduleTaskEvent} instead. -*/ -@Deprecated -public class PluginScheduleEntrySoftStopEvent { - @Getter - private final Plugin plugin; - @Getter - private final ZonedDateTime stopDateTime; - public PluginScheduleEntrySoftStopEvent(Plugin plugin, ZonedDateTime stopDateTime) { - this.plugin = plugin; - this.stopDateTime = stopDateTime; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/model/PluginScheduleEntry.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/model/PluginScheduleEntry.java deleted file mode 100644 index de53fa467fd..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/model/PluginScheduleEntry.java +++ /dev/null @@ -1,3584 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.model; -import java.time.Duration; -import java.time.Instant; -import java.time.LocalTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.DayOfWeek; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.GameState; -import net.runelite.client.config.ConfigDescriptor; -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.pluginscheduler.api.SchedulablePlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionManager; -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.condition.logical.OrCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.enums.UpdateOption; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.DayOfWeekCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.enums.RepeatCycle; -import net.runelite.client.plugins.microbot.pluginscheduler.config.ScheduleEntryConfigManager; -import net.runelite.client.plugins.microbot.pluginscheduler.event.ExecutionResult; -import net.runelite.client.plugins.microbot.pluginscheduler.event.PluginScheduleEntryPostScheduleTaskEvent; -import net.runelite.client.plugins.microbot.pluginscheduler.event.PluginScheduleEntryPreScheduleTaskEvent; -import net.runelite.client.plugins.microbot.pluginscheduler.event.PluginScheduleEntrySoftStopEvent; -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; -@Data -@AllArgsConstructor -@Getter -@Slf4j -/** - * Represents a scheduled plugin entry in the plugin scheduler system. - *

- * This class manages the scheduling, starting, stopping, and condition management - * for a plugin. It handles both start and stop conditions through {@link ConditionManager} - * instances and provides comprehensive state tracking for the plugin's execution. - *

- * PluginScheduleEntry serves as the core model connecting the UI components in the - * scheduler system with the actual plugin execution logic. It maintains information about: - *

    - *
  • When a plugin should start (start conditions)
  • - *
  • When a plugin should stop (stop conditions)
  • - *
  • Current execution state (running, stopped, enabled/disabled)
  • - *
  • Execution statistics (run count, duration, etc.)
  • - *
  • Plugin configuration and watchdog management
  • - *
- */ -public class PluginScheduleEntry implements AutoCloseable { - // Static formatter for time display - public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); - public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); - // Remove the duplicate executor and use the shared one from ConditionManager - - // Store the scheduled futures so they can be cancelled later - private transient ScheduledFuture startConditionWatchdogFuture; - private transient ScheduledFuture stopConditionWatchdogFuture; - private transient Plugin plugin; - private String name; - private boolean enabled; - private boolean allowContinue = true; // Whether to continue running the plugin after a interruption -> stopReasonType = StopReason.Interrupted - private boolean hasStarted = false; // Flag to indicate if the plugin has started - @Setter - private boolean needsStopCondition = false; // Flag to indicate if a time-based stop condition is needed - private transient ScheduleEntryConfigManager scheduleEntryConfigManager; - - // New fields for tracking stop reason - private String lastStopReason; - @Getter - private boolean lastRunSuccessful; - private boolean onLastStopUserConditionsSatisfied = false; // Flag to indicate if the last stop was due to satisfied conditions - private boolean onLastStopPluginConditionsSatisfied = false; // Flag to indicate if the last stop was due to satisfied conditions - private StopReason lastStopReasonType = StopReason.NONE; - private Duration lastRunDuration = Duration.ZERO; // Duration of the last run - private ZonedDateTime lastRunStartTime; // When the plugin started running - private ZonedDateTime lastRunEndTime; // When the plugin finished running - - /** - * Enumeration of reasons why a plugin might stop - */ - public enum StopReason { - NONE("None"), - MANUAL_STOP("Manually Stopped"), - PLUGIN_FINISHED("Plugin Finished"), - ERROR("Error"), - SCHEDULED_STOP("Scheduled Stop"), - INTERRUPTED("Interrupted"), - HARD_STOP("Hard Stop executed"), - PREPOST_SCHEDULE_STOP("Hard Stop executed after post-schedule tasks"), - CLIENT_SHUTDOWN("Client Shutdown"); - - private final String description; - - StopReason(String description) { - this.description = description; - } - - public String getDescription() { - return description; - } - - @Override - public String toString() { - return description; - } - } - - - - private String cleanName; - final private ConditionManager stopConditionManager; - final private ConditionManager startConditionManager; - private transient boolean stopInitiated = false; - - private boolean allowRandomScheduling = true; // Whether this plugin can be randomly scheduled - private int runCount = 0; // Track how many times this plugin has been run - - // Soft failure tracking - private int consecutiveSoftFailures = 0; // Track consecutive soft failures - private int maxSoftFailuresBeforeHardFailure = 3; // Default: disable after 3 consecutive soft failures - private ZonedDateTime lastSoftFailureTime; // When the last soft failure occurred - - // Watchdog configuration - private boolean autoStartWatchdogs = true; // Whether to auto-start watchdogs on creation - private boolean watchdogsEnabled = true; // Whether watchdogs are allowed to run - - // Startup watchdog configuration - private boolean startupWatchdogEnabled = true; // Whether startup watchdog is enabled - private Duration startupWatchdogTimeout = Duration.ofMinutes(2); // Default 2 minutes timeout for plugin to transition from hasStarted to isRunning - private transient volatile ScheduledFuture startupWatchdogFuture; // Future for startup watchdog monitoring - private transient volatile ZonedDateTime startupWatchdogStartTime; // When startup watchdog monitoring began - private transient ScheduledExecutorService startupWatchdogExecutor; // Executor for startup watchdog - - private ZonedDateTime stopInitiatedTime; // When the first stop was attempted - private ZonedDateTime lastStopAttemptTime; // When the last stop attempt was made - private Duration softStopRetryInterval = Duration.ofSeconds(30); // Default 30 seconds between retries - private Duration hardStopTimeout = Duration.ofMinutes(4); // Default 4 Minutes before hard stop - - - private transient Thread stopMonitorThread; - private transient volatile boolean isMonitoringStop = false; - - - private int priority = 0; // Higher numbers = higher priority - private boolean isDefault = false; // Flag to indicate if this is a default plugin - - /** - * Functional interface for handling successful plugin stop events - */ - @FunctionalInterface - public interface StopCompletionCallback { - /** - * Called when a plugin has successfully completed its stop operation - * @param entry The PluginScheduleEntry that has stopped - * @param wasSuccessful Whether the plugin run was successful - */ - void onStopCompleted(PluginScheduleEntry entry, boolean wasSuccessful); - } - - /** - * Callback that will be invoked when this plugin successfully stops - */ - private transient StopCompletionCallback stopCompletionCallback; - - /** - * Sets the callback to be invoked when this plugin successfully stops - * @param callback The callback to invoke - * @return This PluginScheduleEntry for method chaining - */ - public PluginScheduleEntry setStopCompletionCallback(StopCompletionCallback callback) { - this.stopCompletionCallback = callback; - return this; - } - - - /** - * Sets the serialized ConfigDescriptor for this schedule entry - * This is used during deserialization - * - * @param serializedConfigDescriptor The serialized ConfigDescriptor as a JsonObject - */ - public void setSerializedConfigDescriptor(ConfigDescriptor serializedConfigDescriptor) { - // If we already have a scheduleEntryConfigManager, update it with the new config - if (this.scheduleEntryConfigManager != null) { - this.scheduleEntryConfigManager.setConfigScheduleEntryDescriptor(serializedConfigDescriptor); - } - } - - /** - * Gets the serialized ConfigDescriptor for this schedule entry - * - * @return The serialized ConfigDescriptor as a JsonObject, or null if not set - */ - public ConfigDescriptor getConfigScheduleEntryDescriptor() { - // If we have a scheduleEntryConfigManager, get the serialized config from it - if (this.scheduleEntryConfigManager != null) { - return this.scheduleEntryConfigManager.getConfigScheduleEntryDescriptor(); - } - return null; - } - public PluginScheduleEntry(String pluginName, String duration, boolean enabled, boolean allowRandomScheduling) { - this(pluginName, parseDuration(duration), enabled, allowRandomScheduling); - } - private TimeCondition mainTimeStartCondition; - private static Duration parseDuration(String duration) { - // If duration is specified, parse it - if (duration != null && !duration.isEmpty()) { - try { - String[] parts = duration.split(":"); - if (parts.length == 2) { - int hours = Integer.parseInt(parts[0]); - int minutes = Integer.parseInt(parts[1]); - return Duration.ofHours(hours).plusMinutes(minutes); - } - } catch (Exception e) { - // Invalid duration format, no condition added - throw new IllegalArgumentException("Invalid duration format: " + duration); - } - } - return null; - } - - public PluginScheduleEntry(String pluginName, Duration interval, boolean enabled, boolean allowRandomScheduling) { //allowRandomScheduling .>allows soft start - this(pluginName, new IntervalCondition(interval), enabled, allowRandomScheduling); - } - - public PluginScheduleEntry(String pluginName, TimeCondition startingCondition, boolean enabled, boolean allowRandomScheduling) { - this(pluginName, startingCondition, enabled, allowRandomScheduling, true); - } - - public PluginScheduleEntry( String pluginName, - TimeCondition startingCondition, - boolean enabled, - boolean allowRandomScheduling, - boolean autoStartWatchdogs){ - this(pluginName, startingCondition, enabled, allowRandomScheduling, autoStartWatchdogs, true); - } - public PluginScheduleEntry( String pluginName, - TimeCondition startingCondition, - boolean enabled, - boolean allowRandomScheduling, - boolean autoStartWatchdogs, - boolean allowContinue - ) { - this.name = pluginName; - this.enabled = enabled; - this.allowRandomScheduling = allowRandomScheduling; - this.autoStartWatchdogs = autoStartWatchdogs; - this.cleanName = pluginName.replaceAll("|", "") - .replaceAll("<[^>]*>([^<]*)]*>", "$1") - .replaceAll("<[^>]*>", ""); - - this.stopConditionManager = new ConditionManager(); - this.startConditionManager = new ConditionManager(); - - // Check if this is a default/1-second interval plugin - boolean isDefaultByScheduleType = false; - if (startingCondition != null) { - if (startingCondition instanceof IntervalCondition) { - IntervalCondition interval = (IntervalCondition) startingCondition; - if (interval.getInterval().getSeconds() <= 1) { - isDefaultByScheduleType = true; - } - } - this.mainTimeStartCondition = startingCondition; - startConditionManager.setUserLogicalCondition(new OrCondition(startingCondition)); - } - - // If it's a default by schedule type, enforce the default settings - if (isDefaultByScheduleType) { - this.isDefault = true; - this.priority = 0; - } - //registerPluginConditions(); - scheduleConditionWatchdogs(10000, UpdateOption.SYNC); - // Only start watchdogs if auto-start is enabled - if (autoStartWatchdogs) { - //stopConditionManager.resumeWatchdogs(); - //startConditionManager.resumeWatchdogs(); - } - - // Always register events if enabled - if (enabled) { - startConditionManager.registerEvents(); - }else { - startConditionManager.unregisterEventsAndPauseWatchdogs(); - stopConditionManager.unregisterEventsAndPauseWatchdogs(); - } - this.allowContinue = allowContinue; - } - - /** - * Creates a scheduled event with a one-time trigger at a specific time - * - * @param pluginName The plugin name - * @param triggerTime The time when the plugin should trigger once - * @param enabled Whether the schedule is enabled - * @return A new PluginScheduleEntry configured to trigger once at the specified time - */ - public static PluginScheduleEntry createOneTimeSchedule(String pluginName, ZonedDateTime triggerTime, boolean enabled) { - SingleTriggerTimeCondition condition = new SingleTriggerTimeCondition(triggerTime, Duration.ZERO, 1); - PluginScheduleEntry entry = new PluginScheduleEntry( - pluginName, - condition, - enabled, - false); // One-time events are typically not randomized - - return entry; - } - - public void setEnabled(boolean enabled) { - if (this.enabled == enabled) { - return; // No change in enabled state - } - this.enabled = enabled; - if (!enabled) { - stopConditionManager.unregisterEventsAndPauseWatchdogs(); - startConditionManager.unregisterEventsAndPauseWatchdogs(); - runCount = 0; - } else { - //stopConditionManager.registerEvents(); - log.debug("registering start events for plugin '{}'", name); - startConditionManager.registerEvents(); - //log this object id-> memory hashcode - log.debug("PluginScheduleEntry {} - {} - {} - {} - {}", this.hashCode(), this.name, this.cleanName, this.enabled, this.allowRandomScheduling); - //registerPluginConditions(); - this.setLastStopReason(""); - this.setLastRunSuccessful(false); - this.setLastStopReasonType(PluginScheduleEntry.StopReason.NONE); - - // Resume watchdogs if they were previously configured and watchdogs are enabled - if (watchdogsEnabled) { - startConditionManager.resumeWatchdogs(); - stopConditionManager.resumeWatchdogs(); - } - } - } - - /** - * Controls whether watchdogs are allowed to run for this schedule entry. - * This provides a way to temporarily disable watchdogs without losing their configuration. - * - * @param enabled true to enable watchdogs, false to disable them - */ - public void setWatchdogsEnabled(boolean enabled) { - if (this.watchdogsEnabled == enabled) { - return; // No change - } - - this.watchdogsEnabled = enabled; - - if (enabled) { - // Resume watchdogs if the plugin is enabled - if (this.enabled) { - startConditionManager.resumeWatchdogs(); - stopConditionManager.resumeWatchdogs(); - log.debug("Watchdogs resumed for '{}'", name); - } - } else { - // Pause watchdogs regardless of plugin state - startConditionManager.pauseWatchdogs(); - stopConditionManager.pauseWatchdogs(); - log.debug("Watchdogs paused for '{}'", name); - } - } - - /** - * Checks if watchdogs are currently running for this schedule entry - * - * @return true if at least one watchdog is running - */ - public boolean areWatchdogsRunning() { - return startConditionManager.areWatchdogsRunning() || - stopConditionManager.areWatchdogsRunning(); - } - - /** - * Manually start the condition watchdogs for this schedule entry. - * This will only have an effect if watchdogs are enabled and the plugin is enabled. - * - * @param intervalMillis The interval at which to check for condition changes - * @param updateOption How to handle condition changes - * @return true if watchdogs were successfully started - */ - public boolean startConditionWatchdogs(long intervalMillis, UpdateOption updateOption) { - if (!watchdogsEnabled || !enabled) { - return false; - } - - return scheduleConditionWatchdogs(intervalMillis, updateOption); - } - - /** - * Stops all watchdogs associated with this schedule entry - */ - public void stopWatchdogs() { - log.debug("Stopping all watchdogs for '{}'", name); - startConditionManager.pauseWatchdogs(); - stopConditionManager.pauseWatchdogs(); - stopStartupWatchdog(); - } - - /** - * Starts the startup watchdog to monitor plugin transition from hasStarted to isRunning. - * This watchdog will trigger a hard failure if the plugin doesn't transition to running - * state within the configured timeout period. - */ - public void startStartupWatchdog() { - if (!startupWatchdogEnabled) { - return; // Disabled - } - if (startupWatchdogFuture != null) { - if (!startupWatchdogFuture.isDone() && !startupWatchdogFuture.isCancelled()) { - return; // Already running - } - startupWatchdogFuture = null; - } - - startupWatchdogStartTime = ZonedDateTime.now(); - log.debug("Starting startup watchdog for plugin '{}' with timeout of {} minutes", - name, startupWatchdogTimeout.toMinutes()); - - startupWatchdogExecutor = Executors.newSingleThreadScheduledExecutor(r -> { - Thread t = new Thread(r, "StartupWatchdog-" + name); - t.setDaemon(true); - return t; - }); - - // Start periodic monitoring every 2 seconds - startupWatchdogFuture = startupWatchdogExecutor.scheduleAtFixedRate(() -> { - try { - ZonedDateTime now = ZonedDateTime.now(); - Duration elapsed = Duration.between(startupWatchdogStartTime, now); - - // Check if plugin has successfully transitioned to running state - if (isRunning()) { - log.debug("Startup watchdog: Plugin '{}' successfully transitioned to running state after {} seconds", - name, elapsed.getSeconds()); - - // Stop the watchdog since plugin is now running - stopStartupWatchdog(); - return; - } - - // Check if we've exceeded the timeout - if (elapsed.compareTo(startupWatchdogTimeout) >= 0) { - long elapsedMinutes = elapsed.toMinutes(); - String timeoutMessage = String.format( - "Plugin '%s' failed to transition to running state within %d minutes (started but not running)", - getCleanName(), elapsedMinutes); - - log.error("Startup watchdog timeout: {}", timeoutMessage); - - // Stop the plugin with hard failure - stop(ExecutionResult.HARD_FAILURE, StopReason.ERROR, timeoutMessage); - - // Stop the watchdog - stopStartupWatchdog(); - return; - } - - // Continue monitoring - log progress every 30 seconds - if (elapsed.getSeconds() % 30 == 0 && elapsed.getSeconds() > 0) { - log.debug("Startup watchdog: Plugin '{}' still starting... {} seconds elapsed", - name, elapsed.getSeconds()); - } - - } catch (Exception e) { - log.error("Error in startup watchdog for plugin '{}': {}", name, e.getMessage(), e); - stopStartupWatchdog(); - } - }, 0, 2, TimeUnit.SECONDS); // Check every 2 seconds - } - - /** - * Stops the startup watchdog if it's currently running - */ - public void stopStartupWatchdog() { - log.debug("Stopping startup watchdog for plugin '{}'", name); - if (startupWatchdogFuture != null && !startupWatchdogFuture.isDone()) { - startupWatchdogFuture.cancel(false); - } - startupWatchdogFuture = null; - startupWatchdogStartTime = null; - if (startupWatchdogExecutor != null) { - startupWatchdogExecutor.shutdownNow(); - startupWatchdogExecutor = null; - } - } - - /** - * Checks if the startup watchdog is currently active - * - * @return true if startup watchdog is monitoring this plugin - */ - public boolean isStartupWatchdogActive() { - return startupWatchdogFuture != null && !startupWatchdogFuture.isDone(); - } - - /** - * Gets the remaining time until startup watchdog timeout - * - * @return Optional containing remaining time, or empty if watchdog is not active - */ - public Optional getStartupWatchdogTimeRemaining() { - if (!isStartupWatchdogActive() || startupWatchdogStartTime == null) { - return Optional.empty(); - } - - Duration elapsed = Duration.between(startupWatchdogStartTime, ZonedDateTime.now()); - Duration remaining = startupWatchdogTimeout.minus(elapsed); - - return !remaining.isNegative() && !remaining.isZero() ? Optional.of(remaining) : Optional.empty(); - } - - /** - * Sets the startup watchdog timeout duration - * - * @param timeout Duration to wait before timing out plugin startup - */ - public void setStartupWatchdogTimeout(Duration timeout) { - if (timeout != null && timeout.toSeconds() > 0) { - this.startupWatchdogTimeout = timeout; - log.debug("Updated startup watchdog timeout for plugin '{}' to {} minutes", - name, timeout.toMinutes()); - } - } - - /** - * Enables or disables the startup watchdog - * - * @param enabled true to enable startup watchdog, false to disable - */ - public void setStartupWatchdogEnabled(boolean enabled) { - if (this.startupWatchdogEnabled != enabled) { - this.startupWatchdogEnabled = enabled; - log.debug("Startup watchdog {} for plugin '{}'", - enabled ? "enabled" : "disabled", name); - - // If disabling, stop any active watchdog - if (!enabled) { - stopStartupWatchdog(); - } - } - } - - public Plugin getPlugin() { - if (this.plugin == null) { - this.plugin = Microbot.getPluginManager().getPlugins().stream() - .filter(p -> Objects.equals(p.getName(), name)) - .findFirst() - .orElse(null); - - // Initialize scheduleEntryConfigManager when plugin is first retrieved - if (this.plugin instanceof SchedulablePlugin && scheduleEntryConfigManager == null) { - SchedulablePlugin schedulablePlugin = (SchedulablePlugin) this.plugin; - ConfigDescriptor descriptor = schedulablePlugin.getConfigDescriptor(); - if (descriptor != null) { - scheduleEntryConfigManager = new ScheduleEntryConfigManager(descriptor); - } - } - } - return plugin; - } - - /** - * checks if the plugin referenced by this schedule entry is currently available - * in the plugin manager. this is useful for handling hot-loaded plugins from - * the plugin hub that might not be available when the schedule is loaded. - * - * @return true if the plugin is available, not hidden, and implements SchedulablePlugin - */ - public boolean isPluginAvailable() { - if (name == null || name.isEmpty()) { - return false; - } - - // try to find the plugin by name in the current plugin manager - Plugin foundPlugin = Microbot.getPluginManager().getPlugins().stream() - .filter(p -> Objects.equals(p.getName(), name)) - .filter(p -> { - // check if plugin is not hidden - PluginDescriptor descriptor = p.getClass().getAnnotation(PluginDescriptor.class); - return descriptor != null && !descriptor.hidden(); - }) - .filter(p -> p instanceof SchedulablePlugin) // must implement SchedulablePlugin - .findFirst() - .orElse(null); - - return foundPlugin != null; - } - - public boolean start(boolean logConditions) { - if (getPlugin() == null) { - return false; - } - - try { - StringBuilder logBuilder = new StringBuilder(); - logBuilder.append("\nStarting plugin '").append(name).append("':\n"); - - if (!this.isEnabled()) { - logBuilder.append(" - Plugin is disabled, not starting\n"); - log.info(logBuilder.toString()); - return false; - } - - // Log defined conditions when starting - if (logConditions) { - logBuilder.append(" - Starting with conditions\n"); - // These methods do their own logging as they're complex and used elsewhere - logStartConditionsWithDetails(); - logStopConditionsWithDetails(); - } - - // Reset stop conditions before starting, if we are not continuing, and we are not interrupted - if (!this.allowContinue || (lastStopReasonType != StopReason.INTERRUPTED)) { - logBuilder.append(" - Not continuing, resetting stop conditions\n") - .append(" - allowContinue: ").append(allowContinue) - .append("\n - last Stop Reason Type: ").append(lastStopReasonType).append("\n"); - resetStopConditions(); - } else { - logBuilder.append(" - Continuing, not resetting stop conditions\n"); - stopConditionManager.resetPluginConditions(); - - if (!onLastStopUserConditionsSatisfied && areUserDefinedStopConditionsMet()) { - logBuilder.append(" - On last interrupt user stop conditions were not satisfied, now they are, resetting user stop conditions\n"); - stopConditionManager.resetUserConditions(); - } - } - - if (lastStopReasonType != StopReason.NONE) { - logBuilder.append(" - Last stop reason: ").append(lastStopReasonType.getDescription()) - .append("\n - message: ").append(lastStopReason).append("\n"); - } - - this.setLastStopReason(""); - this.setLastRunSuccessful(false); - this.setLastStopReasonType(PluginScheduleEntry.StopReason.NONE); - this.setOnLastStopPluginConditionsSatisfied(false); - this.setOnLastStopUserConditionsSatisfied(false); - - // Set scheduleMode to true in plugin config - if (scheduleEntryConfigManager != null) { - scheduleEntryConfigManager.setScheduleMode(true); - logBuilder.append(" - Set \"scheduleMode\" in config of the plugin\n"); - } - - // Check if plugin implements SchedulablePlugin and use event-driven startup - Plugin plugin = getPlugin(); - if (plugin instanceof SchedulablePlugin) { - logBuilder.append(" - Plugin implements SchedulablePlugin\n"); - } else { - logBuilder.append(" - Plugin does not implement SchedulablePlugin\n"); - } - - // Start the plugin directly - it will handle scheduler mode detection in its startUp() method - Microbot.getClientThread().runOnSeperateThread(() -> { - Plugin pluginToStart = getPlugin(); - if (pluginToStart == null) { - log.error("Plugin '{}' not found -> can't start plugin", name); - return false; - } - Microbot.startPlugin(pluginToStart); - return false; - }); - - stopInitiated = false; - hasStarted = true; - lastRunDuration = Duration.ZERO; // Reset last run duration - lastRunStartTime = ZonedDateTime.now(); // Set the start time of the last run - - // Start startup watchdog to monitor transition to running state - if (startupWatchdogEnabled) { - startStartupWatchdog(); - logBuilder.append(" - Started startup watchdog with timeout of ").append(startupWatchdogTimeout.toMinutes()).append(" minutes\n"); - } - - // Register/unregister appropriate event handlers - logBuilder.append(" - Registering stopping conditions\n"); - stopConditionManager.registerEvents(); - - logBuilder.append(" - Unregistering start conditions\n"); - startConditionManager.unregisterEvents(); - - // Log all collected information at once - log.info(logBuilder.toString()); - - return true; - } catch (Exception e) { - log.error("Error starting plugin '{}': {}", name, e.getMessage(), e); - return false; - } - } - - /** - * Triggers pre-schedule tasks for a SchedulablePlugin. - * This method should be called after the plugin has started and is subscribed to EventBus. - * - * @return true if the trigger was successful, false otherwise - */ - public boolean triggerPreScheduleTasks() { - try { - Plugin plugin = getPlugin(); - if (!(plugin instanceof SchedulablePlugin)) { - log.warn("Plugin '{}' does not implement SchedulablePlugin - cannot trigger pre-schedule tasks", name); - return false; - } - - SchedulablePlugin schedulablePlugin = (SchedulablePlugin) plugin; - if (!isRunning()){ - log.warn("Plugin '{}' is not running - cannot trigger pre-schedule tasks", name); - return false; - } - if (schedulablePlugin.getPrePostScheduleTasks() == null) { - log.debug("Plugin '{}' has no pre/post schedule tasks configured", name); - return false; - } - if (!schedulablePlugin.getPrePostScheduleTasks().canStartPreScheduleTasks()) { - log.warn("Pre-schedule tasks cannot be started for plugin '{}' - already running or not ready", name); - return false; - } - // Check if pre-schedule tasks are already running or completed - if (schedulablePlugin.getPrePostScheduleTasks().isPreScheduleRunning()) { - log.warn("Pre-schedule tasks are already running for plugin '{}' - cannot trigger again", name); - return false; - } - - if (schedulablePlugin.getPrePostScheduleTasks().isPreTaskComplete()) { - log.debug("Pre-schedule tasks already completed for plugin '{}' - skipping trigger", name); - return false; - } - - log.info("Triggering pre-schedule tasks for plugin '{}'", name); - - // Send start event to the running plugin - Microbot.getEventBus().post(new PluginScheduleEntryPreScheduleTaskEvent(plugin, true)); - - return true; - } catch (Exception e) { - log.error("Error triggering pre-schedule tasks for plugin '{}': {}", name, e.getMessage(), e); - return false; - } - } - - /** - * Triggers post-schedule tasks for a plugin that implements SchedulablePlugin. - *

- * This method should be called after the plugin has been stopped. - * - * @param successful Whether the plugin run was successful - * @return true if the trigger was successful, false otherwise - */ - public boolean triggerPostScheduleTasks(StopReason stopReason) { - boolean successful = (stopReason == StopReason.PLUGIN_FINISHED || stopReason == StopReason.SCHEDULED_STOP); - try { - Plugin plugin = getPlugin(); - if (!(plugin instanceof SchedulablePlugin)) { - log.warn("Plugin '{}' does not implement SchedulablePlugin - cannot trigger post-schedule tasks", name); - return false; - } - - SchedulablePlugin schedulablePlugin = (SchedulablePlugin) plugin; - - // Send post-schedule task event to the plugin - ZonedDateTime current_time = ZonedDateTime.now(ZoneId.systemDefault()); - - if (schedulablePlugin.getPrePostScheduleTasks() == null && isRunning()){ - // we must send it, when we dont have a pre/post schedule task, so the plugin can handle it a soft stop without defining a post schedule task - log.info("Plugin '{}' has no pre/post schedule tasks configured, sending soft stop event", name); - Microbot.getEventBus().post(new PluginScheduleEntrySoftStopEvent(plugin, current_time)); - Microbot.getEventBus().post(new PluginScheduleEntryPostScheduleTaskEvent(plugin,current_time)); - return true; - } - if (schedulablePlugin.getPrePostScheduleTasks() == null || !isRunning()) { - log.warn("Plugin '{}' has no pre/post schedule tasks configured", name); - return false; - } - if (!schedulablePlugin.getPrePostScheduleTasks().canStartPostScheduleTasks()) { - log.warn("Post-schedule tasks cannot be started for plugin '{}' - already running or not ready", name); - return false; - } - - // Check if post-schedule tasks are already running - if (schedulablePlugin.getPrePostScheduleTasks().isPostScheduleRunning()) { - log.warn("Post-schedule tasks are already running for plugin '{}' - cannot trigger again", name); - return false; - } - - log.info("Triggering post-schedule tasks for plugin '{}' (successful: {})", name, successful); - - Microbot.getEventBus().post(new PluginScheduleEntrySoftStopEvent(plugin, current_time)); - Microbot.getEventBus().post(new PluginScheduleEntryPostScheduleTaskEvent(plugin,current_time)); - return true; - } catch (Exception e) { - log.error("Error triggering post-schedule tasks for plugin '{}': {}", name, e.getMessage(), e); - return false; - } - } - - /** - * Initiates a graceful (soft) stop of the plugin. - *

- * This method notifies the plugin that it should stop via a {@link PluginScheduleEntryPostScheduleTaskEvent}, - * allowing the plugin to finish critical operations before shutting down. It also: - *

    - *
  • Resets and re-registers start condition monitors
  • - *
  • Unregisters stop condition monitors
  • - *
  • Records timing information about the stop attempt
  • - *
  • Starts a monitoring thread to track the stopping process
  • - *
- *

- * After sending the stop event, the plugin is responsible for handling its own shutdown. - * - * @param successfulRun indicates whether the plugin completed its task successfully - */ - private void softStop(StopReason stopReason) { - if (getPlugin() == null) { - return; - } - - try { - // Reset start conditions - startConditionManager.registerEvents(); - stopConditionManager.unregisterEvents(); - - Microbot.getClientThread().runOnClientThreadOptional(() -> { - ZonedDateTime current_time = ZonedDateTime.now(ZoneId.systemDefault()); - triggerPostScheduleTasks(stopReason); - //Microbot.getEventBus().post(new PluginScheduleEntryPostScheduleTaskEvent(plugin, current_time)); - // Notify the plugin to stop gracefully -> about to removal, switch to the post schedule task event - //Microbot.getEventBus().post(new PluginScheduleEntrySoftStopEvent(plugin, current_time)); - return true; - }); - if(!stopInitiated){ - this.stopInitiated = true; - this.stopInitiatedTime = ZonedDateTime.now(); - // Stop startup watchdog since plugin is being stopped - stopStartupWatchdog(); - } - // If no custom stop reason was set, use the default reason from the enum - if (lastStopReason == null && lastStopReasonType != null) { - lastStopReason = lastStopReasonType.getDescription(); - } - this.lastStopAttemptTime = ZonedDateTime.now(); - this.lastRunDuration = Duration.between(lastRunStartTime, ZonedDateTime.now()); - this.lastRunEndTime = ZonedDateTime.now(); - // Start monitoring for successful stop - startStopMonitoringThread(stopReason); - - if (getPlugin() instanceof SchedulablePlugin) { - log.info("soft stopping for plugin '{}'", name); - } - return; - } catch (Exception e) { - return; - } - } - - /** - * Forces an immediate (hard) stop of the plugin. - *

- * This method is used when a soft stop has failed or timed out and the plugin - * needs to be forcibly terminated. It directly calls Microbot's stopPlugin method - * to immediately terminate the plugin's execution. - *

- * Hard stops should only be used as a last resort when soft stops fail, as they - * don't allow the plugin to perform cleanup operations or save state. - * - * @param successfulRun indicates whether to record this run as successful - */ - private void hardStop(StopReason stopReason) { - if (getPlugin() == null) { - return; - } - boolean successfulRun = (stopReason == StopReason.PLUGIN_FINISHED || stopReason == StopReason.SCHEDULED_STOP); - try { - Microbot.getClientThread().runOnSeperateThread(() -> { - log.info("Hard stopping plugin '{}' - successfulRun {}", name, successfulRun); - Plugin stopPlugin = Microbot.getPlugin(plugin.getClass().getName()); - Microbot.stopPlugin(stopPlugin); - return false; - }); - if(!stopInitiated){ - stopInitiated = true; - stopInitiatedTime = ZonedDateTime.now(); - // Stop startup watchdog since plugin is being stopped - stopStartupWatchdog(); - } - lastStopAttemptTime = ZonedDateTime.now(); - // Set these fields to match what softStop does - lastRunDuration = Duration.between(lastRunStartTime, ZonedDateTime.now()); - lastRunEndTime = ZonedDateTime.now(); - hasStarted = false; - // Also set a descriptive stop reason if one isn't already set - if (lastStopReason == null) { - lastStopReason = lastStopReasonType != null && lastStopReasonType == StopReason.HARD_STOP - ? lastStopReasonType.getDescription() - : "Plugin was forcibly stopped after not responding to soft stop"; - } - // Start monitoring for successful stop - startStopMonitoringThread(stopReason); - - return; - } catch (Exception e) { - return; - } - } - - /** - * Starts a monitoring thread that tracks the stopping process of a plugin. - *

- * This method creates a daemon thread that periodically checks if a plugin - * that is in the process of stopping has completed its shutdown. When the plugin - * successfully stops, this method updates the next scheduled run time and clears - * all stopping-related state flags. - *

- * The monitoring thread will only be started if one is not already running - * (controlled by the isMonitoringStop flag). It checks the plugin's running state - * every 500ms until the plugin stops or monitoring is canceled. - *

- * The thread is created as a daemon thread to prevent it from blocking JVM shutdown. - */ - private void startStopMonitoringThread(StopReason stopReason) { - // Don't start a new thread if one is already running - if (isMonitoringStop) { - return; - } - - isMonitoringStop = true; - - stopMonitorThread = new Thread(() -> { - StringBuilder logMsg = new StringBuilder(); - logMsg.append("\n\tMonitoring thread started for stopping the plugin '").append(getCleanName()).append("' "); - log.info(logMsg.toString()); - logMsg = new StringBuilder(); - boolean successfulRun = (stopReason == StopReason.PLUGIN_FINISHED || stopReason == StopReason.SCHEDULED_STOP); - try { - // Keep checking until the stop completes or is abandoned - while (stopInitiated && isMonitoringStop) { - // Check if plugin has stopped running - if (!isRunning()) { - - logMsg.append("\nPlugin '").append(getCleanName()).append("' has successfully stopped") - .append(" - updating state - successfulRun ").append(successfulRun); - - // Set scheduleMode back to false when the plugin stops - if (scheduleEntryConfigManager != null) { - scheduleEntryConfigManager.setScheduleMode(false); - logMsg.append("\n unset \"scheduleMode\" - flag in the config. of the plugin '").append(getCleanName()).append("'"); - } - - - - break; - } - else { - // Plugin is still running, log the status - if (stopInitiatedTime != null && Duration.between(stopInitiatedTime, ZonedDateTime.now()).getSeconds()% 60==0) { - logMsg = new StringBuilder(); - logMsg.append("\nPlugin '").append(getCleanName()).append("' is still running"); - logMsg.append("\n- stop initiated at: ").append(stopInitiatedTime.format(DATE_TIME_FORMATTER)) - .append("\n- current time: ").append(ZonedDateTime.now().format(DATE_TIME_FORMATTER)); - logMsg.append("\n- elapsed time: ").append(Duration.between(stopInitiatedTime, ZonedDateTime.now()).toSeconds()) - .append(" sec - successfulRun ").append(successfulRun); - log.debug(logMsg.toString()); - } - stop(successfulRun); // Call the stop method to handle any additional logic - } - - // Check every 600ms to be responsive but not wasteful - Thread.sleep(600); - } - } catch (InterruptedException e) { - // Thread was interrupted, just exit - log.warn("\n\tStop monitoring thread for '" + name + "' was interrupted"); - } finally { - // Update lastRunTime and start conditions for next run based on stop reason - if (stopReason == StopReason.PLUGIN_FINISHED || stopReason == StopReason.SCHEDULED_STOP) { - // Success cases - reset start conditions - resetStartConditions(); - } else if (stopReason == StopReason.ERROR || stopReason == StopReason.HARD_STOP) { - // Hard failures - disable the plugin - setEnabled(false); - } else { - // For other cases (interruptions, manual stops), keep enabled but don't reset conditions - log.debug("Plugin '{}' stopped with reason: {}, keeping enabled", name, stopReason.getDescription()); - } - log.debug(logMsg.toString()); - //logStopConditionsWithDetails(); - // Reset stop state - isMonitoringStop = false; // Reset the monitoring flag - stopInitiated = false; - hasStarted = false; - stopInitiatedTime = null; - lastStopAttemptTime = null; - // Invoke the stop completion callback if one is registered - if (stopCompletionCallback != null) { - try { - boolean wasSuccessful = (stopReason == StopReason.PLUGIN_FINISHED || stopReason == StopReason.SCHEDULED_STOP); - stopCompletionCallback.onStopCompleted(PluginScheduleEntry.this, wasSuccessful); - log.debug("Stop completion callback executed for plugin '{}'", name); - } catch (Exception e) { - log.error("Error executing stop completion callback for plugin '{}'", name, e); - } - } - log.debug("Stop monitoring thread exited for plugin '" + name + "'"); - - } - }); - - stopMonitorThread.setName("StopMonitor-" + name); - stopMonitorThread.setDaemon(true); // Use daemon thread to not prevent JVM exit - stopMonitorThread.start(); - - } - public void cancelStop(){ - stopMonitoringThread(); // Stop the monitoring thread if it's running - isMonitoringStop = false; // Reset the monitoring flag - stopInitiated = false; - stopInitiatedTime = null; - lastStopAttemptTime = null; - } - - /** - * Stops the monitoring thread if it's running - */ - private void stopMonitoringThread() { - if (isMonitoringStop && stopMonitorThread != null) { - isMonitoringStop = false; - stopMonitorThread.interrupt(); - stopMonitorThread = null; - } - } - - /** - * Checks if this plugin schedule has any defined stop conditions - * - * @return true if at least one stop condition is defined - */ - public boolean hasAnyStopConditions() { - return stopConditionManager != null && - !stopConditionManager.getConditions().isEmpty(); - } - - /** - * Checks if this plugin has any one-time stop conditions that can only trigger once - * - * @return true if at least one single-trigger condition exists in the stop conditions - */ - public boolean hasAnyOneTimeStopConditions() { - return stopConditionManager != null && - stopConditionManager.hasAnyOneTimeConditions(); - } - - /** - * Checks if any stop conditions have already triggered and cannot trigger again - * - * @return true if at least one stop condition has triggered and cannot trigger again - */ - public boolean hasTriggeredOneTimeStopConditions() { - return stopConditionManager != null && - stopConditionManager.hasTriggeredOneTimeConditions(); - } - - /** - * Determines if the stop conditions can trigger again in the future - * Considers the nested logical structure and one-time conditions - * - * @return true if the stop condition structure can trigger again - */ - public boolean canStopTriggerAgain() { - return stopConditionManager != null && - stopConditionManager.canTriggerAgain(); - } - - /** - * Gets the next time when any stop condition is expected to trigger - * - * @return Optional containing the next stop trigger time, or empty if none exists - */ - public Optional getNextStopTriggerTime() { - if (stopConditionManager == null) { - return Optional.empty(); - } - return stopConditionManager.getCurrentTriggerTime(); - } - - /** - * Gets a human-readable string representing when the next stop condition will trigger - * - * @return String with the time until the next stop trigger, or a message if none exists - */ - public String getNextStopTriggerTimeString() { - if (stopConditionManager == null) { - return "No stop conditions defined"; - } - return stopConditionManager.getCurrentTriggerTimeString(); - } - - /** - * Checks if the stop conditions are fulfillable based on their structure and state - * A condition is considered unfulfillable if it contains one-time conditions that - * have all already triggered in an OR structure, or if any have triggered in an AND structure - * - * @return true if the stop conditions can still be fulfilled - */ - public boolean hasFullfillableStopConditions() { - if (!hasAnyStopConditions()) { - return false; - } - - // If we have any one-time conditions that can't trigger again - // and the structure is such that it can't satisfy anymore, then it's not fulfillable - if (hasAnyOneTimeStopConditions() && !canStopTriggerAgain()) { - return false; - } - - return true; - } - - /** - * Gets the remaining duration until the next stop condition trigger - * - * @return Optional containing the duration until next stop trigger, or empty if none available - */ - public Optional getDurationUntilStopTrigger() { - if (stopConditionManager == null) { - return Optional.empty(); - } - return stopConditionManager.getDurationUntilNextTrigger(); - } - - - - - /** - * Determines whether this plugin is currently running. - *

- * This method checks if the plugin is enabled in the RuneLite plugin system. - * It uses the Microbot API to query if the plugin associated with this schedule - * entry is currently in an active/running state. - * - * @return true if the plugin is currently running, false otherwise - */ - public boolean isRunning() { - Plugin plugin = getPlugin(); - if (plugin != null) { - return Microbot.isPluginEnabled(plugin.getClass()) && hasStarted; - } - return false; - } - - public boolean isStopped() { - Plugin plugin = getPlugin(); - if (plugin != null) { - return !Microbot.isPluginEnabled(plugin.getClass()) && !stopInitiated; - } - return false; - } - public boolean isStopping() { - return stopInitiated; - } - - /** - * Round time to nearest minute (remove seconds and milliseconds) - */ - private ZonedDateTime roundToMinutes(ZonedDateTime time) { - return time.withSecond(0).withNano(0); - } - private void logStartCondtions() { - List conditionList = startConditionManager.getConditions(); - logConditionInfo(conditionList,"Defined Start Conditions", true); - } - private void logStartConditionsWithDetails() { - List conditionList = startConditionManager.getConditions(); - logConditionInfo(conditionList,"Defined Start Conditions", true); - } - - /** - * Checks if this plugin schedule has any defined start conditions - * - * @return true if at least one start condition is defined - */ - public boolean hasAnyStartConditions() { - return startConditionManager != null && - !startConditionManager.getConditions().isEmpty(); - } - - /** - * Checks if this plugin has any one-time start conditions that can only trigger once - * - * @return true if at least one single-trigger condition exists in the start conditions - */ - public boolean hasAnyOneTimeStartConditions() { - return startConditionManager != null && - startConditionManager.hasAnyOneTimeConditions(); - } - - /** - * Checks if any start conditions have already triggered and cannot trigger again - * - * @return true if at least one start condition has triggered and cannot trigger again - */ - public boolean hasTriggeredOneTimeStartConditions() { - return startConditionManager != null && - startConditionManager.hasTriggeredOneTimeConditions(); - } - - /** - * Determines if the start conditions can trigger again in the future - * Considers the nested logical structure and one-time conditions - * - * @return true if the start condition structure can trigger again - */ - public boolean canStartTriggerAgain() { - return startConditionManager != null && - startConditionManager.canTriggerAgain(); - } - - /** - * Gets the next time when any start condition is expected to trigger - * - * @return Optional containing the next start trigger time, or empty if none exists - */ - public Optional getCurrentStartTriggerTime() { - if (startConditionManager == null) { - return Optional.empty(); - } - return startConditionManager.getCurrentTriggerTime(); - } - - /** - * Gets a human-readable string representing when the next start condition will trigger - * - * @return String with the time until the next start trigger, or a message if none exists - */ - public String getCurrentStartTriggerTimeString() { - if (startConditionManager == null) { - return "No start conditions defined"; - } - return startConditionManager.getCurrentTriggerTimeString(); - } - - /** - * Checks if the start conditions are fulfillable based on their structure and state - * A condition is considered unfulfillable if it contains one-time conditions that - * have all already triggered in an OR structure, or if any have triggered in an AND structure - * - * @return true if the start conditions can still be fulfilled - */ - public boolean hasFullfillableStartConditions() { - if (!hasAnyStartConditions()) { - return false; - } - - // If we have any one-time conditions that can't trigger again - // and the structure is such that it can't satisfy anymore, then it's not fulfillable - if (hasAnyOneTimeStartConditions() && !canStartTriggerAgain()) { - return false; - } - - return true; - } - - /** - * Gets the remaining duration until the next start condition trigger - * - * @return Optional containing the duration until next start trigger, or empty if none available - */ - public Optional getDurationUntilStartTrigger() { - if (startConditionManager == null) { - return Optional.empty(); - } - return startConditionManager.getDurationUntilNextTrigger(); - } - /** - * Gets a detailed description of the stop conditions status - * - * @return A string with detailed information about stop conditions - */ - public String getDetailedStopConditionsStatus() { - if (!hasAnyStopConditions()) { - return "No stop conditions defined"; - } - - StringBuilder sb = new StringBuilder("Stop conditions: "); - - // Add logic type - sb.append(stopConditionManager.requiresAll() ? "ALL must be met" : "ANY can be met"); - - // Add fulfillability status - if (!hasFullfillableStopConditions()) { - sb.append(" (UNFULFILLABLE)"); - } - - // Add condition count - int total = getTotalStopConditionCount(); - int satisfied = getSatisfiedStopConditionCount(); - sb.append(String.format(" - %d/%d conditions met", satisfied, total)); - - // Add next trigger time if available - Optional nextTrigger = getNextStopTriggerTime(); - if (nextTrigger.isPresent()) { - sb.append(" - Next trigger: ").append(getNextStopTriggerTimeString()); - } - - return sb.toString(); - } - /** - * Gets a detailed description of the start conditions status - * - * @return A string with detailed information about start conditions - */ - public String getDetailedStartConditionsStatus() { - if (!hasAnyStartConditions()) { - return "No start conditions defined"; - } - - StringBuilder sb = new StringBuilder("Start conditions: "); - - // Add logic type - sb.append(startConditionManager.requiresAll() ? "ALL must be met" : "ANY can be met"); - - // Add fulfillability status - if (!hasFullfillableStartConditions()) { - sb.append(" (UNFULFILLABLE)"); - } - - // Add condition count and satisfaction status - int totalStartConditions = startConditionManager.getConditions().size(); - long satisfiedStartConditions = startConditionManager.getConditions().stream() - .filter(Condition::isSatisfied) - .count(); - sb.append(String.format(" - %d/%d conditions met", satisfiedStartConditions, totalStartConditions)); - - // Add next trigger time if available - Optional nextTrigger = getCurrentStartTriggerTime(); - if (nextTrigger.isPresent()) { - sb.append(" - Next trigger: ").append(getCurrentStartTriggerTimeString()); - } - - return sb.toString(); - } - - /** - * Determines if the plugin should be started immediately based on its current - * start condition status - * - * @return true if the plugin should be started immediately - */ - public boolean shouldStartImmediately() { - // If no start conditions, don't start automatically - if (!hasAnyStartConditions()) { - return false; - } - - // If start conditions are met, start the plugin - if (areUserStartConditionsMet() && arePluginStartConditionsMet()) { - if ( startConditionManager.getConditions().isEmpty()){ - log.info("Plugin '{}' has no start conditions defined, starting immediately", name); - return false; - } - return true; - } - - return false; - } - public boolean canBeStarted() { - // If no start conditions, don't start automatically - if (isRunning()) { - return false; - } - if(!isEnabled()){ - return false; - } - - - // If start conditions are met, start the plugin - if (areUserStartConditionsMet() && arePluginStartConditionsMet()) { - return true; - } - - return false; - - } - - /** - * Logs the defined start conditions with their current states - */ - private void logDefinedStartConditionWithStates() { - logStartConditionsWithDetails(); - - // If the conditions are unfulfillable, log a warning - if (!hasFullfillableStartConditions()) { - log.warn("Plugin {} has unfulfillable start conditions - may not start properly", name); - } - - // Log progress percentage - double progress = startConditionManager.getProgressTowardNextTrigger(); - log.info("Plugin {} start condition progress: {:.2f}%", name, progress); - } - - /** - * Updates the isDueToRun method to use the diagnostic helper for logging - */ - public boolean isDueToRun() { - // Check if we're already running - if (isRunning()) { - return false; - } - - // For plugins with start conditions, check if those conditions are met - if (!hasAnyStartConditions()) { - //log.info("No start conditions defined for plugin '{}'", name); - return false; - } - - - - // Log at appropriate levels - if (Microbot.isDebug()) { - // Build comprehensive log info using our diagnostic helper - String diagnosticInfo = diagnoseStartConditions(); - // In debug mode, log the full detailed diagnostics - log.debug("\n[isDueToRun] - \n"+diagnosticInfo); - } - - - // Check if start conditions are met - return startConditionManager.areAllConditionsMet(); - } - - /** - * Updates the primary time condition for this plugin schedule entry. - * This method replaces the original time condition that was added when the entry was created, - * but preserves any additional conditions that might have been added later. - * - * @param newTimeCondition The new time condition to use - * @return true if a time condition was found and replaced, false otherwise - */ - public boolean updatePrimaryTimeCondition(TimeCondition newTimeCondition) { - if (startConditionManager == null || newTimeCondition == null) { - return false; - } - startConditionManager.pauseWatchdogs(); - // First, find the existing time condition. We'll assume the first time condition - // we find is the primary one that was added at creation - TimeCondition existingTimeCondition = this.mainTimeStartCondition; - - // If we found a time condition, replace it - if (existingTimeCondition != null) { - Optional currentTrigDateTime = existingTimeCondition.getCurrentTriggerTime(); - Optional newTrigDateTime = newTimeCondition.getCurrentTriggerTime(); - log.debug("Replacing time condition {} with {}", - existingTimeCondition.getDescription(), - newTimeCondition.getDescription()); - - - boolean isDefaultByScheduleType = this.isDefault(); - - - // Check if new condition is a one-second interval (default) - boolean willBeDefaultByScheduleType = false; - if (newTimeCondition instanceof IntervalCondition) { - IntervalCondition intervalCondition = (IntervalCondition) newTimeCondition; - if (intervalCondition.getInterval().getSeconds() <= 1) { - willBeDefaultByScheduleType = true; - } - } - - // Remove the existing condition and add the new one - if (startConditionManager.removeCondition(existingTimeCondition)) { - if (!startConditionManager.containsCondition(newTimeCondition)) { - startConditionManager.addUserCondition(newTimeCondition); - } - - // Update default status if needed - if (willBeDefaultByScheduleType) { - //this.setDefault(true); - //this.setPriority(0); - } else if (isDefaultByScheduleType && !willBeDefaultByScheduleType) { - // Only change from default if it was set automatically by condition type - //this.setDefault(false); - } - - this.mainTimeStartCondition = newTimeCondition; - } - if (currentTrigDateTime.isPresent() && newTrigDateTime.isPresent()) { - // Check if the new trigger time is different from the current one - if (!currentTrigDateTime.get().equals(newTrigDateTime.get())) { - log.debug("\n\tUpdated main start time for Plugin'{}'\nfrom {}\nto {}", - name, - currentTrigDateTime.get().format(DATE_TIME_FORMATTER), - newTrigDateTime.get().format(DATE_TIME_FORMATTER)); - } else { - log.debug("\n\tStart next time for Pugin '{}' remains unchanged", name); - } - } - } else { - // No existing time condition found, just add the new one - log.info("No existing time condition found, adding new condition: {}", - newTimeCondition.getDescription()); - // Check if the condition already exists before adding it - if (startConditionManager.containsCondition(newTimeCondition)) { - log.info("Condition {} already exists in the manager, not adding a duplicate", - newTimeCondition.getDescription()); - // Still need to update start conditions in case the existing one needs resetting - }else{ - startConditionManager.addUserCondition(newTimeCondition); - } - this.mainTimeStartCondition = newTimeCondition; - //updateStartConditions();// we have new condition -> new start time ? - } - startConditionManager.resumeWatchdogs(); - return true; - } - - /** - * Update the lastRunTime to now and reset start conditions - */ - private void resetStartConditions() { - if (startConditionManager == null) { - return; - } - - StringBuilder logMsg = new StringBuilder("\n"); - Optional nextTriggerTimeBeforeReset = getCurrentStartTriggerTime(); - - logMsg.append("Updating start conditions for plugin '").append(getCleanName()).append("'"); - logMsg.append("\n -last stop reason: ").append(lastStopReasonType.getDescription()); - logMsg.append("\n -last stop reason message:\n\t").append(lastStopReason); - logMsg.append("\n -allowContinue: ").append(allowContinue); - logMsg.append("\n -last run duration: ").append(lastRunDuration.toMillis()).append(" ms"); - if (this.lastStopReasonType != StopReason.INTERRUPTED || !allowContinue) { - - logMsg.append("\n -Completed successfully, resetting all start conditions"); - startConditionManager.reset(); - // Increment the run count since we completed a full run - incrementRunCount(); - } else { - logMsg.append("\n -Only resetting plugin '").append(getCleanName()).append("' start conditions"); - startConditionManager.resetPluginConditions(); - } - - Optional triggerTimeAfterReset = getCurrentStartTriggerTime(); - - // Update the nextRunTime for legacy compatibility if possible - if (triggerTimeAfterReset.isPresent()) { - ZonedDateTime nextRunTime = triggerTimeAfterReset.get(); - logMsg.append("\n - Updated run time for Plugin '").append(getCleanName()).append("'") - .append("\n Before: ").append(nextTriggerTimeBeforeReset.map(t -> t.format(DATE_TIME_FORMATTER)).orElse("N/A")) - .append("\n After: ").append(nextRunTime.format(DATE_TIME_FORMATTER)); - } else if (hasTriggeredOneTimeStartConditions() && !canStartTriggerAgain()) { - logMsg.append("\n - One-time conditions triggered, not scheduling next run"); - } - - // Output the consolidated log message - log.info(logMsg.toString()); - } - - /** - * Reset stop conditions - */ - private void resetStopConditions() { - if (!stopInitiated){ - log.info("resetting stop conditions on start up of plugin '{}'", name); - if (stopConditionManager != null) { - stopConditionManager.reset(); - // Log that stop conditions were reset - log.debug("Reset stop conditions for plugin '{}'", name); - } - }else{ - - } - } - /** - * Reset stop conditions - */ - public void hardResetConditions() { - - if (stopConditionManager != null) { - stopConditionManager.hardResetUserConditions(); - // Log that stop conditions were reset - log.debug("Hard Reset stop conditions for plugin '{}'", name); - } - if (startConditionManager != null) { - startConditionManager.hardResetUserConditions(); - // Log that stop conditions were reset - log.debug("Hard Reset start conditions for plugin '{}'", name); - } - - } - - - - /** - * Get a formatted display of the scheduling interval - */ - public String getIntervalDisplay() { - if (!hasAnyStartConditions()) { - return "No schedule defined"; - } - - List timeConditions = startConditionManager.getTimeConditions(); - if (timeConditions.isEmpty()) { - return "Non-time conditions only"; - } - - // Check for common condition types - if (timeConditions.size() == 1) { - TimeCondition condition = timeConditions.get(0); - - return getTimeDisplayFromTimeCondition(condition); - } - - // If we have multiple time conditions, find the one that will trigger first - if (timeConditions.size() > 1) { - TimeCondition earliestTriggerCondition = findEarliestTriggerTimeCondition(timeConditions); - if (earliestTriggerCondition != null) { - return getTimeDisplayFromTimeCondition(earliestTriggerCondition) + " (Next to trigger)"; - } - } - - // If we have multiple time conditions or other complex scenarios but couldn't determine earliest - return "Complex time schedule"; - } - - /** - * Finds the time condition that will trigger first among a list of time conditions - * - * @param timeConditions List of time conditions to check - * @return The time condition that will trigger first, or null if none is found - */ - private TimeCondition findEarliestTriggerTimeCondition(List timeConditions) { - ZonedDateTime earliestTriggerTime = null; - TimeCondition earliestCondition = null; - - for (TimeCondition condition : timeConditions) { - Optional triggerTime = condition.getCurrentTriggerTime(); - if (triggerTime.isPresent()) { - ZonedDateTime nextTrigger = triggerTime.get(); - - // If this is the first valid trigger time we've found, or it's earlier than our current earliest - if (earliestTriggerTime == null || nextTrigger.isBefore(earliestTriggerTime)) { - earliestTriggerTime = nextTrigger; - earliestCondition = condition; - } - } - } - - return earliestCondition; - } - private String getTimeDisplayFromTimeCondition(TimeCondition condition) { - if (condition instanceof SingleTriggerTimeCondition) { - Optional triggerTime = ((SingleTriggerTimeCondition) condition).getNextTriggerTimeWithPause(); - if (!triggerTime.isPresent()) { - return "No trigger time available"; - } - return "Once at " + triggerTime.get().format(DATE_TIME_FORMATTER); - } - else if (condition instanceof IntervalCondition) { - return formatIntervalCondition((IntervalCondition) condition); - } - else if (condition instanceof TimeWindowCondition) { - return formatTimeWindowCondition((TimeWindowCondition) condition); - } - else if (condition instanceof DayOfWeekCondition) { - return formatDayOfWeekCondition((DayOfWeekCondition) condition); - } - return "Unknown time condition type: " + condition.getClass().getSimpleName(); - } - - /** - * Formats an interval condition into a user-friendly string - */ - private String formatIntervalCondition(IntervalCondition condition) { - Duration avgInterval = condition.getInterval(); - Duration minInterval = condition.getMinInterval(); - Duration maxInterval = condition.getMaxInterval(); - boolean isRandomized = condition.isRandomize(); - - if (!isRandomized) { - return formatTimeRange(avgInterval, null, false); - } else { - return "Randomized " + formatTimeRange(minInterval, maxInterval, true); - } - } - - /** - * Formats a time window condition into a user-friendly string - */ - private String formatTimeWindowCondition(TimeWindowCondition condition) { - LocalTime startTime = condition.getStartTime(); - LocalTime endTime = condition.getEndTime(); - String timesStr = String.format("%s-%s", - startTime.format(DateTimeFormatter.ofPattern("HH:mm")), - endTime.format(DateTimeFormatter.ofPattern("HH:mm"))); - - // Check repeat cycle - String cycleStr = ""; - boolean useRandomization = false; - - try { - useRandomization = condition.isUseRandomization(); - RepeatCycle repeatCycle = condition.getRepeatCycle(); - int interval = condition.getRepeatIntervalUnit(); - - switch (repeatCycle) { - case DAYS: - cycleStr = (interval == 1) ? "daily" : "every " + interval + " days"; - break; - case WEEKS: - cycleStr = (interval == 1) ? "weekly" : "every " + interval + " weeks"; - break; - case HOURS: - cycleStr = (interval == 1) ? "hourly" : "every " + interval + " hours"; - break; - case MINUTES: - cycleStr = (interval == 1) ? "every minute" : "every " + interval + " minutes"; - break; - case ONE_TIME: - cycleStr = "once"; - break; - default: - cycleStr = "daily"; - } - } catch (Exception e) { - // Fallback if we can't access some property - cycleStr = "daily"; - } - - return useRandomization - ? String.format("Randomized %s %s", timesStr, cycleStr) - : String.format("%s %s", timesStr, cycleStr); - } - - /** - * Formats a day of week condition into a user-friendly string - */ - private String formatDayOfWeekCondition(DayOfWeekCondition condition) { - Set activeDays = condition.getActiveDays(); - - // Format day names - StringBuilder daysStr = new StringBuilder(); - - if (activeDays.size() == 7) { - daysStr.append("Every day"); - } else if (activeDays.size() == 5 && activeDays.contains(DayOfWeek.MONDAY) && - activeDays.contains(DayOfWeek.TUESDAY) && activeDays.contains(DayOfWeek.WEDNESDAY) && - activeDays.contains(DayOfWeek.THURSDAY) && activeDays.contains(DayOfWeek.FRIDAY)) { - daysStr.append("Weekdays"); - } else if (activeDays.size() == 2 && activeDays.contains(DayOfWeek.SATURDAY) && - activeDays.contains(DayOfWeek.SUNDAY)) { - daysStr.append("Weekends"); - } else { - List dayNames = new ArrayList<>(); - for (DayOfWeek day : activeDays) { - // Convert to short day name (Mon, Tue, etc.) - String dayName = day.toString().substring(0, 3); - dayNames.add(dayName.charAt(0) + dayName.substring(1).toLowerCase()); - } - // Sort days in week order (Monday first) - Collections.sort(dayNames); - daysStr.append(String.join("/", dayNames)); - } - - // Check if it has an interval condition - if (condition.hasIntervalCondition()) { - Optional intervalOpt = condition.getIntervalCondition(); - if (intervalOpt.isPresent()) { - IntervalCondition interval = intervalOpt.get(); - - // Add interval info - if (interval.isRandomize()) { - Duration minInterval = interval.getMinInterval(); - Duration maxInterval = interval.getMaxInterval(); - daysStr.append(", random ").append(formatTimeRange(minInterval, maxInterval, true)); - } else { - Duration avgInterval = interval.getInterval(); - daysStr.append(", ").append(formatTimeRange(avgInterval, null, false)); - } - } - } - - // Add max repeats information if applicable - long maxPerDay = condition.getMaxRepeatsPerDay(); - long maxPerWeek = condition.getMaxRepeatsPerWeek(); - - if (maxPerDay > 0 || maxPerWeek > 0) { - daysStr.append(" ("); - boolean needsComma = false; - - if (maxPerDay > 0) { - daysStr.append("max ").append(maxPerDay).append("/day"); - needsComma = true; - } - - if (maxPerWeek > 0) { - if (needsComma) { - daysStr.append(", "); - } - daysStr.append("max ").append(maxPerWeek).append("/week"); - } - - daysStr.append(")"); - } - - return daysStr.toString(); - } - - /** - * Helper to format time durations in a user-friendly string - */ - private String formatTimeRange(Duration duration, Duration maxDuration, boolean isRange) { - if (duration == null) { - return "unknown interval"; - } - - long hours = duration.toHours(); - long minutes = duration.toMinutes() % 60; - - if (!isRange) { - if (hours > 0) { - return String.format("every %d hour%s%s", - hours, - hours > 1 ? "s" : "", - minutes > 0 ? " " + minutes + " min" : ""); - } else { - return String.format("every %d minute%s", - minutes, - minutes > 1 ? "s" : ""); - } - } else { - // Format a range - "every X to Y hours/minutes" - if (maxDuration == null) { - return formatTimeRange(duration, null, false); - } - - long maxHours = maxDuration.toHours(); - long maxMinutes = maxDuration.toMinutes() % 60; - - if (hours > 0) { - if (maxHours > 0) { - // Both have hours component - String minStr = String.format("%d hour%s%s", - hours, - hours > 1 ? "s" : "", - minutes > 0 ? " " + minutes + "m" : ""); - - String maxStr = String.format("%d hour%s%s", - maxHours, - maxHours > 1 ? "s" : "", - maxMinutes > 0 ? " " + maxMinutes + "m" : ""); - - return String.format("every %s to %s", minStr, maxStr); - } else { - // Min has hours but max only has minutes - return String.format("every %d hour%s%s to %d minutes", - hours, - hours > 1 ? "s" : "", - minutes > 0 ? " " + minutes + "m" : "", - maxMinutes); - } - } else { - if (maxHours > 0) { - // Min has only minutes but max has hours - return String.format("every %d minutes to %d hour%s%s", - minutes, - maxHours, - maxHours > 1 ? "s" : "", - maxMinutes > 0 ? " " + maxMinutes + "m" : ""); - } else { - // Both only have minutes - return String.format("every %d to %d minute%s", - minutes, - maxMinutes, - maxMinutes > 1 ? "s" : ""); - } - } - } - } - - - /** - * Gets the time remaining until the next plugin - * - * @return Duration until next plugin or null if no plugins scheduled - */ - public Optional getTimeUntilNextRun() { - if (!enabled) { - return Optional.empty(); - } - // Get the next trigger time for this plugin - Optional nextTriggerTime = this.getCurrentStartTriggerTime(); - if (!nextTriggerTime.isPresent()) { - // If no trigger time is available, return empty - return Optional.empty(); - } - - // Calculate time until trigger - return Optional.of(Duration.between(ZonedDateTime.now(ZoneId.systemDefault()), nextTriggerTime.get())); - } - /** - * Get a formatted display of when this plugin will run next - */ - public String getNextRunDisplay() { - return getNextRunDisplay(System.currentTimeMillis()); - } - - /** - * Get a formatted display of when this plugin will run next, including - * condition information. - * - * @param currentTimeMillis Current system time in milliseconds - * @return Human-readable description of next run time or condition status - */ - public String getNextRunDisplay(long currentTimeMillis) { - if (!enabled) { - return "Disabled"; - } - - // If plugin is running, show progress or status information - if (isRunning()) { - String prefixLabel = "Running"; - if(stopConditionManager.isPaused()){ - prefixLabel = "Paused"; - } - - if (!stopConditionManager.getConditions().isEmpty()) { - double progressPct = getStopConditionProgress(); - if (progressPct > 0 && progressPct < 100) { - return String.format("%s (%.1f%% complete)", prefixLabel,progressPct); - } - return String.format("%s with conditions", prefixLabel); - } - return prefixLabel; - } - - // Check for start conditions - if (hasAnyStartConditions()) { - // Check if we can determine the next trigger time - Optional nextTrigger = getCurrentStartTriggerTime(); - if (nextTrigger.isPresent()) { - ZonedDateTime triggerTime = nextTrigger.get(); - ZonedDateTime currentTime = ZonedDateTime.ofInstant( - Instant.ofEpochMilli(currentTimeMillis), - ZoneId.systemDefault()); - - // If it's due to run now - if (!currentTime.isBefore(triggerTime)) { - return "Due to run"; - } - - // Calculate time until next run - Duration timeUntil = Duration.between(currentTime, triggerTime); - long hours = timeUntil.toHours(); - long minutes = timeUntil.toMinutes() % 60; - long seconds = timeUntil.getSeconds() % 60; - - if (hours > 0) { - return String.format("In %dh %dm", hours, minutes); - } else if (minutes > 0) { - return String.format("In %dm %ds", minutes, seconds); - } else { - return String.format("In %ds", seconds); - } - } else if (shouldStartImmediately()) { - return "Due to run"; - } else if (hasTriggeredOneTimeStartConditions() && !canStartTriggerAgain()) { - return "Completed"; - } - - return "Waiting for conditions"; - } - - - - return "Schedule not set"; - } - - /** - * Adds a user-defined start condition to this plugin schedule entry. - * Start conditions determine when the plugin should be executed. - * - * @param condition The condition to add to the start conditions list - */ - public void addStartCondition(Condition condition) { - startConditionManager.addUserCondition(condition); - } - - /** - * Adds a user-defined stop condition to this plugin schedule entry. - * Stop conditions determine when the plugin should terminate. - * - * @param condition The condition to add to the stop conditions list - */ - public void addStopCondition(Condition condition) { - stopConditionManager.addUserCondition(condition); - } - - /** - * Returns all stop conditions configured for this plugin schedule entry. - * - * @return List of currently active stop conditions - */ - public List getStopConditions() { - return stopConditionManager.getConditions(); - } - - /** - * Checks whether any stop conditions are defined for this plugin. - * - * @return true if at least one stop condition exists, false otherwise - */ - public boolean hasStopConditions() { - return stopConditionManager.hasConditions(); - } - - /** - * Checks whether any start conditions are defined for this plugin. - * - * @return true if at least one start condition exists, false otherwise - */ - public boolean hasStartConditions() { - return startConditionManager.hasConditions(); - } - - /** - * Returns all start conditions configured for this plugin schedule entry. - * - * @return List of currently active start conditions - */ - public List getStartConditions() { - return startConditionManager.getConditions(); - } - - /** - * Determines if the plugin can be stopped based on its current state and conditions. - *

- * A plugin can be stopped if: - *

    - *
  • It has finished its task
  • - *
  • It is running but has been disabled
  • - *
  • Plugin-defined stop conditions are met
  • - *
- * - * @return true if the plugin can be stopped, false otherwise - */ - public boolean allowedToBeStop() { - - if (isRunning()) { - if (!isEnabled()){ - return true; //enabled was disabled -> stop the plugin gracefully -> soft stop should be trigged when possible - } - } - // Check if conditions are met and we should stop when conditions are met - if (arePluginStopConditionsMet() ) { - return true; - } - - return false; - } - public boolean shouldBeStopped() { - if (isRunning()) { - if (!isEnabled()){ - return true; //enabled was disabled -> stop the plugin gracefully -> soft stop should be trigged when possible - } - } - // Check if conditions are met and we should stop when conditions are met - if (arePluginStopConditionsMet() && (areUserDefinedStopConditionsMet())) { - if (stopConditionManager.getUserConditions().isEmpty()) { - //* -> do not stop a plugin if there are no user defined stop conditions - //* -> in that case the plugin must report finished to be stop or the user must manually stop it - return false; // we have plugin stop conditions -> we can stop the plugin -> stop condition of the plugin are look conditions, so if no condition are defined, we are allow to stop the plugin - } - return true; - } - - return false; - } - - /** - * Checks if plugin-defined stop conditions are met. - * If no plugin conditions are defined, returns true to allow stopping. - * - * @return true if plugin-defined stop conditions are met or none are defined - */ - private boolean arePluginStopConditionsMet() { - if (stopConditionManager.getPluginConditions().isEmpty()) { - return true; // we have plugin stop conditions -> we can stop the plugin -> stop condition of the plugin are look conditions, so if no condition are defined, we are allow to stop the plugin - } - return stopConditionManager.arePluginConditionsMet(); - } - - /** - * Checks if user-defined stop conditions are met. - * These are conditions added through the UI rather than by the plugin itself. - - * @return true if user-defined stop conditions are met, false if none exist or they're not met - */ - private boolean areUserDefinedStopConditionsMet() { - if (stopConditionManager.getUserConditions().isEmpty()) { - return true; - } - return stopConditionManager.areUserConditionsMet(); - } - - /** - * Checks if user-defined start conditions are met. - * These are conditions added through the UI rather than by the plugin itself. - * - * @return true if user-defined start conditions are met, false if none exist or they're not met - */ - private boolean areUserStartConditionsMet() { - if (startConditionManager.getUserConditions().isEmpty()) { - return true; - } - return startConditionManager.areUserConditionsMet(); - } - /** - * Checks if plugin-defined start conditions are met. - * If no plugin conditions are defined, returns true to allow starting. - * - * @return true if plugin-defined start conditions are met or none are defined - */ - private boolean arePluginStartConditionsMet() { - if (startConditionManager.getPluginConditions().isEmpty()) { - return true; // we have plugin start conditions -> we can start the plugin -> start condition of the plugin are look conditions, so if no condition are defined, we are allow to start the plugin - } - return startConditionManager.arePluginConditionsMet(); - } - /** - * Gets a description of the stop conditions for this plugin. - * - * @return A string describing the stop conditions - */ - public String getConditionsDescription() { - return stopConditionManager.getDescription(); - } - - /** - * Stops the plugin with ExecutionResult and custom message for the stop reason. - *

- * This method handles the graceful shutdown of a plugin by first attempting a soft - * stop, with a custom message indicating why the plugin was stopped. The ExecutionResult - * determines whether the plugin should be disabled (HARD_FAILURE) or remain enabled (SUCCESS/SOFT_FAILURE). - * - * @param result the execution result indicating success state - * @param reason the enum reason why the plugin is being stopped - * @param reasonMessage a custom message explaining why the plugin was stopped - * @return true if stop was initiated, false otherwise - */ - public boolean stop(ExecutionResult result, StopReason reason, String reasonMessage) { - // Set the custom stop reason message - if (!stopInitiated){ - this.lastStopReason = reasonMessage; - } - - // Call the ExecutionResult-based stop method - return stop(result, reason); - } - - /** - * @deprecated Use {@link #stop(ExecutionResult, StopReason, String)} instead for granular result reporting - */ - @Deprecated - public boolean stop(boolean successfulRun, StopReason reason, String reasonMessage) { - ExecutionResult result = successfulRun ? ExecutionResult.SUCCESS : ExecutionResult.HARD_FAILURE; - return stop(result, reason, reasonMessage); - } - /** - * Initiates the stopping process for a plugin with ExecutionResult-based monitoring. - *

- * This method handles the graceful shutdown of a plugin by first attempting a soft - * stop, which allows the plugin to finish any critical operations. The ExecutionResult - * determines whether the plugin should be disabled (HARD_FAILURE) or remain enabled (SUCCESS/SOFT_FAILURE). - *

- * For SOFT_FAILURE, the plugin remains enabled but the failure is tracked. After - * multiple consecutive soft failures, the plugin may be disabled. - * - * @param result the execution result indicating success state - * @param reason the enum reason why the plugin is being stopped - * @return true if stop was initiated, false otherwise - */ - public boolean stop(ExecutionResult result, StopReason reason) { - // Handle soft failure tracking - if (result == ExecutionResult.SOFT_FAILURE) { - recordSoftFailure(reason.getDescription()); - // If soft failures exceed threshold, convert to hard failure and disable - if (shouldConvertToHardFailure()) { - log.warn("Plugin '{}' exceeded soft failure threshold ({}), converting to hard failure and disabling", - getCleanName(), getMaxSoftFailuresBeforeHardFailure()); - result = ExecutionResult.HARD_FAILURE; - } - } else if (result == ExecutionResult.SUCCESS) { - recordSuccess(); - } - - // Convert ExecutionResult to boolean for existing logic - boolean successfulRun = result.isSuccess(); - - // Call the existing stop method with boolean - boolean stopInitiated = stop(successfulRun, reason); - if (this.getStopConditions() != null ){ - // Check if any LockConditions are still locked - boolean hasLockedConditions = this.getStopConditions().stream() - .filter(condition -> condition instanceof LockCondition) - .map(condition -> (LockCondition) condition) - .anyMatch(LockCondition::isLocked); - if(hasLockedConditions){ - //Should be done by the plugin itself, on shout down.. - //-- it also should only be relevant on a hard stop-> because otherwise the plugin should not be able to be stoped at the first hand** - //-- safeguard only, if the plugin is not running any more - if (reason != StopReason.HARD_STOP ){ - log.warn("Plugin '{}' has locked conditions but stop reason is '{}'. This may indicate the plugin did not handle shutdown properly.", - this.getCleanName(), reason); - } - log.debug("Unlocking any remaining lock conditions for the plugin '{}'", this.getCleanName()); - this.getStopConditions().stream() - .filter(condition -> condition instanceof LockCondition) - .map(condition -> (LockCondition) condition) - .forEach(lockCondition -> { - if (lockCondition.isLocked()) { - log.debug("Unlocking condition: {}", lockCondition.getReason()); - lockCondition.unlock(); - } - }); - - } - } - // Handle disabling for hard failures AFTER the stop is initiated - if (stopInitiated && result == ExecutionResult.HARD_FAILURE) { - log.warn("Plugin '{}' hard failure - will be disabled after stopping", getCleanName()); - // Schedule the disable operation to run after the plugin stops - // This will be handled in the monitoring thread's finally block - // by checking if lastRunSuccessful is false - } - - return stopInitiated; - } - - /** - * @deprecated Use {@link #stop(ExecutionResult, StopReason)} instead for granular result reporting - */ - @Deprecated - public boolean stop(boolean successfulRun, StopReason reason) { - ZonedDateTime now = ZonedDateTime.now(); - // Initial stop attempt - if (allowedToBeStop() || reason == StopReason.HARD_STOP || reason == StopReason.PREPOST_SCHEDULE_STOP || reason == StopReason.PLUGIN_FINISHED ){ - if(!stopInitiated){ - if (stopInitiatedTime == null) { - stopInitiatedTime = now; - } - if (lastStopAttemptTime == null) { - lastStopAttemptTime = now; - } - this.setLastRunSuccessful(successfulRun); - this.setLastStopReasonType(reason); - this.onLastStopPluginConditionsSatisfied = arePluginStopConditionsMet(); - this.onLastStopUserConditionsSatisfied = areUserDefinedStopConditionsMet(); - } - StringBuilder logMsg = new StringBuilder(); - logMsg.append("\n\tStopping the plugin \"").append(getCleanName()+"\""); - String blockingStartMsg = startConditionManager.getBlockingExplanation(); - String blockingStopMsg = stopConditionManager.getBlockingExplanation(); - if (reason != null) { - logMsg.append("\n\t---current stop reason:").append("\n\t\t"+reason.toString()); - if (this.lastStopReason != null && !this.lastStopReason.isEmpty()) { - logMsg.append("\n\t---last stop reason:\n********\n").append(this.lastStopReason+ "\n********"); - } - } - logMsg.append("\n\t---is running: ").append(isRunning()); - logMsg.append("\n\t---plugin stop conditions satisfied: ").append(arePluginStopConditionsMet()); - logMsg.append("\n\t---user stop conditions satisfied: ").append(areUserDefinedStopConditionsMet()); - log.info(logMsg.toString()); - logStopConditionsWithDetails(); - if (!stopInitiated && reason != StopReason.HARD_STOP && reason != StopReason.PREPOST_SCHEDULE_STOP) { - this.softStop(reason); // This will start the monitoring thread - }else if (reason == StopReason.HARD_STOP || reason == StopReason.PREPOST_SCHEDULE_STOP) { - // If we are already stopping and the reason is hard stop, just log it - this.hardStop(reason); // frist try soft stop, then hard stop if needed - } - }else{ - StringBuilder logMsg = new StringBuilder(); - logMsg.append("\n\tPlugin ").append(name).append(" is not allowed to stop. "); - String blockingStartMsg = startConditionManager.getBlockingExplanation(); - String blockingStopMsg = stopConditionManager.getBlockingExplanation(); - if (blockingStopMsg != null) { - logMsg.append("\n\t -Blocking reason: ").append(blockingStopMsg); - } - if (reason != null) { - logMsg.append("\n\t -Current stop reason: ").append(reason.toString()).append(" -- ").append(this.lastStopReason); - } - logMsg.append("\n\t -is running: ").append(isRunning()); - logMsg.append("\n\t -plugin stop conditions: ").append(arePluginStopConditionsMet()); - logMsg.append("\n\t -user stop conditions: ").append(areUserDefinedStopConditionsMet()); - log.info(logMsg.toString()); - } - log.info("\n\tPlugin {} stop initiated: {}", name, stopInitiated); - return this.stopInitiated; - } - private void stop(boolean successfulRun) { - ZonedDateTime now = ZonedDateTime.now(); - // Plugin didn't stop after previous attempts - if (isRunning()) { - Duration timeSinceFirstAttempt = Duration.between(this.stopInitiatedTime, now); - Duration timeSinceLastAttempt = Duration.between(this.lastStopAttemptTime, now); - // Force hard stop if we've waited too long - if ( (hardStopTimeout.compareTo(Duration.ZERO) > 0 && timeSinceFirstAttempt.compareTo(hardStopTimeout) > 0) - && (getPlugin() instanceof SchedulablePlugin) - && ((SchedulablePlugin) getPlugin()).allowHardStop()) { - log.warn("Plugin {} failed to respond to soft stop after {} seconds - forcing hard stop", - name, timeSinceFirstAttempt.toSeconds()); - - // Stop current monitoring and start new one for hard stop - stopMonitoringThread(); - this.setLastStopReasonType(StopReason.HARD_STOP); - this.hardStop(StopReason.HARD_STOP); - }else if(getLastStopReasonType() == StopReason.HARD_STOP){ // Stop current monitoring and start new one for hard stop - log.warn("Plugin {} user requested hard stop after {} seconds - forcing hard stop", - name, timeSinceFirstAttempt.toSeconds()); - stopMonitoringThread(); - this.setLastStopReasonType(StopReason.HARD_STOP); - this.hardStop(StopReason.HARD_STOP); - }else if (getLastStopReasonType() == StopReason.PREPOST_SCHEDULE_STOP){ - stopMonitoringThread(); - this.setLastStopReasonType(StopReason.PREPOST_SCHEDULE_STOP); - this.hardStop(StopReason.PREPOST_SCHEDULE_STOP); - } - // Retry soft stop at configured intervals - else if (timeSinceLastAttempt.compareTo(softStopRetryInterval) > 0) { - log.info("Plugin {} still running after soft stop - retrying (attempt time: {} seconds)", - name, timeSinceFirstAttempt.toSeconds()); - lastStopAttemptTime = now; - this.setLastStopReasonType(getLastStopReasonType()); - this.softStop(getLastStopReasonType()); - }else if (hardStopTimeout.compareTo(Duration.ZERO) > 0 && timeSinceFirstAttempt.compareTo(hardStopTimeout.multipliedBy(3)) > 0) { - log.error("Forcibly shutting down the client due to unresponsive plugin: {}", name); - // Schedule client shutdown on the client thread to ensure it happens safely - Microbot.getClientThread().invoke(() -> { - try { - // Log that we're shutting down - log.warn("Initiating emergency client shutdown due to plugin: {} cant be stopped", name); - // Give a short delay for logging to complete - Thread.sleep(1000); - // Forcibly exit the JVM with a non-zero status code to indicate abnormal termination - System.exit(1); - } catch (Exception e) { - log.error("Failed to shut down client", e); - // Ultimate fallback - Runtime.getRuntime().halt(1); - } - return true; - }); - } - } - } - - /** - * Checks if the plugin should be stopped based on its conditions and performs a stop if needed. - *

- * This method evaluates both plugin-defined and user-defined stop conditions to determine - * if the plugin should be stopped. If conditions indicate the plugin should stop, it initiates - * the stop process. - *

- * It also handles resetting stop state if conditions no longer require the plugin to stop. - * - * @param successfulRun whether to mark this run as successful when stopping - * @return true if stop process was initiated or is in progress, false otherwise - */ - public boolean checkConditionsAndStop(boolean successfulRun) { - - if (shouldBeStopped()) { - if (!this.stopInitiated){ - this.stopInitiated = this.stop(successfulRun,StopReason.SCHEDULED_STOP); - } - // Monitor thread will handle the successful stop case - } - // Reset stop tracking if conditions no longer require stopping - else if (!isRunning() && stopInitiated) { - log.info("Plugin {} conditions no longer require stopping - resetting stop state", name); - this.stopInitiated = false; - this.stopInitiatedTime = null; - this.lastStopAttemptTime = null; - stopMonitoringThread(); - } - return this.stopInitiated; - - } - - /** - * Logs all defined conditions when plugin starts - */ - private void logStopConditions() { - List conditionList = stopConditionManager.getConditions(); - logConditionInfo(conditionList,"Defined Stop Conditions", true); - } - - /** - * Logs which conditions are met and which aren't when plugin stops - */ - private void logStopConditionsWithDetails() { - List conditionList = stopConditionManager.getConditions(); - logConditionInfo(conditionList,"Defined Stop Conditions", true); - } - - - - - /** - * Creates a consolidated log of all condition-related information - * @param logINFOHeader The header to use for the log message - * @param includeDetails Whether to include full details of conditions - */ - public void logConditionInfo(List conditionList, String logINFOHeader, boolean includeDetails) { - - StringBuilder sb = new StringBuilder(); - - sb.append("\n\tPlugin '").append(cleanName).append("' [").append(logINFOHeader).append("]: "); - - if (conditionList.isEmpty()) { - sb.append("\n\t\tNo stop conditions defined"); - log.info(sb.toString()); - return; - } - - // Basic condition count and logic - sb.append(" \n\t\t"+conditionList.size()+" condition(s) using ") - .append(stopConditionManager.requiresAll() ? "AND" : "OR").append(" logic\n\t\t"); - - if (!includeDetails) { - log.info(sb.toString()); - return; - } - - // Detailed condition listing with status - - int metCount = 0; - - for (int i = 0; i < conditionList.size(); i++) { - Condition condition = conditionList.get(i); - boolean isSatisfied = condition.isSatisfied(); - if (isSatisfied) metCount++; - - // Use the new getStatusInfo method for detailed status - sb.append(" ").append(i + 1).append(". ") - .append(condition.getStatusInfo(0, includeDetails).replace("\n", "\n\t\t ")); - - sb.append("\n\t\t"); - } - - if (includeDetails) { - sb.append("Summary: ").append(metCount).append("/").append(conditionList.size()) - .append(" conditions met"); - } - - log.info(sb.toString()); - } - /** - * Registers conditions from the plugin in an efficient manner. - * This method uses the new updatePluginCondition approach to intelligently - * merge conditions while preserving state and reducing unnecessary reinitializations. - * - * @param updateMode Controls how conditions are merged (default: ADD_ONLY) - */ - private void registerPluginConditions(UpdateOption updateOption) { - if (this.plugin == null) { - this.plugin = getPlugin(); - } - - log.debug("Registering plugin conditions for plugin '{}' with update mode: {}", name, updateOption); - - // Register start conditions - boolean startConditionsUpdated = registerPluginStartingConditions(updateOption); - - // Register stop conditions - boolean stopConditionsUpdated = registerPluginStoppingConditions(updateOption); - - if (startConditionsUpdated || stopConditionsUpdated) { - log.debug("Successfully updated plugin conditions for '{}'", name); - // Optimize structure if changes were made - if (updateOption != UpdateOption.REMOVE_ONLY) { - optimizeConditionStructures(); - } - } else { - log.debug("No changes needed to plugin conditions for '{}'", name); - } - } - - /** - * Default version of registerPluginConditions that uses ADD_ONLY mode - */ - private void registerPluginConditions() { - registerPluginConditions(UpdateOption.SYNC); - } - - /** - * Registers or updates starting conditions from the plugin. - * Uses the updatePluginCondition method to efficiently merge conditions. - * - * @return true if conditions were updated, false if no changes were needed - */ - private boolean registerPluginStartingConditions(UpdateOption updateOption) { - if (this.plugin == null) { - this.plugin = getPlugin(); - } - - log.debug("Registering start conditions for plugin '{}'", name); - this.startConditionManager.pauseWatchdogs(); - this.startConditionManager.setPluginCondition(new OrCondition()); - if (!(this.plugin instanceof SchedulablePlugin)) { - log.debug("Plugin '{}' is not a SchedulablePlugin, skipping start condition registration", name); - return false; - } - - SchedulablePlugin provider = (SchedulablePlugin) plugin; - - // Get conditions from the provider - if (provider.getStartCondition() == null) { - log.warn("Plugin '{}' implements ConditionProvider but provided no start conditions", plugin.getName()); - return false; - } - - List pluginConditions = provider.getStartCondition().getConditions(); - if (pluginConditions == null || pluginConditions.isEmpty()) { - log.debug("Plugin '{}' provided no explicit start conditions", plugin.getName()); - return false; - } - - // Get or create plugin's logical structure - LogicalCondition pluginLogic = provider.getStartCondition(); - - if (pluginLogic == null) { - log.warn("Plugin '{}' returned null start condition", name); - return false; - } - // Use the new update method with the specified option - boolean updated = getStartConditionManager().updatePluginCondition(pluginLogic, updateOption); - - // Log with a consolidated method if changes were made - if (updated) { - log.debug("Updated start conditions for plugin '{}'", name); - logStartConditionsWithDetails(); - - // Validate the condition structure - validateStartConditions(); - } - this.startConditionManager.resumeWatchdogs(); - - return updated; - } - - /** - * Registers or updates stopping conditions from the plugin. - * Uses the updatePluginCondition method to efficiently merge conditions. - * - * @return true if conditions were updated, false if no changes were needed - */ - private boolean registerPluginStoppingConditions(UpdateOption updateOption) { - if (this.plugin == null) { - this.plugin = getPlugin(); - } - this.stopConditionManager.pauseWatchdogs(); - this.stopConditionManager.setPluginCondition(new OrCondition()); - log.debug("Registering stopping conditions for plugin '{}'", name); - - if (!(this.plugin instanceof SchedulablePlugin)) { - log.debug("Plugin '{}' is not a SchedulablePlugin, skipping stop condition registration", name); - return false; - } - - SchedulablePlugin provider = (SchedulablePlugin) plugin; - - // Get conditions from the provider - if (provider.getStopCondition() == null) { - log.debug("Plugin '{}' provided no explicit stop conditions", plugin.getName()); - return false; - } - List pluginConditions = provider.getStopCondition().getConditions(); - if (pluginConditions == null || pluginConditions.isEmpty()) { - log.debug("Plugin '{}' provided no explicit stop conditions", plugin.getName()); - return false; - } - - // Get plugin's logical structure - LogicalCondition pluginLogic = provider.getStopCondition(); - - if (pluginLogic == null) { - log.warn("Plugin '{}' returned null stop condition", name); - return false; - } - - // Use the new update method with the specified option - boolean updated = getStopConditionManager().updatePluginCondition(pluginLogic, updateOption); - - // Log with the consolidated method if changes were made - if (updated) { - log.debug("Updated stop conditions for plugin '{}'", name); - logStopConditionsWithDetails(); - - // Validate the condition structure - validateStopConditions(); - } - this.stopConditionManager.resumeWatchdogs(); - - return updated; - } - - /** - * Creates and schedules watchdogs to monitor for condition changes from the plugin. - * This allows plugins to dynamically update their conditions at runtime, - * and have those changes automatically detected and integrated. - * - * Both start and stop condition watchdogs are scheduled using the shared thread pool - * from ConditionManager to avoid creating redundant resources. - * - * @param checkIntervalMillis How often to check for condition changes in milliseconds - * @param updateMode Controls how conditions are merged during updates - * @return true if at least one watchdog was successfully scheduled - */ - public boolean scheduleConditionWatchdogs(long checkIntervalMillis, UpdateOption updateOption) { - if(this.plugin == null) { - this.plugin = getPlugin(); - } - - if (!watchdogsEnabled) { - log.debug("Watchdogs are disabled for '{}', not scheduling", name); - return false; - } - - log.debug("\nScheduling condition watchdogs for plugin \n\t:'{}' with interval {}ms using update mode: {}", - name, checkIntervalMillis, updateOption); - - if (!(this.plugin instanceof SchedulablePlugin)) { - log.debug("Cannot schedule condition watchdogs for non-SchedulablePlugin"); - return false; - } - - // Cancel any existing watchdog tasks first - //cancelConditionWatchdogs(); - - SchedulablePlugin schedulablePlugin = (SchedulablePlugin) this.plugin; - boolean anyScheduled = false; - - try { - // Create suppliers that get the current plugin conditions - Supplier startConditionSupplier = - () -> schedulablePlugin.getStartCondition(); - - Supplier stopConditionSupplier = - () -> schedulablePlugin.getStopCondition(); - - // Schedule the start condition watchdog - startConditionWatchdogFuture = startConditionManager.scheduleConditionWatchdog( - startConditionSupplier, - checkIntervalMillis, - updateOption - ); - - // Schedule the stop condition watchdog - stopConditionWatchdogFuture = stopConditionManager.scheduleConditionWatchdog( - stopConditionSupplier, - checkIntervalMillis, - updateOption - ); - - anyScheduled = true; - log.debug("Scheduled condition watchdogs for plugin '{}' with interval {} ms using update mode: {}", - name, checkIntervalMillis, updateOption); - } catch (Exception e) { - log.error("Failed to schedule condition watchdogs for '{}'", name, e); - } - - return anyScheduled; - } - - /** - * Schedules condition watchdogs with the default ADD_ONLY update mode. - * - * @param checkIntervalMillis How often to check for condition changes in milliseconds - * @return true if at least one watchdog was successfully scheduled - */ - public boolean scheduleConditionWatchdogs(long checkIntervalMillis) { - return scheduleConditionWatchdogs(checkIntervalMillis, UpdateOption.SYNC); - } - -/** - * Validates the start conditions structure and logs any issues found. - * This helps identify potential problems with condition hierarchies. - */ - private void validateStartConditions() { - LogicalCondition startLogical = getStartConditionManager().getFullLogicalCondition(); - if (startLogical != null) { - List issues = startLogical.validateStructure(); - if (!issues.isEmpty()) { - log.warn("Validation issues found in start conditions for '{}':", name); - for (String issue : issues) { - log.warn(" - {}", issue); - } - } - } - } - /** - * Validates the stop conditions structure and logs any issues found. - * This helps identify potential problems with condition hierarchies. - */ - private void validateStopConditions() { - LogicalCondition stopLogical = getStopConditionManager().getFullLogicalCondition(); - if (stopLogical != null) { - List issues = stopLogical.validateStructure(); - if (!issues.isEmpty()) { - log.warn("Validation issues found in stop conditions for '{}':", name); - for (String issue : issues) { - log.warn(" - {}", issue); - } - } - } - } - /** - * Optimizes both start and stop condition structures by flattening unnecessary nesting - * and removing empty logical conditions. - */ - private void optimizeConditionStructures() { - // Optimize start conditions - LogicalCondition startLogical = getStartConditionManager().getFullLogicalCondition(); - if (startLogical != null) { - boolean optimized = startLogical.optimizeStructure(); - if (optimized) { - log.debug("Optimized start condition structure for '{}'", name); - } - } - - // Optimize stop conditions - LogicalCondition stopLogical = getStopConditionManager().getFullLogicalCondition(); - if (stopLogical != null) { - boolean optimized = stopLogical.optimizeStructure(); - if (optimized) { - log.debug("Optimized stop condition structure for '{}'", name); - } - } - } - - - /** - * Checks if any condition watchdogs are currently active for this plugin. - * - * @return true if at least one watchdog is active - */ - public boolean hasActiveWatchdogs() { - return (startConditionManager != null && startConditionManager.areWatchdogsRunning()) || - (stopConditionManager != null && stopConditionManager.areWatchdogsRunning()); - } - - /** - * Properly clean up resources when this object is closed or disposed. - * This is more reliable than using finalize() which is deprecated. - */ - @Override - public void close() { - // Clean up watchdogs and other resources - //cancelConditionWatchdogs(); - - // Stop any monitoring threads - stopMonitoringThread(); - stopStartupWatchdog(); - // Ensure both condition managers are closed properly - if (startConditionManager != null) { - startConditionManager.close(); - } - - if (stopConditionManager != null) { - stopConditionManager.close(); - } - - log.debug("Resources cleaned up for plugin schedule entry: '{}'", name); - } - - /** - * Calculates overall progress percentage across all conditions. - * This respects the logical structure of conditions. - * Returns 0 if progress cannot be determined. - */ - public double getStopConditionProgress() { - // If there are no conditions, no progress to report - if (stopConditionManager == null || stopConditionManager.getConditions().isEmpty()) { - return 0; - } - - // If using logical root condition, respect its logical structure - LogicalCondition rootLogical = stopConditionManager.getFullLogicalCondition(); - if (rootLogical != null) { - return rootLogical.getProgressPercentage(); - } - - // Fallback for direct condition list: calculate based on AND/OR logic - boolean requireAll = stopConditionManager.requiresAll(); - List conditions = stopConditionManager.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 total number of conditions being tracked. - */ - public int getTotalStopConditionCount() { - if (stopConditionManager == null) { - return 0; - } - - LogicalCondition rootLogical = stopConditionManager.getFullLogicalCondition(); - if (rootLogical != null) { - return rootLogical.getTotalConditionCount(); - } - - return stopConditionManager.getConditions().stream() - .mapToInt(Condition::getTotalConditionCount) - .sum(); - } - - /** - * Gets the number of conditions that are currently met. - */ - public int getSatisfiedStopConditionCount() { - if (stopConditionManager == null) { - return 0; - } - - LogicalCondition rootLogical = stopConditionManager.getFullLogicalCondition(); - if (rootLogical != null) { - return rootLogical.getMetConditionCount(); - } - - return stopConditionManager.getConditions().stream() - .mapToInt(Condition::getMetConditionCount) - .sum(); - } - public LogicalCondition getLogicalStopCondition() { - return stopConditionManager.getFullLogicalCondition(); - } - - - // Add getter/setter for the new fields - public boolean isAllowRandomScheduling() { - return allowRandomScheduling; - } - - public void setAllowRandomScheduling(boolean allowRandomScheduling) { - this.allowRandomScheduling = allowRandomScheduling; - } - - public int getRunCount() { - return runCount; - } - - private void incrementRunCount() { - this.runCount++; - } - - // Setter methods for the configurable timeouts - public void setSoftStopRetryInterval(Duration interval) { - if (interval == null || interval.isNegative() || interval.isZero()) { - return; // Invalid interval, do not set - } - if(interval.compareTo(Duration.ofSeconds(30)) < 0) { - interval = Duration.ofSeconds(30); // Ensure minimum interval of 1 second - } - this.softStopRetryInterval = interval; - } - - public void setHardStopTimeout(Duration timeout) { - this.hardStopTimeout = timeout; - } - - - - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != getClass()) return false; - - PluginScheduleEntry that = (PluginScheduleEntry) o; - - // Two entries are equal if: - // 1. They have the same name AND - // 2. They have the same start conditions and stop conditions - // OR they are the same object reference - - if (!Objects.equals(name, that.name)) return false; - - // If they're the same name, we need to distinguish by conditions - if (startConditionManager != null && that.startConditionManager != null) { - if (!startConditionManager.getConditions().equals(that.startConditionManager.getConditions())) { - return false; - } - } else if (startConditionManager != null || that.startConditionManager != null) { - return false; - } - - if (stopConditionManager != null && that.stopConditionManager != null) { - return stopConditionManager.getConditions().equals(that.stopConditionManager.getConditions()); - } else { - return stopConditionManager == null && that.stopConditionManager == null; - } - } - - @Override - public int hashCode() { - int result = name != null ? name.hashCode() : 0; - result = 31 * result + (startConditionManager != null ? startConditionManager.getConditions().hashCode() : 0); - result = 31 * result + (stopConditionManager != null ? stopConditionManager.getConditions().hashCode() : 0); - return result; - } - - public int getPriority() { - return priority; - } - - public void setPriority(int priority) { - this.priority = priority; - } - - public boolean isDefault() { - return isDefault; - } - - public void setDefault(boolean isDefault) { - this.isDefault = isDefault; - } - /** - * Generic helper method to build condition diagnostics for both start and stop conditions - * - * @param isStartCondition Whether to diagnose start conditions (true) or stop conditions (false) - * @return A detailed diagnostic string - */ - private String buildConditionDiagnostics(boolean isStartCondition) { - StringBuilder sb = new StringBuilder(); - String conditionType = isStartCondition ? "Start" : "Stop"; - ConditionManager conditionManager = isStartCondition ? startConditionManager : stopConditionManager; - List conditions = isStartCondition ? getStartConditions() : getStopConditions(); - - // Header with plugin name - sb.append("[").append(cleanName).append("] ").append(conditionType).append(" condition diagnostics:\n"); - - // Check if running (only relevant for start conditions) - if (isStartCondition && isRunning()) { - sb.append("- Plugin is already running (will not start again until stopped)\n"); - return sb.toString(); - } - - // Check for conditions - if (conditions.isEmpty()) { - sb.append("- No ").append(conditionType.toLowerCase()).append(" conditions defined\n"); - return sb.toString(); - } - - // Condition logic type - sb.append("- Logic: ") - .append(conditionManager.requiresAll() ? "ALL conditions must be met" : "ANY condition can be met") - .append("\n"); - - // Condition description - sb.append("- Conditions: ") - .append(conditionManager.getDescription()) - .append("\n"); - - // Check if they can be fulfilled - boolean canBeFulfilled = isStartCondition ? - hasFullfillableStartConditions() : - hasFullfillableStopConditions(); - - if (!canBeFulfilled) { - sb.append("- Conditions cannot be fulfilled (e.g., one-time conditions already triggered)\n"); - } - - // Progress - double progress = isStartCondition ? - conditionManager.getProgressTowardNextTrigger() : - getStopConditionProgress(); - sb.append("- Progress: ") - .append(String.format("%.1f%%", progress)) - .append("\n"); - - // Next trigger time - Optional nextTrigger = isStartCondition ? - getCurrentStartTriggerTime() : - getNextStopTriggerTime(); - - sb.append("- Next trigger: "); - if (nextTrigger.isPresent()) { - ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); - ZonedDateTime triggerTime = nextTrigger.get(); - - sb.append(triggerTime).append("\n"); - sb.append("- Current time: ").append(now).append("\n"); - - if (triggerTime.isBefore(now)) { - sb.append("- Trigger time is in the past but conditions not met - may need reset\n"); - } else { - Duration timeUntil = Duration.between(now, triggerTime); - sb.append("- Time until trigger: ").append(formatDuration(timeUntil)).append("\n"); - } - } else { - sb.append("No future trigger time determined\n"); - } - - // Overall condition status - boolean areConditionsMet = isStartCondition ? - startConditionManager.areAllConditionsMet() : - arePluginStopConditionsMet() && areUserDefinedStopConditionsMet(); - - sb.append("- Status: ") - .append(areConditionsMet ? - "CONDITIONS MET - Plugin is " + (isStartCondition ? "due to run" : "due to stop") : - "CONDITIONS NOT MET - Plugin " + (isStartCondition ? "will not run" : "will continue running")) - .append("\n"); - - // Individual condition status - sb.append("- Individual conditions:\n"); - for (int i = 0; i < conditions.size(); i++) { - Condition condition = conditions.get(i); - sb.append(" ").append(i+1).append(". ") - .append(condition.getDescription()) - .append(": ") - .append(condition.isSatisfied() ? "SATISFIED" : "NOT SATISFIED"); - - // Add progress if available - double condProgress = condition.getProgressPercentage(); - if (condProgress > 0 && condProgress < 100) { - sb.append(String.format(" (%.1f%%)", condProgress)); - } - - // For time conditions, show next trigger time - if (condition instanceof TimeCondition) { - Optional condTrigger = condition.getCurrentTriggerTime(); - if (condTrigger.isPresent()) { - sb.append(" (next trigger: ").append(condTrigger.get()).append(")"); - } - } - - sb.append("\n"); - } - - return sb.toString(); - } - - /** - * Performs a diagnostic check on start conditions and returns detailed information - * about why a plugin might not be due to run - * - * @return A string containing diagnostic information - */ - public String diagnoseStartConditions() { - return buildConditionDiagnostics(true); - } - - /** - * Performs a diagnostic check on stop conditions and returns detailed information - * about why a plugin might or might not be due to stop - * - * @return A string containing diagnostic information - */ - public String diagnoseStopConditions() { - return buildConditionDiagnostics(false); - } - - /** - * Formats a duration in a human-readable way - */ - private String formatDuration(Duration duration) { - long seconds = duration.getSeconds(); - if (seconds < 60) { - return seconds + " seconds"; - } else if (seconds < 3600) { - return String.format("%dm %ds", seconds / 60, seconds % 60); - } else if (seconds < 86400) { - return String.format("%dh %dm %ds", seconds / 3600, (seconds % 3600) / 60, seconds % 60); - } else { - return String.format("%dd %dh %dm", seconds / 86400, (seconds % 86400) / 3600, (seconds % 3600) / 60); - } - } - - /** - * Checks whether this schedule entry contains only time-based conditions. - * This is useful to determine if the plugin schedule is purely time-based - * or if it has other types of conditions (e.g., resource, skill, etc.). - * - * @return true if the schedule only contains TimeCondition instances, false otherwise - */ - public boolean hasOnlyTimeConditions() { - // Check if start conditions contain only time conditions - if (startConditionManager != null && !startConditionManager.hasOnlyTimeConditions()) { - return false; - } - - // Check if stop conditions contain only time conditions - if (stopConditionManager != null && !stopConditionManager.hasOnlyTimeConditions()) { - return false; - } - - // Both condition managers contain only time conditions (or are empty) - return true; - } - - /** - * Returns all non-time-based conditions from both start and stop conditions. - * This can help identify which non-time conditions are present in the schedule. - * - * @return A list of all non-TimeCondition instances in this schedule entry - */ - public List getNonTimeConditions() { - List nonTimeConditions = new ArrayList<>(); - - // Add non-time conditions from start conditions - if (startConditionManager != null) { - nonTimeConditions.addAll(startConditionManager.getNonTimeConditions()); - } - - // Add non-time conditions from stop conditions - if (stopConditionManager != null) { - nonTimeConditions.addAll(stopConditionManager.getNonTimeConditions()); - } - - return nonTimeConditions; - } - - /** - * Checks if this plugin would be due to run based only on its time conditions, - * ignoring any non-time conditions that may be present in the schedule. - * This is useful to determine if a plugin is being blocked from running by - * time conditions or by other types of conditions. - * - * @return true if the plugin would be scheduled to run based solely on time conditions - */ - public boolean wouldRunBasedOnTimeConditionsOnly() { - // Check if we're already running - if (isRunning()) { - return false; - } - - // If no start conditions defined, plugin can't run automatically - if (!hasAnyStartConditions()) { - return false; - } - - // Check if time conditions alone would be satisfied - return startConditionManager.wouldBeTimeOnlySatisfied(); - } - - /** - * Provides detailed diagnostic information about why a plugin is or isn't - * running based on its time conditions only. - * - * @return A diagnostic string explaining the time condition status - */ - public String diagnoseTimeConditionScheduling() { - StringBuilder sb = new StringBuilder(); - sb.append("Time condition scheduling diagnosis for '").append(cleanName).append("':\n"); - - // First check if plugin is already running - if (isRunning()) { - sb.append("Plugin is already running - will not be scheduled again until stopped.\n"); - return sb.toString(); - } - - // Check if there are any start conditions - if (!hasAnyStartConditions()) { - sb.append("No start conditions defined - plugin can't be automatically scheduled.\n"); - return sb.toString(); - } - - // Get time-only condition status - boolean wouldRunOnTimeOnly = startConditionManager.wouldBeTimeOnlySatisfied(); - boolean allConditionsMet = startConditionManager.areAllConditionsMet(); - - sb.append("Time conditions only: ").append(wouldRunOnTimeOnly ? "WOULD RUN" : "WOULD NOT RUN").append("\n"); - sb.append("All conditions: ").append(allConditionsMet ? "SATISFIED" : "NOT SATISFIED").append("\n"); - - // If time conditions would run but all conditions wouldn't, non-time conditions are blocking - if (wouldRunOnTimeOnly && !allConditionsMet) { - sb.append("Plugin is being blocked by non-time conditions.\n"); - - // List the non-time conditions that are not satisfied - List nonTimeConditions = startConditionManager.getNonTimeConditions(); - sb.append("Non-time conditions blocking execution:\n"); - - for (Condition condition : nonTimeConditions) { - if (!condition.isSatisfied()) { - sb.append(" - ").append(condition.getDescription()) - .append(" (").append(condition.getType()).append(")\n"); - } - } - } - // If time conditions would not run, show time condition status - else if (!wouldRunOnTimeOnly) { - sb.append("Plugin is waiting for time conditions to be met.\n"); - - // Show next trigger time if available - Optional nextTrigger = startConditionManager.getCurrentTriggerTime(); - if (nextTrigger.isPresent()) { - ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); - Duration until = Duration.between(now, nextTrigger.get()); - - sb.append("Next time trigger at: ").append(nextTrigger.get()) - .append(" (").append(formatDuration(until)).append(" from now)\n"); - } else { - sb.append("No future time trigger determined.\n"); - } - } - - // Add detailed time condition diagnosis from condition manager - sb.append("\n").append(startConditionManager.diagnoseTimeConditionsSatisfaction()); - - return sb.toString(); - } - - /** - * Creates a modified version of this schedule entry that contains only time conditions. - * This is useful for evaluating how the plugin would be scheduled if only time - * conditions were considered. - * - * @return A new PluginScheduleEntry with the same configuration but only time conditions - */ - public PluginScheduleEntry createTimeOnlySchedule() { - // Create a new schedule entry with the same basic properties - PluginScheduleEntry timeOnlyEntry = new PluginScheduleEntry( - name, - mainTimeStartCondition != null ? mainTimeStartCondition : null, - enabled, - allowRandomScheduling - ); - - // Create time-only condition managers - if (startConditionManager != null) { - ConditionManager timeOnlyStartManager = startConditionManager.createTimeOnlyConditionManager(); - timeOnlyEntry.startConditionManager.setUserLogicalCondition( - timeOnlyStartManager.getUserLogicalCondition()); - timeOnlyEntry.startConditionManager.setPluginCondition( - timeOnlyStartManager.getPluginCondition()); - } - - if (stopConditionManager != null) { - ConditionManager timeOnlyStopManager = stopConditionManager.createTimeOnlyConditionManager(); - timeOnlyEntry.stopConditionManager.setUserLogicalCondition( - timeOnlyStopManager.getUserLogicalCondition()); - timeOnlyEntry.stopConditionManager.setPluginCondition( - timeOnlyStopManager.getPluginCondition()); - } - - return timeOnlyEntry; - } - - /** - * Flag to track whether this plugin entry is currently paused - */ - private boolean paused = false; - - /** - * Pauses all time conditions in both stop and start condition managers. - * When paused, time conditions cannot be satisfied and their trigger times - * will be shifted when resumed. - * - * @return true if successfully paused, false if already paused - */ - public boolean pause() { - if (paused) { - return false; // Already paused - } - - // Pause both condition managers - if (stopConditionManager != null) { - stopConditionManager.pause(); - } - - if (startConditionManager != null) { - startConditionManager.pause(); - } - - paused = true; - log.debug("Paused time conditions for plugin: {}", name); - return true; - } - - /** - * resumes all time conditions in both stop and start condition managers. - * When resumed, time conditions will resume with their trigger times shifted - * by the duration of the pause. - * - * @return true if successfully resumed, false if not currently paused - */ - public boolean resume() { - if (!paused) { - return false; // Not paused - } - // resume both condition managers - if (stopConditionManager != null) { - stopConditionManager.resume(); - } - - if (startConditionManager != null) { - startConditionManager.resume(); - } - paused = false; - return true; - } - - /** - * Checks if this plugin entry is currently paused. - * - * @return true if paused, false otherwise - */ - public boolean isPaused() { - return paused; - } - - /** - * Gets the estimated time until start conditions will be satisfied. - * This method uses the new estimation system to provide more accurate - * predictions for when the plugin can start running. - * - * @return Optional containing the estimated duration until start conditions are satisfied - */ - public Optional getEstimatedStartTimeWhenIsSatisfied() { - if (!enabled) { - return Optional.empty(); - } - - if (startConditionManager == null) { - // No start conditions means plugin can start immediately - return Optional.of(Duration.ZERO); - } - - return startConditionManager.getEstimatedDurationUntilSatisfied(); - } - - /** - * Gets the estimated time until start conditions will be satisfied, considering only user-defined conditions. - * This method focuses only on user-configurable start conditions. - * - * @return Optional containing the estimated duration until user start conditions are satisfied - */ - public Optional getEstimatedStartTimeWhenIsSatisfiedUserBased() { - if (!enabled) { - return Optional.empty(); - } - - if (startConditionManager == null) { - return Optional.of(Duration.ZERO); - } - - return startConditionManager.getEstimatedDurationUntilUserConditionsSatisfied(); - } - - /** - * Gets the estimated time until stop conditions will be satisfied. - * This method uses only user-defined stop conditions to predict when the plugin - * should stop based on user configuration. - * - * @return Optional containing the estimated duration until stop conditions are satisfied - */ - public Optional getEstimatedStopTimeWhenIsSatisfied() { - if (stopConditionManager == null) { - // No stop conditions means plugin will run indefinitely - return Optional.empty(); - } - - return stopConditionManager.getEstimatedDurationUntilUserConditionsSatisfied(); - } - - /** - * Gets a formatted string representation of the estimated start time. - * - * @return A human-readable string describing when the plugin is estimated to start - */ - public String getEstimatedStartTimeDisplay() { - Optional estimate = getEstimatedStartTimeWhenIsSatisfied(); - if (estimate.isPresent()) { - return formatEstimatedDuration(estimate.get(), "start"); - } - return "Cannot estimate start time"; - } - - /** - * Gets a formatted string representation of the estimated stop time. - * - * @return A human-readable string describing when the plugin is estimated to stop - */ - public String getEstimatedStopTimeDisplay() { - Optional estimate = getEstimatedStopTimeWhenIsSatisfied(); - if (estimate.isPresent()) { - return formatEstimatedDuration(estimate.get(), "stop"); - } - return "No stop conditions or cannot estimate"; - } - - /** - * Helper method to format estimated durations into human-readable strings. - * - * @param duration The duration to format - * @param action The action description ("start" or "stop") - * @return A formatted string representation - */ - private String formatEstimatedDuration(Duration duration, String action) { - long seconds = duration.getSeconds(); - - if (seconds <= 0) { - return "Ready to " + action + " now"; - } else if (seconds < 60) { - return String.format("Estimated to %s in ~%d seconds", action, seconds); - } else if (seconds < 3600) { - return String.format("Estimated to %s in ~%d minutes", action, seconds / 60); - } else if (seconds < 86400) { - return String.format("Estimated to %s in ~%d hours", action, seconds / 3600); - } else { - long days = seconds / 86400; - return String.format("Estimated to %s in ~%d days", action, days); - } - } - - // ==================== Soft Failure Tracking Methods ==================== - - /** - * Records a soft failure for this plugin schedule entry. - * Soft failures are tracked consecutively and can lead to hard failure (disabling) - * if the maximum threshold is reached. - * - * @param reason The reason for the soft failure - */ - public void recordSoftFailure(String reason) { - consecutiveSoftFailures++; - lastSoftFailureTime = ZonedDateTime.now(); - - log.warn("Plugin '{}' soft failure #{}: {}", - getCleanName(), consecutiveSoftFailures, reason); - - if (consecutiveSoftFailures >= maxSoftFailuresBeforeHardFailure) { - log.error("Plugin '{}' reached maximum soft failures ({}). Converting to hard failure and disabling entry.", - getCleanName(), maxSoftFailuresBeforeHardFailure); - - // Convert to hard failure by disabling the entry - setEnabled(false); - setLastStopReason("Disabled due to " + maxSoftFailuresBeforeHardFailure + " consecutive soft failures. Last failure: " + reason); - setLastStopReasonType(StopReason.ERROR); - } - } - - /** - * Records a successful execution, which resets the consecutive soft failure counter. - */ - public void recordSuccess() { - if (consecutiveSoftFailures > 0) { - log.info("Plugin '{}' successfully executed after {} soft failures. Resetting failure counter.", - getCleanName(), consecutiveSoftFailures); - } - consecutiveSoftFailures = 0; - lastSoftFailureTime = null; - } - - /** - * @return The number of consecutive soft failures for this plugin - */ - public int getConsecutiveSoftFailures() { - return consecutiveSoftFailures; - } - - /** - * @return The maximum number of soft failures before converting to hard failure - */ - public int getMaxSoftFailuresBeforeHardFailure() { - return maxSoftFailuresBeforeHardFailure; - } - - /** - * Sets the maximum number of soft failures before converting to hard failure. - * - * @param maxFailures The maximum number of consecutive soft failures allowed - */ - public void setMaxSoftFailuresBeforeHardFailure(int maxFailures) { - this.maxSoftFailuresBeforeHardFailure = Math.max(1, maxFailures); - } - - /** - * Checks if the consecutive soft failures have exceeded the threshold - * and should be converted to a hard failure. - * - * @return true if soft failures should be converted to hard failure - */ - public boolean shouldConvertToHardFailure() { - return consecutiveSoftFailures >= maxSoftFailuresBeforeHardFailure; - } - - /** - * @return The time of the last soft failure, or null if no soft failures have occurred - */ - public ZonedDateTime getLastSoftFailureTime() { - return lastSoftFailureTime; - } - - /** - * @return true if this plugin is currently in a soft failure state - */ - public boolean isInSoftFailureState() { - return consecutiveSoftFailures > 0; - } - - /** - * @return true if this plugin is approaching the hard failure threshold - */ - public boolean isApproachingHardFailure() { - return consecutiveSoftFailures >= (maxSoftFailuresBeforeHardFailure - 1); - } - - /** - * Resets the soft failure counter manually. This should be used sparingly, - * typically only when the underlying issue has been resolved manually. - */ - public void resetSoftFailureCounter() { - int previousFailures = consecutiveSoftFailures; - consecutiveSoftFailures = 0; - lastSoftFailureTime = null; - - if (previousFailures > 0) { - log.info("Manually reset soft failure counter for plugin '{}' (was at {} failures)", - getCleanName(), previousFailures); - } - } - - - /** - * 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 getPrePostTasks() { - if (this.getPlugin() == null || Microbot.getClient() == null || Microbot.getClient().getGameState() != GameState.LOGGED_IN) { - return null; - } - Plugin plugin = this.getPlugin(); - if (plugin instanceof SchedulablePlugin && isRunning() - ) { - SchedulablePlugin schedulablePlugin = - (SchedulablePlugin) plugin; - return schedulablePlugin.getPrePostScheduleTasks(); - } - - return null; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/ExcludeTransientAndNonSerializableFieldsStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/ExcludeTransientAndNonSerializableFieldsStrategy.java deleted file mode 100644 index cfa0f479cf0..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/ExcludeTransientAndNonSerializableFieldsStrategy.java +++ /dev/null @@ -1,47 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization; - -import com.google.gson.ExclusionStrategy; -import com.google.gson.FieldAttributes; - -import java.util.function.Consumer; - -/** - * Excludes fields that shouldn't be serialized: - * - Transient fields - * - Functional interfaces (Consumer, Supplier, etc.) - * - Thread objects - * - Any other non-serializable types we identify - */ -public class ExcludeTransientAndNonSerializableFieldsStrategy implements ExclusionStrategy { - @Override - public boolean shouldSkipField(FieldAttributes field) { - // Skip transient fields - if (field.hasModifier(java.lang.reflect.Modifier.TRANSIENT)) { - return true; - } - - // Get the field type - Class fieldType = field.getDeclaredClass(); - - // Skip functional interfaces and other non-serializable types - return fieldType != null && ( - java.util.function.Consumer.class.isAssignableFrom(fieldType) || - java.util.function.Supplier.class.isAssignableFrom(fieldType) || - java.util.function.Function.class.isAssignableFrom(fieldType) || - java.util.function.Predicate.class.isAssignableFrom(fieldType) || - java.lang.Thread.class.isAssignableFrom(fieldType) || - java.util.concurrent.ScheduledFuture.class.isAssignableFrom(fieldType) || - java.awt.Component.class.isAssignableFrom(fieldType) - ); - } - - @Override - public boolean shouldSkipClass(Class clazz) { - // Skip functional interfaces at class level too - return Consumer.class.isAssignableFrom(clazz) || - java.util.function.Supplier.class.isAssignableFrom(clazz) || - java.lang.Thread.class.isAssignableFrom(clazz) || - java.util.concurrent.ScheduledFuture.class.isAssignableFrom(clazz) || - java.awt.Component.class.isAssignableFrom(clazz); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/ScheduledSerializer.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/ScheduledSerializer.java deleted file mode 100644 index 71741758ecc..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/ScheduledSerializer.java +++ /dev/null @@ -1,205 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization; - -import com.google.gson.*; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.config.*; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionManager; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization.DayOfWeekConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization.DurationAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization.IntervalConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization.LocalDateAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization.LocalTimeAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization.SingleTriggerTimeConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization.TimeConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization.TimeWindowConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.VarbitCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.serialization.VarbitConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.ConditionTypeAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.ConditionManagerAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.ZonedDateTimeAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config.*; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.DayOfWeekCondition; -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.serialization.LogicalConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.serialization.NotConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.PluginScheduleEntryAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.BankItemCountCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.GatheredResourceCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.InventoryItemCountCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.LootItemCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ProcessItemCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ResourceCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization.BankItemCountConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization.GatheredResourceConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization.InventoryItemCountConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization.LootItemConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization.ProcessItemConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization.ResourceConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.SkillLevelCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.SkillXpCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.serialization.SkillLevelConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.serialization.SkillXpConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.npc.NpcKillCountCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.npc.serialization.NpcKillCountConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.AreaCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.RegionCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.PositionCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.serialization.AreaConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.serialization.RegionConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.serialization.PositionConditionAdapter; - -import java.io.IOException; -import java.lang.reflect.Type; -import java.time.LocalDate; -import java.time.LocalTime; -import java.time.ZonedDateTime; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; - -/** - * Handles serialization and deserialization of ScheduledPlugin objects. - * This centralizes all JSON conversion logic. - */ -@Slf4j -public class ScheduledSerializer { - - /** - * Creates a properly configured Gson instance with all necessary type adapters - */ - private static Gson createGson() { - GsonBuilder builder = new GsonBuilder() - .setExclusionStrategies(new ExcludeTransientAndNonSerializableFieldsStrategy()) - .setPrettyPrinting(); - - // Register all the type adapters - builder.registerTypeAdapter(LocalDate.class, new LocalDateAdapter()); - builder.registerTypeAdapter(LocalTime.class, new LocalTimeAdapter()); - builder.registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeAdapter()); - builder.registerTypeAdapter(Duration.class, new DurationAdapter()); - builder.registerTypeAdapter(Condition.class, new ConditionTypeAdapter()); - - // Register our custom PluginScheduleEntry adapter - builder.registerTypeAdapter(PluginScheduleEntry.class, new PluginScheduleEntryAdapter()); - - // Register config descriptor adapters - builder.registerTypeAdapter(ConfigDescriptor.class, new ConfigDescriptorAdapter()); - builder.registerTypeAdapter(ConfigGroup.class, new ConfigGroupAdapter()); - builder.registerTypeAdapter(ConfigSection.class, new ConfigSectionAdapter()); - builder.registerTypeAdapter(ConfigSectionDescriptor.class, new ConfigSectionDescriptorAdapter()); - builder.registerTypeAdapter(ConfigItem.class, new ConfigItemAdapter()); - builder.registerTypeAdapter(ConfigItemDescriptor.class, new ConfigItemDescriptorAdapter()); - builder.registerTypeAdapter(ConfigInformation.class, new ConfigInformationAdapter()); - builder.registerTypeAdapter(Range.class, new RangeAdapter()); - builder.registerTypeAdapter(Alpha.class, new AlphaAdapter()); - builder.registerTypeAdapter(Units.class, new UnitsAdapter()); - - // Time condition adapters - builder.registerTypeAdapter(TimeCondition.class, new TimeConditionAdapter()); - builder.registerTypeAdapter(IntervalCondition.class, new IntervalConditionAdapter()); - builder.registerTypeAdapter(SingleTriggerTimeCondition.class, new SingleTriggerTimeConditionAdapter()); - builder.registerTypeAdapter(TimeWindowCondition.class, new TimeWindowConditionAdapter()); - builder.registerTypeAdapter(DayOfWeekCondition.class, new DayOfWeekConditionAdapter()); - - // Logical condition adapters - builder.registerTypeAdapter(LogicalCondition.class, new LogicalConditionAdapter()); - builder.registerTypeAdapter(NotCondition.class, new NotConditionAdapter()); - - // Resource condition adapters - builder.registerTypeAdapter(ResourceCondition.class, new ResourceConditionAdapter()); - builder.registerTypeAdapter(BankItemCountCondition.class, new BankItemCountConditionAdapter()); - builder.registerTypeAdapter(InventoryItemCountCondition.class, new InventoryItemCountConditionAdapter()); - builder.registerTypeAdapter(LootItemCondition.class, new LootItemConditionAdapter()); - builder.registerTypeAdapter(GatheredResourceCondition.class, new GatheredResourceConditionAdapter()); - builder.registerTypeAdapter(ProcessItemCondition.class, new ProcessItemConditionAdapter()); - - // Skill condition adapters - builder.registerTypeAdapter(SkillLevelCondition.class, new SkillLevelConditionAdapter()); - builder.registerTypeAdapter(SkillXpCondition.class, new SkillXpConditionAdapter()); - - // NPC condition adapters - builder.registerTypeAdapter(NpcKillCountCondition.class, new NpcKillCountConditionAdapter()); - - // Location condition adapters - builder.registerTypeAdapter(AreaCondition.class, new AreaConditionAdapter()); - builder.registerTypeAdapter(RegionCondition.class, new RegionConditionAdapter()); - builder.registerTypeAdapter(PositionCondition.class, new PositionConditionAdapter()); - - // Varbit condition adapter - builder.registerTypeAdapter(VarbitCondition.class, new VarbitConditionAdapter()); - - // ConditionManager adapter - builder.registerTypeAdapter(ConditionManager.class, new ConditionManagerAdapter()); - builder.registerTypeAdapter(Pattern.class, new TypeAdapter() { - @Override - public void write(JsonWriter out, Pattern value) throws IOException { - if (value == null) { - out.nullValue(); - } else { - out.value(value.pattern()); - } - } - - @Override - public Pattern read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - String pattern = in.nextString(); - return Pattern.compile(pattern); - } - }); - return builder.create(); - } - - /** - * Serialize a list of ScheduledPlugin objects to JSON - */ - public static String toJson(List plugins, String version) { - try { - return createGson().toJson(plugins); - } catch (Exception e) { - log.error("Error serializing scheduled plugins", e); - return "[]"; - } - } - - /** - * Deserialize a JSON string to a list of ScheduledPlugin objects - */ - public static List fromJson(String json,String version) { - if (json == null || json.isEmpty()) { - return new ArrayList<>(); - } - - try { - // Check if the JSON contains the old class name - if (json.contains("\"ScheduledPlugin\"")) { - json = json.replace("\"ScheduledPlugin\"", "\"PluginScheduleEntry\""); - } - - Gson gson = createGson(); - Type listType = new TypeToken>(){}.getType(); - - // Let Gson and our adapter handle everything - return gson.fromJson(json, listType); - } catch (Exception e) { - log.error("Error deserializing scheduled plugins", e); - return new ArrayList<>(); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ConditionManagerAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ConditionManagerAdapter.java deleted file mode 100644 index 4289afbc1d2..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ConditionManagerAdapter.java +++ /dev/null @@ -1,105 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionManager; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LogicalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; - -import java.lang.reflect.Type; -import java.time.ZoneId; -import java.util.ArrayList; -import java.util.List; - -/** - * Handles serialization and deserialization of ConditionManager - * with improved timezone handling for time-based conditions - */ -@Slf4j -public class ConditionManagerAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ConditionManager src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Add type information to identify whether this is a start or stop condition manager - // This will be detected and used during deserialization - result.addProperty("requireAll", src.requiresAll()); - - // Serialize plugin-defined logical condition (if present) - LogicalCondition pluginCondition = src.getPluginCondition(); - if (pluginCondition != null && !pluginCondition.getConditions().isEmpty()) { - result.add("pluginLogicalCondition", context.serialize(pluginCondition)); - } - // Serialize user-defined logical condition - LogicalCondition userCondition = src.getUserLogicalCondition(); - if (userCondition != null) { - result.add("userLogicalCondition", context.serialize(userCondition)); - } - return result; - } - - @Override - public ConditionManager deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - ConditionManager manager = new ConditionManager(); - - if (!json.isJsonObject()) { - return manager; // Return empty manager for non-object elements - } - - JsonObject jsonObject = json.getAsJsonObject(); - - // Set requireAll based on serialized value - if (jsonObject.has("requireAll")) { - boolean requireAll = jsonObject.get("requireAll").getAsBoolean(); - if (requireAll) { - manager.setRequireAll(); - } else { - manager.setRequireAny(); - } - } - - // Handle pluginLogicalCondition if present - if (jsonObject.has("pluginLogicalCondition")) { - JsonObject pluginLogicalObj = jsonObject.getAsJsonObject("pluginLogicalCondition"); - - // Only process if there are actual conditions - if (pluginLogicalObj.has("conditions") && - pluginLogicalObj.getAsJsonArray("conditions").size() > 0) { - LogicalCondition logicalCondition = context.deserialize( - pluginLogicalObj, LogicalCondition.class); - if (logicalCondition != null) { - manager.setPluginCondition(logicalCondition); - } - } - } - - // Handle userLogicalCondition properly - if (jsonObject.has("userLogicalCondition")) { - JsonObject userLogicalObj = jsonObject.getAsJsonObject("userLogicalCondition"); - - // Handle case where userLogicalCondition might have an empty conditions array - if (userLogicalObj.has("conditions")) { - - JsonArray conditionsArray = userLogicalObj.getAsJsonArray("conditions"); - if (conditionsArray.size() > 0) { - // Only process if there are actual conditions - LogicalCondition logicalCondition = context.deserialize( - userLogicalObj, LogicalCondition.class); - if (logicalCondition != null) { - manager.setUserLogicalCondition(logicalCondition); - - } - } - } - } - - - - return manager; - } - - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ConditionTypeAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ConditionTypeAdapter.java deleted file mode 100644 index a83069d9a22..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ConditionTypeAdapter.java +++ /dev/null @@ -1,303 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter; -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Skill; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.coords.WorldArea; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.*; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.*; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.npc.*; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.*; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.*; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.*; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.enums.RepeatCycle; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.VarbitCondition; - -import java.lang.reflect.Type; -import java.time.DayOfWeek; -import java.time.Duration; -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.LocalTime; -import java.time.LocalDate; -import java.util.EnumSet; -import java.util.Set; - -@Slf4j -public class ConditionTypeAdapter implements JsonSerializer, JsonDeserializer { - private static final String TYPE_FIELD = "type"; - private static final String DATA_FIELD = "data"; - - @Override - public JsonElement serialize(Condition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - String className = src.getClass().getName(); - result.add(TYPE_FIELD, new JsonPrimitive(className)); - - JsonObject data = new JsonObject(); - - if (src instanceof LogicalCondition) { - data = (JsonObject) context.serialize(src, LogicalCondition.class); - } - else if (src instanceof NotCondition) { - data = (JsonObject) context.serialize(src, NotCondition.class); - } - else if (src instanceof IntervalCondition) { - data = (JsonObject) context.serialize(src, IntervalCondition.class); - } - else if (src instanceof DayOfWeekCondition) { - - data = (JsonObject) context.serialize(src, DayOfWeekCondition.class); - } - else if (src instanceof TimeWindowCondition) { - // Defer to the specialized adapter for TimeWindowCondition - - data = (JsonObject) context.serialize(src, TimeWindowCondition.class); - } - else if (src instanceof VarbitCondition) { - // Use the specialized adapter for VarbitCondition - data = (JsonObject) context.serialize(src, VarbitCondition.class); - } - else if (src instanceof SkillLevelCondition) { - // Use the specialized adapter for SkillLevelCondition - data = (JsonObject) context.serialize(src, SkillLevelCondition.class); - } - else if (src instanceof SkillXpCondition) { - // Use the specialized adapter for SkillXpCondition - data = (JsonObject) context.serialize(src, SkillXpCondition.class); - } - else if (src instanceof LootItemCondition) { - LootItemCondition item = (LootItemCondition) src; - data.addProperty("itemName", item.getItemName()); - data.addProperty("targetAmountMin", item.getTargetAmountMin()); - data.addProperty("targetAmountMax", item.getTargetAmountMax()); - data.addProperty("currentTargetAmount", item.getCurrentTargetAmount()); - data.addProperty("currentTrackedCount", item.getCurrentTrackedCount()); - data.addProperty("includeNoneOwner", item.isIncludeNoneOwner()); - data.addProperty("includeNoted", item.isIncludeNoted()); - } - else if (src instanceof InventoryItemCountCondition) { - InventoryItemCountCondition item = (InventoryItemCountCondition) src; - data.addProperty("itemName", item.getItemName()); - data.addProperty("targetCountMin", item.getTargetCountMin()); - data.addProperty("targetCountMax", item.getTargetCountMax()); - data.addProperty("includeNoted", item.isIncludeNoted()); - data.addProperty("currentTargetCount", item.getCurrentTargetCount()); - data.addProperty("currentItemCount", item.getCurrentItemCount()); - } - else if (src instanceof BankItemCountCondition) { - BankItemCountCondition item = (BankItemCountCondition) src; - data.addProperty("itemName", item.getItemName()); - data.addProperty("targetCountMin", item.getTargetCountMin()); - data.addProperty("targetCountMax", item.getTargetCountMax()); - data.addProperty("currentTargetCount", item.getCurrentTargetCount()); - data.addProperty("currentItemCount", item.getCurrentItemCount()); - } - else if (src instanceof PositionCondition) { - // Use the specialized adapter for PositionCondition - data = (JsonObject) context.serialize(src, PositionCondition.class); - } - else if (src instanceof AreaCondition) { - // Use the specialized adapter for AreaCondition - data = (JsonObject) context.serialize(src, AreaCondition.class); - } - else if (src instanceof RegionCondition) { - // Use the specialized adapter for RegionCondition - data = (JsonObject) context.serialize(src, RegionCondition.class); - } - else if (src instanceof NpcKillCountCondition) { - // Use the specialized adapter for NpcKillCountCondition - data = (JsonObject) context.serialize(src, NpcKillCountCondition.class); - } - else if (src instanceof SingleTriggerTimeCondition) { - assert(1==0); // This should never be serialized here, sperate adapter for this - } - else if (src instanceof LockCondition) { - LockCondition lock = (LockCondition) src; - data.addProperty("reason", lock.getReason()); - data.addProperty("withBreakHandlerLock", lock.isWithBreakHandlerLock()); - } - else { - log.warn("Unknown condition type: {}", src.getClass().getName()); - } - - result.add(DATA_FIELD, data); - return result; - } - - @Override - public Condition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - if (!json.isJsonObject()) { - return null; // Return null for non-object elements - } - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if the type field exists - if (!jsonObject.has(TYPE_FIELD)) { - return null; - } - String typeStr = jsonObject.get(TYPE_FIELD).getAsString(); - JsonObject data = jsonObject.getAsJsonObject(DATA_FIELD); - - try { - Class clazz = Class.forName(typeStr); - - // Handle specific condition types based on their class - if (AndCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, LogicalCondition.class); - } - else if (OrCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, LogicalCondition.class); - } - else if (NotCondition.class.isAssignableFrom(clazz)) { - - return context.deserialize(data, NotCondition.class); - } - else if (IntervalCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, IntervalCondition.class); - } - else if (DayOfWeekCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, DayOfWeekCondition.class); - } - else if (TimeWindowCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, TimeWindowCondition.class); - } - else if (VarbitCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, VarbitCondition.class); - } - else if (SkillLevelCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, SkillLevelCondition.class); - } - else if (SkillXpCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, SkillXpCondition.class); - } - else if (LootItemCondition.class.isAssignableFrom(clazz)) { - return deserializeLootItemCondition(data); - } - else if (InventoryItemCountCondition.class.isAssignableFrom(clazz)) { - return deserializeInventoryItemCountCondition(data); - } - else if (BankItemCountCondition.class.isAssignableFrom(clazz)) { - return deserializeBankItemCountCondition(data); - } - else if (PositionCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, PositionCondition.class); - } - else if (AreaCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, AreaCondition.class); - } - else if (RegionCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, RegionCondition.class); - } - else if (NpcKillCountCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, NpcKillCountCondition.class); - } - else if (SingleTriggerTimeCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, SingleTriggerTimeCondition.class); - } - else if (LockCondition.class.isAssignableFrom(clazz)) { - if (data.has("data")){ - data = data.getAsJsonObject("data"); - } - boolean isWithBreakHandlerLock = data.has("withBreakHandlerLock") ? data.get("withBreakHandlerLock").getAsBoolean(): false; - return new LockCondition(data.get("reason").getAsString(),isWithBreakHandlerLock); - } - - throw new JsonParseException("Unknown condition type: " + typeStr); - } catch (ClassNotFoundException e) { - throw new JsonParseException("Unknown element type: " + typeStr, e); - } - } - - // Helper methods for each condition type - private AndCondition deserializeAndCondition(JsonObject data, JsonDeserializationContext context) { - AndCondition and = new AndCondition(); - JsonArray conditions = data.getAsJsonArray("conditions"); - for (JsonElement element : conditions) { - Condition condition = context.deserialize(element, Condition.class); - if (condition != null) { - and.addCondition(condition); - } - } - return and; - } - - private OrCondition deserializeOrCondition(JsonObject data, JsonDeserializationContext context) { - OrCondition or = new OrCondition(); - JsonArray conditions = data.getAsJsonArray("conditions"); - for (JsonElement element : conditions) { - Condition condition = context.deserialize(element, Condition.class); - if (condition != null) { - or.addCondition(condition); - } - } - return or; - } - - private NotCondition deserializeNotCondition(JsonObject data, JsonDeserializationContext context) { - Condition inner = context.deserialize(data.get("condition"), Condition.class); - return new NotCondition(inner); - } - - private LootItemCondition deserializeLootItemCondition(JsonObject data) { - if (data.has("data")){ - data = data.getAsJsonObject("data"); - } - String itemName = data.get("itemName").getAsString(); - int targetAmountMin = data.get("targetAmountMin").getAsInt(); - int targetAmountMax = data.get("targetAmountMax").getAsInt(); - boolean includeNoted = false; - if (data.has("includeNoted")){ - includeNoted= data.get("includeNoted").getAsBoolean(); - } - boolean includeNoneOwner = false; - if (data.has("includeNoneOwner")) { - includeNoneOwner = data.get("includeNoneOwner").getAsBoolean(); - } - - return LootItemCondition.builder() - .itemName(itemName) - .targetAmountMin(targetAmountMin) - .targetAmountMax(targetAmountMax) - .includeNoneOwner(includeNoneOwner) - .includeNoted(includeNoted) - .build(); - } - - private InventoryItemCountCondition deserializeInventoryItemCountCondition(JsonObject data) { - if (data.has("data")){ - data = data.getAsJsonObject("data"); - } - String itemName = data.get("itemName").getAsString(); - int targetCountMin = data.get("targetCountMin").getAsInt(); - int targetCountMax = data.get("targetCountMax").getAsInt(); - boolean includeNoted = data.get("includeNoted").getAsBoolean(); - - return InventoryItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .includeNoted(includeNoted) - .build(); - } - - private BankItemCountCondition deserializeBankItemCountCondition(JsonObject data) { - if (data.has("data")){ - data = data.getAsJsonObject("data"); - } - String itemName = data.get("itemName").getAsString(); - - int targetCountMin = data.get("targetCountMin").getAsInt(); - int targetCountMax = data.get("targetCountMax").getAsInt(); - - return BankItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .build(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/PluginScheduleEntryAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/PluginScheduleEntryAdapter.java deleted file mode 100644 index c72c88e6cde..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/PluginScheduleEntryAdapter.java +++ /dev/null @@ -1,275 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.config.ConfigDescriptor; - -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionManager; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; - -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; - - -import java.lang.reflect.Type; -import java.time.Duration; -import java.time.ZonedDateTime; - - - -/** - * Custom adapter for PluginScheduleEntry that correctly handles mainTimeStartCondition - * and serializes/deserializes ConfigDescriptor - */ -@Slf4j -public class PluginScheduleEntryAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(PluginScheduleEntry src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Serialize all fields - result.addProperty("name", src.getName()); - result.addProperty("enabled", src.isEnabled()); - result.addProperty("cleanName", src.getCleanName()); - result.addProperty("needsStopCondition", src.isNeedsStopCondition()); - result.addProperty("hardResetOnLoad", false); - // Serialize time fields - if (src.getLastRunStartTime() != null) { - result.addProperty("lastRunStartTime", src.getLastRunStartTime().toInstant().toEpochMilli()); - } - if (src.getLastRunEndTime() != null) { - result.addProperty("lastRunEndTime", src.getLastRunEndTime().toInstant().toEpochMilli()); - } - - // Serialize last run duration - if (src.getLastRunDuration() != null) { - result.add("lastRunDuration", context.serialize(src.getLastRunDuration())); - } - - // Serialize stop reason info - if (src.getLastStopReason() != null) { - result.addProperty("lastStopReason", src.getLastStopReason()); - } - if (src.getLastStopReasonType() != null) { - result.addProperty("lastStopReasonType", src.getLastStopReasonType().name()); - } - - // Serialize condition managers - if (src.getStopConditionManager() != null) { - result.add("stopConditionManager", context.serialize(src.getStopConditionManager())); - } - if (src.getStartConditionManager() != null) { - result.add("startConditionManager", context.serialize(src.getStartConditionManager())); - } - - // Serialize the main time condition - if (src.getMainTimeStartCondition() != null) { - result.add("mainTimeStartCondition", context.serialize(src.getMainTimeStartCondition())); - } - - - // Serialize other properties - - result.addProperty("allowRandomScheduling", src.isAllowRandomScheduling()); - result.addProperty("allowContinue", src.isAllowContinue()); - result.addProperty("runCount", src.getRunCount()); - result.addProperty("onLastStopUserConditionsSatisfied", src.isOnLastStopUserConditionsSatisfied()); - result.addProperty("onLastStopPluginConditionsSatisfied", src.isOnLastStopPluginConditionsSatisfied()); - - // Serialize durations - if (src.getSoftStopRetryInterval() != null) { - result.add("softStopRetryInterval", context.serialize(src.getSoftStopRetryInterval())); - } - if (src.getHardStopTimeout() != null) { - result.add("hardStopTimeout", context.serialize(src.getHardStopTimeout())); - } - - // Serialize priority and default flag - result.addProperty("priority", src.getPriority()); - result.addProperty("isDefault", src.isDefault()); - ConfigDescriptor configDescriptor = src.getConfigScheduleEntryDescriptor() != null ? src.getConfigScheduleEntryDescriptor(): null; - if (configDescriptor != null) { - result.add("configDescriptor", context.serialize(configDescriptor)); - }else { - result.add("configDescriptor", new JsonObject()); - } - - return result; - } - - @Override - public PluginScheduleEntry deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - // Get basic properties - String name = jsonObject.get("name").getAsString(); - boolean enabled = jsonObject.has("enabled") ? jsonObject.get("enabled").getAsBoolean() : false; - // Handle mainTimeStartCondition - TimeCondition mainTimeCondition = null; - if (jsonObject.has("mainTimeStartCondition")) { - try { - mainTimeCondition = context.deserialize( - jsonObject.get("mainTimeStartCondition"), TimeCondition.class); - } catch (Exception e) { - log.error("Failed to parse mainTimeStartCondition", e); - } - } - // Create a basic plugin entry first - PluginScheduleEntry entry = new PluginScheduleEntry(name, (TimeCondition)mainTimeCondition, enabled, false); - // Deserialize cleanName if available - if (jsonObject.has("cleanName")) { - entry.setCleanName(jsonObject.get("cleanName").getAsString()); - } - - // Deserialize time fields - handle both old and new time fields for backward compatibility - if (jsonObject.has("lastRunTime")) { - try { - long timestamp = jsonObject.get("lastRunTime").getAsLong(); - ZonedDateTime prvLastRunTime = ZonedDateTime.ofInstant( - java.time.Instant.ofEpochMilli(timestamp), - java.time.ZoneId.systemDefault()); - - // For backward compatibility, set both start and end time to the old lastRunTime - entry.setLastRunStartTime(prvLastRunTime); - entry.setLastRunEndTime(prvLastRunTime); - } catch (Exception e) { - log.error("Failed to parse lastRunTime", e); - } - } - - // Deserialize new time fields - if (jsonObject.has("lastRunStartTime")) { - try { - long timestamp = jsonObject.get("lastRunStartTime").getAsLong(); - ZonedDateTime lastRunStartTime = ZonedDateTime.ofInstant( - java.time.Instant.ofEpochMilli(timestamp), - java.time.ZoneId.systemDefault()); - entry.setLastRunStartTime(lastRunStartTime); - } catch (Exception e) { - log.error("Failed to parse lastRunStartTime", e); - } - } - - if (jsonObject.has("lastRunEndTime")) { - try { - long timestamp = jsonObject.get("lastRunEndTime").getAsLong(); - ZonedDateTime lastRunEndTime = ZonedDateTime.ofInstant( - java.time.Instant.ofEpochMilli(timestamp), - java.time.ZoneId.systemDefault()); - entry.setLastRunEndTime(lastRunEndTime); - } catch (Exception e) { - log.error("Failed to parse lastRunEndTime", e); - } - } - - // Deserialize last run duration - if (jsonObject.has("lastRunDuration")) { - try { - Duration lastRunDuration = context.deserialize( - jsonObject.get("lastRunDuration"), Duration.class); - entry.setLastRunDuration(lastRunDuration); - } catch (Exception e) { - log.error("Failed to parse lastRunDuration", e); - } - } - - // Deserialize condition managers - if (jsonObject.has("stopConditionManager")) { - ConditionManager stopManager = context.deserialize( - jsonObject.get("stopConditionManager"), ConditionManager.class); - if (!entry.getStopConditionManager().getUserConditions().isEmpty()) { - throw new Error("StopConditionManager should be empty"); - } else { - for (Condition condition : stopManager.getUserConditions()) { - if(entry.getStopConditionManager().containsCondition(condition)){ - throw new Error("Condition already exists in startConditionManager"); - } - entry.addStopCondition(condition); - } - } - } - if (jsonObject.has("startConditionManager")) { - ConditionManager startManager = context.deserialize( - jsonObject.get("startConditionManager"), ConditionManager.class); - - for (Condition condition : startManager.getUserConditions()) { - if(!entry.getStartConditionManager().containsCondition(condition)){ - entry.addStartCondition(condition); - } - } - } - - - if (jsonObject.has("allowRandomScheduling")) { - entry.setAllowRandomScheduling(jsonObject.get("allowRandomScheduling").getAsBoolean()); - } - - if (jsonObject.has("allowContinue")) { - entry.setAllowContinue(jsonObject.get("allowContinue").getAsBoolean()); - } - - if (jsonObject.has("runCount")) { - int runCount = jsonObject.get("runCount").getAsInt(); - entry.setRunCount(runCount); - } - - // Deserialize stop reason info - if (jsonObject.has("lastStopReason")) { - entry.setLastStopReason(jsonObject.get("lastStopReason").getAsString()); - } - - if (jsonObject.has("lastStopReasonType")) { - try { - String stopReasonType = jsonObject.get("lastStopReasonType").getAsString(); - entry.setLastStopReasonType(PluginScheduleEntry.StopReason.valueOf(stopReasonType)); - } catch (Exception e) { - log.error("Failed to parse lastStopReasonType", e); - } - } - if (jsonObject.has("onLastStopUserConditionsSatisfied")) { - entry.setOnLastStopUserConditionsSatisfied(jsonObject.get("onLastStopUserConditionsSatisfied").getAsBoolean()); - } - if (jsonObject.has("onLastStopPluginConditionsSatisfied")) { - entry.setOnLastStopPluginConditionsSatisfied(jsonObject.get("onLastStopPluginConditionsSatisfied").getAsBoolean()); - } - - // Deserialize durations - if (jsonObject.has("softStopRetryInterval")) { - Duration softStopRetryInterval = context.deserialize( - jsonObject.get("softStopRetryInterval"), Duration.class); - entry.setSoftStopRetryInterval(softStopRetryInterval); - } - - if (jsonObject.has("hardStopTimeout")) { - Duration hardStopTimeout = context.deserialize( - jsonObject.get("hardStopTimeout"), Duration.class); - entry.setHardStopTimeout(hardStopTimeout); - } - - // Deserialize priority and default flag - if (jsonObject.has("priority")) { - entry.setPriority(jsonObject.get("priority").getAsInt()); - } - - if (jsonObject.has("isDefault")) { - entry.setDefault(jsonObject.get("isDefault").getAsBoolean()); - } - if (jsonObject.has("needsStopCondition")) { - entry.setNeedsStopCondition(jsonObject.get("needsStopCondition").getAsBoolean()); - }else{ - entry.setNeedsStopCondition(false); - } - //entry.registerPluginConditions(); - - if (jsonObject.has("hardResetOnLoad")) { - boolean hardResetFlag = jsonObject.get("hardResetOnLoad").getAsBoolean(); - if (hardResetFlag) { - entry.hardResetConditions(); - } - } - - return entry; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ZonedDateTimeAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ZonedDateTimeAdapter.java deleted file mode 100644 index 5aa3c906e40..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ZonedDateTimeAdapter.java +++ /dev/null @@ -1,36 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; -import java.io.IOException; -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZonedDateTime; -/** - * Gson TypeAdapter for ZonedDateTime serialization/deserialization - */ -public class ZonedDateTimeAdapter extends TypeAdapter { - @Override - public void write(JsonWriter out, ZonedDateTime value) throws IOException { - if (value == null) { - out.nullValue(); - } else { - out.value(value.toInstant().toEpochMilli()); - } - } - - @Override - public ZonedDateTime read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - long timestamp = in.nextLong(); - return ZonedDateTime.ofInstant( - Instant.ofEpochMilli(timestamp), - ZoneId.systemDefault() - ); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/AlphaAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/AlphaAdapter.java deleted file mode 100644 index 1044946d459..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/AlphaAdapter.java +++ /dev/null @@ -1,30 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config; - -import com.google.gson.*; -import net.runelite.client.config.Alpha; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; - -/** - * Adapter for serializing and deserializing Alpha annotations - */ -public class AlphaAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(Alpha src, Type typeOfSrc, JsonSerializationContext context) { - // Alpha annotation has no properties, so we just return an empty object - return new JsonObject(); - } - - @Override - public Alpha deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - // Create a proxy implementation of Alpha annotation - return new Alpha() { - @Override - public Class annotationType() { - return Alpha.class; - } - }; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigDescriptorAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigDescriptorAdapter.java deleted file mode 100644 index e2d91738354..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigDescriptorAdapter.java +++ /dev/null @@ -1,87 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.config.*; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; - -/** - * Adapter for serializing and deserializing ConfigDescriptor objects - * This allows us to store and restore complete configuration structures - * without needing the actual plugin classes at deserialization time - */ -@Slf4j -public class ConfigDescriptorAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ConfigDescriptor src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Serialize group - if (src.getGroup() != null) { - result.add("group", context.serialize(src.getGroup(), ConfigGroup.class)); - } - - // Serialize sections - if (src.getSections() != null && !src.getSections().isEmpty()) { - result.add("sections", context.serialize(src.getSections(), Collection.class)); - } - - // Serialize items - if (src.getItems() != null && !src.getItems().isEmpty()) { - result.add("items", context.serialize(src.getItems(), Collection.class)); - } - - // Serialize information if present - if (src.getInformation() != null) { - result.add("information", context.serialize(src.getInformation(), ConfigInformation.class)); - } - - return result; - } - - @Override - public ConfigDescriptor deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - // Deserialize group - ConfigGroup group = null; - if (jsonObject.has("group")) { - group = context.deserialize(jsonObject.get("group"), ConfigGroup.class); - } - - // Deserialize sections - Collection sections = Collections.emptyList(); - if (jsonObject.has("sections")) { - Type sectionListType = new TypeToken>() {}.getType(); - sections = context.deserialize(jsonObject.get("sections"), sectionListType); - } - - // Deserialize items - Collection items = Collections.emptyList(); - if (jsonObject.has("items")) { - Type itemsListType = new TypeToken>() {}.getType(); - items = context.deserialize(jsonObject.get("items"), itemsListType); - } - - // Deserialize information - ConfigInformation information = null; - if (jsonObject.has("information")) { - information = context.deserialize(jsonObject.get("information"), ConfigInformation.class); - } - - return new ConfigDescriptor(group, sections, items, information); - } - - private static class TypeToken { - private TypeToken() {} - - public Type getType() { - return getClass().getGenericSuperclass(); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigGroupAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigGroupAdapter.java deleted file mode 100644 index ff01a26f9fa..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigGroupAdapter.java +++ /dev/null @@ -1,41 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config; - -import com.google.gson.*; -import net.runelite.client.config.ConfigGroup; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; - -/** - * Adapter for serializing and deserializing ConfigGroup annotations - * Since annotations are immutable, we need to create a proxy implementation - */ -public class ConfigGroupAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ConfigGroup src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - result.addProperty("value", src.value()); - return result; - } - - @Override - public ConfigGroup deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - final String value = jsonObject.get("value").getAsString(); - // Create a proxy implementation of ConfigGroup annotation - return new ConfigGroup() { - @Override - public Class annotationType() { - return ConfigGroup.class; - } - - @Override - public String value() { - return value; - } - - }; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigInformationAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigInformationAdapter.java deleted file mode 100644 index c76bc1730f3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigInformationAdapter.java +++ /dev/null @@ -1,40 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config; - -import com.google.gson.*; -import net.runelite.client.config.ConfigInformation; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; - -/** - * Adapter for serializing and deserializing ConfigInformation annotations - */ -public class ConfigInformationAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ConfigInformation src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - result.addProperty("value", src.value()); - return result; - } - - @Override - public ConfigInformation deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - final String value = jsonObject.has("value") ? jsonObject.get("value").getAsString() : ""; - - // Create a proxy implementation of ConfigInformation annotation - return new ConfigInformation() { - @Override - public Class annotationType() { - return ConfigInformation.class; - } - - @Override - public String value() { - return value; - } - }; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigItemAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigItemAdapter.java deleted file mode 100644 index 0c7807366ce..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigItemAdapter.java +++ /dev/null @@ -1,90 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config; - -import com.google.gson.*; -import net.runelite.client.config.ConfigItem; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; - -/** - * Adapter for serializing and deserializing ConfigItem annotations - * Since annotations are immutable, we need to create a proxy implementation - */ -public class ConfigItemAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ConfigItem src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - result.addProperty("keyName", src.keyName()); - result.addProperty("name", src.name()); - result.addProperty("description", src.description()); - result.addProperty("section", src.section()); - result.addProperty("position", src.position()); - result.addProperty("hidden", src.hidden()); - result.addProperty("secret", src.secret()); - result.addProperty("warning", src.warning()); - return result; - } - - @Override - public ConfigItem deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - final String keyName = jsonObject.has("keyName") ? jsonObject.get("keyName").getAsString() : ""; - final String name = jsonObject.has("name") ? jsonObject.get("name").getAsString() : ""; - final String description = jsonObject.has("description") ? jsonObject.get("description").getAsString() : ""; - final String section = jsonObject.has("section") ? jsonObject.get("section").getAsString() : ""; - final int position = jsonObject.has("position") ? jsonObject.get("position").getAsInt() : 0; - final boolean hidden = jsonObject.has("hidden") && jsonObject.get("hidden").getAsBoolean(); - final boolean secret = jsonObject.has("secret") && jsonObject.get("secret").getAsBoolean(); - final String warning = jsonObject.has("warning") ? jsonObject.get("warning").getAsString() : ""; - - // Create a proxy implementation of ConfigItem annotation - return new ConfigItem() { - @Override - public Class annotationType() { - return ConfigItem.class; - } - - @Override - public String keyName() { - return keyName; - } - - @Override - public String name() { - return name; - } - - @Override - public String description() { - return description; - } - - @Override - public String section() { - return section; - } - - @Override - public int position() { - return position; - } - - @Override - public boolean hidden() { - return hidden; - } - - @Override - public boolean secret() { - return secret; - } - - @Override - public String warning() { - return warning; - } - }; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigItemDescriptorAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigItemDescriptorAdapter.java deleted file mode 100644 index 787bf00b1c8..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigItemDescriptorAdapter.java +++ /dev/null @@ -1,107 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.config.*; - -import java.lang.reflect.Type; - -/** - * Adapter for serializing and deserializing ConfigItemDescriptor objects - */ -@Slf4j -public class ConfigItemDescriptorAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ConfigItemDescriptor src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Serialize the ConfigItem annotation - if (src.getItem() != null) { - result.add("item", context.serialize(src.getItem(), ConfigItem.class)); - } - - // Serialize the type - if (src.getType() != null) { - result.addProperty("type", src.getType().getTypeName()); - } - - // Serialize Range annotation if present - if (src.getRange() != null) { - result.add("range", context.serialize(src.getRange(), Range.class)); - } - - // Serialize Alpha annotation if present - if (src.getAlpha() != null) { - result.add("alpha", context.serialize(src.getAlpha(), Alpha.class)); - } - - // Serialize Units annotation if present - if (src.getUnits() != null) { - result.add("units", context.serialize(src.getUnits(), Units.class)); - } - - return result; - } - - @Override - public ConfigItemDescriptor deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - // Deserialize the ConfigItem annotation - ConfigItem item = null; - if (jsonObject.has("item")) { - item = context.deserialize(jsonObject.get("item"), ConfigItem.class); - } - - // Deserialize the type - Type itemType = null; - if (jsonObject.has("type")) { - String typeName = jsonObject.get("type").getAsString(); - try { - // Try to load the class if available - itemType = Class.forName(typeName); - } catch (ClassNotFoundException e) { - // If class isn't available, store the type name as a placeholder - log.debug("Class not found for type: {}", typeName); - // For primitive types, handle them specially - if (typeName.equals("boolean") || typeName.equals("java.lang.Boolean")) { - itemType = boolean.class; - } else if (typeName.equals("int") || typeName.equals("java.lang.Integer")) { - itemType = int.class; - } else if (typeName.equals("double") || typeName.equals("java.lang.Double")) { - itemType = double.class; - } else if (typeName.equals("long") || typeName.equals("java.lang.Long")) { - itemType = long.class; - } else if (typeName.equals("float") || typeName.equals("java.lang.Float")) { - itemType = float.class; - } else if (typeName.equals("java.lang.String")) { - itemType = String.class; - } else { - // Use Object as fallback - itemType = Object.class; - } - } - } - - // Deserialize Range annotation if present - Range range = null; - if (jsonObject.has("range")) { - range = context.deserialize(jsonObject.get("range"), Range.class); - } - - // Deserialize Alpha annotation if present - Alpha alpha = null; - if (jsonObject.has("alpha")) { - alpha = context.deserialize(jsonObject.get("alpha"), Alpha.class); - } - - // Deserialize Units annotation if present - Units units = null; - if (jsonObject.has("units")) { - units = context.deserialize(jsonObject.get("units"), Units.class); - } - - return new ConfigItemDescriptor(item, itemType, range, alpha, units); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigSectionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigSectionAdapter.java deleted file mode 100644 index 1354d6dd32b..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigSectionAdapter.java +++ /dev/null @@ -1,62 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config; - -import com.google.gson.*; -import net.runelite.client.config.ConfigSection; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; - -/** - * Adapter for serializing and deserializing ConfigSection annotations - * Since annotations are immutable, we need to create a proxy implementation - */ -public class ConfigSectionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ConfigSection src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - result.addProperty("name", src.name()); - result.addProperty("description", src.description()); - result.addProperty("position", src.position()); - result.addProperty("closedByDefault", src.closedByDefault()); - return result; - } - - @Override - public ConfigSection deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - final String name = jsonObject.has("name") ? jsonObject.get("name").getAsString() : ""; - final String description = jsonObject.has("description") ? jsonObject.get("description").getAsString() : ""; - final int position = jsonObject.has("position") ? jsonObject.get("position").getAsInt() : 0; - final boolean closedByDefault = jsonObject.has("closedByDefault") && jsonObject.get("closedByDefault").getAsBoolean(); - - // Create a proxy implementation of ConfigSection annotation - return new ConfigSection() { - @Override - public Class annotationType() { - return ConfigSection.class; - } - - @Override - public String name() { - return name; - } - - @Override - public String description() { - return description; - } - - @Override - public int position() { - return position; - } - - @Override - public boolean closedByDefault() { - return closedByDefault; - } - }; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigSectionDescriptorAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigSectionDescriptorAdapter.java deleted file mode 100644 index b22c4262b90..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigSectionDescriptorAdapter.java +++ /dev/null @@ -1,51 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.config.ConfigSection; -import net.runelite.client.config.ConfigSectionDescriptor; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; - -/** - * Adapter for serializing and deserializing ConfigSectionDescriptor objects - */ -@Slf4j -public class ConfigSectionDescriptorAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ConfigSectionDescriptor src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Serialize the key - result.addProperty("key", src.getKey()); - - // Serialize the section annotation - if (src.getSection() != null) { - result.add("section", context.serialize(src.getSection(), ConfigSection.class)); - } - - return result; - } - - @Override - public ConfigSectionDescriptor deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - // Deserialize the key - String key = null; - if (jsonObject.has("key")) { - key = jsonObject.get("key").getAsString(); - } - - // Deserialize the section annotation - ConfigSection section = null; - if (jsonObject.has("section")) { - section = context.deserialize(jsonObject.get("section"), ConfigSection.class); - } - - // Create a ConfigSectionDescriptor instance - return new ConfigSectionDescriptor(key, section); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/RangeAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/RangeAdapter.java deleted file mode 100644 index cb2c3111c33..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/RangeAdapter.java +++ /dev/null @@ -1,47 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config; - -import com.google.gson.*; -import net.runelite.client.config.Range; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; - -/** - * Adapter for serializing and deserializing Range annotations - */ -public class RangeAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(Range src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - result.addProperty("min", src.min()); - result.addProperty("max", src.max()); - return result; - } - - @Override - public Range deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - final int min = jsonObject.has("min") ? jsonObject.get("min").getAsInt() : 0; - final int max = jsonObject.has("max") ? jsonObject.get("max").getAsInt() : Integer.MAX_VALUE; - - // Create a proxy implementation of Range annotation - return new Range() { - @Override - public Class annotationType() { - return Range.class; - } - - @Override - public int min() { - return min; - } - - @Override - public int max() { - return max; - } - }; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/UnitsAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/UnitsAdapter.java deleted file mode 100644 index 8980a56734b..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/UnitsAdapter.java +++ /dev/null @@ -1,40 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config; - -import com.google.gson.*; -import net.runelite.client.config.Units; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; - -/** - * Adapter for serializing and deserializing Units annotations - */ -public class UnitsAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(Units src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - result.addProperty("value", src.value()); - return result; - } - - @Override - public Units deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - final String value = jsonObject.has("value") ? jsonObject.get("value").getAsString() : ""; - - // Create a proxy implementation of Units annotation - return new Units() { - @Override - public Class annotationType() { - return Units.class; - } - - @Override - public String value() { - return value; - } - }; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/AbstractPrePostScheduleTasks.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/AbstractPrePostScheduleTasks.java deleted file mode 100644 index 7b09e9b0530..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/AbstractPrePostScheduleTasks.java +++ /dev/null @@ -1,1102 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks; - -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerPlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.api.SchedulablePlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.event.ExecutionResult; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LockCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.PrePostScheduleRequirements; -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.tasks.state.TaskExecutionState; -import net.runelite.client.plugins.microbot.util.events.PluginPauseEvent; -import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; - -import java.awt.event.KeyEvent; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.config.ConfigDescriptor; -import net.runelite.client.input.KeyListener; -import net.runelite.client.input.KeyManager; - -/** - * Abstract base class for managing pre and post schedule tasks for plugins operating under scheduler control. - *

- * This class provides a common infrastructure for handling: - *

    - *
  • Executor service management for both pre and post tasks
  • - *
  • CompletableFuture lifecycle management with timeout support
  • - *
  • Thread-safe shutdown procedures
  • - *
  • Common error handling patterns
  • - *
  • AutoCloseable implementation for resource cleanup
  • - *
  • Emergency cancel hotkey (Ctrl+C) for aborting all tasks
  • - *
- *

- * Concrete implementations must provide: - *

    - *
  • {@link #executePreScheduleTask(LockCondition)} - Plugin-specific preparation logic
  • - *
  • {@link #executePostScheduleTask(LockCondition)} - Plugin-specific cleanup logic
  • - *
  • {@link #isScheduleMode()} - Detection of scheduler mode
  • - *
- * - * @see SchedulablePlugin - * @since 1.0.0 - */ -@Slf4j -public abstract class AbstractPrePostScheduleTasks implements AutoCloseable, KeyListener { - - // TODO: Consider adding configuration for default timeout values - // TODO: Add metrics collection for task execution times and success rates - // TODO: Implement retry mechanism for failed tasks with exponential backoff - // TODO: Add support for task priority levels (critical vs optional tasks) - // TODO: Consider adding a mechanism to pause/resume tasks based on external conditions - // TODO: add custom tasks as callbacks to allow plugins to define their own pre/post tasks in addtion to the pre/post schedule requirements - - protected final SchedulablePlugin plugin; - private ScheduledExecutorService postExecutorService; - private ScheduledExecutorService preExecutorService; - private CompletableFuture preScheduledFuture; - private CompletableFuture postScheduledFuture; - - // Emergency cancel hotkey support (injected via plugin) - - private final KeyManager keyManager; - - // Centralized state tracking - @Getter - private final TaskExecutionState executionState = new TaskExecutionState(); - - private final LockCondition prePostScheduleTaskLock = new LockCondition("Pre/Post Schedule Task Lock", false, true); - - /** - * Constructor for AbstractPrePostScheduleTasks. - * Initializes the task manager with the provided plugin instance. - * - * @param plugin The SchedulablePlugin instance to manage - */ - protected AbstractPrePostScheduleTasks(SchedulablePlugin plugin, KeyManager keyManager) { - this.plugin = plugin; - this.keyManager = keyManager; - initializeCancel(); - log.info("Initialized pre/post schedule task manager for plugin: {}", plugin.getClass().getSimpleName()); - } - - /** - * Initializes the emergency cancel hotkey functionality. - * This method should be called after the plugin's KeyManager is available. - * - * @param keyManager The KeyManager instance from the plugin - */ - public final void initializeCancel() { - - // Register emergency cancel hotkey if KeyManager is available - try { - if (keyManager != null) { - keyManager.registerKeyListener(this); - log.info("Registered emergency cancel hotkey (Ctrl+C) for plugin: {}", plugin.getClass().getSimpleName()); - }else{ - log.warn("KeyManager is not available, cannot register emergency cancel hotkey for plugin: {}", plugin.getClass().getSimpleName()); - } - } catch (Exception e) { - log.warn("Failed to register emergency cancel hotkey for plugin {}: {}", - plugin.getClass().getSimpleName(), e.getMessage()); - } - } - private boolean canStartAnyTask(){ - // Check if the plugin is running in schedule mode - if (plugin == null) { - log.warn("Plugin instance is null, cannot determine schedule mode"); - return false; // Cannot determine schedule mode without plugin instance - } - - if (getRequirements() == null || !getRequirements().isInitialized()) { - log.warn("Requirements are not initialized, cannot execute pre-schedule tasks"); - return false; // Cannot run pre-schedule tasks if requirements are not met - } - - - if (postScheduledFuture != null && !postScheduledFuture.isDone()) { - log.warn("Post-schedule task is still running, cannot execute pre-schedule tasks yet"); - return false; // Cannot run pre-schedule tasks while post-schedule is still running - } - if (preScheduledFuture != null && !preScheduledFuture.isDone()) { - log.warn("Pre-schedule task already running, skipping duplicate execution"); - return false; // Pre-schedule task already running, skip - } - return true; - } - public boolean canStartPreScheduleTasks() { - // Check state before execution - if (!executionState.canExecutePreTasks()) { - log.warn("Pre-schedule tasks cannot be executed - already started and not completed. Use reset() to allow re-execution."); - return false; - } - // Check if the plugin is running in schedule mode - return canStartAnyTask(); - - } - public boolean canStartPostScheduleTasks() { - // Check state before execution - if (!executionState.canExecutePostTasks()) { - log.warn("Post-schedule tasks cannot be executed - already started and not completed. Use reset() to allow re-execution.\n -executionState: {}",executionState); - return false; - } - // Check if the plugin is running in schedule mode - return canStartAnyTask(); - } - /** - * Executes pre-schedule preparation tasks on a separate thread. - * This method runs preparation tasks asynchronously and calls the provided callback when complete. - * - * @param callback The callback to execute when preparation is finished - * @param timeout The timeout value (0 or negative means no timeout) - * @param timeUnit The time unit for the timeout - */ - public final void executePreScheduleTasks(Runnable callback, LockCondition lockCondition, int timeout, TimeUnit timeUnit) { - if (!canStartPreScheduleTasks()) { - log.warn("Cannot execute pre-schedule tasks - conditions not met"); - return; // Cannot run pre-schedule tasks if conditions are not met - } - - // Update state to indicate pre-schedule tasks are starting - executionState.update( TaskExecutionState.ExecutionPhase.PRE_SCHEDULE,TaskExecutionState.ExecutionState.STARTING); - - // Initialize executor service for pre-actions - if (preExecutorService == null || preExecutorService.isShutdown()) { - preExecutorService = Executors.newScheduledThreadPool(2, r -> { - Thread t = new Thread(r, getClass().getSimpleName() + "-PreSchedule"); - t.setDaemon(true); - return t; - }); - } - lockPrePostTask(); - preScheduledFuture = CompletableFuture.supplyAsync(() -> { - try { - log.info("\n --> Starting pre-schedule preparation on separate thread for plugin: \n\t\t{}", - plugin.getClass().getSimpleName()); - - // Execute preparation actions - boolean success = executePreScheduleTask(lockCondition); - - if (success) { - log.info("\n\tPre-schedule preparation completed successfully - executing callback"); - executionState.update( TaskExecutionState.ExecutionPhase.PRE_SCHEDULE,TaskExecutionState.ExecutionState.COMPLETED); - if (callback != null) { - callback.run(); - } - } else { - log.warn("\n\tPre-schedule preparation failed - stopping plugin"); - executionState.update( TaskExecutionState.ExecutionPhase.PRE_SCHEDULE,TaskExecutionState.ExecutionState.FAILED); - } - - return success; - } catch (Exception e) { - log.error("Error during pre-schedule preparation: {}", e.getMessage(), e); - executionState.update( TaskExecutionState.ExecutionPhase.PRE_SCHEDULE,TaskExecutionState.ExecutionState.ERROR); - // Unlock is handled in handlePreTaskCompletion via whenComplete - throw new RuntimeException("Pre-schedule preparation failed", e); - } - }, preExecutorService); - - // Handle timeout and completion - if (timeout > 0) { - preScheduledFuture.orTimeout(timeout, timeUnit) - .whenComplete((result, throwable) -> { - handlePreTaskCompletion(result, throwable); - }); - } else { - preScheduledFuture.whenComplete((result, throwable) -> { - handlePreTaskCompletion(result, throwable); - }); - } - } - private void lockPrePostTask(){ - PluginPauseEvent.setPaused(true); - prePostScheduleTaskLock.lock(); - } - private void unlock() { - PluginPauseEvent.setPaused(false); - prePostScheduleTaskLock.unlock(); - } - - /** - * Convenience method for executing pre-schedule tasks with default timeout. - * - * @param callback The callback to execute when preparation is finished - * @param lockCondition The lock condition to prevent running the pre-schedule tasks while the plugin is in a critical operation - */ - public final void executePreScheduleTasks(Runnable callback, LockCondition lockCondition) { - executePreScheduleTasks(callback,lockCondition, 0, TimeUnit.SECONDS); - } - /** - * Convenience method for executing pre-schedule tasks with default timeout. - * - * @param callback The callback to execute when preparation is finished - */ - public final void executePreScheduleTasks(Runnable callback) { - executePreScheduleTasks(callback,null, 0, TimeUnit.SECONDS); - } - - - public final boolean isPreScheduleRunning() { - return preScheduledFuture != null && !preScheduledFuture.isDone(); - } - /** - * Executes post-schedule cleanup tasks when running under scheduler control. - * This includes graceful shutdown procedures and resource cleanup. - * - * @param lockCondition The lock condition to prevent interruption during critical operations - * @param timeout The timeout value (0 or negative means no timeout) - * @param timeUnit The time unit for the timeout - */ - public final void executePostScheduleTasks(Runnable callback, LockCondition lockCondition, int timeout, TimeUnit timeUnit) { - - if (!canStartPostScheduleTasks()) { - log.warn("Cannot execute post-schedule tasks - conditions not met"); - return; // Cannot run post-schedule tasks if conditions are not met - } - executionState.update( TaskExecutionState.ExecutionPhase.POST_SCHEDULE,TaskExecutionState.ExecutionState.STARTING); - - initializePostExecutorService(); - lockPrePostTask(); - postScheduledFuture = CompletableFuture.supplyAsync(() -> { - try { - if (lockCondition != null && lockCondition.isLocked()) { - log.info("Post-schedule: waiting for current operation to complete"); - // Wait for lock to be released with reasonable timeout - int waitAttempts = 0; - while (lockCondition.isLocked() && waitAttempts < 60) { // Wait up to 60 seconds - try { - Thread.sleep(1000); - waitAttempts++; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.warn("Interrupted while waiting for lock release"); - break; - } - } - } - - log.info("\n\tStarting post-schedule tasks for plugin: \n\t\t{}", plugin.getClass().getSimpleName()); - - // Execute cleanup actions - boolean success = executePostScheduleTask(lockCondition); - - if (success) { - log.info("Post-schedule cleanup completed successfully"); - if (callback != null) { - callback.run(); - } - } else { - log.warn("Post-schedule cleanup failed - stopping plugin"); - } - - return success; - - } catch (Exception ex) { - log.error("Error during post-schedule cleanup: {}", ex.getMessage(), ex); - throw new RuntimeException("Post-schedule cleanup failed", ex); - } - }, postExecutorService); - - // Handle timeout and completion - if (timeout > 0) { - postScheduledFuture.orTimeout(timeout, timeUnit) - .whenComplete((result, throwable) -> { - handlePostTaskCompletion(result, throwable); - postScheduledFuture = null; - }); - } else { - postScheduledFuture.whenComplete((result, throwable) -> { - handlePostTaskCompletion(result, throwable); - postScheduledFuture = null; - }); - } - } - - /** - * Convenience method for executing post-schedule tasks with default timeout. - * - * @param callback The callback to execute when cleanup is finished - * @param lockCondition The lock condition to prevent interruption during critical operations - */ - public final void executePostScheduleTasks(Runnable callback, LockCondition lockCondition) { - executePostScheduleTasks(callback, lockCondition, 0, TimeUnit.SECONDS); - } - /** - * Convenience method for executing post-schedule tasks with default timeout. - * @param callback The callback to execute when cleanup is finished - * @return - */ - public final void executePostScheduleTasks(Runnable callback) { - executePostScheduleTasks(callback, null, 0, TimeUnit.SECONDS); - } - /** - * Convenience method for executing post-schedule tasks with default timeout. - * @param callback The callback to execute when cleanup is finished - * @return - */ - public final void executePostScheduleTasks(LockCondition lockCondition) { - executePostScheduleTasks( () ->{} , lockCondition, 0, TimeUnit.SECONDS); - } - - /** - * Final implementation of pre-schedule task execution that enforces proper threading. - * This method cannot be overridden - it ensures all pre-schedule tasks run through the proper - * executor service infrastructure. Child classes provide their custom logic through - * {@link #executeCustomPreScheduleTask(LockCondition)}. - * - * @param lockCondition The lock condition to prevent interruption during critical operations - * @return true if preparation was successful, false otherwise - */ - protected final boolean executePreScheduleTask(LockCondition lockCondition) { - try { - executionState.update( TaskExecutionState.ExecutionPhase.PRE_SCHEDULE,TaskExecutionState.ExecutionState.FULFILLING_REQUIREMENTS); - log.debug("Executing standard pre-schedule requirements fulfillment"); - - // Always fulfill the standard requirements first - boolean standardRequirementsFulfilled = fulfillPreScheduleRequirements(); - - if (!standardRequirementsFulfilled) { - log.warn("Standard pre-schedule requirements fulfillment failed, but continuing with custom tasks"); - } - - // Execute any custom pre-schedule logic from the child class - executionState.update( TaskExecutionState.ExecutionPhase.PRE_SCHEDULE,TaskExecutionState.ExecutionState.CUSTOM_TASKS); - log.debug("Executing custom pre-schedule tasks"); - boolean customTasksSuccessful = executeCustomPreScheduleTask(preScheduledFuture,lockCondition); - - // Clear state when finished - if (standardRequirementsFulfilled && customTasksSuccessful) { - executionState.update( TaskExecutionState.ExecutionPhase.PRE_SCHEDULE,TaskExecutionState.ExecutionState.COMPLETED); - } else { - executionState.update( TaskExecutionState.ExecutionPhase.PRE_SCHEDULE,TaskExecutionState.ExecutionState.FAILED); - } - - // Return true only if both standard and custom tasks succeeded - // (or if we want to be more lenient, we could return true if custom tasks succeeded) - return standardRequirementsFulfilled && customTasksSuccessful; - - } catch (Exception e) { - executionState.update( TaskExecutionState.ExecutionPhase.PRE_SCHEDULE,TaskExecutionState.ExecutionState.ERROR); - log.error("Error during pre-schedule task execution: {}", e.getMessage(), e); - return false; - } finally { - - } - } - - /** - * Final implementation of post-schedule task execution that enforces proper threading. - * This method cannot be overridden - it ensures all post-schedule tasks run through the proper - * executor service infrastructure. Child classes provide their custom logic through - * {@link #executeCustomPostScheduleTask(LockCondition)}. - * - * @param lockCondition The lock condition to prevent interruption during critical operations - * @return true if cleanup was successful, false otherwise - */ - protected final boolean executePostScheduleTask(LockCondition lockCondition) { - try { - executionState.update( TaskExecutionState.ExecutionPhase.POST_SCHEDULE,TaskExecutionState.ExecutionState.CUSTOM_TASKS); - log.debug("Executing custom post-schedule tasks"); - // Execute any custom post-schedule logic from the child class first - // This allows plugins to handle their specific cleanup (like stopping scripts) - - boolean customTasksSuccessful = executeCustomPostScheduleTask(postScheduledFuture, lockCondition); - - if (!customTasksSuccessful) { - log.warn("Custom post-schedule tasks failed, but continuing with standard cleanup"); - return false; - } - - // Always fulfill the standard requirements after custom tasks - executionState.update( TaskExecutionState.ExecutionPhase.POST_SCHEDULE,TaskExecutionState.ExecutionState.FULFILLING_REQUIREMENTS); - log.debug("Executing standard post-schedule requirements fulfillment"); - boolean standardRequirementsFulfilled = fulfillPostScheduleRequirements(); - log.info("Standard post-schedule requirements fulfilled: {}", standardRequirementsFulfilled); - - // Update completion state - if (customTasksSuccessful && standardRequirementsFulfilled) { - executionState.update( TaskExecutionState.ExecutionPhase.POST_SCHEDULE,TaskExecutionState.ExecutionState.COMPLETED); - // Clear state after a brief delay to show completion - scheduleStateClear(2000); - } else { - executionState.update( TaskExecutionState.ExecutionPhase.POST_SCHEDULE,TaskExecutionState.ExecutionState.FAILED); - } - - // Return true if both succeeded (cleanup is more lenient than setup) - return customTasksSuccessful && standardRequirementsFulfilled; - - } catch (Exception e) { - executionState.update( TaskExecutionState.ExecutionPhase.POST_SCHEDULE,TaskExecutionState.ExecutionState.ERROR); - log.error("Error during post-schedule task execution: {}", e.getMessage(), e); - return false; - } finally { - } - } - public final boolean isPostScheduleRunning() { - return postScheduledFuture != null && !postScheduledFuture.isDone(); - } - - /** - * Abstract method that concrete implementations must provide for custom pre-schedule logic. - * This method is called AFTER the standard requirement fulfillment logic and should contain - * any plugin-specific preparation tasks that are not covered by the standard requirements. - * - * IMPORTANT: This method is always called within the proper executor service threading context. - * Do not call this method directly - use {@link #executePreScheduleTasks} instead. - * - * @param lockCondition The lock condition to prevent interruption during critical operations - * @return true if custom preparation was successful, false otherwise - */ - protected abstract boolean executeCustomPreScheduleTask(CompletableFuture preScheduledFuture,LockCondition lockCondition); - - /** - * Abstract method that concrete implementations must provide for custom post-schedule logic. - * This method is called BEFORE the standard requirement fulfillment logic and should contain - * any plugin-specific cleanup tasks (like stopping scripts, leaving minigames, etc.). - * - * IMPORTANT: This method is always called within the proper executor service threading context. - * Do not call this method directly - use {@link #executePostScheduleTasks} instead. - * - * @param lockCondition The lock condition to prevent interruption during critical operations - * @return true if custom cleanup was successful, false otherwise - */ - protected abstract boolean executeCustomPostScheduleTask(CompletableFuture postScheduledFuture, LockCondition lockCondition); - - /** - * Abstract method to determine if the plugin is running in schedule mode. - * Concrete implementations should check their specific configuration to determine this. - * - * @return true if the plugin is running under scheduler control, false otherwise - */ - protected String getConfigGroupName(){ - /** - * Returns the configuration group name for this plugin. - * This is used by the scheduler to manage configuration state. - * - * @return The configuration group name - */ - ConfigDescriptor pluginConfigDescriptor = this.plugin.getConfigDescriptor(); - if (pluginConfigDescriptor == null) { - log.warn("\"{}\" plugin config descriptor is null", this.plugin.getClass().getSimpleName()); - return ""; // Default group name if descriptor is not available - } - String configGroupName = pluginConfigDescriptor.getGroup().value(); - if (configGroupName == null || configGroupName.isEmpty()) { - log.warn("\"{}\" plugin config group name is null or empty"); - return ""; // Default group name if descriptor is not available - } - log.info("\"{}\" plugin config group name: {}", this.plugin.getClass().getSimpleName(),configGroupName); - return configGroupName; - } - - public boolean isScheduleMode() { - // Check if the plugin is running in schedule mode - if (plugin == null) { - log.warn("Plugin instance is null, cannot determine schedule mode"); - return false; // Cannot determine schedule mode without plugin instance - } - - // Check if the plugin is running in schedule mode - return isScheduleMode(this.plugin, getConfigGroupName()); - - } - /** - * Checks if the plugin is running in schedule mode by checking the GOTR configuration. - * - * @return true if scheduleMode flag is set, false otherwise - */ - public static boolean isScheduleMode( SchedulablePlugin plugin, String configGroupName) { - - SchedulerPlugin schedulablePlugin = (SchedulerPlugin) Microbot.getPlugin(SchedulerPlugin.class.getName()); - Boolean scheduleModeConfig = false; - Boolean scheduleModeDetect = false; - try { - scheduleModeConfig = Microbot.getConfigManager().getConfiguration( - configGroupName, "scheduleMode", Boolean.class); - } catch (Exception e) { - log.error("Failed to check schedule mode: {}", e.getMessage()); - return false; - } - if (schedulablePlugin == null) { - log.warn("SchedulerPlugin is not running, cannot can not be in schedule mode"); - scheduleModeDetect = false; // SchedulerPlugin is not running, cannot determine schedule mode, so we dont run in schedule mode - }else{ - PluginScheduleEntry currentPlugin = schedulablePlugin.getCurrentPlugin(); - if (currentPlugin == null) { - log.warn("\nNo current plugin is running by the Scheduler Plugin, so it also can not be the plugin is start in scheduler mode"); - scheduleModeDetect = false; // No current plugin is running, so it can not be in schedule mode - }else{ - if (currentPlugin.isRunning() && currentPlugin.getPlugin() != null && !currentPlugin.getPlugin().equals(plugin)) { - log.warn("\n\tCurrent plugin {} is running, but it is not the same as the pluginScheduleEntry {}, so it can not be in schedule mode", - currentPlugin.getPlugin().getClass().getSimpleName(), - plugin.getClass().getSimpleName()); - scheduleModeDetect = false; // Current plugin is running, but it's not the same as the pluginSchedule - - }else{ - scheduleModeDetect = true; - } - } - } - - if (configGroupName.isEmpty()) { - log.warn("Config group name is empty, cannot determine schedule mode"); - }else if(scheduleModeConfig){ - Microbot.getConfigManager().setConfiguration(configGroupName, "scheduleMode", scheduleModeConfig); - scheduleModeDetect = true; // If scheduleMode config is set, we are in schedule mode - } - log.debug("\nPlugin {}, with config group name {}, \nis running in schedule mode (plugin detect): {}\n\t\tSchedule mode config: {}", - plugin.getClass().getSimpleName(), configGroupName, scheduleModeDetect, scheduleModeConfig); - return scheduleModeDetect; - - } - private void setScheduleMode(boolean scheduleMode) { - try { - String configGroupName = getConfigGroupName(); - if (configGroupName == null || configGroupName.isEmpty()) { - log.warn("\"{}\" plugin config group name is null or empty", this.plugin.getClass().getSimpleName()); - return; // Cannot set schedule mode without config group - } - Microbot.getConfigManager().setConfiguration(configGroupName, "scheduleMode", scheduleMode); - } catch (Exception e) { - log.error("Failed to set schedule mode: {}", e.getMessage()); - } - } - protected void enableScheduleMode() { - setScheduleMode(true); - } - protected void disableScheduleMode() { - setScheduleMode(false); - } - - - /** - * Abstract method that concrete implementations must provide to supply their requirements. - * This method should return the PrePostScheduleRequirements instance that defines - * what the plugin needs for optimal operation. - * - * @return The PrePostScheduleRequirements instance for this plugin - */ - protected abstract PrePostScheduleRequirements getPrePostScheduleRequirements(); - - /** - * Public accessor for the pre/post schedule requirements. - * This allows external components (like UI panels) to access requirement information. - * - * @return The PrePostScheduleRequirements instance, or null if not implemented - */ - public final PrePostScheduleRequirements getRequirements() { - return getPrePostScheduleRequirements(); - } - - /** - * Adds a custom requirement to this plugin's pre/post schedule requirements. - * Custom requirements are marked as CUSTOM type and are fulfilled after all standard requirements. - * - * @param requirement The requirement to add - * @param TaskContext The context in which this requirement should be fulfilled - * @return true if the requirement was successfully added, false otherwise - */ - public boolean addCustomRequirement(Requirement requirement, - TaskContext taskContext) { - PrePostScheduleRequirements requirements = getPrePostScheduleRequirements(); - if (requirements == null) { - log.warn("Cannot add custom requirement: No pre/post schedule requirements defined for this plugin"); - return false; - } - - // Mark this requirement as a custom requirement by creating a wrapper - // We'll modify the requirement to ensure it's recognized as custom - boolean success = requirements.addCustomRequirement(requirement, taskContext); - - if (success) { - log.info("Successfully added custom requirement: {} for context: {}", - requirement.getDescription(), taskContext); - } else { - log.warn("Failed to add custom requirement: {} for context: {}", - requirement.getDescription(), taskContext); - } - - return success; - } - - /** - * Default implementation for fulfilling pre-schedule requirements. - * This method attempts to fulfill all pre-schedule requirements including: - * - Location requirements (travel to pre-schedule location) - * - Spellbook requirements (switch to required spellbook) - * - Equipment and inventory setup (using the static methods from PrePostScheduleRequirements) - * - * Child classes can override this method to provide custom behavior while still - * leveraging the default requirement fulfillment logic. - * - * @return true if all requirements were successfully fulfilled, false otherwise - */ - protected boolean fulfillPreScheduleRequirements() { - try { - PrePostScheduleRequirements requirements = getPrePostScheduleRequirements(); - if (requirements == null) { - log.info("No pre-schedule requirements defined"); - return true; - } - - executionState.update( TaskExecutionState.ExecutionPhase.PRE_SCHEDULE,TaskExecutionState.ExecutionState.FULFILLING_REQUIREMENTS); - log.info("\n\tFulfilling pre-schedule requirements for {}", requirements.getActivityType()); - - // Use the unified fulfillment method that handles all requirement types including conditional requirements - boolean fulfilled = requirements.fulfillPreScheduleRequirements(preScheduledFuture, true, executionState); // Pass executor service - - if (!fulfilled) { - log.error("Failed to fulfill pre-schedule requirements"); - return false; - } - - log.info("Successfully fulfilled pre-schedule requirements"); - return true; - - } catch (Exception e) { - log.error("Error fulfilling pre-schedule requirements: {}", e.getMessage(), e); - return false; - } - } - - /** - * Default implementation for fulfilling post-schedule requirements. - * This method attempts to fulfill all post-schedule requirements including: - * - Location requirements (travel to post-schedule location) - * - Spellbook restoration (switch back to original spellbook) - * - Banking and cleanup operations - * - * Child classes can override this method to provide custom behavior while still - * leveraging the default requirement fulfillment logic. - * - * @return true if all requirements were successfully fulfilled, false otherwise - */ - protected boolean fulfillPostScheduleRequirements() { - try { - PrePostScheduleRequirements requirements = getPrePostScheduleRequirements(); - if (requirements == null) { - log.info("No post-schedule requirements defined"); - return true; - } - executionState.update( TaskExecutionState.ExecutionPhase.POST_SCHEDULE,TaskExecutionState.ExecutionState.FULFILLING_REQUIREMENTS); - log.info("Fulfilling post-schedule requirements for {}", requirements.getActivityType()); - - // Use the unified fulfillment method that handles all requirement types including conditional requirements - boolean fulfilled = requirements.fulfillPostScheduleRequirements(postScheduledFuture, true,executionState ); // Pass executor service - - if (!fulfilled) { - log.error("Failed to fulfill all post-schedule requirements"); - return false; - } - - log.info("Successfully fulfilled post-schedule requirements"); - return true; - - } catch (Exception e) { - log.error("Error fulfilling post-schedule requirements: {}", e.getMessage(), e); - return false; - } finally { - // Clear requirements state - PrePostScheduleRequirements requirements = getPrePostScheduleRequirements(); - if (requirements != null) { - try { - clearRequirementState(); - } catch (Exception e) { - log.warn("Failed to clear fulfillment state: {}", e.getMessage()); - } - } - } - } - - /** - * Handles the completion of pre-schedule tasks. - * - * @param result The result of the task execution - * @param throwable Any exception that occurred during execution - */ - private void handlePreTaskCompletion(Boolean result, Throwable throwable) { - unlock(); - if (throwable != null) { - if (throwable instanceof TimeoutException) { - log.warn("Pre-schedule task timed out for plugin: {}", plugin.getClass().getSimpleName()); - plugin.reportPreScheduleTaskFinished("Pre-schedule task timed out", ExecutionResult.SOFT_FAILURE); - } else { - log.error("Pre-schedule task failed for plugin: {} - {}", - plugin.getClass().getSimpleName(), throwable.getMessage()); - plugin.reportPreScheduleTaskFinished("Pre-schedule task failed: " + throwable.getMessage(), ExecutionResult.HARD_FAILURE); - } - } else if (result != null && result) { - log.info("Pre-schedule task completed successfully for plugin: {}", plugin.getClass().getSimpleName()); - executionState.update( TaskExecutionState.ExecutionPhase.MAIN_EXECUTION,TaskExecutionState.ExecutionState.STARTING); - plugin.reportPreScheduleTaskFinished("Pre-schedule preparation completed successfully", ExecutionResult.SUCCESS); - }else{ - executionState.update( TaskExecutionState.ExecutionPhase.MAIN_EXECUTION,TaskExecutionState.ExecutionState.ERROR); - plugin.reportPreScheduleTaskFinished("\n\tPre-schedule preparation was not successful", ExecutionResult.SOFT_FAILURE); - } - - } - - /** - * Handles the completion of post-schedule tasks. - * - * @param result The result of the task execution - * @param throwable Any exception that occurred during execution - */ - private void handlePostTaskCompletion(Boolean result, Throwable throwable) { - unlock(); - if (throwable != null) { - if (throwable instanceof TimeoutException) { - log.warn("Post-schedule task timed out for plugin: {}", plugin.getClass().getSimpleName()); - plugin.reportPostScheduleTaskFinished("Post-schedule task timed out", ExecutionResult.SOFT_FAILURE); - } else { - log.error("Post-schedule task failed for plugin: {} - {}", - plugin.getClass().getSimpleName(), throwable.getMessage()); - plugin.reportPostScheduleTaskFinished("Post-schedule task failed: " + throwable.getMessage(), ExecutionResult.HARD_FAILURE); - } - } else if (result != null && result) { - log.debug("Post-schedule task completed successfully for plugin: {}", plugin.getClass().getSimpleName()); - executionState.update( TaskExecutionState.ExecutionPhase.POST_SCHEDULE,TaskExecutionState.ExecutionState.COMPLETED); - plugin.reportPostScheduleTaskFinished("Post-schedule task completed successfully", ExecutionResult.SUCCESS); - }else{ - executionState.update( TaskExecutionState.ExecutionPhase.POST_SCHEDULE,TaskExecutionState.ExecutionState.ERROR); - plugin.reportPostScheduleTaskFinished("\n\tPost-schedule task was not successful", ExecutionResult.SOFT_FAILURE); - } - } - - /** - * Initializes the post executor service if not already initialized. - */ - private void initializePostExecutorService() { - if (postExecutorService == null || postExecutorService.isShutdown()) { - postExecutorService = Executors.newScheduledThreadPool(2, r -> { - Thread t = new Thread(r, getClass().getSimpleName() + "-PostSchedule"); - t.setDaemon(true); - return t; - }); - } - } - - /** - * Safely shuts down an executor service with proper timeout handling. - * - * @param executorService The executor service to shutdown - * @param taskType The type of task (for logging purposes) - */ - private void shutdownExecutorService(ScheduledExecutorService executorService, String taskType) { - if (executorService != null && !executorService.isShutdown()) { - try { - executorService.shutdown(); - - if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { - log.warn("Executor service for {} tasks did not terminate gracefully, forcing shutdown", taskType); - executorService.shutdownNow(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.warn("Interrupted while shutting down {} executor service", taskType); - executorService.shutdownNow(); - } - } - } - - /** - * Returns whether any tasks are currently running. - * - * @return true if any pre or post tasks are currently executing - */ - public final boolean isRunning() { - return (preScheduledFuture != null && !preScheduledFuture.isDone()) || - (postScheduledFuture != null && !postScheduledFuture.isDone()); - } - - /** - * Cancels any running tasks and shuts down all executor services. - * This method implements AutoCloseable for proper resource management. - */ - @Override - public void close() { - // Unregister emergency cancel hotkey - try { - if (keyManager != null) { - keyManager.unregisterKeyListener(this); - log.debug("Unregistered emergency cancel hotkey for plugin: {}", plugin.getClass().getSimpleName()); - } - } catch (Exception e) { - log.warn("Failed to unregister emergency cancel hotkey for plugin {}: {}", - plugin.getClass().getSimpleName(), e.getMessage()); - } - unlock(); - // Cancel any running futures - if (preScheduledFuture != null && !preScheduledFuture.isDone()) { - preScheduledFuture.cancel(true); - preScheduledFuture = null; - } - - if (postScheduledFuture != null && !postScheduledFuture.isDone()) { - postScheduledFuture.cancel(true); - postScheduledFuture = null; - } - executionState.reset(); - - // Shutdown executor services - shutdownExecutorService(preExecutorService, "pre-schedule"); - shutdownExecutorService(postExecutorService, "post-schedule"); - - preExecutorService = null; - postExecutorService = null; - disableScheduleMode(); // Reset schedule mode - log.info("Closed {} pre-post Schedule task task manager", getClass().getSimpleName()); - } - public void cancelPreScheduleTasks() { - if (preScheduledFuture != null && !preScheduledFuture.isDone()) { - preScheduledFuture.cancel(true); - preScheduledFuture = null; - log.info("Cancelled pre-schedule tasks for plugin: {}", plugin.getClass().getSimpleName()); - } else { - log.warn("No pre-schedule tasks to cancel for plugin: {}", plugin.getClass().getSimpleName()); - } - } - - public void cancelPostScheduleTasks() { - if (postScheduledFuture != null && !postScheduledFuture.isDone()) { - postScheduledFuture.cancel(true); - postScheduledFuture = null; - log.info("Cancelled post-schedule tasks for plugin: {}", plugin.getClass().getSimpleName()); - } else { - log.warn("No post-schedule tasks to cancel for plugin: {}", plugin.getClass().getSimpleName()); - } - } - - /** - * Shutdown alias for compatibility. Calls {@link #close()}. - */ - public final void shutdown() { - close(); - } - /** - * Clears the current task state - */ - protected void clearTaskState() { - executionState.clear(); - } - protected void clearRequirementState() { - executionState.clearRequirementState(); - } - - /** - * Gets the current execution status for overlay display - * @return A formatted string describing the current state, or null if not executing - */ - public String getCurrentExecutionStatus() { - return executionState.getDisplayStatus(); - } - - /** - * Checks if any pre/post schedule task is currently executing - */ - public boolean isExecuting() { - return executionState.isExecuting(); - } - - /** - * Resets the task execution state and cancels any running tasks. - * This allows pre/post schedule tasks to be executed again. - * - * WARNING: This will forcibly cancel any currently running tasks. - */ - public synchronized void reset() { - log.info("Resetting pre/post schedule tasks for plugin: {}", plugin.getClass().getSimpleName()); - unlock(); - // Cancel and cleanup running futures - if (preScheduledFuture != null && !preScheduledFuture.isDone()) { - log.info("Cancelling running pre-schedule task"); - preScheduledFuture.cancel(true); - preScheduledFuture = null; - } - - if (postScheduledFuture != null && !postScheduledFuture.isDone()) { - log.info("Cancelling running post-schedule task"); - postScheduledFuture.cancel(true); - postScheduledFuture = null; - } - - // Shutdown executor services - shutdownExecutorService(preExecutorService, "Pre-schedule"); - shutdownExecutorService(postExecutorService, "Post-schedule"); - - preExecutorService = null; - postExecutorService = null; - - // Reset execution state - executionState.reset(); - if ( getRequirements()!= null) { - clearRequirementState(); - getRequirements().reset(); - } - log.info("Reset completed - pre/post schedule tasks can now be executed again"); - } - - // Convenience methods for checking task states - - /** - * Checks if pre-schedule tasks are completed - */ - public boolean isPreTaskComplete() { - return executionState.isPreTaskComplete(); - } - - public boolean isHasPreTaskStarted() { - return executionState.isPreTaskRunning(); - } - /** - * Checks if main task is running - */ - public boolean isHasMainTaskStarted() { - return executionState.isMainTaskRunning(); - } - - /** - * Checks if main task is completed - */ - public boolean isMainTaskComplete() { - return executionState.isMainTaskComplete(); - } - - /** - * Checks if post-schedule tasks are completed - */ - public boolean isPostTaskComplete() { - return executionState.isPostTaskComplete(); - } - - /** - * Checks if pre-schedule tasks are currently running - */ - public boolean isPreTaskRunning() { - return executionState.isPreTaskRunning(); - } - - /** - * Checks if post-schedule tasks are currently running - */ - public boolean isPostTaskRunning() { - return executionState.isPostTaskRunning(); - } - - /** - * Gets a detailed status string for debugging - */ - public String getDetailedExecutionStatus() { - return executionState.getDetailedStatus(); - } - - /** - * Schedules clearing of the task state after a delay - * @param delayMs Delay in milliseconds before clearing the state - */ - private void scheduleStateClear(int delayMs) { - new Thread(() -> { - try { - Thread.sleep(delayMs); - clearTaskState(); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - }).start(); - } - - /** - * Emergency cancellation method for aborting all tasks and operations. - * This method will: - * 1. Cancel all pre and post schedule futures - * 2. Shutdown all executor services - * 3. Clear Rs2Walker target - * 4. Reset task execution state - * - * This is used by the Ctrl+C hotkey for emergency stops. - */ - public final void emergencyCancel() { - try { - log.warn("\n=== EMERGENCY CANCELLATION TRIGGERED ==="); - log.warn("Plugin: {}", plugin.getClass().getSimpleName()); - - // Cancel all futures immediately - if (preScheduledFuture != null && !preScheduledFuture.isDone()) { - log.info(" â€Ē Cancelling pre-schedule future"); - preScheduledFuture.cancel(true); - preScheduledFuture = null; - } - - if (postScheduledFuture != null && !postScheduledFuture.isDone()) { - log.info(" â€Ē Cancelling post-schedule future"); - postScheduledFuture.cancel(true); - postScheduledFuture = null; - } - - // Shutdown executor services immediately - if (preExecutorService != null && !preExecutorService.isShutdown()) { - log.info(" â€Ē Shutting down pre-schedule executor service"); - preExecutorService.shutdownNow(); - preExecutorService = null; - } - - if (postExecutorService != null && !postExecutorService.isShutdown()) { - log.info(" â€Ē Shutting down post-schedule executor service"); - postExecutorService.shutdownNow(); - postExecutorService = null; - } - - // Clear Rs2Walker target to stop any walking operations - try { - log.info(" â€Ē Clearing Rs2Walker target"); - Rs2Walker.setTarget(null); - } catch (Exception e) { - log.warn("Failed to clear Rs2Walker target: {}", e.getMessage()); - } - - // Reset task execution state - log.info(" â€Ē Resetting task execution state"); - executionState.reset(); - unlock(); - log.warn("=== EMERGENCY CANCELLATION COMPLETED ===\n"); - - } catch (Exception e) { - log.error("Error during emergency cancellation: {}", e.getMessage(), e); - } - } - - // ==================== KeyListener Interface Methods ==================== - - @Override - public void keyTyped(KeyEvent e) { - // Not needed for hotkey detection - } - - @Override - public void keyPressed(KeyEvent e) { - // Check for Ctrl+C hotkey combination - if (e.isControlDown() && e.getKeyCode() == KeyEvent.VK_C) { - log.info("Emergency cancel hotkey (Ctrl+C) detected for plugin: {}", plugin.getClass().getSimpleName()); - - // Only trigger if we have running tasks - if (isRunning()) { - emergencyCancel(); - } else { - log.info("No tasks currently running - emergency cancel not needed"); - } - } - } - - @Override - public void keyReleased(KeyEvent e) { - // Not needed for hotkey detection - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/README.md b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/README.md deleted file mode 100644 index 392edb7bd1f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/README.md +++ /dev/null @@ -1,263 +0,0 @@ -# Pre and Post Schedule Tasks Infrastructure - -## Overview - -The Pre and Post Schedule Tasks infrastructure provides a standardized way for Microbot plugins to handle preparation and cleanup when operating under scheduler control. This system ensures consistent resource management, proper plugin lifecycle handling, and graceful startup/shutdown procedures. - -## Architecture - -### Abstract Base Class: `AbstractPrePostScheduleTasks` - -The `AbstractPrePostScheduleTasks` class provides: - -- **Executor Service Management**: Automatic creation and lifecycle management of thread pools for pre and post tasks -- **CompletableFuture Handling**: Asynchronous task execution with timeout support and proper error handling -- **Resource Cleanup**: AutoCloseable implementation ensures proper shutdown of all resources -- **Common Error Patterns**: Standardized logging and error handling across all implementations -- **Thread Safety**: Safe concurrent execution and cancellation of tasks - -### Key Features - -1. **Asynchronous Execution**: Tasks run on separate threads to avoid blocking the main plugin thread -2. **Timeout Support**: Configurable timeouts with graceful handling of timeout scenarios -3. **Lock Integration**: Support for LockCondition to prevent interruption during critical operations -4. **Callback Support**: Execute callbacks when pre-tasks complete successfully -5. **Automatic Cleanup**: Resources are automatically cleaned up on plugin shutdown - -## Implementation Guide - -### Step 1: Create Your Task Implementation - -Extend `AbstractPrePostScheduleTasks` and implement the three required abstract methods: - -```java -public class YourPluginPrePostScheduleTasks extends AbstractPrePostScheduleTasks { - - public YourPluginPrePostScheduleTasks(SchedulablePlugin plugin) { - super(plugin); - // Initialize plugin-specific requirements or dependencies - } - - @Override - protected boolean executePreScheduleTask(LockCondition lockCondition) { - // Add your plugin's preparation logic here - // Return true if successful, false otherwise - } - - @Override - protected boolean executePostScheduleTask(LockCondition lockCondition) { - // Add your plugin's cleanup logic here - // Return true if successful, false otherwise - } - - @Override - protected boolean isScheduleMode() { - // Check your plugin's configuration to determine if running under scheduler - Boolean scheduleMode = Microbot.getConfigManager().getConfiguration( - "YourPluginConfig", "scheduleMode", Boolean.class); - return scheduleMode != null && scheduleMode; - } -} -``` - -### Step 2: Integrate with Your Plugin - -In your plugin class that implements `SchedulablePlugin`: - -```java -@PluginDescriptor(name = "Your Plugin") -public class YourPlugin extends Plugin implements SchedulablePlugin { - - private YourPluginPrePostScheduleTasks prePostTasks; - private LockCondition lockCondition; - - @Override - protected void startUp() { - // Initialize the task manager - prePostTasks = new YourPluginPrePostScheduleTasks(this); - - // Execute pre-schedule tasks with callback - prePostTasks.executePreScheduleTasks(() -> { - // This callback runs when preparation is complete - yourScript.run(config); - }); - } - - @Override - protected void shutDown() { - // Clean up resources - if (prePostTasks != null) { - prePostTasks.close(); - } - } - - @Subscribe - public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - if (event.getPlugin() == this && prePostTasks != null) { - if (lockCondition != null && lockCondition.isLocked()) { - return; // respect critical section - } - // Execute post-schedule cleanup - prePostTasks.executePostScheduleTasks(lockCondition); - } - } - - @Override - public LogicalCondition getStopCondition() { - if (lockCondition == null) { - lockCondition = new LockCondition("Plugin locked during critical operation", false,true); //ensure unlock on shutdown of the plugin ! - } - AndCondition condition = new AndCondition(); - condition.addCondition(lockCondition); - return condition; - } -} -``` - -### Step 3: Common Patterns - -#### Banking and Equipment Management - -```java -private boolean prepareOptimalSetup() { - if (!Rs2Bank.openBank()) { - return false; - } - - // Deposit current items - Rs2Bank.depositAll(); - Rs2Bank.depositEquipment(); - - // Withdraw required items - Rs2Bank.withdrawOne(ItemID.BRONZE_PICKAXE); - Rs2Bank.withdrawX(ItemID.SALMON, 10); - - Rs2Bank.closeBank(); - return true; -} - -private boolean bankAllItems() { - if (!Rs2Bank.openBank()) { - return false; - } - - Rs2Bank.depositAll(); - Rs2Bank.depositEquipment(); - Rs2Bank.closeBank(); - return true; -} -``` - -#### Walking to Locations - -```java -private boolean walkToLocation() { - if (Rs2Bank.isNearBank(BankLocation.GRAND_EXCHANGE, 6)) { - return true; - } - - boolean walkResult = Rs2Walker.walkWithBankedTransports( - BankLocation.GRAND_EXCHANGE.getWorldPoint(), - true, // Use bank items for transportation - false // Don't force banking route - ); - - return sleepUntil(() -> Rs2Bank.isNearBank(BankLocation.GRAND_EXCHANGE, 6), 30000); -} -``` - -#### Lock Management - -```java -@Override -protected boolean executeCustomPreScheduleTask(CompletableFuture preScheduledFuture, LockCondition lockCondition) { - if (lockCondition != null) { - lockCondition.lock(); // Prevent interruption during setup - } - - try { - // Perform critical setup operations - return performSetup(); - - } finally { - if (lockCondition != null) { - lockCondition.unlock(); - } - } -} -``` - -## Method Reference - -### AbstractPrePostScheduleTasks Methods - -#### Public Methods - -- `executePreScheduleTasks(Runnable callback)` - Execute pre-tasks with callback -- `executePreScheduleTasks(Runnable callback, int timeout, TimeUnit timeUnit)` - Execute pre-tasks with timeout -- `executePostScheduleTasks(LockCondition lockCondition)` - Execute post-tasks -- `executePostScheduleTasks(LockCondition lockCondition, int timeout, TimeUnit timeUnit)` - Execute post-tasks with timeout -- `isRunning()` - Check if any tasks are currently executing -- `close()` - Clean up all resources and cancel running tasks -- `shutdown()` - Alias for close() - -#### Abstract Methods (Must Implement) - -- `executeCustomPreScheduleTask(CompletableFuture preScheduledFuture, LockCondition lockCondition)` - Plugin-specific preparation logic -- `executeCustomPostScheduleTask(CompletableFuture postScheduledFuture, LockCondition lockCondition)` - Plugin-specific cleanup logic -- `getPrePostScheduleRequirements()` - Return the requirements instance for this plugin - -## Error Handling - -The infrastructure provides comprehensive error handling through centralized try-catch blocks in the base class: - -1. **Centralized Exception Handling**: All exceptions from custom task methods are caught and logged by the parent class -2. **Timeout Handling**: Tasks that exceed their timeout are automatically cancelled -3. **Proper Error Reporting**: Failures are reported to the scheduler with appropriate ExecutionResult values -4. **Resource Cleanup**: Resources are always cleaned up, even when errors occur -5. **No Redundant Error Handling**: Custom task methods should NOT include try-catch blocks - let errors bubble up to the centralized handlers - -## TODO Items for Future Enhancement - -The following TODO items are included in the abstract class for future improvements: - -- **Configuration for default timeout values**: Allow global configuration of timeout defaults -- **Metrics collection**: Track task execution times and success rates for monitoring -- **Retry mechanism**: Implement exponential backoff for failed tasks -- **Task priority levels**: Support for critical vs optional task classification - -## Integration with Requirements System - -The task infrastructure works seamlessly with the existing requirements system located in: -`runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/` - -You can use requirement classes like `ItemRequirement`, `RequirementCollection`, etc. within your task implementations for standardized equipment and item management. - -## Examples - -See the following implementations for reference: - -1. **GotrPrePostScheduleTasks**: Complete implementation for Guardians of the Rift plugin -2. **ExamplePrePostScheduleTasks**: Template implementation showing common patterns - -## Best Practices - -1. **Always check isScheduleMode()**: Only perform schedule-specific logic when actually running under scheduler -2. **Use lock conditions**: Prevent interruption during critical operations like minigame participation -3. **Handle failures gracefully**: Return false from task methods to indicate failure - exceptions will be handled by the parent class -4. **Log appropriately**: Use structured logging with appropriate log levels -5. **Avoid redundant error handling**: Do not add try-catch blocks in custom task methods - the parent class provides centralized error handling -6. **Clean up resources**: Always implement proper resource cleanup in your task methods -7. **Test both modes**: Ensure your plugin works both with and without scheduler control - - -<<<<<<< HEAD -Design note: -Further improvemnts: We should improve the requirement fulfillment flow and clarify ItemRequirement semantics. Equipment requirements target a specific EquipmentInventorySlot; inventory requirements should not. To avoid overloading a single type with sentinel values (e.g., null slot), consider: -======= -Design note: We should improve the requirement fulfillment flow and clarify ItemRequirement semantics. Equipment requirements target a specific EquipmentInventorySlot; inventory requirements should not. To avoid overloading a single type with sentinel values (e.g., null slot), consider: ->>>>>>> ff36783985 ((feat,bugfixes,core): cache architecture overhaul and comprehensive pre/post schedule tasks system) -- Making ItemRequirement an abstract base type. -- Introduce EquipmentRequirement (has a non-null EquipmentInventorySlot). -- Introduce InventoryRequirement (no slot; optional quantity/stack rules). -This separation will eliminate magic values, reduce null checks, and make the fulfillment process simpler and safer. \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleRequirements.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleRequirements.java deleted file mode 100644 index 4c735e698b0..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleRequirements.java +++ /dev/null @@ -1,151 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.examples; - -import java.util.Arrays; - -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.api.Skill; -import net.runelite.api.gameval.ItemID; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; - - -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.PrePostScheduleRequirements; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location.LocationRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.SpellbookRequirement; -import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; -import net.runelite.client.plugins.microbot.util.magic.Rs2Spellbook; - -/** - * Example implementation of PrePostScheduleRequirements showing basic usage patterns. - * This serves as a template for creating requirements collections for other plugins. - * - * This example demonstrates: - * - Basic equipment requirements with different priority levels - * - Location requirements for pre and post schedule positioning - * - Optional spellbook requirements - * - How to organize requirements by category and effectiveness - */ -public class ExamplePrePostScheduleRequirements extends PrePostScheduleRequirements { - - public ExamplePrePostScheduleRequirements() { - super("Example", "General", false); - } - - /** - * Initializes the item requirement collection with example items. - * This demonstrates typical patterns for different equipment slots and priorities. - */ - @Override - protected boolean initializeRequirements() { - this.getRegistry().clear(); // Clear previous requirements if any - - - - // Example: Optional teleport spellbook for faster travel - SpellbookRequirement normalSpellbookRequirement = new SpellbookRequirement( - Rs2Spellbook.MODERN, - TaskContext.PRE_SCHEDULE, // Only need it before script - RequirementPriority.RECOMMENDED, - 6, // Rating 6/10 - moderately useful - "Modern spellbook for teleport spells during travel" - ); - this.register(normalSpellbookRequirement); - - // Set location requirements - // Pre-schedule: Start at Grand Exchange for easy access to supplies - this.register(new LocationRequirement(BankLocation.GRAND_EXCHANGE, true,-1,TaskContext.PRE_SCHEDULE, RequirementPriority.RECOMMENDED)); - // Post-schedule: Return to Grand Exchange for selling/organizing items - this.register(new LocationRequirement(BankLocation.GRAND_EXCHANGE, true,-1,TaskContext.POST_SCHEDULE, RequirementPriority.RECOMMENDED)); - - TaskContext taskContext = TaskContext.PRE_SCHEDULE; // Default to pre-schedule context - // HEAD - Example progression: best to worst - this.register(new ItemRequirement( - ItemID.GRACEFUL_HOOD, - EquipmentInventorySlot.HEAD, RequirementPriority.RECOMMENDED, 8, "Graceful hood for weight reduction",taskContext - )); - this.register(new ItemRequirement( - ItemID.VIKING_HELMET, - EquipmentInventorySlot.HEAD, RequirementPriority.RECOMMENDED, 5, "Basic head protection",taskContext - )); - - // CAPE - Example with skill requirements - this.register(new ItemRequirement( - ItemID.GRACEFUL_CAPE, - EquipmentInventorySlot.CAPE, RequirementPriority.RECOMMENDED, 8, "Graceful cape for weight reduction", taskContext - )); - this.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.SKILLCAPE_AGILITY, ItemID.SKILLCAPE_AGILITY_TRIMMED), - 1, - EquipmentInventorySlot.CAPE, - -2, - RequirementPriority.RECOMMENDED, - 10, - "Agility cape (99 Agility required)", - taskContext, - Skill.AGILITY, - 99, - null, - null - )); - - - this.register(new ItemRequirement( - ItemID.AMULET_OF_POWER, - EquipmentInventorySlot.AMULET, RequirementPriority.RECOMMENDED, 4, "Basic amulet of power", taskContext - )); - - // BODY - Example weight reduction focus - this.register(new ItemRequirement( - ItemID.GRACEFUL_TOP, - EquipmentInventorySlot.BODY, RequirementPriority.RECOMMENDED, 8, "Graceful top for weight reduction",taskContext - )); - this.register(new ItemRequirement( - ItemID.HARDLEATHER_BODY, - EquipmentInventorySlot.BODY, RequirementPriority.RECOMMENDED, 3, "Basic leather body",taskContext - )); - - // LEGS - Continue weight reduction theme - this.register(new ItemRequirement( - ItemID.GRACEFUL_LEGS, - EquipmentInventorySlot.LEGS, RequirementPriority.RECOMMENDED, 8, "Graceful legs for weight reduction",taskContext - )); - this.register(new ItemRequirement( - ItemID.LEATHER_CHAPS, - EquipmentInventorySlot.LEGS, RequirementPriority.RECOMMENDED, 3, "Basic leather chaps",taskContext - )); - - // BOOTS - Complete the graceful set - this.register(new ItemRequirement( - ItemID.GRACEFUL_BOOTS, - EquipmentInventorySlot.BOOTS, RequirementPriority.RECOMMENDED, 8, "Graceful boots for weight reduction",taskContext - )); - this.register(new ItemRequirement( - ItemID.LEATHER_BOOTS, - EquipmentInventorySlot.BOOTS, RequirementPriority.RECOMMENDED, 3, "Basic leather boots",taskContext - )); - - // GLOVES - Graceful gloves to complete the set - this.register(new ItemRequirement( - ItemID.GRACEFUL_GLOVES, - EquipmentInventorySlot.GLOVES, RequirementPriority.RECOMMENDED, 8, "Graceful gloves for weight reduction",taskContext - )); - this.register(new ItemRequirement( - ItemID.LEATHER_GLOVES, - EquipmentInventorySlot.GLOVES, RequirementPriority.RECOMMENDED, 3, "Basic leather gloves",taskContext - )); - - // INVENTORY ITEMS - Example tools and supplies - this.register(new ItemRequirement( - ItemID.COINS, 1, -1, - RequirementPriority.RECOMMENDED, 9, "Coins for purchases and teleports",taskContext - )); - - this.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.LOBSTER, ItemID.SWORDFISH, ItemID.TUNA), 1, null,-1, - RequirementPriority.RECOMMENDED, 5, "Food for healing if needed",taskContext - )); - return true; // Initialization successful - // EITHER ITEMS - Items that can be equipped or kept in inventory - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleTasks.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleTasks.java deleted file mode 100644 index e56b64c7829..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleTasks.java +++ /dev/null @@ -1,206 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.examples; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.api.SchedulablePlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LockCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.AbstractPrePostScheduleTasks; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.PrePostScheduleRequirements; -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.util.equipment.Rs2Equipment; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; - -import java.util.concurrent.CompletableFuture; - -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -/** - * Example implementation showing how to extend {@link AbstractPrePostScheduleTasks} - * for a generic plugin with basic banking and equipment management. - *

- * This serves as a template for creating pre/post schedule tasks for other plugins. - * Simply copy this class and customize the preparation and cleanup logic for your specific plugin needs. - * - * TODO: Customize the following methods for your plugin: - * - {@link #executePreScheduleTask(LockCondition)} - Add your plugin's preparation logic - * - {@link #executePostScheduleTask(LockCondition)} - Add your plugin's cleanup logic - * - {@link #isScheduleMode()} - Check your plugin's configuration for schedule mode - * - Add any plugin-specific helper methods as needed - */ -@Slf4j -public class ExamplePrePostScheduleTasks extends AbstractPrePostScheduleTasks { - - private ExamplePrePostScheduleRequirements exampleRequirements; - - /** - * Constructor for ExamplePrePostScheduleTasks. - * - * @param plugin The SchedulablePlugin instance to manage - */ - public ExamplePrePostScheduleTasks(SchedulablePlugin plugin) { - super(plugin,null); - this.exampleRequirements = new ExamplePrePostScheduleRequirements(); - } - - /** - * Provides the example requirements for the default implementation to use. - * - * @return The ExamplePrePostScheduleRequirements instance - */ - @Override - protected PrePostScheduleRequirements getPrePostScheduleRequirements() { - return exampleRequirements; - } - - /** - * Executes pre-schedule preparation for the example plugin. - * Customize this method with your plugin's specific preparation logic. - * - * @param lockCondition The lock condition to prevent interruption during critical operations - * @return true if preparation was successful, false otherwise - */ - @Override - protected boolean executeCustomPreScheduleTask(CompletableFuture preScheduledFuture, LockCondition lockCondition) { - if (lockCondition != null) { - lockCondition.lock(); - } - - try { - log.info("Starting example plugin pre-schedule preparation..."); - - // Example: Walk to Grand Exchange bank - if (!walkToBank()) { - log.error("Failed to reach bank location"); - return false; - } - - // Example: Prepare basic equipment and inventory - if (!prepareBasicSetup()) { - log.error("Failed to prepare basic setup"); - return false; - } - - log.info("Example plugin pre-schedule preparation completed successfully"); - return true; - - } finally { - if (lockCondition != null) { - lockCondition.unlock(); - } - } - } - - /** - * Executes post-schedule cleanup for the example plugin. - * Customize this method with your plugin's specific cleanup logic. - * - * @param lockCondition The lock condition to prevent interruption during critical operations - * @return true if cleanup was successful, false otherwise - */ - @Override - protected boolean executeCustomPostScheduleTask(CompletableFuture postScheduledFuturem, LockCondition lockCondition) { - log.info("Starting example plugin post-schedule cleanup..."); - - // Example: Bank all items for safe shutdown - if (!bankAllItems()) { - log.warn("Warning: Failed to bank all items during post-schedule cleanup"); - } - - log.info("Example plugin post-schedule cleanup completed - stopping plugin"); - Microbot.getClientThread().invokeLater(() -> { - Microbot.stopPlugin((net.runelite.client.plugins.Plugin) plugin); - return true; - }); - - return true; - } - - - - /** - * Example helper method: Walks to a bank location. - * Customize this for your plugin's specific bank location needs. - * - * @return true if successfully reached the bank, false otherwise - */ - private boolean walkToBank() { - // Example: Walk to Grand Exchange bank - if (Rs2Bank.isNearBank(BankLocation.GRAND_EXCHANGE, 6)) { - return true; - } - - log.info("Walking to bank..."); - - boolean walkResult = Rs2Walker.walkWithBankedTransports( - BankLocation.GRAND_EXCHANGE.getWorldPoint(), - false // Don't force banking route if direct is faster - ); - - if (!walkResult) { - log.warn("Failed to initiate walking to bank, trying fallback method"); - Rs2Walker.walkTo(BankLocation.GRAND_EXCHANGE.getWorldPoint(), 4); - } - - return sleepUntil(() -> Rs2Bank.isNearBank(BankLocation.GRAND_EXCHANGE, 6), 30000); - } - - /** - * Example helper method: Prepares basic equipment and inventory setup. - * Customize this for your plugin's specific equipment and item needs. - * - * @return true if setup was successful, false otherwise - */ - private boolean prepareBasicSetup() { - if (!Rs2Bank.openBank()) { - log.error("Failed to open bank"); - return false; - } - - // Deposit all current items - Rs2Bank.depositAll(); - sleepUntil(() -> Rs2Inventory.isEmpty(), 5000); - Rs2Bank.depositEquipment(); - sleepUntil(() -> !Rs2Equipment.isWearing(), 5000); - - // TODO: Add your plugin's specific equipment and item withdrawal logic here - // Example: - // Rs2Bank.withdrawOne(ItemID.BRONZE_PICKAXE); - // Rs2Bank.withdrawX(ItemID.SALMON, 10); - - Rs2Bank.closeBank(); - log.info("Successfully prepared basic setup"); - return true; - } - - /** - * Example helper method: Banks all equipment and inventory items for safe shutdown. - * This is a generic implementation that most plugins can use as-is. - * - * @return true if banking was successful, false otherwise - */ - private boolean bankAllItems() { - // Walk to bank if not already there - if (!Rs2Bank.isNearBank(6)) { - if (!walkToBank()) { - return false; - } - } - - if (!Rs2Bank.openBank()) { - return false; - } - - // Deposit all inventory items - Rs2Bank.depositAll(); - sleepUntil(() -> Rs2Inventory.isEmpty(), 5000); - - // Deposit all equipment - Rs2Bank.depositEquipment(); - - Rs2Bank.closeBank(); - log.info("Successfully banked all items"); - return true; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/overlay/PrePostScheduleTasksOverlayComponents.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/overlay/PrePostScheduleTasksOverlayComponents.java deleted file mode 100644 index 29ee42f37fa..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/overlay/PrePostScheduleTasksOverlayComponents.java +++ /dev/null @@ -1,480 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.overlay; - -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.AbstractPrePostScheduleTasks; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.PrePostScheduleRequirements; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location.LocationRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.collection.LootRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.SpellbookRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.state.TaskExecutionState; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import net.runelite.client.ui.overlay.components.LineComponent; -import net.runelite.client.ui.overlay.components.TitleComponent; - -import lombok.extern.slf4j.Slf4j; - -import java.awt.Color; -import java.util.ArrayList; -import java.util.List; - -/** - * Factory class for creating overlay components that display the current state of - * PrePostScheduleRequirements and AbstractPrePostScheduleTasks. - * - * This provides convenience methods to generate overlay components showing: - * - Current task execution phase and status - * - Active requirements being processed - * - Progress indicators for different requirement types - * - * The components are designed to be concise and non-cluttering for in-game display. - */ -@Slf4j -public class PrePostScheduleTasksOverlayComponents { - - // Color scheme for different states - private static final Color TITLE_COLOR = Color.CYAN; - private static final Color ACTIVE_COLOR = Color.YELLOW; - private static final Color SUCCESS_COLOR = Color.GREEN; - private static final Color ERROR_COLOR = Color.RED; - private static final Color INFO_COLOR = Color.WHITE; - private static final Color DISABLED_COLOR = Color.GRAY; - - /** - * Creates a title component for the requirement overlay. - * - * @param pluginName The name of the plugin - * @param tasks The task manager instance - * @return A TitleComponent for the overlay - */ - public static TitleComponent createTitleComponent(String pluginName, AbstractPrePostScheduleTasks tasks) { - String title = pluginName + " Tasks"; - Color titleColor = TITLE_COLOR; - - if (tasks != null && tasks.isExecuting()) { - title += " (ACTIVE)"; - titleColor = ACTIVE_COLOR; - } - - return TitleComponent.builder() - .text(title) - .color(titleColor) - .build(); - } - - /** - * Creates line components showing the current execution status. - * - * @param tasks The task manager instance - * @param requirements The requirements instance - * @return List of LineComponents showing current status - */ - public static List createExecutionStatusComponents(AbstractPrePostScheduleTasks tasks, PrePostScheduleRequirements requirements) { - List components = new ArrayList<>(); - - if (tasks == null) { - return components; - } - - // Get state from either tasks or requirements (they should be synchronized) - TaskExecutionState state = tasks.getExecutionState(); - if (requirements != null && state.isExecuting()) { - // If requirements are actively being fulfilled, use that state instead - state = tasks.getExecutionState(); - } - - String displayStatus = state.getDisplayStatus(); - if (displayStatus != null) { - Color statusColor = getStatusColor(state); - components.add(LineComponent.builder() - .left("Status:") - .right(displayStatus) - .leftColor(INFO_COLOR) - .rightColor(statusColor) - .build()); - - // Show current requirement name if available - String currentRequirementName = state.getCurrentRequirementName(); - if (currentRequirementName != null && !currentRequirementName.isEmpty() && state.isFulfillingRequirements()) { - // Truncate long requirement names for overlay display - String displayName = currentRequirementName.length() > 25 ? - currentRequirementName.substring(0, 22) + "..." : currentRequirementName; - - components.add(LineComponent.builder() - .left(" Processing:") - .right(displayName) - .leftColor(INFO_COLOR) - .rightColor(ACTIVE_COLOR) - .build()); - } - - // Show step progress if available - if (state.isFulfillingRequirements() && state.getTotalRequirementsInStep() > 0) { - String progress = String.format("Item %d/%d", - state.getCurrentRequirementIndex(), state.getTotalRequirementsInStep()); - components.add(LineComponent.builder() - .left(" Progress:") - .right(progress) - .leftColor(INFO_COLOR) - .rightColor(state.getCurrentRequirementIndex() == state.getTotalRequirementsInStep() ? SUCCESS_COLOR : ACTIVE_COLOR) - .build()); - } - - // Show overall progress for requirement fulfillment - if (state.isFulfillingRequirements() && state.getTotalSteps() > 0) { - int overallProgress = state.getProgressPercentage(); - components.add(LineComponent.builder() - .left(" Overall:") - .right(overallProgress + "% (" + state.getCurrentStepNumber() + "/" + state.getTotalSteps() + ")") - .leftColor(INFO_COLOR) - .rightColor(overallProgress == 100 ? SUCCESS_COLOR : ACTIVE_COLOR) - .build()); - } - } - - return components; - } - - /** - * Gets the appropriate color for the current execution state - */ - private static Color getStatusColor(TaskExecutionState state) { - if (state.isInErrorState()) { - return ERROR_COLOR; - } - - switch (state.getCurrentState()) { - case COMPLETED: - return SUCCESS_COLOR; - case FAILED: - case ERROR: - return ERROR_COLOR; - case FULFILLING_REQUIREMENTS: - case CUSTOM_TASKS: - return ACTIVE_COLOR; - case STARTING: - default: - return INFO_COLOR; - } - } - - /** - * Creates line components showing current location requirement status. - * - * @param requirements The requirements instance - * @param context The schedule context (PRE_SCHEDULE or POST_SCHEDULE) - * @return List of LineComponents showing location status - */ - public static List createLocationStatusComponents(PrePostScheduleRequirements requirements, TaskContext context) { - List components = new ArrayList<>(); - - if (requirements == null) { - return components; - } - - List locationReqs = requirements.getRegistry().getRequirements(LocationRequirement.class, context); - if (locationReqs.isEmpty()) { - return components; - } - - LocationRequirement locationReq = locationReqs.get(0); // Take first one - WorldPoint targetLocation = locationReq.getBestAvailableLocation().getWorldPoint(); - WorldPoint currentLocation = Rs2Player.getWorldLocation(); - - // Calculate distance - int distance = currentLocation != null && targetLocation != null ? - currentLocation.distanceTo(targetLocation) : -1; - - String contextLabel = context == TaskContext.PRE_SCHEDULE ? "Pre-Loc" : "Post-Loc"; - - components.add(LineComponent.builder() - .left(contextLabel + ":") - .right(locationReq.getName()) - .leftColor(INFO_COLOR) - .rightColor(distance <= 10 ? SUCCESS_COLOR : ACTIVE_COLOR) - .build()); - - if (distance >= 0) { - components.add(LineComponent.builder() - .left("Distance:") - .right(distance + " tiles") - .leftColor(INFO_COLOR) - .rightColor(distance <= 10 ? SUCCESS_COLOR : (distance <= 50 ? ACTIVE_COLOR : ERROR_COLOR)) - .build()); - } - - return components; - } - - /** - * Creates line components showing current spellbook requirement status. - * - * @param requirements The requirements instance - * @param context The schedule context - * @return List of LineComponents showing spellbook status - */ - public static List createSpellbookStatusComponents(PrePostScheduleRequirements requirements, TaskContext context) { - List components = new ArrayList<>(); - - if (requirements == null) { - return components; - } - - List spellbookReqs = requirements.getRegistry().getRequirements(SpellbookRequirement.class, context); - if (spellbookReqs.isEmpty()) { - return components; - } - - SpellbookRequirement spellbookReq = spellbookReqs.get(0); // Take first one - String contextLabel = context == TaskContext.PRE_SCHEDULE ? "Pre-Spell" : "Post-Spell"; - - components.add(LineComponent.builder() - .left(contextLabel + ":") - .right(spellbookReq.getRequiredSpellbook().name()) - .leftColor(INFO_COLOR) - .rightColor(ACTIVE_COLOR) - .build()); - - return components; - } - - /** - * Creates line components showing current loot requirement status. - * - * @param requirements The requirements instance - * @param context The schedule context - * @return List of LineComponents showing loot status - */ - public static List createLootStatusComponents(PrePostScheduleRequirements requirements, TaskContext context) { - List components = new ArrayList<>(); - - if (requirements == null) { - return components; - } - - List lootReqs = requirements.getRegistry().getRequirements(LootRequirement.class, context); - if (lootReqs.isEmpty()) { - return components; - } - - String contextLabel = context == TaskContext.PRE_SCHEDULE ? "Pre-Loot" : "Post-Loot"; - - for (LootRequirement lootReq : lootReqs) { - // Calculate total amount from the loot requirements map - int totalAmount = lootReq.getAmounts().values().stream() - .mapToInt(Integer::intValue) - .sum(); - - components.add(LineComponent.builder() - .left(contextLabel + ":") - .right(lootReq.getName() + " (" + totalAmount + ")") - .leftColor(INFO_COLOR) - .rightColor(ACTIVE_COLOR) - .build()); - } - - return components; - } - - /** - * Creates line components showing current item requirement status. - * Only shows the most critical items to avoid clutter. - * - * @param requirements The requirements instance - * @param context The schedule context - * @return List of LineComponents showing item status - */ - public static List createItemStatusComponents(PrePostScheduleRequirements requirements, TaskContext context) { - List components = new ArrayList<>(); - - if (requirements == null) { - return components; - } - - List itemReqs = requirements.getRegistry().getRequirements(ItemRequirement.class, context); - if (itemReqs.isEmpty()) { - return components; - } - - String contextLabel = context == TaskContext.PRE_SCHEDULE ? "Pre-Items" : "Post-Items"; - - // Only show first few items to avoid clutter - int maxItems = 3; - int count = 0; - - for (ItemRequirement itemReq : itemReqs) { - if (count >= maxItems) { - break; - } - - // For now, use a simplified approach to get item name - String itemName = "Item"; - try { - itemName = itemReq.toString(); // Fallback to toString if getName() is not accessible - if (itemName.length() > 30) { - itemName = itemName.substring(0, 27) + "..."; - } - } catch (Exception e) { - itemName = "Unknown Item"; - } - - components.add(LineComponent.builder() - .left(contextLabel + ":") - .right(itemName + " (" + itemReq.getAmount() + ")") - .leftColor(INFO_COLOR) - .rightColor(ACTIVE_COLOR) - .build()); - - count++; - } - - if (itemReqs.size() > maxItems) { - components.add(LineComponent.builder() - .left("") - .right("+" + (itemReqs.size() - maxItems) + " more items") - .leftColor(INFO_COLOR) - .rightColor(DISABLED_COLOR) - .build()); - } - - return components; - } - - /** - * Creates line components showing the current requirement being processed. - * Only shows information about the specific requirement currently being fulfilled. - * - * @param requirements The requirements instance - * @return List of LineComponents showing current requirement details - */ - public static List createCurrentRequirementComponents(AbstractPrePostScheduleTasks tasks, PrePostScheduleRequirements requirements) { - List components = new ArrayList<>(); - - if (requirements == null || tasks == null) { - return components; - } - TaskExecutionState state = tasks.getExecutionState(); - if (state == null || !state.isFulfillingRequirements()) { - return components; - } - - - - // Show the current step being processed - if (state.getCurrentStep() != null) { - components.add(LineComponent.builder() - .left("Current Step:") - .right(state.getCurrentStep().getDisplayName()) - .leftColor(INFO_COLOR) - .rightColor(ACTIVE_COLOR) - .build()); - } - - // Show current details if available - String details = state.getCurrentDetails(); - if (details != null && !details.isEmpty()) { - // Shorten details if too long - if (details.length() > 30) { - details = details.substring(0, 27) + "..."; - } - - components.add(LineComponent.builder() - .left("Details:") - .right(details) - .leftColor(INFO_COLOR) - .rightColor(INFO_COLOR) - .build()); - } - - return components; - } - - /** - * Creates a complete set of overlay components for the current requirement status. - * This is the main method that should be called from plugin overlays. - * - * @param pluginName The name of the plugin - * @param tasks The task manager instance - * @param requirements The requirements instance - * @return List of all overlay components - */ - public static List createAllComponents(String pluginName, AbstractPrePostScheduleTasks tasks, PrePostScheduleRequirements requirements) { - List components = new ArrayList<>(); - - // Add title - components.add(createTitleComponent(pluginName, tasks)); - - // Add execution status - components.addAll(createExecutionStatusComponents(tasks, requirements)); - TaskExecutionState state = tasks.getExecutionState(); - if (requirements != null && tasks != null && (tasks.isExecuting() || state.isFulfillingRequirements())) { - // Show only the current requirement being processed - components.addAll(createCurrentRequirementComponents(tasks,requirements)); - } - - return components; - } - - /** - * Creates concise summary components for main overlay display. - * Shows only essential information to avoid clutter. - * - * @param pluginName The name of the plugin - * @param tasks The task manager instance - * @param requirements The requirements instance - * @return List of overlay components for concise display - */ - public static List createConciseComponents(String pluginName, AbstractPrePostScheduleTasks tasks, PrePostScheduleRequirements requirements) { - List components = new ArrayList<>(); - - // Only show title and execution status for concise view - if (tasks != null && tasks.isExecuting()) { - TaskExecutionState state = tasks.getExecutionState(); - - // Concise title with status - String titleText = pluginName + " Tasks"; - Color titleColor = ACTIVE_COLOR; - - if (state.isInErrorState()) { - titleColor = ERROR_COLOR; - titleText += " (ERROR)"; - } else if (state.isExecuting()) { - titleColor = ACTIVE_COLOR; - titleText += " (ACTIVE)"; - } - - components.add(TitleComponent.builder() - .text(titleText) - .color(titleColor) - .build()); - - // Show only current phase and progress - String phase = state.getCurrentPhase() != null ? state.getCurrentPhase().toString() : "UNKNOWN"; - int progress = state.getProgressPercentage(); - String progressText = progress > 0 ? progress + "%" : "Working..."; - - components.add(LineComponent.builder() - .left(phase + ":") - .right(progressText) - .leftColor(INFO_COLOR) - .rightColor(ACTIVE_COLOR) - .build()); - } else { - // Show status when not executing - components.add(TitleComponent.builder() - .text(pluginName + " Tasks") - .color(INFO_COLOR) - .build()); - - components.add(LineComponent.builder() - .left("Status:") - .right("Ready") - .leftColor(INFO_COLOR) - .rightColor(SUCCESS_COLOR) - .build()); - } - - return components; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/PrePostScheduleRequirements.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/PrePostScheduleRequirements.java deleted file mode 100644 index 31a69ed5889..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/PrePostScheduleRequirements.java +++ /dev/null @@ -1,1513 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements; -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.api.GameState; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.inventorysetups.InventorySetup; -import net.runelite.client.plugins.microbot.inventorysetups.MInventorySetupsPlugin; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.cache.Rs2CacheManager; -import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; -import net.runelite.client.plugins.microbot.util.inventory.Rs2FuzzyItem; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; -import net.runelite.client.plugins.microbot.util.magic.Rs2Spellbook; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; -import net.runelite.api.Constants; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import org.slf4j.event.Level; - -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.OrRequirementMode; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.registry.RequirementRegistry; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.InventorySetupPlanner; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location.LocationRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.collection.LootRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.ShopRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.SpellbookRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.InventorySetupRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.ConditionalRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.OrderedRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.LogicalRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.OrRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.util.RequirementSolver; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.state.FulfillmentStep; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.state.TaskExecutionState; - -import java.util.stream.Collectors; - -/** - * Enhanced collection that manages ItemRequirement objects with support for inventory and equipment requirements. - * - * This class now uses a centralized RequirementRegistry for improved consistency, uniqueness enforcement, - * and simplified requirement management while maintaining backward compatibility. - */ -@Slf4j -public abstract class PrePostScheduleRequirements { - - - @Getter - private final String collectionName; - @Getter - private final String activityType; - private boolean initialized = false; - private InventorySetupPlanner currentPreScheduleLayoutPlan = null; - private InventorySetupPlanner currentPostScheduleLayoutPlan = null; - // Centralized requirement management - - private final RequirementRegistry registry = new RequirementRegistry(); - - /** - * Mode for handling OR requirements during planning. - * Default is ANY_COMBINATION for backward compatibility. - */ - @Getter - @Setter - private OrRequirementMode orRequirementMode = OrRequirementMode.ANY_COMBINATION; - - /** - * Tracks the original spellbook before switching for pre-schedule requirements. - * This is used to restore the original spellbook during post-schedule fulfillment. - */ - private volatile Rs2Spellbook originalSpellbook; - - - - @Getter - private final boolean isWildernessCollection; - - public PrePostScheduleRequirements() { - this("", "", false); - } - - /** - * Creates a new collection with name and activity type for better organization. - * - * @param collectionName A descriptive name for this collection - * @param activityType The type of activity these requirements are for (e.g., "GOTR", "Mining", "Combat") - */ - public PrePostScheduleRequirements(String collectionName, String activityType, boolean isWildernessCollection) { - this.collectionName = collectionName; - this.activityType = activityType; - this.isWildernessCollection = isWildernessCollection; // Set wilderness flag - this.currentPreScheduleLayoutPlan = null; - this.currentPostScheduleLayoutPlan = null; - initialize(); // try to Initialize requirements collection - - } - public boolean isInitialized(){ - if (!initialized) { - initialized = initialize(); // Initialize if not already done - if (initialized) log.info("\nPrePostScheduleRequirements <{}> initialized:\n{}",collectionName, this.getDetailedDisplay()); - return initialized; - } - return initialized; // Return current initialization state - } - public boolean initialize() { - if (initialized) { - log.warn("Requirements collection already initialized: " + collectionName); - return false; // Already initialized - } - if (!Microbot.isLoggedIn() || !Rs2CacheManager.isCacheDataValid()){ - log.error("Cannot initialize requirements collection: " + collectionName + " - not logged in or cache data invalid"); - return false; // Cannot initialize if not logged in or cache is invalid - } - try { - boolean success = initializeRequirements(); - if (success) { - initialized = true; - this.currentPreScheduleLayoutPlan = null; - this.currentPostScheduleLayoutPlan = null; - log.info("Successfully initialized requirements collection: " + collectionName); - } else { - log.error("Failed to initialize requirements collection: " + collectionName); - } - return success; - } catch (Exception e) { - log.error("Error initializing requirements collection: " + collectionName, e); - return false; - } - } - /** - * Initializes the requirements for this collection. - * This method should be called after the plugin is started to ensure all requirements are set up. - * - * @return true if initialization was successful, false otherwise - */ - protected abstract boolean initializeRequirements(); - public void reset() { - initialized = false; - clearOriginalSpellbook(); - this.getRegistry().clear(); // Clear the registry to remove all requirements - } - - /** - * Gets access to the internal requirement registry. - * This is useful for overlay components that need to access requirements by type and context. - * - * @return The requirement registry - */ - public RequirementRegistry getRegistry() { - return registry; - } - - /** - * Adds a custom requirement to this requirements collection. - * Custom requirements are marked with CUSTOM type and are fulfilled after all standard requirements. - * - * @param requirement The requirement to add - * @param TaskContext The context in which this requirement should be fulfilled - * @return true if the requirement was successfully added, false otherwise - */ - public boolean addCustomRequirement(Requirement requirement, TaskContext taskContext) { - if (requirement == null) { - log.warn("Cannot add null custom requirement"); - return false; - } - - if (taskContext == null) { - log.warn("Cannot add custom requirement without schedule context"); - return false; - } - - try { - // Update the requirement's schedule context if needed - if (requirement.getTaskContext() != taskContext && - requirement.getTaskContext() != TaskContext.BOTH) { - requirement.setTaskContext(taskContext); - } - - // Register in the registry as an external requirement - boolean registered = registry.registerExternal(requirement); - - if (registered) { - log.info("Successfully registered custom requirement: {} for context: {}", - requirement.getDescription(), taskContext); - return true; - } else { - log.warn("Failed to register custom requirement in registry: {}", - requirement.getDescription()); - return false; - } - - } catch (Exception e) { - log.error("Error adding custom requirement: {}", e.getMessage(), e); - return false; - } - } - - /** - * Merges another collection into this one. - */ - public void merge(PrePostScheduleRequirements other) { - // Merge all requirements through the registry - for (Requirement requirement : other.registry.getAllRequirements()) { - registry.register(requirement); - } - } - - - - - /** - * Switches back to the original spellbook that was active before pre-schedule requirements. - * This method should be called after completing activities that required a specific spellbook. - * - * @return true if switch was successful or no switch was needed, false if switch failed - */ - public boolean switchBackToOriginalSpellbook() { - if (originalSpellbook == null) { - log.info("No original spellbook saved - no switch needed"); - return true; // No original spellbook saved, so no switch needed - } - - Rs2Spellbook currentSpellbook = Rs2Spellbook.getCurrentSpellbook(); - if (currentSpellbook == originalSpellbook) { - log.info("Already on original spellbook: " + originalSpellbook); - return true; // Already on the original spellbook - } - - log.info("Switching back to original spellbook: " + originalSpellbook + " from current: " + currentSpellbook); - boolean success = SpellbookRequirement.switchBackToSpellbook(originalSpellbook); - - if (success) { - // Clear the saved spellbook after successful restoration - originalSpellbook = null; - log.info("Successfully restored original spellbook"); - } else { - log.error("Failed to restore original spellbook: " + originalSpellbook); - } - - return success; - } - - /** - * Gets the original spellbook that was saved before pre-schedule requirements. - * - * @return The original spellbook, or null if none was saved - */ - public Rs2Spellbook getOriginalSpellbook() { - return originalSpellbook; - } - - /** - * Checks if an original spellbook is currently saved. - * - * @return true if an original spellbook is saved, false otherwise - */ - public boolean hasOriginalSpellbook() { - return originalSpellbook != null; - } - - /** - * Clears the saved original spellbook. This can be useful for cleanup - * or when you want to start fresh without restoring the previous spellbook. - */ - public void clearOriginalSpellbook() { - if (originalSpellbook != null) { - log.debug("Clearing saved original spellbook: " + originalSpellbook); - originalSpellbook = null; - } - } - - - /** - * Registers a requirement in the central registry. - * The registry automatically handles categorization, uniqueness, and consistency. - * - * @param requirement The requirement to register - */ - public void register(Requirement requirement) { - if (requirement == null) { - return; - } - if(registry.contains(requirement)) { - log.debug("Requirement already registered: " + requirement.getName(), Level.WARN); - return; // Avoid duplicate registration - } - registry.register(requirement); - } - - /** - * Fulfills all requirements for the specified schedule context. - * This is a convenience method that calls all the specific fulfillment methods. - * - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param scheduledFuture The CompletableFuture to monitor for cancellation - * @param saveCurrentSpellbook Whether to save the current spellbook for restoration - * @return true if all requirements were fulfilled successfully, false otherwise - */ - - private boolean fulfillAllRequirements(CompletableFuture scheduledFuture, TaskContext context, - boolean saveCurrentSpellbook, - TaskExecutionState executionState) - { - boolean success = true; - ScheduledExecutorService cancellationWatchdogService = null; - ScheduledFuture cancellationWatchdog = null; - - try { - // Fulfill requirements in logical order -> we should always fulfill loot requirements first, then shop, then item, then spellbook, and finally location requirements - // when adding new requirements, make sure to follow this order or think about the order in which they should be fulfilled - // we can also think about changing the order for pre and post schedule requirements, but for now we will keep it the same - // Initialize state tracking - TaskExecutionState.ExecutionPhase phase = context == TaskContext.PRE_SCHEDULE ? - TaskExecutionState.ExecutionPhase.PRE_SCHEDULE : TaskExecutionState.ExecutionPhase.POST_SCHEDULE; - if (context == null) { - log.error("Schedule Context is null!"); - executionState.markError("Context cannot be null"); - return false; - } - if (Microbot.getClient().isClientThread()) { - log.error("\n\tPlease run fulfillAllRequirements() on a non-client thread."); - executionState.markError("Cannot run on client thread"); - return false; - } - - - executionState.update(phase,TaskExecutionState.ExecutionState.FULFILLING_REQUIREMENTS ); - - // Start cancellation watchdog if we have a scheduledFuture to monitor - if (scheduledFuture != null) { - cancellationWatchdogService = Executors.newSingleThreadScheduledExecutor(r -> { - Thread thread = new Thread(r, "RequirementFulfillment - CancellationWatchdog - context:" + context); - thread.setDaemon(true); - return thread; - }); - - cancellationWatchdog = startCancellationWatchdog(cancellationWatchdogService, scheduledFuture, context); - log.debug("Started cancellation watchdog for requirement fulfillment: {}", context); - } - StringBuilder logMessage = new StringBuilder(); - logMessage.append("\n" + "=".repeat(80)); - logMessage.append(String.format("\nFULFILLING REQUIREMENTS FOR CONTEXT: {}", context)); - logMessage.append(String.format("\nCollection: %s| Activity: %s | Wilderness: %s\n", collectionName, activityType, isWildernessCollection)); - logMessage.append("=".repeat(80)); - - // Display complete registry information - logMessage.append("\n=== COMPLETE REQUIREMENT REGISTRY ==="); - logMessage.append(registry.getDetailedCacheStringForContext(context)); - //logMessage.append(this.registry.getDetailedCacheStringForContext(context)); - log.info(logMessage.toString()); - // Step 0: Conditional and Ordered Requirements (execute first as they may contain prerequisites) - List conditionalReqs = this.registry.getRequirements(ConditionalRequirement.class, context); - List orderedReqs = this.registry.getRequirements(OrderedRequirement.class, context); - - StringBuilder conditionalReqInfo = new StringBuilder(); - conditionalReqInfo.append("\n=== STEP 0: CONDITIONAL REQUIREMENTS ===\n"); - if (conditionalReqs.isEmpty()) { - conditionalReqInfo.append(String.format("No conditional requirements for context: %s\n", context)); - } else { - conditionalReqInfo.append(String.format("Found %d conditional requirement(s):\n", conditionalReqs.size())); - for (int i = 0; i < conditionalReqs.size(); i++) { - conditionalReqInfo.append(String.format("\n--- Conditional Requirement %d ---\n", i + 1)); - conditionalReqInfo.append(conditionalReqs.get(i).displayString()).append("\n"); - } - } - - conditionalReqInfo.append("\n=== STEP 1: ORDERED REQUIREMENTS ===\n"); - if (orderedReqs.isEmpty()) { - conditionalReqInfo.append(String.format("No ordered requirements for context: %s\n", context)); - } else { - conditionalReqInfo.append(String.format("Found %d ordered requirement(s):\n", orderedReqs.size())); - for (int i = 0; i < orderedReqs.size(); i++) { - conditionalReqInfo.append(String.format("\n--- Ordered Requirement %d ---\n", i + 1)); - conditionalReqInfo.append(orderedReqs.get(i).displayString()).append("\n"); - } - } - - - - // Only fulfill 'mixed' conditional requirements (not just item requirements alone) - List mixedConditionalReqs = this.registry.getMixedConditionalRequirements(context); - if (!mixedConditionalReqs.isEmpty() || !orderedReqs.isEmpty()) { - success &= RequirementSolver.fulfillConditionalRequirements(scheduledFuture,executionState, mixedConditionalReqs, orderedReqs, context); - } - - // Check for cancellation after conditional requirements - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Requirements fulfillment cancelled after conditional requirements"); - return false; - } - - if (!success) { - executionState.markFailed("Failed to fulfill conditional requirements"); - return false; - } - - // Step 2: Loot Requirements - List lootReqs = this.registry.getRequirements(LootRequirement.class, context); - StringBuilder lootReqInfo = new StringBuilder(); - lootReqInfo.append("\n=== STEP 2: LOOT REQUIREMENTS ===\n"); - if (lootReqs.isEmpty()) { - lootReqInfo.append(String.format("\tNo loot requirements for context: %s\n", context)); - } else { - lootReqInfo.append(String.format("\tFound %d loot requirement(s):\n", lootReqs.size())); - for (int i = 0; i < lootReqs.size(); i++) { - lootReqInfo.append(String.format("\n\t\t--- Loot Requirement %d ---\n\t\t\t", i + 1)); - lootReqInfo.append(lootReqs.get(i).displayString()).append("\n"); - } - } - log.info(lootReqInfo.toString()); - success &= fulfillPrePostLootRequirements(scheduledFuture, executionState,context); - - // Check for cancellation after loot requirements - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Requirements fulfillment cancelled after loot requirements"); - return false; - } - - if (!success) { - executionState.markFailed("Failed to fulfill loot requirements"); - return false; - } - - // Step 3: Shop Requirements - List shopReqs = this.registry.getRequirements(ShopRequirement.class, context); - StringBuilder shopReqInfo = new StringBuilder(); - shopReqInfo.append("\n=== STEP 3: SHOP REQUIREMENTS ===\n"); - if (shopReqs.isEmpty()) { - shopReqInfo.append(String.format("\tNo shop requirements for context: %s\n", context)); - } else { - shopReqInfo.append(String.format("\tFound %d shop requirement(s):\n", shopReqs.size())); - for (int i = 0; i < shopReqs.size(); i++) { - shopReqInfo.append(String.format("\n--- Shop Requirement %d ---\n", i + 1)); - shopReqInfo.append(shopReqs.get(i).displayString()).append("\n"); - } - } - - log.info(shopReqInfo.toString()); - success &= fulfillPrePostShopRequirements(scheduledFuture, executionState,context); - - // Check for cancellation after shop requirements - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("\n\tRequirements fulfillment cancelled after shop requirements"); - return false; - } - - if (!success) { - executionState.markFailed("Failed to fulfill shop requirements"); - return false; - } - - // Step 4: Item Requirements - StringBuilder itemReqInfo = new StringBuilder(); - RequirementRegistry.RequirementBreakdown itemReqBreakdown = this.registry.getItemRequirementBreakdown( context); - itemReqInfo.append("\n=== STEP 4: ITEM REQUIREMENTS ===\n"); - if (itemReqBreakdown.isEmpty()) { - itemReqInfo.append(String.format("\tNo item requirements for context: %s\n", context)); - } else { - itemReqInfo.append("\t"+itemReqBreakdown.getDetailedBreakdownString()); - } - - log.info(itemReqInfo.toString()); - executionState.updateFulfillmentStep(FulfillmentStep.ITEMS, "Preparing inventory and equipment"); - success &= fulfillPrePostItemRequirements(scheduledFuture,executionState,context); - - // Check for cancellation after item requirements - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Requirements fulfillment cancelled after item requirements"); - return false; - } - - if (!success) { - executionState.markFailed("Failed to fulfill item requirements"); - return false; - } - - // Step 5: Spellbook Requirements - List spellbookReqs = this.registry.getRequirements(SpellbookRequirement.class, context); - StringBuilder spellbookReqInfo = new StringBuilder(); - spellbookReqInfo.append("\n=== STEP 5: SPELLBOOK REQUIREMENTS ===\n"); - if (spellbookReqs.isEmpty()) { - spellbookReqInfo.append(String.format("No spellbook requirements for context: %s\n", context)); - } else { - spellbookReqInfo.append(String.format("Found %d spellbook requirement(s):\n", spellbookReqs.size())); - for (int i = 0; i < spellbookReqs.size(); i++) { - spellbookReqInfo.append(String.format("\n--- Spellbook Requirement %d ---\n", i + 1)); - spellbookReqInfo.append(spellbookReqs.get(i).displayString()).append("\n"); - } - } - - log.info(spellbookReqInfo.toString()); - executionState.updateFulfillmentStep(FulfillmentStep.SPELLBOOK, "Switching spellbook"); - success &= fulfillPrePostSpellbookRequirements(scheduledFuture, executionState,context, saveCurrentSpellbook); - - // Check for cancellation after spellbook requirements - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Requirements fulfillment cancelled after spellbook requirements"); - return false; - } - - if (!success) { - executionState.markFailed("Failed to fulfill spellbook requirements"); - return false; - } - - // Step 6: Location Requirements (always fulfill location requirements last) - List locationReqs = this.registry.getRequirements(LocationRequirement.class, context); - StringBuilder locationReqInfo = new StringBuilder(); - locationReqInfo.append("\n=== STEP 6: LOCATION REQUIREMENTS ===\n"); - if (locationReqs.isEmpty()) { - locationReqInfo.append(String.format("No location requirements for context: %s\n", context)); - } else { - locationReqInfo.append(String.format("Found %d location requirement(s):\n", locationReqs.size())); - for (int i = 0; i < locationReqs.size(); i++) { - locationReqInfo.append(String.format("\n--- Location Requirement %d ---\n", i + 1)); - locationReqInfo.append(locationReqs.get(i).displayString()).append("\n"); - } - } - - log.info(locationReqInfo.toString()); - - success &= fulfillPrePostLocationRequirements(scheduledFuture,executionState, context); - - // Check for cancellation after location requirements - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Requirements fulfillment cancelled after location requirements"); - return false; - } - - // Step 7: Handle External Requirements (added by plugins or external systems) - if (success) { - List externalRequirements = registry.getExternalRequirements(context); - - StringBuilder externalReqInfo = new StringBuilder(); - externalReqInfo.append("\n=== STEP 7: EXTERNAL REQUIREMENTS ===\n"); - if (externalRequirements.isEmpty()) { - externalReqInfo.append(String.format("No external requirements for context: %s\n", context)); - } else { - externalReqInfo.append(String.format("Found %d external requirement(s):\n", externalRequirements.size())); - for (int i = 0; i < externalRequirements.size(); i++) { - externalReqInfo.append(String.format("\n--- External Requirement %d ---\n", i + 1)); - externalReqInfo.append(externalRequirements.get(i).displayString()).append("\n"); - } - } - log.info(externalReqInfo.toString()); - - if (!externalRequirements.isEmpty()) { - updateFulfillmentStep( executionState,FulfillmentStep.EXTERNAL_REQUIREMENTS, "Fulfilling external requirements", externalRequirements.size()); - - for (Requirement externalReq : externalRequirements) { - try { - log.info("\nFulfilling external requirement: \n\t{}", externalReq.getDescription()); - boolean externalSuccess = externalReq.fulfillRequirement(scheduledFuture); - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Executor service is shutdown, skipping external requirement fulfillment: {}", externalReq.getDescription()); - return false; // Skip if executor service is shutdown - } - if (!externalSuccess) { - log.error("Failed to fulfill external requirement: {}", externalReq.getDescription()); - success = false; - break; - } else { - log.info("Successfully fulfilled external requirement: {}", externalReq.getDescription()); - } - } catch (Exception e) { - log.error("Error fulfilling external requirement: {}", externalReq.getDescription(), e); - success = false; - break; - } - } - - if (success) { - log.info("All external requirements fulfilled successfully"); - } else { - log.error("Failed to fulfill external requirements"); - } - } else { - log.info("External requirements step completed - no external requirements to fulfill"); - } - } - - if (success) { - executionState.update(phase,TaskExecutionState.ExecutionState.COMPLETED ); - log.info("\n" + "=".repeat(80) + "\nALL REQUIREMENTS FULFILLED SUCCESSFULLY FOR CONTEXT: {}\n" + "=".repeat(80), context); - } else { - executionState.markFailed("Failed to fulfill location requirements"); - log.error("\n" + "=".repeat(80) + "\nFAILED TO FULFILL REQUIREMENTS FOR CONTEXT: {}\n" + "=".repeat(80), context); - } - - return success; - } finally { - // Always clean up the watchdog - if (cancellationWatchdog != null && !cancellationWatchdog.isDone()) { - cancellationWatchdog.cancel(true); - } - - // Shutdown the executor service - if (cancellationWatchdogService != null) { - cancellationWatchdogService.shutdown(); - try { - if (!cancellationWatchdogService.awaitTermination(2, TimeUnit.SECONDS)) { - cancellationWatchdogService.shutdownNow(); - if (!cancellationWatchdogService.awaitTermination(1, TimeUnit.SECONDS)) { - log.warn("Cancellation watchdog executor service did not terminate cleanly for context: {}", context); - } - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - cancellationWatchdogService.shutdownNow(); - } - } - } - } - - /** - * Starts a cancellation watchdog that monitors for task cancellation and stops any ongoing walking operations. - * This prevents walking operations from continuing when the overall requirement fulfillment has been cancelled. - * - * @param executorService The executor service to run the watchdog on - * @param scheduledFuture The future to monitor for cancellation - * @param context The schedule context for logging purposes - * @return The scheduled future for the watchdog task - */ - private ScheduledFuture startCancellationWatchdog( ScheduledExecutorService executorService, - CompletableFuture scheduledFuture, - TaskContext context) { - return executorService.scheduleAtFixedRate(() -> { - try { - // Check for cancellation - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.info("Requirement fulfillment cancellation watchdog triggered for context: {}", context); - - // Stop any ongoing walking by clearing the walker target - Rs2Walker.setTarget(null); - - // Cancel this watchdog by throwing an exception - throw new RuntimeException("Requirement fulfillment cancelled - stopping walking operations"); - } - } catch (Exception e) { - log.debug("Cancellation watchdog stopping for context {}: {}", context, e.getMessage()); - throw e; // Re-throw to stop the scheduled task - } - }, 2, 2, TimeUnit.SECONDS); // Check every 2 seconds (more frequent than location watchdog) - } - - /** - * Convenience method to fulfill all pre-schedule requirements. - * - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @param saveCurrentSpellbook Whether to save current spellbook for restoration - * @return true if all pre-schedule requirements were fulfilled successfully, false otherwise - */ - public boolean fulfillPreScheduleRequirements(CompletableFuture scheduledFuture, boolean saveCurrentSpellbook,TaskExecutionState executionState) { - return fulfillAllRequirements(scheduledFuture,TaskContext.PRE_SCHEDULE, saveCurrentSpellbook,executionState); - } - - - - /** - * Convenience method to fulfill all post-schedule requirements. - * - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @param saveCurrentSpellbook Whether to save current spellbook for restoration - * @return true if all post-schedule requirements were fulfilled successfully, false otherwise - */ - public boolean fulfillPostScheduleRequirements(CompletableFuture scheduledFuture, boolean saveCurrentSpellbook, TaskExecutionState executionState) { - return fulfillAllRequirements(scheduledFuture, TaskContext.POST_SCHEDULE, saveCurrentSpellbook, executionState); - } - - - // === UNIFIED REQUIREMENT FULFILLMENT FUNCTIONS === - - /** - * Fulfills all item requirements (equipment, inventory, either) for the specified schedule context. - * - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if all item requirements were fulfilled successfully, false otherwise - */ - public boolean fulfillPrePostItemRequirements(CompletableFuture scheduledFuture, TaskExecutionState executionState, TaskContext context) { - // Check for InventorySetupRequirements first - they take precedence over progressive item management - List inventorySetupReqs = registry.getRequirements(InventorySetupRequirement.class, context); - - if (!inventorySetupReqs.isEmpty()) { - log.info("Found {} inventory setup requirement(s) for context: {} - using inventory setup approach instead of progressive item management", - inventorySetupReqs.size(), context); - - // Initialize step tracking for inventory setup - updateFulfillmentStep(executionState, FulfillmentStep.ITEMS, "Loading inventory setup(s)", inventorySetupReqs.size()); - - boolean success = true; - for (int i = 0; i < inventorySetupReqs.size(); i++) { - InventorySetupRequirement inventorySetupReq = inventorySetupReqs.get(i); - - // Update current requirement tracking - updateCurrentRequirement(executionState, inventorySetupReq, i + 1); - - try { - log.info("Fulfilling inventory setup requirement {}/{}: {}", - i + 1, inventorySetupReqs.size(), inventorySetupReq.getName()); - - boolean fulfilled = inventorySetupReq.fulfillRequirement(scheduledFuture); - if (!fulfilled && inventorySetupReq.isMandatory()) { - log.error("Failed to fulfill mandatory inventory setup requirement: {}", inventorySetupReq.getName()); - success = false; - break; - } else if (!fulfilled) { - log.warn("Failed to fulfill optional inventory setup requirement: {}", inventorySetupReq.getName()); - } else { - log.info("Successfully fulfilled inventory setup requirement: {}", inventorySetupReq.getName()); - } - } catch (Exception e) { - log.error("Error fulfilling inventory setup requirement {}: {}", inventorySetupReq.getName(), e.getMessage()); - if (inventorySetupReq.isMandatory()) { - success = false; - break; - } - } - } - - log.debug("Inventory setup requirements fulfillment completed. Success: {}", success); - return success; - } - - // No inventory setup requirements found - use progressive item management approach - log.debug("No inventory setup requirements found - using progressive item management approach"); - - // Get count of logical requirements for this context using the unified API - int logicalReqsCount = registry.getItemCount(context); - - if (logicalReqsCount == 0) { - log.debug("No item requirements to fulfill for context: {}", context); - return true; // No requirements to fulfill - } - - // Initialize step tracking - updateFulfillmentStep(executionState, FulfillmentStep.ITEMS, "Processing item requirements", logicalReqsCount); - - boolean success = fulfillOptimalInventoryAndEquipmentLayout(scheduledFuture, context); - - log.debug("Item requirements fulfillment completed. Success: {}", success); - return success; - } - - /** - * Fulfills shop requirements for the specified schedule context. - * Uses the unified RequirementSolver to handle both standard and external requirements. - * - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param scheduledFuture The CompletableFuture to monitor for cancellation - * @return true if all shop requirements were fulfilled successfully, false otherwise - */ - public boolean fulfillPrePostShopRequirements(CompletableFuture scheduledFuture,TaskExecutionState executionState, TaskContext context ) { - LinkedHashSet shopLogical = registry.getShopRequirements(context); - - if (shopLogical.isEmpty()) { - log.debug("No shop requirements to fulfill for context: {}", context); - return true; // No requirements to fulfill - } - - // Initialize step tracking - - updateFulfillmentStep(executionState, FulfillmentStep.SHOP, "Processing shop requirements", shopLogical.size()); - log.info("Processing shop requirements for context: {} number of req.", context, shopLogical.size()); - // Use the utility class for fulfillment - return LogicalRequirement.fulfillLogicalRequirements( - scheduledFuture, - new ArrayList<>(shopLogical), - "shop" - ); - } - - - /** - * Fulfills loot requirements for the specified schedule context. - * Uses the unified RequirementSolver to handle both standard and external requirements. - * - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param scheduledFuture The CompletableFuture to monitor for cancellation - * @return true if all loot requirements were fulfilled successfully, false otherwise - */ - public boolean fulfillPrePostLootRequirements(CompletableFuture scheduledFuture, TaskExecutionState executionState, TaskContext context ) { - // Get requirements count for step tracking - LinkedHashSet lootLogical = registry.getLootLogicalRequirements(context); - - if (lootLogical.isEmpty()) { - log.debug("No loot requirements to fulfill for context: {}", context); - return true; // No requirements to fulfill - } - - // Initialize step tracking - List contextReqs = LogicalRequirement.filterByContext(new ArrayList<>(lootLogical), context); - updateFulfillmentStep(executionState,FulfillmentStep.LOOT, "Collecting loot items", contextReqs.size()); - - // Use the utility class for fulfillment - return LogicalRequirement.fulfillLogicalRequirements(scheduledFuture,contextReqs, "loot"); - - - } - - - - /** - * Fulfills location requirements for the specified schedule context. - * Uses the unified filtering system to automatically handle pre/post schedule requirements. - * - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if all location requirements were fulfilled successfully, false otherwise - */ - public boolean fulfillPrePostLocationRequirements(CompletableFuture scheduledFuture,TaskExecutionState executionState, TaskContext context) { - // Get requirements count for step tracking - List locationReqs = this.registry.getRequirements(LocationRequirement.class, context); - - if (locationReqs.isEmpty()) { - log.debug("No location requirements to fulfill for context: {}", context); - return true; // No requirements to fulfill - } - if (locationReqs.size() > 1) { - Microbot.log("Multiple location requirements found for context " + context + ". Only one should be set at a time.", Level.ERROR); - return false; // Only one location requirement should be set per context - } - - // Initialize step tracking - LocationRequirement locationReq = locationReqs.get(0); - updateFulfillmentStep(executionState,FulfillmentStep.LOCATION, "Moving to " + locationReq.getName(), locationReqs.size()); - - boolean success = true; - - for (int i = 0; i < locationReqs.size(); i++) { - LocationRequirement requirement = locationReqs.get(i); - - // Update current requirement tracking - updateCurrentRequirement(executionState,requirement, i + 1); - - try { - log.debug("Processing location requirement {}/{}: {}", i + 1, locationReqs.size(), requirement.getName()); - boolean fulfilled = requirement.fulfillRequirement(scheduledFuture); - if (!fulfilled && requirement.isMandatory()) { - Microbot.log("Failed to fulfill mandatory location requirement: " + requirement.getName()); - success = false; - } else if (!fulfilled) { - Microbot.log("Failed to fulfill optional location requirement: " + requirement.getName()); - } - } catch (Exception e) { - Microbot.log("Error fulfilling location requirement " + requirement.getName() + ": " + e.getMessage()); - if (requirement.isMandatory()) { - success = false; - } - } - } - - log.info("Location requirements fulfillment completed. Success: {}", success); - return success; - } - - - - /** - * Fulfills spellbook requirements for the specified schedule context. - * Uses the unified filtering system to automatically handle pre/post schedule requirements. - * - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param saveCurrentSpellbook Whether to save the current spellbook before switching (for pre-schedule) - * @return true if all spellbook requirements were fulfilled successfully, false otherwise - */ - public boolean fulfillPrePostSpellbookRequirements(CompletableFuture scheduledFuture,TaskExecutionState executionState, TaskContext context, boolean saveCurrentSpellbook) { - List spellbookReqs = this.registry.getRequirements(SpellbookRequirement.class, context); - - if (spellbookReqs.isEmpty()) { - log.debug("No spellbook requirements to fulfill for context: {}", context); - return true; // No requirements to fulfill - } - if(spellbookReqs.size() > 1) { - Microbot.log("Multiple spellbook requirements found for context " + context + ". Only one should be set at a time.", Level.ERROR); - return false; // Only one spellbook requirement should be set per context - } - - SpellbookRequirement spellbookReq = spellbookReqs.get(0); - - // Initialize step tracking - updateFulfillmentStep(executionState,FulfillmentStep.SPELLBOOK, "Switching to " + spellbookReq.getRequiredSpellbook().name(), spellbookReqs.size()); - - boolean success = true; - - // Save original spellbook if this is for pre-schedule and we should save it - if (context == TaskContext.PRE_SCHEDULE && saveCurrentSpellbook) { - originalSpellbook = Rs2Spellbook.getCurrentSpellbook(); - log.debug("Saved original spellbook: " + originalSpellbook + " before switching for pre-schedule requirements"); - } - - for (int i = 0; i < spellbookReqs.size(); i++) { - SpellbookRequirement requirement = spellbookReqs.get(i); - - // Update current requirement tracking - updateCurrentRequirement(executionState,requirement, i + 1); - - try { - log.debug("Processing spellbook requirement {}/{}: {}", i + 1, spellbookReqs.size(), requirement.getName()); - boolean fulfilled = requirement.fulfillRequirement(scheduledFuture); - if (!fulfilled && requirement.isMandatory()) { - Microbot.log("Failed to fulfill mandatory spellbook requirement: " + requirement.getName()); - success = false; - } else if (!fulfilled) { - Microbot.log("Failed to fulfill optional spellbook requirement: " + requirement.getName()); - } - } catch (Exception e) { - Microbot.log("Error fulfilling spellbook requirement " + requirement.getName() + ": " + e.getMessage()); - if (requirement.isMandatory()) { - success = false; - } - } - } - - // Special handling for post-schedule: if no post-schedule spellbook requirement is defined - // but we have a saved original spellbook, automatically restore it - if (context == TaskContext.POST_SCHEDULE && spellbookReqs.isEmpty() && originalSpellbook != null) { - log.debug("No post-schedule spellbook requirement defined, automatically restoring original spellbook"); - boolean restored = switchBackToOriginalSpellbook(); - if (!restored) { - Microbot.log("Failed to automatically restore original spellbook during post-schedule fulfillment", Level.WARN); - // Don't mark as failure since this is automatic restoration, not an explicit requirement - } - } - - log.debug("Spellbook requirements fulfillment completed. Success: {}", success); - return success; - } - - /** - * Fulfills conditional and ordered requirements for the specified schedule context. - * These requirements are processed first as they may contain prerequisites for other requirements. - * - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param scheduledFuture The CompletableFuture to monitor for cancellation - * @return true if all conditional requirements were fulfilled successfully, false otherwise - */ - public boolean fulfillConditionalRequirements(CompletableFuture scheduledFuture, TaskExecutionState executionState, TaskContext context ) { - // Get requirements count for step tracking - List conditionalReqs = this.registry.getRequirements(ConditionalRequirement.class, context); - List orderedReqs = this.registry.getRequirements(OrderedRequirement.class, context); - - // Initialize step tracking - int totalReqs = conditionalReqs.size() + orderedReqs.size(); - updateFulfillmentStep(executionState,FulfillmentStep.CONDITIONAL, - "Processing " + totalReqs + " conditional/ordered requirement(s)", totalReqs); - - // Use the utility class for fulfillment - return RequirementSolver.fulfillConditionalRequirements(scheduledFuture,executionState,conditionalReqs, orderedReqs, context); - - } - - - - /** - * Updates the fulfillment state for a specific step with requirement counting. - * This is the preferred method for tracking step-level progress. - * - * @param step The fulfillment step being processed - * @param details Descriptive text about what's happening in this step - * @param totalRequirements Total number of requirements in this step - */ - protected void updateFulfillmentStep(TaskExecutionState executionState, FulfillmentStep step, String details, int totalRequirements) { - executionState.updateFulfillmentStep(step, details, totalRequirements); - } - - /** - * Updates the current requirement being processed within a step. - * This provides granular tracking of individual requirement progress. - * - * @param requirement The specific requirement being processed - * @param requirementIndex The 1-based index of this requirement in the current step - */ - protected void updateCurrentRequirement(TaskExecutionState executionState, Requirement requirement, int requirementIndex) { - if (requirement != null) { - executionState.updateCurrentRequirement(requirement, requirement.getName(), requirementIndex); - } - } - /** - * Generates the inventory setup name for pre-schedule requirements. - * The format is: [OS]_{collectionName}_PRE_SCHEDULE - * This name is used to identify the corresponding InventorySetup in the plugin. - * - * @return The inventory setup name for pre-schedule requirements - */ - public String getPreInventorySetupName() { - return "[OS]_" + collectionName + "_" + TaskContext.PRE_SCHEDULE.name(); - - } - /** - * Retrieves the InventorySetup object for pre-schedule requirements. - * Searches the MInventorySetupsPlugin for a setup matching the pre-schedule name. - * - * @return The InventorySetup for pre-schedule, or null if not found - */ - public InventorySetup getPreInventorySetup() { - String setupName = getPreInventorySetupName(); - return getInventorySetup(setupName); - } - private InventorySetup getInventorySetup(String setupName) { - InventorySetup inventorySetup = MInventorySetupsPlugin.getInventorySetups().stream() - .filter(Objects::nonNull) - .filter(x -> x.getName().equalsIgnoreCase(setupName)) - .findFirst() - .orElse(null); - return inventorySetup; - - } - - /** - * Generates the inventory setup name for post-schedule requirements. - * The format is: [OS]_{collectionName}_POST_SCHEDULE - * This name is used to identify the corresponding InventorySetup in the plugin. - * - * @return The inventory setup name for post-schedule requirements - */ - public String getPostInventorySetupName() { - return "[OS]_" + collectionName + "_" + TaskContext.POST_SCHEDULE.name(); - } - /** - * Retrieves the InventorySetup object for post-schedule requirements. - * Searches the MInventorySetupsPlugin for a setup matching the post-schedule name. - * - * @return The InventorySetup for post-schedule, or null if not found - */ - public InventorySetup getPostInventorySetup() { - String setupName = getPostInventorySetupName(); - - return getInventorySetup(setupName); - } - - - /** - * Comprehensive inventory and equipment layout planning and fulfillment system. - * This method analyzes all requirements and creates optimal item placement maps, - * considering slot constraints, priority levels, and availability. - * - * @param context The schedule context - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if all mandatory requirements can be fulfilled - */ - private boolean fulfillOptimalInventoryAndEquipmentLayout(CompletableFuture scheduledFuture, TaskContext context) { - try { - StringBuilder sb = new StringBuilder(); - sb.append("\n" + "=".repeat(60)); - sb.append("\n\tOPTIMAL INVENTORY AND EQUIPMENT LAYOUT PLANNING"); - sb.append("\n\tContext: "+ context +"| Collection: "+collectionName).append("\n"); - sb.append("=".repeat(60)); - log.info(sb.toString()); - - - // Step 1: Analyze all requirements and create constraint maps - log.info("\n--- Step 1: Analyzing Requirements and Creating Layout Plan ---"); - InventorySetupPlanner layoutPlan = null; - String inventorySetupName = null; - if (context == TaskContext.PRE_SCHEDULE) { - log.info("Using pre-schedule inventory setup name: {}", getPreInventorySetupName()); - inventorySetupName = getPreInventorySetupName(); - if (currentPreScheduleLayoutPlan == null) { - log.info("No existing layout plan found, analyzing requirements to create new plan"); - this.currentPreScheduleLayoutPlan = analyzeRequirementsAndCreateLayoutPlan(scheduledFuture,context); - } else { - log.info("Existing layout plan found, re-analyzing requirements to update plan"); - } - layoutPlan = this.currentPreScheduleLayoutPlan; - }else if( context == TaskContext.POST_SCHEDULE) { - log.info("Using post-schedule inventory setup name: {}", getPostInventorySetupName()); - inventorySetupName = getPostInventorySetupName(); - if (currentPostScheduleLayoutPlan == null) { - log.info("No existing layout plan found, analyzing requirements to create new plan"); - this.currentPostScheduleLayoutPlan = analyzeRequirementsAndCreateLayoutPlan(scheduledFuture,context); - } else { - log.info("Existing layout plan found, re-analyzing requirements to update plan"); - } - layoutPlan = this.currentPostScheduleLayoutPlan; - } else { - log.error("Invalid context for inventory and equipment layout planning: {}", context); - return false; - } - - if (layoutPlan == null) { - log.error("Failed to create inventory layout plan"); - return false; - } - - // Display detailed plan information - log.info("\n--- Generated Layout Plan ---"); - log.info("\n"+layoutPlan.getDetailedPlanString()); - - // Step 2: Check if the plan is feasible (all mandatory items can be fulfilled) - log.info("\n--- Step 2: Feasibility Check ---"); - boolean feasible = layoutPlan.isFeasible(); - log.info("\n---Plan Feasibility: {}", feasible ? "FEASIBLE" : "NOT FEASIBLE"); - - if (!feasible) { - - log.error("Layout plan is not feasible - missing mandatory items or insufficient space"); - - // Log detailed failure reasons - if (!layoutPlan.getMissingMandatoryItems().isEmpty()) { - log.error("Missing mandatory items:"); - for (ItemRequirement missing : layoutPlan.getMissingMandatoryItems()) { - log.error("\t- {}", missing.getName()); - } - } - - if (!layoutPlan.getMissingMandatoryEquipment().isEmpty()) { - log.error("Missing mandatory equipment slots:"); - for (Map.Entry> entry : layoutPlan.getMissingMandatoryEquipment().entrySet()) { - EquipmentInventorySlot slot = entry.getKey(); - List missingItems = entry.getValue(); - String itemNames = missingItems.stream() - .map(ItemRequirement::getName) - .collect(Collectors.joining(", ")); - log.error("\t- {}: {}", slot.name(), itemNames); - } - } - - return false; - } - - // Display slot utilization summary - log.debug("\n--- Slot Utilization Summary ---"); - log.debug("\n"+layoutPlan.getOccupiedSlotsSummary()); - - // Step 2.5: Convert plan to InventorySetup and add to plugin BEFORE execution - log.debug("\n--- Step 2.5: Creating InventorySetup from Plan ---"); - InventorySetup createdSetup = layoutPlan.addToInventorySetupsPlugin(inventorySetupName); - - if (createdSetup == null) { - log.error("Failed to create InventorySetup from plan"); - return false; - } - - log.debug("Successfully created InventorySetup: {}", createdSetup.getName()); - - // Step 3: Execute using Rs2InventorySetup approach - log.debug("\n--- Step 3: Executing Plan Using Rs2InventorySetup ---"); - boolean success = layoutPlan.executeUsingRs2InventorySetup(scheduledFuture, createdSetup.getName()); - - if (success) { - log.debug("\n" + "=".repeat(60)); - log.debug("SUCCESSFULLY EXECUTED OPTIMAL INVENTORY AND EQUIPMENT LAYOUT"); - log.debug("Used Rs2InventorySetup approach with setup: {}", createdSetup.getName()); - log.debug("=".repeat(60)); - } else { - log.error("\n" + "=".repeat(60)); - log.error("FAILED TO EXECUTE INVENTORY AND EQUIPMENT LAYOUT PLAN"); - log.error("Rs2InventorySetup approach failed for setup: {}", createdSetup.getName()); - log.error("=".repeat(60)); - } - if (Rs2Bank.isOpen()) { - Rs2Bank.closeBank(); - log.info("Closed bank after inventory and equipment fulfillment"); - } - return success; - - } catch (Exception e) { - log.error("\nError in comprehensive inventory and equipment fulfillment: {}", e.getMessage(), e); - return false; - } - } - - /** - * Analyzes all requirements and creates an optimal inventory and equipment layout plan. - * Uses the enhanced InventorySetupPlanner with requirement registry support. - * - * @param scheduledFuture The scheduled future for cancellation checking - * @param context The schedule context (PRE_SCHEDULE or POST_SCHEDULE) - * @return The created inventory setup plan, or null if planning failed - */ - private InventorySetupPlanner analyzeRequirementsAndCreateLayoutPlan(CompletableFuture scheduledFuture, TaskContext context) { - // Ensure bank is open for all operations - if (Rs2Bank.bankItems().size() == 0 && !Rs2Bank.isOpen()) { - log.info("\n\tBank Cach is null and bank not open, attempting to open bank for item management, update bank data"); - if (!Rs2Bank.walkToBankAndUseBank() && !Rs2Player.isInteracting() && !Rs2Player.isMoving()) { - log.error("\n\tFailed to open bank for comprehensive item management"); - } - boolean openBank= sleepUntil(() -> Rs2Bank.isOpen(), 5000); - if (!openBank) { - log.error("\n\tFailed to open bank within timeout for context: {}", context); - return null; - } - } - // get all inventory Item (Rs2Item) and all equipment items currently in inventory or equipped which are not in the inenvtory or equipment requirements -// List currentInventoryItems = Rs2Inventory.get(); - // List currentEquipmentItems = Rs2Equipment.getAllItems(); - List itemNotInItemReqsInventory = new ArrayList<>(); - List itemNotInItemReqsEquipment = new ArrayList<>(); - Map> registeredEquipmentMap = registry.getEquipmentSlotItems(context); - Map> registeredInventoryMap = registry.getInventorySlotItems(context); - LinkedHashSet registeredAnyInventoryMap = registry.getAnyInventorySlotItems(context); - List registeredConditionalInventory = registry.getConditionalItemRequirements(context); - //iterate over inventoy, retrive unnecessary inventory items - for (int iiSlot = 0; iiSlot < 28; iiSlot++) { - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Executor service is shutdown, skipping inventory scan at slot: {}", iiSlot); - return null; // Skip if executor service is shutdown - } - Rs2ItemModel invItem = Rs2Inventory.getItemInSlot(iiSlot); - if (invItem != null) { - boolean foundInReqs = false; - // Check against registered inventory requirements - if (registeredInventoryMap.containsKey(iiSlot)) { - for (ItemRequirement req : registeredInventoryMap.get(iiSlot)) { - if(invItem.getId() == req.getId()){ - foundInReqs = true; - break; - } - //if (invItem.getName().toLowerCase().contains(req.getName().toLowerCase())) { - // foundInReqs = true; - // break; - // } - } - } - // Check against registered any-inventory requirements - if (!foundInReqs && !registeredAnyInventoryMap.isEmpty()) { - for (ItemRequirement req : registeredAnyInventoryMap) { - if(invItem.getId() == req.getId()){ - foundInReqs = true; - break; - } - } - } - if (!foundInReqs && !registeredConditionalInventory.isEmpty()) { - for (ConditionalRequirement condReq : registeredConditionalInventory) { - for (ItemRequirement req : condReq.getActiveItemRequirements()) { - if(invItem.getId() == req.getId()){ - foundInReqs = true; - break; - } - } - if(foundInReqs) break; - } - } - if (!foundInReqs) { - itemNotInItemReqsInventory.add(invItem); - } - } - } - //iterate over equipment slots, retrive unnecessary equipment items - for (EquipmentInventorySlot slot : EquipmentInventorySlot.values()) { - Rs2ItemModel equipItem = Rs2Equipment.get(slot); - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Executor service is shutdown, skipping equipment scan at slot: {}", slot); - return null; // Skip if executor service is shutdown - } - if (equipItem != null) { - boolean foundInReqs = false; - // Check against registered equipment requirements - if (registeredEquipmentMap.containsKey(slot)) { - for (ItemRequirement req : registeredEquipmentMap.get(slot)) { - if (equipItem.getName().toLowerCase().contains(req.getName().toLowerCase())) { - foundInReqs = true; - break; - } - } - } - if (!foundInReqs && !registeredConditionalInventory.isEmpty()) { - for (ConditionalRequirement condReq : registeredConditionalInventory) { - for (ItemRequirement req : condReq.getActiveItemRequirements()) { - if(equipItem.getId() == req.getId()){ - foundInReqs = true; - break; - } - } - if(foundInReqs) break; - } - } - if (!foundInReqs) { - itemNotInItemReqsEquipment.add(equipItem); - } - } - } - // we need do bank items which are not in the item requirements, move to bank and deposit - if (!itemNotInItemReqsInventory.isEmpty() || !itemNotInItemReqsEquipment.isEmpty()) { - log.info("\n\tFound {} inventory items and {} equipment items not in item requirements, depositing them to bank", itemNotInItemReqsInventory.size(), itemNotInItemReqsEquipment.size()); - Rs2Bank.walkToBankAndUseBank(); - boolean bankOpened = sleepUntil(() -> Rs2Bank.isOpen(), 5000); - if (!bankOpened) { - log.error("\n\tFailed to open bank within timeout for context: {}", context); - return null; - } - // Deposit inventory items - for (Rs2ItemModel item : itemNotInItemReqsInventory) { - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Executor service is shutdown, skipping item deposit: {}", item.getName()); - return null; // Skip if executor service is shutdown - } - Rs2Bank.depositAll(item.getId()); - boolean deposited = sleepUntil(()->!Rs2Inventory.hasItem(item.getName()), Constants.GAME_TICK_LENGTH*2); - if (deposited) { - log.info("\n\tDeposited inventory item: {} x{}", item.getName(), item.getQuantity()); - } else { - log.warn("\n\tFailed to deposit inventory item: {} x{}", item.getName(), item.getQuantity()); - } - } - // deposit all unnecessary equipment items at once using bulk deposit - if (!itemNotInItemReqsEquipment.isEmpty()) { - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Executor service is shutdown, skipping equipment deposit"); - return null; // Skip if executor service is shutdown - } - - // collect all equipment slots to deposit - EquipmentInventorySlot[] slotsToDeposit = itemNotInItemReqsEquipment.stream() - .mapToInt(Rs2ItemModel::getSlot) - .filter(slot -> slot >= 0 && slot < EquipmentInventorySlot.values().length) - .mapToObj(slot -> EquipmentInventorySlot.values()[slot]) - .toArray(EquipmentInventorySlot[]::new); - - log.info("Depositing {} unnecessary equipment items in bulk", slotsToDeposit.length); - boolean deposited = Rs2Bank.depositEquippedItems(slotsToDeposit); - - if (deposited) { - StringBuilder sb = new StringBuilder(); - sb.append("Successfully deposited equipment items:\\n"); - for (Rs2ItemModel item : itemNotInItemReqsEquipment) { - sb.append(String.format("\\t✓ %s x%d\\n", item.getName(), item.getQuantity())); - } - log.info(sb.toString()); - } else { - log.warn("Failed to deposit some or all equipment items"); - // fallback to individual deposits if bulk fails - for (Rs2ItemModel item : itemNotInItemReqsEquipment) { - EquipmentInventorySlot itemSlot = EquipmentInventorySlot.values()[item.getSlot()]; - Rs2Bank.depositEquippedItem(itemSlot); - boolean individualDeposited = sleepUntil(()->!Rs2Equipment.isWearing(item.getId()), Constants.GAME_TICK_LENGTH*2); - if (individualDeposited) { - log.info("\\tDeposited individual equipment item: {} x{}", item.getName(), item.getQuantity()); - } else { - log.warn("\\tFailed to deposit individual equipment item: {} x{}", item.getName(), item.getQuantity()); - } - } - } - } - } - - // Create enhanced planner with registry and OR requirement mode - InventorySetupPlanner plan = new InventorySetupPlanner(registry, context, orRequirementMode); - - // Create the plan from requirements - boolean planningSuccessful = plan.createPlanFromRequirements(); - - if (!planningSuccessful) { - log.error("Failed to create inventory setup plan for context: {}", context); - return null; - } - - log.info("Successfully created inventory setup plan for context: {} with OR mode: {}", context, orRequirementMode); - return plan; - } - - /** - * Adds a dummy equipment requirement to block a specific equipment slot. - * Dummy items are used to reserve slots without specifying actual items. - * - * @param equipmentSlot The equipment slot to block - * @param TaskContext When this requirement applies - * @param description Description for the dummy requirement - */ - protected void addDummyEquipmentRequirement(EquipmentInventorySlot equipmentSlot, - TaskContext taskContext, - String description) { - ItemRequirement dummy = ItemRequirement.createDummyEquipmentRequirement( - equipmentSlot, taskContext, description); - registry.register(dummy); - log.debug("Added dummy equipment requirement for slot {}: {}", equipmentSlot, description); - } - - /** - * Adds a dummy inventory requirement to block a specific inventory slot. - * Dummy items are used to reserve slots without specifying actual items. - * - * @param inventorySlot The inventory slot to block (0-27) - * @param TaskContext When this requirement applies - * @param description Description for the dummy requirement - */ - protected void addDummyInventoryRequirement(int inventorySlot, - TaskContext taskContext, - String description) { - ItemRequirement dummy = ItemRequirement.createDummyInventoryRequirement( - inventorySlot, taskContext, description); - registry.register(dummy); - log.debug("Added dummy inventory requirement for slot {}: {}", inventorySlot, description); - } - public String getDetailedDisplay(){ - StringBuilder sb = new StringBuilder(); - sb.append("=== Pre/Post Schedule Requirements Summary ===\n"); - sb.append("Collection: ").append(collectionName).append("\n"); - sb.append("Current Spellbook: ").append(Rs2Spellbook.getCurrentSpellbook()).append("\n"); - if (originalSpellbook != null) { - sb.append("Original Spellbook: ").append(originalSpellbook).append("\n"); - } - sb.append("Total Pre\\Post Requirements Registered: ").append(getRegistry().getAllRequirements().size()).append("\n"); - - // Pre-Schedule Requirements - sb.append(" Pre Requirements Registered: ").append(this.registry.getRequirements(TaskContext.PRE_SCHEDULE).size()).append("\n"); - sb.append(" - Spellbook Requirements: ").append(this.registry.getRequirements(SpellbookRequirement.class,TaskContext.PRE_SCHEDULE).size()).append("\n"); - sb.append(" - Location Requirements: ").append(this.registry.getRequirements(LocationRequirement.class, TaskContext.PRE_SCHEDULE).size()).append("\n"); - sb.append(" - Loot Requirements: ").append(this.registry.getRequirements(LootRequirement.class, TaskContext.PRE_SCHEDULE).size()).append("\n"); - - // Equipment Requirements breakdown - RequirementRegistry.RequirementBreakdown preEquipBreakdown = registry.getItemRequirementBreakdown(TaskContext.PRE_SCHEDULE); - sb.append(" - Equipment Requirements: ").append(preEquipBreakdown.getTotalEquipmentCount()).append("\n"); - sb.append(" └─ Mandatory: ").append(preEquipBreakdown.getEquipmentCount(RequirementPriority.MANDATORY)).append(", "); - sb.append("Recommended: ").append(preEquipBreakdown.getEquipmentCount(RequirementPriority.RECOMMENDED)).append("\n "); - - // Equipment slot details for Pre - Map> preEquipSlots = preEquipBreakdown.getEquipmentSlotBreakdown(); - if (!preEquipSlots.isEmpty()) { - sb.append(" └─ Equipment Slots Detail:\n"); - for (Map.Entry> entry : preEquipSlots.entrySet()) { - EquipmentInventorySlot slot = entry.getKey(); - Map counts = entry.getValue(); - sb.append(" ").append(slot.name()).append(": M=").append(counts.getOrDefault(RequirementPriority.MANDATORY, 0)) - .append(", R=").append(counts.getOrDefault(RequirementPriority.RECOMMENDED, 0)).append("\n"); - } - } - - // Inventory Requirements breakdown - sb.append(" - Inventory Requirements: ").append(preEquipBreakdown.getTotalInventoryCount()).append("\n"); - sb.append(" └─ Mandatory: ").append(preEquipBreakdown.getInventoryCount(RequirementPriority.MANDATORY)).append("\n"); - sb.append(" └─ Recommended: ").append(preEquipBreakdown.getInventoryCount(RequirementPriority.RECOMMENDED)).append(""); - - // Inventory slot details for Pre - Map> preInventorySlots = preEquipBreakdown.getInventorySlotBreakdown(); - if (!preInventorySlots.isEmpty()) { - sb.append(" └─ Inventory Slots Detail:\n"); - for (Map.Entry> entry : preInventorySlots.entrySet()) { - Integer slot = entry.getKey(); - Map counts = entry.getValue(); - sb.append(" Slot ").append(slot).append(": M=").append(counts.getOrDefault(RequirementPriority.MANDATORY, 0)) - .append(", R=").append(counts.getOrDefault(RequirementPriority.RECOMMENDED, 0)) - .append(", O=").append(counts.getOrDefault(RequirementPriority.RECOMMENDED, 0)).append("\n"); - } - } - - sb.append("\n - Shop Requirements: ").append(this.registry.getRequirements(ShopRequirement.class, TaskContext.PRE_SCHEDULE).size()).append("\n"); - sb.append(" - all external requirements: ").append(registry.getExternalRequirements(TaskContext.PRE_SCHEDULE).size()).append("\n"); - - // Post-Schedule Requirements - sb.append(" Post Requirements Registered: ").append(this.registry.getRequirements(TaskContext.POST_SCHEDULE).size()).append("\n"); - sb.append(" - Spellbook Requirements: ").append(this.registry.getRequirements(SpellbookRequirement.class, TaskContext.POST_SCHEDULE).size()).append("\n"); - sb.append(" - Location Requirements: ").append(this.registry.getRequirements(LocationRequirement.class, TaskContext.POST_SCHEDULE).size()).append("\n"); - sb.append(" - Loot Requirements: ").append(this.registry.getRequirements(LootRequirement.class, TaskContext.POST_SCHEDULE).size()).append("\n"); - - // Equipment Requirements breakdown for Post - RequirementRegistry.RequirementBreakdown postEquipBreakdown = registry.getItemRequirementBreakdown(TaskContext.POST_SCHEDULE); - sb.append(" - Equipment Requirements: ").append(postEquipBreakdown.getTotalEquipmentCount()).append("\n"); - sb.append(" └─ Mandatory: ").append(postEquipBreakdown.getEquipmentCount(RequirementPriority.MANDATORY)).append(", "); - sb.append("Recommended: ").append(postEquipBreakdown.getEquipmentCount(RequirementPriority.RECOMMENDED)); - - // Equipment slot details for Post - Map> postEquipSlots = postEquipBreakdown.getEquipmentSlotBreakdown(); - if (!postEquipSlots.isEmpty()) { - sb.append(" └─ Equipment Slots Detail:\n"); - for (Map.Entry> entry : postEquipSlots.entrySet()) { - EquipmentInventorySlot slot = entry.getKey(); - Map counts = entry.getValue(); - sb.append(" ").append(slot.name()).append(": M=").append(counts.getOrDefault(RequirementPriority.MANDATORY, 0)) - .append(", R=").append(counts.getOrDefault(RequirementPriority.RECOMMENDED, 0)).append("\n"); - } - } - - // Extra statistics before Inventory Requirements breakdown for Post - Map> postInventorySlots = postEquipBreakdown.getInventorySlotBreakdown(); - sb.append("\n - Inventory Slot Statistics (specific slots only):\n"); - sb.append(" └─ Total Specific Slots Used: ").append(postInventorySlots.size()).append("\n"); - if (!postInventorySlots.isEmpty()) { - sb.append(" └─ Slot Range: ").append(postInventorySlots.keySet().stream().min(Integer::compareTo).orElse(0)) - .append(" to ").append(postInventorySlots.keySet().stream().max(Integer::compareTo).orElse(0)).append("\n"); - } - - // Inventory Requirements breakdown for Post - sb.append(" - Inventory Requirements: ").append(postEquipBreakdown.getTotalInventoryCount()).append("\n"); - sb.append(" └─ Mandatory: ").append(postEquipBreakdown.getInventoryCount(RequirementPriority.MANDATORY)).append(", "); - sb.append("Recommended: ").append(postEquipBreakdown.getInventoryCount(RequirementPriority.RECOMMENDED)); - - - // Inventory slot details for Post - if (!postInventorySlots.isEmpty()) { - sb.append(" └─ Inventory Slots Detail:\n"); - for (Map.Entry> entry : postInventorySlots.entrySet()) { - Integer slot = entry.getKey(); - Map counts = entry.getValue(); - sb.append(" Slot ").append(slot).append(": M=").append(counts.getOrDefault(RequirementPriority.MANDATORY, 0)) - .append(", R=").append(counts.getOrDefault(RequirementPriority.RECOMMENDED, 0)) - .append(", O=").append(counts.getOrDefault(RequirementPriority.RECOMMENDED, 0)).append("\n"); - } - } - - sb.append("\n - Shop Requirements: ").append(this.registry.getRequirements(ShopRequirement.class, TaskContext.POST_SCHEDULE).size()).append("\n"); - sb.append(" - all external requirements: ").append(registry.getExternalRequirements(TaskContext.POST_SCHEDULE).size()).append("\n"); - sb.append("=============================================\n"); - return sb.toString(); - } - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/data/ItemRequirementCollection.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/data/ItemRequirementCollection.java deleted file mode 100644 index dd920dffcba..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/data/ItemRequirementCollection.java +++ /dev/null @@ -1,1444 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.data; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.api.Skill; -import net.runelite.api.gameval.ItemID; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.PrePostScheduleRequirements; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.OrRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.ConditionalRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.OrderedRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.RunePouchRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.util.ConditionalRequirementBuilder; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location.LocationRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.SpellbookRequirement; -import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; -import net.runelite.client.plugins.microbot.util.magic.Rs2Spellbook; -import net.runelite.client.plugins.microbot.util.magic.Runes; -import net.runelite.client.plugins.microbot.util.misc.Rs2Food; - -/** - * Static collection of requirement registration methods for common OSRS equipment sets, - * outfits, and progression-based tool collections. - * - * This class provides standardized requirement collections that can be registered - * with PrePostScheduleRequirements instances to ensure consistency across plugins. - */ -public class ItemRequirementCollection { - - /** - * Registers basic mining equipment requirements for the plugin scheduler. - * This includes various pickaxes with their respective requirements. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerPickAxes(PrePostScheduleRequirements requirements, RequirementPriority priority, TaskContext taskContext) { - // because all are in the same slot, these are or requirements, any of these is important - requirements.register(new ItemRequirement( - ItemID.CRYSTAL_PICKAXE, 1, - EquipmentInventorySlot.WEAPON, -1,//-1 allows to be in inventory - priority, 10, "Crystal pickaxe (best for mining fragments)", - taskContext, Skill.MINING, 71, Skill.ATTACK, 70 // Mining level 71 required to equip - )); - requirements.register(new ItemRequirement( - ItemID.DRAGON_PICKAXE, 1, - EquipmentInventorySlot.WEAPON,-1, - - priority, 8, "Dragon pickaxe (excellent for mining fragments)", - taskContext, Skill.MINING, 61, Skill.ATTACK, 60 // Mining level 61 required to equip - )); - requirements.register(new ItemRequirement( - ItemID.RUNE_PICKAXE, 1, - EquipmentInventorySlot.WEAPON,-1, priority, 6, "Rune pickaxe (good for mining fragments)", - taskContext, Skill.MINING, 41, Skill.ATTACK, 40 // Mining level 41 required to equip - )); - requirements.register(new ItemRequirement( - ItemID.ADAMANT_PICKAXE, 1, - EquipmentInventorySlot.WEAPON,-1, priority, 4, "Adamant pickaxe (adequate for mining fragments)", - taskContext, Skill.MINING, 31, Skill.ATTACK, 30 // Mining level 31 required to equip - )); - requirements.register(new ItemRequirement( - ItemID.MITHRIL_PICKAXE, 1, - EquipmentInventorySlot.WEAPON,-1, priority, 4, "Mithril pickaxe (adequate for mining fragments)", - taskContext, Skill.MINING, 21, Skill.ATTACK, 20 // Mining level 21 required to equip - )); - requirements.register(new ItemRequirement( - ItemID.STEEL_PICKAXE, 1, - EquipmentInventorySlot.WEAPON,-1, priority, 2, "Steel pickaxe (for mining fragments)", - taskContext, Skill.MINING, 6, Skill.ATTACK, 10 // Mining level 6 required to equip - )); - requirements.register(new ItemRequirement( - ItemID.IRON_PICKAXE, 1, - EquipmentInventorySlot.WEAPON,-1, priority, 2, "Iron pickaxe (for mining fragments)", - taskContext, Skill.MINING, 0, Skill.ATTACK, 0// Mining level 1 required to equip - )); - requirements.register(new ItemRequirement( - ItemID.BRONZE_PICKAXE, 1, - EquipmentInventorySlot.WEAPON,-1, priority, 1, "Bronze pickaxe (for mining fragments, if no better option available)", - taskContext - // No skill requirement for bronze pickaxe - anyone can use it - )); - } - - /** - * Registers progression-based woodcutting axes for the plugin scheduler. - * This includes all axes from bronze to crystal with their respective requirements. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerWoodcuttingAxes(PrePostScheduleRequirements requirements, RequirementPriority priority, TaskContext taskContext, int inventorySlot) { - requirements.register(new ItemRequirement( - ItemID._3A_AXE, 1, - EquipmentInventorySlot.WEAPON, inventorySlot, priority, 10, "3rd age axe (best woodcutting axe available)", - taskContext, Skill.WOODCUTTING, 65, Skill.ATTACK, 65 // 3rd age axe requirements - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.CRYSTAL_AXE, ItemID.CRYSTAL_AXE_INACTIVE), 1, - EquipmentInventorySlot.WEAPON, inventorySlot, priority, 9, "Crystal axe (excellent for woodcutting)", - taskContext, Skill.WOODCUTTING, 71, Skill.ATTACK, 70 // Crystal axe requirements - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.INFERNAL_AXE, ItemID.INFERNAL_AXE_EMPTY), 1, - EquipmentInventorySlot.WEAPON, inventorySlot, priority, 8, "Infernal axe (burns logs automatically)", - taskContext, Skill.WOODCUTTING, 61, Skill.ATTACK, 60 // Infernal axe requirements - )); - requirements.register(new ItemRequirement( - ItemID.DRAGON_AXE, 1, - EquipmentInventorySlot.WEAPON, inventorySlot, - priority, 8, "Dragon axe (excellent for woodcutting)", - taskContext, Skill.WOODCUTTING, 61, Skill.ATTACK, 60 // Dragon axe requirements - )); - requirements.register(new ItemRequirement( - ItemID.RUNE_AXE, 1, - EquipmentInventorySlot.WEAPON, inventorySlot, priority, 6, "Rune axe (good for woodcutting)", - taskContext, Skill.WOODCUTTING, 41, Skill.ATTACK, 40 // Rune axe requirements - )); - requirements.register(new ItemRequirement( - ItemID.ADAMANT_AXE, 1, - EquipmentInventorySlot.WEAPON, inventorySlot, priority, 4, "Adamant axe (adequate for woodcutting)", - taskContext, Skill.WOODCUTTING, 31, Skill.ATTACK, 30 // Adamant axe requirements - )); - requirements.register(new ItemRequirement( - ItemID.MITHRIL_AXE, 1, - EquipmentInventorySlot.WEAPON, inventorySlot, priority, 4, "Mithril axe (adequate for woodcutting)", - taskContext, Skill.WOODCUTTING, 21, Skill.ATTACK, 20 // Mithril axe requirements - )); - requirements.register(new ItemRequirement( - ItemID.BLACK_AXE, 1, - EquipmentInventorySlot.WEAPON, inventorySlot, priority, 3, "Black axe (for woodcutting)", - taskContext, Skill.WOODCUTTING, 6, Skill.ATTACK, 10 // Black axe requirements - )); - requirements.register(new ItemRequirement( - ItemID.STEEL_AXE, 1, - EquipmentInventorySlot.WEAPON, inventorySlot, priority, 2, "Steel axe (for woodcutting)", - taskContext, Skill.WOODCUTTING, 6, Skill.ATTACK, 5 // Steel axe requirements - )); - requirements.register(new ItemRequirement( - ItemID.IRON_AXE, 1, - EquipmentInventorySlot.WEAPON, inventorySlot, priority, 2, "Iron axe (for woodcutting)", - taskContext, Skill.WOODCUTTING, 1, Skill.ATTACK, 1 // Iron axe requirements - )); - requirements.register(new ItemRequirement( - ItemID.BRONZE_AXE, 1, - EquipmentInventorySlot.WEAPON, inventorySlot, priority, 1, "Bronze axe (basic woodcutting axe)", - taskContext - // No skill requirement for bronze axe - anyone can use it - )); - } - - /** - * Registers the complete Graceful outfit for the plugin scheduler. - * Includes all regular graceful pieces plus all Zeah house variants. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the graceful outfit (MANDATORY, RECOMMENDED, or OPTIONAL) - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerGracefulOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, TaskContext taskContext,int rating) { - registerGracefulOutfit(requirements, priority, taskContext, rating, false, false, false, false, false, false); - } - - /** - * Registers the complete Graceful outfit for the plugin scheduler with convenience flags. - * Includes all regular graceful pieces plus all Zeah house variants. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the graceful outfit (MANDATORY, RECOMMENDED, or OPTIONAL) - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param skipHead Skip head slot if true - * @param skipCape Skip cape slot if true - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - * @param skipGloves Skip gloves slot if true - * @param skipBoots Skip boots slot if true - */ - public static void registerGracefulOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, TaskContext taskContext, int rating, - boolean skipHead, boolean skipCape, boolean skipBody, - boolean skipLegs, boolean skipGloves, boolean skipBoots) { - - // Combined Graceful outfit (all variants in one requirement) - if (!skipHead) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.GRACEFUL_HOOD, ItemID.ZEAH_GRACEFUL_HOOD_ARCEUUS, ItemID.ZEAH_GRACEFUL_HOOD_PISCARILIUS, - ItemID.ZEAH_GRACEFUL_HOOD_LOVAKENGJ, ItemID.ZEAH_GRACEFUL_HOOD_SHAYZIEN, - ItemID.ZEAH_GRACEFUL_HOOD_HOSIDIUS, ItemID.ZEAH_GRACEFUL_HOOD_KOUREND), 1, - EquipmentInventorySlot.HEAD, -2, - priority, rating, "Graceful hood (weight reduction and run energy restoration)", - taskContext - )); - } - if (!skipCape) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.GRACEFUL_CAPE, ItemID.ZEAH_GRACEFUL_CAPE_ARCEUUS, ItemID.ZEAH_GRACEFUL_CAPE_PISCARILIUS, - ItemID.ZEAH_GRACEFUL_CAPE_LOVAKENGJ, ItemID.ZEAH_GRACEFUL_CAPE_SHAYZIEN, - ItemID.ZEAH_GRACEFUL_CAPE_HOSIDIUS, ItemID.ZEAH_GRACEFUL_CAPE_KOUREND),1, - EquipmentInventorySlot.CAPE, -2, - priority, rating, "Graceful cape (weight reduction and run energy restoration)", - taskContext - )); - } - if (!skipBody) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.GRACEFUL_TOP, ItemID.ZEAH_GRACEFUL_TOP_ARCEUUS, ItemID.ZEAH_GRACEFUL_TOP_PISCARILIUS, - ItemID.ZEAH_GRACEFUL_TOP_LOVAKENGJ, ItemID.ZEAH_GRACEFUL_TOP_SHAYZIEN, - ItemID.ZEAH_GRACEFUL_TOP_HOSIDIUS, ItemID.ZEAH_GRACEFUL_TOP_KOUREND),1, - EquipmentInventorySlot.BODY,-2, - priority, rating, "Graceful top (weight reduction and run energy restoration)", - taskContext - )); - } - if (!skipLegs) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.GRACEFUL_LEGS, ItemID.ZEAH_GRACEFUL_LEGS_ARCEUUS, ItemID.ZEAH_GRACEFUL_LEGS_PISCARILIUS, - ItemID.ZEAH_GRACEFUL_LEGS_LOVAKENGJ, ItemID.ZEAH_GRACEFUL_LEGS_SHAYZIEN, - ItemID.ZEAH_GRACEFUL_LEGS_HOSIDIUS, ItemID.ZEAH_GRACEFUL_LEGS_KOUREND),1, - EquipmentInventorySlot.LEGS,-2, - priority, rating, "Graceful legs (weight reduction and run energy restoration)", - taskContext - )); - } - if (!skipGloves) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.GRACEFUL_GLOVES, ItemID.ZEAH_GRACEFUL_GLOVES_ARCEUUS, ItemID.ZEAH_GRACEFUL_GLOVES_PISCARILIUS, - ItemID.ZEAH_GRACEFUL_GLOVES_LOVAKENGJ, ItemID.ZEAH_GRACEFUL_GLOVES_SHAYZIEN, - ItemID.ZEAH_GRACEFUL_GLOVES_HOSIDIUS, ItemID.ZEAH_GRACEFUL_GLOVES_KOUREND), 1, - EquipmentInventorySlot.GLOVES,-2, - priority, rating, "Graceful gloves (weight reduction and run energy restoration)", - taskContext - )); - } - if (!skipBoots) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.GRACEFUL_BOOTS, ItemID.ZEAH_GRACEFUL_BOOTS_ARCEUUS, ItemID.ZEAH_GRACEFUL_BOOTS_PISCARILIUS, - ItemID.ZEAH_GRACEFUL_BOOTS_LOVAKENGJ, ItemID.ZEAH_GRACEFUL_BOOTS_SHAYZIEN, - ItemID.ZEAH_GRACEFUL_BOOTS_HOSIDIUS, ItemID.ZEAH_GRACEFUL_BOOTS_KOUREND), 1, - EquipmentInventorySlot.BOOTS, -2, - priority, rating, "Graceful boots (weight reduction and run energy restoration)", - taskContext - )); - } - } - - /** - * Registers the complete Runecrafting Outfit (Robes of the Eye) for the plugin scheduler. - * This is the specialized outfit for runecrafting activities. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the runecrafting outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerRunecraftingOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, TaskContext taskContext) { - registerRunecraftingOutfit(requirements, priority, taskContext, false, false, false); - } - - /** - * Registers the complete Runecrafting Outfit (Robes of the Eye) for the plugin scheduler with convenience flags. - * This is the specialized outfit for runecrafting activities. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the runecrafting outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param skipHead Skip head slot if true - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - */ - public static void registerRunecraftingOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, TaskContext taskContext, - boolean skipHead, boolean skipBody, boolean skipLegs) { - // Original (default) color variants - highest priority - if (!skipHead) { - requirements.register(new ItemRequirement( - ItemID.HAT_OF_THE_EYE, 1, - EquipmentInventorySlot.HEAD, - priority, 10, "Hat of the Eye (optimal for runecrafting)", - taskContext - )); - } - if (!skipBody) { - requirements.register(new ItemRequirement( - ItemID.ROBE_TOP_OF_THE_EYE, 1, - EquipmentInventorySlot.BODY, - priority, 10, "Robe top of the Eye (optimal for runecrafting)", - taskContext - )); - } - if (!skipLegs) { - requirements.register(new ItemRequirement( - ItemID.ROBE_BOTTOM_OF_THE_EYE, 1, - EquipmentInventorySlot.LEGS, - priority, 10, "Robe bottoms of the Eye (optimal for runecrafting)", - taskContext - )); - } - - // Colored variants (red, green, blue) - slightly lower priority - int coloredVariantRating = Math.max(1, priority == RequirementPriority.MANDATORY ? 8 : priority == RequirementPriority.RECOMMENDED ? 6 : 4); - - if (!skipHead) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.HAT_OF_THE_EYE_RED, ItemID.HAT_OF_THE_EYE_GREEN, ItemID.HAT_OF_THE_EYE_BLUE), - EquipmentInventorySlot.HEAD, - priority, coloredVariantRating, "Hat of the Eye (colored variants)", - taskContext - )); - } - if (!skipBody) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.ROBE_TOP_OF_THE_EYE_RED, ItemID.ROBE_TOP_OF_THE_EYE_GREEN, ItemID.ROBE_TOP_OF_THE_EYE_BLUE), - EquipmentInventorySlot.BODY, - priority, coloredVariantRating, "Robe top of the Eye (colored variants)", - taskContext - )); - } - if (!skipLegs) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.ROBE_BOTTOM_OF_THE_EYE_RED, ItemID.ROBE_BOTTOM_OF_THE_EYE_GREEN, ItemID.ROBE_BOTTOM_OF_THE_EYE_BLUE), - EquipmentInventorySlot.LEGS, - priority, coloredVariantRating, "Robe bottoms of the Eye (colored variants)", - taskContext - )); - } - } - - /** - * Registers the complete Lumberjack Outfit for the plugin scheduler. - * This is the specialized outfit for woodcutting activities. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the lumberjack outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerLumberjackOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext) { - registerLumberjackOutfit(requirements, priority, rating, taskContext, false, false, false, false); - } - - /** - * Registers the complete Lumberjack Outfit for the plugin scheduler with convenience flags. - * This is the specialized outfit for woodcutting activities. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the lumberjack outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param skipHead Skip head slot if true - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - * @param skipBoots Skip boots slot if true - */ - public static void registerLumberjackOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext, - boolean skipHead, boolean skipBody, boolean skipLegs, boolean skipBoots) { - // Combined Lumberjack outfit (all variants in one requirement) - if (!skipHead) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.FORESTRY_LUMBERJACK_HAT, ItemID.RAMBLE_LUMBERJACK_HAT), - EquipmentInventorySlot.HEAD, - priority, rating, "Lumberjack hat - optimal for woodcutting XP", - taskContext - )); - } - if (!skipBody) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.FORESTRY_LUMBERJACK_TOP, ItemID.RAMBLE_LUMBERJACK_TOP), - EquipmentInventorySlot.BODY, - priority, rating, "Lumberjack top - optimal for woodcutting XP", - taskContext - )); - } - if (!skipLegs) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.FORESTRY_LUMBERJACK_LEGS, ItemID.RAMBLE_LUMBERJACK_LEGS), - EquipmentInventorySlot.LEGS, - priority, rating, "Lumberjack legs - optimal for woodcutting XP", - taskContext - )); - } - if (!skipBoots) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.FORESTRY_LUMBERJACK_BOOTS, ItemID.RAMBLE_LUMBERJACK_BOOTS), - EquipmentInventorySlot.BOOTS, - priority, rating, "Lumberjack boots - optimal for woodcutting XP", - taskContext - )); - } - } - - /** - * Registers the complete Angler Outfit for the plugin scheduler. - * This is the specialized outfit for fishing activities including all variants. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the angler outfit - */ - public static void registerAnglerOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext) { - registerAnglerOutfit(requirements, priority, rating, taskContext, false, false, false, false); - } - - /** - * Registers the complete Angler Outfit for the plugin scheduler with convenience flags. - * This is the specialized outfit for fishing activities including all variants. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the angler outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param skipHead Skip head slot if true - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - * @param skipBoots Skip boots slot if true - */ - public static void registerAnglerOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext, - boolean skipHead, boolean skipBody, boolean skipLegs, boolean skipBoots) { - // Spirit Angler outfit (enhanced version) - highest priority - int spiritRating = rating; - - if (!skipHead) { - requirements.register(new ItemRequirement( - ItemID.SPIRIT_ANGLER_HAT, 1, - EquipmentInventorySlot.HEAD, - priority, spiritRating, "Spirit angler hat - enhanced fishing XP bonus",taskContext - )); - } - if (!skipBody) { - requirements.register(new ItemRequirement( - ItemID.SPIRIT_ANGLER_TOP, 1, - EquipmentInventorySlot.BODY, - priority, spiritRating, "Spirit angler top - enhanced fishing XP bonus",taskContext - )); - } - if (!skipLegs) { - requirements.register(new ItemRequirement( - ItemID.SPIRIT_ANGLER_LEGS, 1, - EquipmentInventorySlot.LEGS, - priority, spiritRating, "Spirit angler legs - enhanced fishing XP bonus",taskContext - )); - } - if (!skipBoots) { - requirements.register(new ItemRequirement( - ItemID.SPIRIT_ANGLER_BOOTS, 1, - EquipmentInventorySlot.BOOTS, - priority, spiritRating, "Spirit angler boots - enhanced fishing XP bonus",taskContext - )); - } - - // Regular Angler outfit (Trawler reward) - int regularRating = Math.max(1,spiritRating-1); - - if (!skipHead) { - requirements.register(new ItemRequirement( - ItemID.TRAWLER_REWARD_HAT, 1, - EquipmentInventorySlot.HEAD, - priority, regularRating, "Angler hat - provides fishing XP bonus",taskContext - )); - } - if (!skipBody) { - requirements.register(new ItemRequirement( - ItemID.TRAWLER_REWARD_TOP, 1, - EquipmentInventorySlot.BODY, - priority, regularRating, "Angler top - provides fishing XP bonus",taskContext - )); - } - if (!skipLegs) { - requirements.register(new ItemRequirement( - ItemID.TRAWLER_REWARD_LEGS, 1, - EquipmentInventorySlot.LEGS, - priority, regularRating, "Angler legs - provides fishing XP bonus",taskContext - )); - } - if (!skipBoots) { - requirements.register(new ItemRequirement( - ItemID.TRAWLER_REWARD_BOOTS, 1, - EquipmentInventorySlot.BOOTS, - priority, regularRating, "Angler boots - provides fishing XP bonus",taskContext - )); - } - } - - /** - * Registers high-healing food items for combat and survival activities. - * Uses the Rs2Food enum to get the most effective food options. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for food items - * @param quantity The quantity of food to require - */ - public static void registerHighHealingFood(PrePostScheduleRequirements requirements, RequirementPriority priority, TaskContext taskContext,int quantity) { - // Register food in order of healing effectiveness (highest heal values first) - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.Dark_Crab.getId()), quantity, - null, -1, priority, 10, "Dark crab - heals " + Rs2Food.Dark_Crab.getHeal() + " HP (best healing food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.ROCKTAIL.getId()), quantity, - null, -1, priority, 9, "Rocktail - heals " + Rs2Food.ROCKTAIL.getHeal() + " HP (excellent healing)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.MANTA.getId()), quantity, - null, -1, priority, 8, "Manta ray - heals " + Rs2Food.MANTA.getHeal() + " HP (very good healing)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.SEA_TURTLE.getId()), quantity, - null, -1, priority, 7, "Sea turtle - heals " + Rs2Food.SEA_TURTLE.getHeal() + " HP (good healing)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.SHARK.getId()), quantity, - null, -1, priority, 6, "Shark - heals " + Rs2Food.SHARK.getHeal() + " HP (standard high-level food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.KARAMBWAN.getId()), quantity, - null, -1, priority, 5, "Cooked karambwan - heals " + Rs2Food.KARAMBWAN.getHeal() + " HP (can combo with other food)",taskContext - )); - } - - /** - * Registers mid-tier healing food items for general use. - * Includes commonly available and cost-effective food options. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for food items - * @param quantity The quantity of food to require - */ - public static void registerMidTierFood(PrePostScheduleRequirements requirements, RequirementPriority priority,TaskContext taskContext ,int quantity) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.MONKFISH.getId()), quantity, - null, -1, priority, 8, "Monkfish - heals " + Rs2Food.MONKFISH.getHeal() + " HP (good mid-tier food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.SWORDFISH.getId()), quantity, - null, -1, priority, 6, "Swordfish - heals " + Rs2Food.SWORDFISH.getHeal() + " HP (decent mid-tier food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.BASS.getId()), quantity, - null, -1, priority, 5, "Bass - heals " + Rs2Food.BASS.getHeal() + " HP (alternative mid-tier food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.LOBSTER.getId()), quantity, - null, -1, priority, 4, "Lobster - heals " + Rs2Food.LOBSTER.getHeal() + " HP (common mid-tier food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.TUNA.getId()), quantity, - null, -1, priority, 3, "Tuna - heals " + Rs2Food.TUNA.getHeal() + " HP (affordable mid-tier food)",taskContext - )); - } - - /** - * Registers fast food items that can be eaten in 1 tick. - * Useful for combo eating or quick healing during combat. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for food items - * @param quantity The quantity of food to require - */ - public static void registerFastFood(PrePostScheduleRequirements requirements, RequirementPriority priority, TaskContext taskContext,int quantity) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.KARAMBWAN.getId()), quantity, - null, -1, priority, 9, "Cooked karambwan - heals " + Rs2Food.KARAMBWAN.getHeal() + " HP (1-tick food, good for combos)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.CHOCOLATE_CAKE.getId()), quantity, - null, -1, priority, 6, "Chocolate cake - heals " + Rs2Food.CHOCOLATE_CAKE.getHeal() + " HP (1-tick food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.CAKE.getId()), quantity, - null, -1, priority, 5, "Cake - heals " + Rs2Food.CAKE.getHeal() + " HP (1-tick food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.PLAIN_PIZZA.getId()), quantity, - null, -1, priority, 4, "Plain pizza - heals " + Rs2Food.PLAIN_PIZZA.getHeal() + " HP (1-tick food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.MEAT_PIZZA.getId()), quantity, - null, -1, priority, 4, "Meat pizza - heals " + Rs2Food.MEAT_PIZZA.getHeal() + " HP (1-tick food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.ANCHOVY_PIZZA.getId()), quantity, - null, -1, priority, 4, "Anchovy pizza - heals " + Rs2Food.ANCHOVY_PIZZA.getHeal() + " HP (1-tick food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.PINEAPPLE_PIZZA.getId()), quantity, - null, -1, priority, 5, "Pineapple pizza - heals " + Rs2Food.PINEAPPLE_PIZZA.getHeal() + " HP (1-tick food)",taskContext - )); - } - - /** - * Registers basic/emergency food items for low-level activities. - * Includes cheap and easily obtainable food options. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for food items - * @param quantity The quantity of food to require - */ - public static void registerBasicFood(PrePostScheduleRequirements requirements, RequirementPriority priority, TaskContext taskContext,int quantity) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.SALMON.getId()), quantity, - null, -1, priority, 5, "Salmon - heals " + Rs2Food.SALMON.getHeal() + " HP (basic food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.TROUT.getId()), quantity, - null, -1, priority, 4, "Trout - heals " + Rs2Food.TROUT.getHeal() + " HP (basic food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.PIKE.getId()), quantity, - null, -1, priority, 4, "Pike - heals " + Rs2Food.PIKE.getHeal() + " HP (basic food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.COD.getId()), quantity, - null, -1, priority, 3, "Cod - heals " + Rs2Food.COD.getHeal() + " HP (basic food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.HERRING.getId()), quantity, - null, -1, priority, 3, "Herring - heals " + Rs2Food.HERRING.getHeal() + " HP (basic food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.SARDINE.getId()), quantity, - null, -1, priority, 2, "Sardine - heals " + Rs2Food.SARDINE.getHeal() + " HP (basic food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.SHRIMPS.getId()), quantity, - null, -1, priority, 2, "Shrimps - heals " + Rs2Food.SHRIMPS.getHeal() + " HP (basic food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.BREAD.getId()), quantity, - null, -1, priority, 2, "Bread - heals " + Rs2Food.BREAD.getHeal() + " HP (emergency food)",taskContext - )); - } - - /** - * Registers runes required for NPC Contact spell, which is used for pouch repair. - * This is recommended for efficiency in the Guardians of the Rift minigame. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - */ - public static void registerRunesForNPCContact(PrePostScheduleRequirements requirements, RequirementPriority priority,TaskContext taskContext, int rating) { - // NPC Contact runes for pouch repair (recommended for efficiency) - requirements.register(new ItemRequirement( - ItemID.COSMICRUNE, - 1, -1, priority, rating, "Cosmic runes (for NPC Contact spell)",taskContext - )); - requirements.register(new ItemRequirement( - ItemID.ASTRALRUNE, - 1, -1, priority, rating, "Astral runes (for NPC Contact spell)",taskContext - )); - requirements.register(new ItemRequirement( - ItemID.AIRRUNE, - 1, -1, priority, rating, "Air runes (for NPC Contact spell)",taskContext - )); - } - - /** - * Registers rune pouches for efficient runecrafting. - * This includes all pouch types with their level requirements. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the rune pouches - */ - public static void registerRunePouches(PrePostScheduleRequirements requirements, RequirementPriority priority,TaskContext taskContext) { - // Rune pouches for efficient rune storage - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.RCU_POUCH_COLOSSAL, ItemID.RCU_POUCH_COLOSSAL_DEGRADE),1, - -1, priority, 10, "Colossal pouch (for maximum essence carrying)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.RCU_POUCH_GIANT, ItemID.RCU_POUCH_GIANT_DEGRADE),1, - -1, priority, 8, "Giant pouch (for high essence carrying)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.RCU_POUCH_LARGE, ItemID.RCU_POUCH_LARGE_DEGRADE),1, - -1, priority, 6, "Large pouch (for good essence carrying)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.RCU_POUCH_MEDIUM, ItemID.RCU_POUCH_MEDIUM_DEGRADE),1, - -1, priority, 4, "Medium pouch (for decent essence carrying)",taskContext - )); - requirements.register(new ItemRequirement( - ItemID.RCU_POUCH_SMALL, - 1, -1, priority, 2, "Small pouch (basic essence carrying)",taskContext - )); - } - - /** - * Registers the Varrock diary armour for the plugin scheduler. - * This includes all tiers of Varrock diary armour (Easy, Medium, Hard, Elite). - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the Varrock armour - */ - public static void registerVarrockDiaryArmour(PrePostScheduleRequirements requirements, RequirementPriority priority,TaskContext taskContext) { - // Varrock armour progression (Elite > Hard > Medium > Easy) - requirements.register(new ItemRequirement( - ItemID.VARROCK_ARMOUR_ELITE, 1, - EquipmentInventorySlot.BODY, priority, 10, "Varrock armour 4 (Elite) - best diary armour with all benefits",taskContext - )); - requirements.register(new ItemRequirement( - ItemID.VARROCK_ARMOUR_HARD, 1, - EquipmentInventorySlot.BODY, priority, 8, "Varrock armour 3 (Hard) - good diary armour",taskContext - )); - requirements.register(new ItemRequirement( - ItemID.VARROCK_ARMOUR_MEDIUM, 1, - EquipmentInventorySlot.BODY, priority, 6, "Varrock armour 2 (Medium) - decent diary armour",taskContext - )); - requirements.register(new ItemRequirement( - ItemID.VARROCK_ARMOUR_EASY, 1, - EquipmentInventorySlot.BODY, priority, 4, "Varrock armour 1 (Easy) - basic diary armour",taskContext - )); - } - - /** - * Registers the complete Prospector/Motherlode Mine outfit for the plugin scheduler. - * This includes all variants (regular, gold, and fossil variants). - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the prospector outfit - */ - public static void registerProspectorOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority,int rating,TaskContext taskContext) { - registerProspectorOutfit(requirements, priority,rating,taskContext, false, false, false, false); - } - - /** - * Registers the complete Prospector/Motherlode Mine outfit for the plugin scheduler with convenience flags. - * This includes all variants (regular, gold, and fossil variants). - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the prospector outfit - * @param skipHead Skip head slot if true - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - * @param skipBoots Skip boots slot if true - */ - public static void registerProspectorOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority,int rating,TaskContext taskContext, - boolean skipHead, boolean skipBody, boolean skipLegs, boolean skipBoots) { - - - // Prospector outfit pieces for additional mining benefits - if (!skipHead) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.MOTHERLODE_REWARD_HAT, ItemID.MOTHERLODE_REWARD_HAT_GOLD, ItemID.FOSSIL_MOTHERLODE_REWARD_HAT), - EquipmentInventorySlot.HEAD, priority, rating, "Prospector helmet", taskContext - )); - } - if (!skipBody) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.MOTHERLODE_REWARD_TOP, ItemID.MOTHERLODE_REWARD_TOP_GOLD, ItemID.FOSSIL_MOTHERLODE_REWARD_TOP), - EquipmentInventorySlot.BODY, priority, rating, "Prospector jacket", taskContext - )); - } - if (!skipLegs) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.MOTHERLODE_REWARD_LEGS, ItemID.MOTHERLODE_REWARD_LEGS_GOLD, ItemID.FOSSIL_MOTHERLODE_REWARD_LEGS), - EquipmentInventorySlot.LEGS, priority, rating, "Prospector legs", taskContext - )); - } - if (!skipBoots) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.MOTHERLODE_REWARD_BOOTS, ItemID.MOTHERLODE_REWARD_BOOTS_GOLD, ItemID.FOSSIL_MOTHERLODE_REWARD_BOOTS), - EquipmentInventorySlot.BOOTS, priority, rating, "Prospector boots", taskContext - )); - } - - - } - public static void registerRunecraftingCape(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext) { - // Register runecrafting capes for additional benefits - // Skill capes for additional benefits - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.SKILLCAPE_RUNECRAFTING, ItemID.SKILLCAPE_RUNECRAFTING_TRIMMED),1, - EquipmentInventorySlot.CAPE, -2,priority, rating, - "Runecrafting cape (any variant)",taskContext,null,-1, - Skill.RUNECRAFT,99 )); - - } - public static void registerMiningCape(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext) { - // Register mining capes for additional benefits - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.SKILLCAPE_MINING, ItemID.SKILLCAPE_MINING_TRIMMED),1, - EquipmentInventorySlot.CAPE, -2,priority, rating, - "Mining cape (any variant)",taskContext,null,-1, - Skill.MINING,99 )); - } - - /** - * Registers the complete Pyromancer Outfit for the plugin scheduler. - * This is the specialized outfit for firemaking activities from Wintertodt. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the pyromancer outfit - * @param rating The rating for the pyromancer outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerPyromancerOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext) { - registerPyromancerOutfit(requirements, priority, rating, taskContext, false, false, false, false, false); - } - - /** - * Registers the complete Pyromancer Outfit for the plugin scheduler with convenience flags. - * This is the specialized outfit for firemaking activities from Wintertodt. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the pyromancer outfit - * @param rating The rating for the pyromancer outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param skipHead Skip head slot if true - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - * @param skipBoots Skip boots slot if true - * @param skipGloves Skip gloves slot if true - */ - public static void registerPyromancerOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext, - boolean skipHead, boolean skipBody, boolean skipLegs, boolean skipBoots, boolean skipGloves) { - if (!skipHead) { - requirements.register(new ItemRequirement( - ItemID.PYROMANCER_HOOD, 1, - EquipmentInventorySlot.HEAD, - priority, rating, "Pyromancer hood - provides firemaking XP bonus", - taskContext - )); - } - if (!skipBody) { - requirements.register(new ItemRequirement( - ItemID.PYROMANCER_TOP, 1, - EquipmentInventorySlot.BODY, - priority, rating, "Pyromancer garb - provides firemaking XP bonus", - taskContext - )); - } - if (!skipLegs) { - requirements.register(new ItemRequirement( - ItemID.PYROMANCER_BOTTOM, 1, - EquipmentInventorySlot.LEGS, - priority, rating, "Pyromancer robe - provides firemaking XP bonus", - taskContext - )); - } - if (!skipBoots) { - requirements.register(new ItemRequirement( - ItemID.PYROMANCER_BOOTS, 1, - EquipmentInventorySlot.BOOTS, - priority, rating, "Pyromancer boots - provides firemaking XP bonus", - taskContext - )); - } - if (!skipGloves) { - requirements.register(new ItemRequirement( - ItemID.PYROMANCER_GLOVES, 1, - EquipmentInventorySlot.GLOVES, - priority, rating, "Pyromancer gloves - provides firemaking XP bonus", - taskContext - )); - } - } - - /** - * Registers the complete Farmer's Outfit for the plugin scheduler. - * This is the specialized outfit for farming activities from Tithe Farm. - * Note: These are cosmetic farmer clothing items, not the actual skilling outfit. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the farmer's outfit - * @param rating The rating for the farmer's outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerFarmersOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext) { - registerFarmersOutfit(requirements, priority, rating, taskContext, false, false, false, false); - } - - /** - * Registers the complete Farmer's Outfit for the plugin scheduler with convenience flags. - * This is the specialized outfit for farming activities from Tithe Farm. - * Note: These are cosmetic farmer clothing items, not the actual skilling outfit. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the farmer's outfit - * @param rating The rating for the farmer's outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param skipHead Skip head slot if true - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - * @param skipBoots Skip boots slot if true - */ - public static void registerFarmersOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext, - boolean skipHead, boolean skipBody, boolean skipLegs, boolean skipBoots) { - if (!skipHead) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.TITHE_REWARD_HAT_MALE, ItemID.TITHE_REWARD_HAT_FEMALE), // Farmer's strawhats - EquipmentInventorySlot.HEAD, - priority, rating, "Farmer's strawhat - cosmetic farming item", - taskContext - )); - } - if (!skipBody) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.TITHE_REWARD_TORSO_MALE,ItemID.TITHE_REWARD_TORSO_FEMALE), // Farmer's jacket - EquipmentInventorySlot.BODY, - priority, rating, "Farmer's jacket - cosmetic farming item", - taskContext - )); - } - if (!skipLegs) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.TITHE_REWARD_LEGS_MALE, ItemID.TITHE_REWARD_LEGS_FEMALE), // Farmer's boro trousers - EquipmentInventorySlot.LEGS, - priority, rating, "Farmer's boro trousers - cosmetic farming item", - taskContext - )); - } - if (!skipBoots) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.TITHE_REWARD_FEET_MALE, ItemID.TITHE_REWARD_FEET_FEMALE), // Farmer's boots - EquipmentInventorySlot.BOOTS, - priority, rating, "Farmer's boots - cosmetic farming item", - taskContext - )); - } - } - - /** - * Registers the complete Carpenter's Outfit for the plugin scheduler. - * This is the specialized outfit for construction activities from Mahogany Homes. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the carpenter's outfit - * @param rating The rating for the carpenter's outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerCarpentersOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext) { - registerCarpentersOutfit(requirements, priority, rating, taskContext, false, false, false, false); - } - - /** - * Registers the complete Carpenter's Outfit for the plugin scheduler with convenience flags. - * This is the specialized outfit for construction activities from Mahogany Homes. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the carpenter's outfit - * @param rating The rating for the carpenter's outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param skipHead Skip head slot if true - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - * @param skipBoots Skip boots slot if true - */ - public static void registerCarpentersOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext, - boolean skipHead, boolean skipBody, boolean skipLegs, boolean skipBoots) { - if (!skipHead) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(24872), // Carpenter's helmet - EquipmentInventorySlot.HEAD, - priority, rating, "Carpenter's helmet - provides construction XP bonus", - taskContext - )); - } - if (!skipBody) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(24874), // Carpenter's shirt - EquipmentInventorySlot.BODY, - priority, rating, "Carpenter's shirt - provides construction XP bonus", - taskContext - )); - } - if (!skipLegs) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(24876), // Carpenter's trousers - EquipmentInventorySlot.LEGS, - priority, rating, "Carpenter's trousers - provides construction XP bonus", - taskContext - )); - } - if (!skipBoots) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(24878), // Carpenter's boots - EquipmentInventorySlot.BOOTS, - priority, rating, "Carpenter's boots - provides construction XP bonus", - taskContext - )); - } - } - - /** - * Registers the complete Zealot's Robes for the plugin scheduler. - * This is the specialized outfit for prayer activities from Shade Catacombs. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the zealot's robes - * @param rating The rating for the zealot's robes - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerZealotsRobes(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext) { - registerZealotsRobes(requirements, priority, rating, taskContext, false, false, false, false); - } - - /** - * Registers the complete Zealot's Robes for the plugin scheduler with convenience flags. - * This is the specialized outfit for prayer activities from Shade Catacombs. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the zealot's robes - * @param rating The rating for the zealot's robes - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param skipHead Skip head slot if true - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - * @param skipBoots Skip boots slot if true - */ - public static void registerZealotsRobes(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext, - boolean skipHead, boolean skipBody, boolean skipLegs, boolean skipBoots) { - if (!skipHead) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(25438), // Zealot's helm - EquipmentInventorySlot.HEAD, - priority, rating, "Zealot's helm - chance to save bones/ensouled heads", - taskContext - )); - } - if (!skipBody) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(25434), // Zealot's robe top - EquipmentInventorySlot.BODY, - priority, rating, "Zealot's robe top - chance to save bones/ensouled heads", - taskContext - )); - } - if (!skipLegs) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(25436), // Zealot's robe bottom - EquipmentInventorySlot.LEGS, - priority, rating, "Zealot's robe bottom - chance to save bones/ensouled heads", - taskContext - )); - } - if (!skipBoots) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(25440), // Zealot's boots - EquipmentInventorySlot.BOOTS, - priority, rating, "Zealot's boots - chance to save bones/ensouled heads", - taskContext - )); - } - } - - /** - * Registers the complete Rogue Equipment for the plugin scheduler. - * This is the specialized outfit for thieving activities from Rogues' Den. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the rogue equipment - * @param rating The rating for the rogue equipment - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerRogueEquipment(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext) { - registerRogueEquipment(requirements, priority, rating, taskContext, false, false, false, false, false); - } - - /** - * Registers the complete Rogue Equipment for the plugin scheduler with convenience flags. - * This is the specialized outfit for thieving activities from Rogues' Den. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the rogue equipment - * @param rating The rating for the rogue equipment - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param skipHead Skip head slot if true - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - * @param skipGloves Skip gloves slot if true - * @param skipBoots Skip boots slot if true - */ - public static void registerRogueEquipment(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext, - boolean skipHead, boolean skipBody, boolean skipLegs, boolean skipGloves, boolean skipBoots) { - if (!skipHead) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(5554), // Rogue mask - EquipmentInventorySlot.HEAD, - priority, rating, "Rogue mask - chance for double loot when pickpocketing", - taskContext - )); - } - if (!skipBody) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.ROGUESDEN_BODY), // Rogue top - EquipmentInventorySlot.BODY, - priority, rating, "Rogue top - chance for double loot when pickpocketing", - taskContext - )); - } - if (!skipLegs) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.ROGUESDEN_LEGS), // Rogue trousers - EquipmentInventorySlot.LEGS, - priority, rating, "Rogue trousers - chance for double loot when pickpocketing", - taskContext - )); - } - if (!skipGloves) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.ROGUESDEN_GLOVES), // Rogue gloves - EquipmentInventorySlot.GLOVES, - priority, rating, "Rogue gloves - chance for double loot when pickpocketing", - taskContext - )); - } - if (!skipBoots) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.ROGUESDEN_BOOTS), // Rogue boots - EquipmentInventorySlot.BOOTS, - priority, rating, "Rogue boots - chance for double loot when pickpocketing", - taskContext - )); - } - } - - /** - * Registers the complete Smith's Uniform for the plugin scheduler. - * This is the specialized outfit for smithing activities from Giants' Foundry. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the smith's uniform - * @param rating The rating for the smith's uniform - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerSmithsUniform(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext) { - registerSmithsUniform(requirements, priority, rating, taskContext, false, false, false, false); - } - - /** - * Registers the complete Smith's Uniform for the plugin scheduler with convenience flags. - * This is the specialized outfit for smithing activities from Giants' Foundry. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the smith's uniform - * @param rating The rating for the smith's uniform - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - * @param skipGloves Skip gloves slot if true - * @param skipBoots Skip boots slot if true - */ - public static void registerSmithsUniform(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext, - boolean skipBody, boolean skipLegs, boolean skipGloves, boolean skipBoots) { - if (!skipBody) { - requirements.register(new ItemRequirement( - ItemID.SMITHING_UNIFORM_TORSO, 1, - EquipmentInventorySlot.BODY, - priority, rating, "Smith's uniform torso - speeds up smithing actions", - taskContext - )); - } - if (!skipLegs) { - requirements.register(new ItemRequirement( - ItemID.SMITHING_UNIFORM_LEGS, 1, - EquipmentInventorySlot.LEGS, - priority, rating, "Smith's uniform legs - speeds up smithing actions", - taskContext - )); - } - if (!skipGloves) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.SMITHING_UNIFORM_GLOVES, ItemID.SMITHING_UNIFORM_GLOVES_ICE), - EquipmentInventorySlot.GLOVES, - priority, rating, "Smith's uniform gloves - speeds up smithing actions", - taskContext - )); - } - if (!skipBoots) { - requirements.register(new ItemRequirement( - ItemID.SMITHING_UNIFORM_BOOTS, 1, - EquipmentInventorySlot.BOOTS, - priority, rating, "Smith's uniform boots - speeds up smithing actions", - taskContext - )); - } - } - /** - * Registers mid-tier healing food items using logical OR requirement. - * This demonstrates the new logical requirement system where only one food type is needed. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for food items - * @param TaskContext The schedule context for these requirements - * @param quantity The quantity of food to require - */ - public static void registerMidTierFoodLogical(PrePostScheduleRequirements requirements, RequirementPriority priority, - TaskContext taskContext, int quantity) { - // Create individual food item requirements - ItemRequirement monkfish = new ItemRequirement( - Rs2Food.MONKFISH.getId(), quantity, - null, -1, priority, 8, - "Monkfish - heals " + Rs2Food.MONKFISH.getHeal() + " HP (good mid-tier food)", taskContext - ); - - ItemRequirement swordfish = new ItemRequirement( - Rs2Food.SWORDFISH.getId(), quantity, - null, -1, priority, 6, - "Swordfish - heals " + Rs2Food.SWORDFISH.getHeal() + " HP (decent mid-tier food)", taskContext - ); - - ItemRequirement bass = new ItemRequirement( - Rs2Food.BASS.getId(), quantity, - null, -1, priority, 5, - "Bass - heals " + Rs2Food.BASS.getHeal() + " HP (alternative mid-tier food)", taskContext - ); - - ItemRequirement lobster = new ItemRequirement( - Rs2Food.LOBSTER.getId(), quantity, - null, -1, priority, 4, - "Lobster - heals " + Rs2Food.LOBSTER.getHeal() + " HP (common mid-tier food)", taskContext - ); - - ItemRequirement tuna =new ItemRequirement( - Rs2Food.TUNA.getId(), quantity, - null, -1, priority, 3, - "Tuna - heals " + Rs2Food.TUNA.getHeal() + " HP (affordable mid-tier food)", taskContext - ); - - // Create an OR requirement combining all food options - // Only one of these food types needs to be available - OrRequirement midTierFoodOptions = new OrRequirement( - priority, 8, "Mid-tier food options", taskContext, - monkfish, swordfish, bass, lobster, tuna - ); - - // Register the logical OR requirement - requirements.register(midTierFoodOptions); - } - - /** - * Demonstrates a more complex logical requirement with both AND and OR logic. - * This shows how you might require a complete combat setup where: - * - You need BOTH a weapon AND armor (AND requirement) - * - For each category, any suitable option will do (OR requirements) - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the combat setup - * @param TaskContext The schedule context for these requirements - */ - public static void registerCombatSetupLogical(PrePostScheduleRequirements requirements, RequirementPriority priority, - TaskContext taskContext) { - // TODO: Implement combat setup requirements using ConditionalRequirement - // Example: Create weapon and armor requirements with OR logic for each category - // and AND logic between categories - - } - public static void registerStaminaPotions(PrePostScheduleRequirements requirements,int amount, RequirementPriority priority,int rating, - TaskContext taskContext,boolean preferLowerCharges) { - requirements.register(OrRequirement.fromItemIds( - Arrays.asList(ItemID._1DOSESTAMINA, - ItemID._2DOSESTAMINA, - ItemID._3DOSESTAMINA, - ItemID._4DOSESTAMINA),amount, - null, -1, - priority, rating, "Stamina potions for energy restoration", - taskContext, preferLowerCharges - )); - } - public static void registerRingOfDueling(PrePostScheduleRequirements requirements, RequirementPriority priority,int rating, - TaskContext taskContext,boolean preferLowerCharges) { - requirements.register(OrRequirement.fromItemIds( - Arrays.asList(ItemID.RING_OF_DUELING_8, ItemID.RING_OF_DUELING_7, ItemID.RING_OF_DUELING_6, - ItemID.RING_OF_DUELING_5, ItemID.RING_OF_DUELING_4, ItemID.RING_OF_DUELING_3, - ItemID.RING_OF_DUELING_2, ItemID.RING_OF_DUELING_1),1, - EquipmentInventorySlot.RING, -1, - priority, - rating, "Ring of dueling for teleports",taskContext,preferLowerCharges - - )); - - } - public static void registerAmuletOfGlory(PrePostScheduleRequirements requirements, RequirementPriority priority,int rating, - TaskContext taskContext,boolean preferLowerCharges) { - - // AMULET - Example with charged items - requirements.register( OrRequirement.fromItemIds( - Arrays.asList(ItemID.AMULET_OF_GLORY_6, - ItemID.AMULET_OF_GLORY_5, - ItemID.AMULET_OF_GLORY_4, - ItemID.AMULET_OF_GLORY_3, - ItemID.AMULET_OF_GLORY_2, - ItemID.AMULET_OF_GLORY_1),1, - EquipmentInventorySlot.AMULET, -1, - priority, rating, - "Amulet of glory for teleports", - taskContext, preferLowerCharges - )); - } - /** - * Registers a smart mining equipment conditional requirement that upgrades equipment based on player capabilities. - * This demonstrates the power of conditional requirements over simple AND/OR logic. - * - * Workflow: - * 1. Ensure basic pickaxe if none available - * 2. If player has sufficient GP and mining level, upgrade to better pickaxe - * 3. If player has high mining level, consider dragon pickaxe - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param TaskContext The schedule context for these requirements - */ - public static void registerSmartMiningEquipment(PrePostScheduleRequirements requirements, TaskContext taskContext) { - // Create a conditional requirement for smart mining equipment management - ConditionalRequirement smartMiningEquipment = ConditionalRequirementBuilder.createEquipmentUpgrader( - new int[]{ItemID.BRONZE_PICKAXE, ItemID.IRON_PICKAXE}, // Basic equipment - new int[]{ItemID.RUNE_PICKAXE, ItemID.DRAGON_PICKAXE}, // Upgrade equipment - 100000, // Min GP for upgrade - EquipmentInventorySlot.WEAPON, - "mining pickaxe", - RequirementPriority.RECOMMENDED, - taskContext - ); - - requirements.register(smartMiningEquipment); - } - - /** - * Registers a complete preparation workflow for wilderness activities using OrderedRequirement. - - * This shows how ordered requirements can manage complex preparation sequences. - * - * Order: - * 1. Bank valuable items - * 2. Withdraw wilderness supplies - * 3. Equip appropriate gear - * 4. Set up inventory - * 5. Move to wilderness entry point - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param TaskContext The schedule context for these requirements - */ - public static void registerWildernessPreparation(PrePostScheduleRequirements requirements, TaskContext taskContext) { - OrderedRequirement wildernessPrep = new OrderedRequirement( - RequirementPriority.MANDATORY, 9, "Complete Wilderness Preparation", taskContext - ); - /* - // Step 1: Bank valuable items first - wildernessPrep.addStep( - new ItemRequirement(RequirementType.INVENTORY, Priority.MANDATORY, 8, - "Bank valuable items before wilderness", Arrays.asList(), taskContext), - "Bank valuable items" - ); - - // Step 2: Withdraw wilderness food - wildernessPrep.addStep( - new ItemRequirement(RequirementType.INVENTORY, Priority.MANDATORY, 9, - "Food for wilderness survival", - Arrays.asList(ItemID.SHARK, ItemID.KARAMBWAN, ItemID.MANTA_RAY), taskContext), - "Withdraw food for wilderness" - ); - - // Step 3: Equip budget gear (optional step - can proceed without) - wildernessPrep.addOptionalStep( - new ItemRequirement(RequirementType.EQUIPMENT, Priority.OPTIONAL, 6, - "Budget wilderness combat gear", - Arrays.asList(ItemID.RUNE_SCIMITAR, ItemID.RUNE_FULL_HELM), taskContext), - "Equip budget wilderness gear" - ); */ - - // Step 4: Final location check - wildernessPrep.addStep( - new LocationRequirement(BankLocation.EDGEVILLE, true, -1, taskContext, RequirementPriority.MANDATORY), - "Move to wilderness entry point" - ); - - requirements.register(wildernessPrep); - } - - /** - * Registers a level-based spellbook progression using ConditionalRequirement. - * This demonstrates conditional logic based on player skill levels. - * - * Logic: - * - If Magic < 50: Stay on standard spellbook - * - If Magic 50-64: Consider Ancient spellbook for combat - * - If Magic 65+: Consider Lunar spellbook for utility - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param TaskContext The schedule context for these requirements - */ - public static void registerSmartSpellbookProgression(PrePostScheduleRequirements requirements, TaskContext taskContext) { - // Ancient spellbook conditional (for combat) - ConditionalRequirement ancientSpellbook = ConditionalRequirementBuilder.createLevelBasedRequirement( - Skill.MAGIC, 50, - new SpellbookRequirement(Rs2Spellbook.ANCIENT, taskContext, RequirementPriority.RECOMMENDED, 7, - "Ancient spellbook for combat spells"), - "Ancient spellbook for combat (Magic 50+)", - RequirementPriority.RECOMMENDED, - taskContext - ); - - // Lunar spellbook conditional (for utility) - ConditionalRequirement lunarSpellbook = ConditionalRequirementBuilder.createLevelBasedRequirement( - Skill.MAGIC, 65, - new SpellbookRequirement(Rs2Spellbook.LUNAR, taskContext, RequirementPriority.RECOMMENDED, 8, - "Lunar spellbook for utility spells"), - "Lunar spellbook for utility (Magic 65+)", - RequirementPriority.RECOMMENDED, - taskContext - ); - - requirements.register(ancientSpellbook); - requirements.register(lunarSpellbook); - } - - - - /** - * Creates a rune pouch requirement for teleportation magic. - * Requires various teleport runes. - */ - public static RunePouchRequirement createTeleportRunePouch() { - Map requiredRunes = new HashMap<>(); - requiredRunes.put(Runes.LAW, 50); // Law runes for teleports - requiredRunes.put(Runes.WATER, 50); // Water runes for various teleports - requiredRunes.put(Runes.AIR, 50); // Air runes for various teleports - requiredRunes.put(Runes.EARTH, 50); // Earth runes for various teleports - - return new RunePouchRequirement( - requiredRunes, - false, // Strict matching - no combination runes - RequirementPriority.MANDATORY, - 9, // Very high rating for essential teleportation - "Rune pouch for essential teleportation magic", - TaskContext.PRE_SCHEDULE - ); - } - - /** - * Creates a rune pouch requirement for alchemy training. - * Requires Nature runes and Fire runes for High Level Alchemy. - */ - public static void registerAlchemyRunePouch(int runeAmount,PrePostScheduleRequirements requirements, RequirementPriority priority,int rating, - TaskContext taskContext) { - Map requiredRunes = new HashMap<>(); - requiredRunes.put(Runes.NATURE, runeAmount); // 1000 nature runes for alchemy - requiredRunes.put(Runes.FIRE, runeAmount*5); // 5000 fire runes for alchemy - - RunePouchRequirement runePouchRequirement = new RunePouchRequirement( - requiredRunes, - false, // Dont Allow lava runes to substitute for fire runes - priority, - rating, - "Rune pouch for high level alchemy training", - taskContext - ); - requirements.register(runePouchRequirement); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/OrRequirementMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/OrRequirementMode.java deleted file mode 100644 index 8d89c0342cf..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/OrRequirementMode.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums; - -/** - * Defines how OR requirements should be planned and fulfilled. - */ -public enum OrRequirementMode { - /** - * ANY_COMBINATION mode: The total amount can be fulfilled by any combination of items in the OR requirement. - * For example, if 5 food items are needed, we could have 2 lobsters + 3 swordfish. - * This is the default mode and matches the current behavior. - */ - ANY_COMBINATION, - - /** - * SINGLE_TYPE mode: Must fulfill the entire amount with exactly one type of item from the OR requirement. - * For example, if 5 food items are needed, we must have exactly 5 lobsters OR 5 swordfish OR 5 monkfish, - * but not a combination. - */ - SINGLE_TYPE -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementMode.java deleted file mode 100644 index 1490aca1159..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementMode.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums; - -/** - * Defines the source mode for requirement fulfillment. - * This determines which cache (standard or external) to use for getting requirements. - */ -public enum RequirementMode { - /** - * Use only standard requirements from the main cache. - * This is the default for normal pre/post schedule requirement fulfillment. - */ - STANDARD, - - /** - * Use only external requirements from the external cache. - * This is used for externally added requirements that should not mix with standard ones. - */ - EXTERNAL, - - /** - * Use both standard and external requirements combined. - * This is rarely used but available for special cases where both sources are needed. - */ - BOTH -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementPriority.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementPriority.java deleted file mode 100644 index 4419b7c2e4d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementPriority.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums; - -/** - * Represents the priority level of a requirement. - * Used to determine how essential a requirement is for optimal plugin performance. - */ -public enum RequirementPriority { - /** - * Essential requirements that are absolutely required for the plugin to function. - * Plugin should not start or should warn user if these requirements are unavailable. - */ - MANDATORY, - - /** - * Important requirements that significantly improve plugin performance or efficiency. - * Plugin can function without these but with reduced effectiveness. - */ - RECOMMENDED -} - - diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementType.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementType.java deleted file mode 100644 index 1e221b39f62..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementType.java +++ /dev/null @@ -1,64 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums; - -/** - * Defines where a requirement needs to be located or what state it needs to be in - * for optimal gameplay. - */ -public enum RequirementType { - /** - * Requirement must be equipped in an equipment slot - */ - EQUIPMENT, - - /** - * Requirement must be in inventory but not necessarily equipped - */ - INVENTORY, - - /** - * Requirement can be either equipped or in inventory - */ - EITHER, - - /** - * Requirement is related to player state (skills, quests, etc.) - */ - PLAYER_STATE, - - /** - * Requirement is related to game configuration (spellbook, etc.) - */ - GAME_CONFIG, - - /** - * Requirement is related to player location (must be at specific world point) - */ - LOCATION, - - /** - * Logical OR requirement - at least one child requirement must be fulfilled - */ - OR_LOGICAL, - - /** - * Conditional requirement - executes requirements in sequence based on conditions - * This provides much more powerful workflow control than simple AND/OR logic - */ - CONDITIONAL, - - /** - * Shop requirement - buying or selling items from shops - */ - SHOP, - - /** - * Loot requirement - looting specific items from the ground or activities - */ - LOOT, - - /** - * Custom requirement - externally added requirements that should be fulfilled last - * These are added by plugins through the custom requirement registration system - */ - CUSTOM -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/TaskContext.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/TaskContext.java deleted file mode 100644 index 52f0c901825..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/TaskContext.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums; - -public enum TaskContext { - PRE_SCHEDULE, // Before script execution (start location) -> can also be meant to be used as a pre task in script - POST_SCHEDULE, // After script completion (end location) -> can also be ment to be use a post task in script - BOTH // Both before and after (if same location needed for both pre post schedule) -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/registry/RequirementRegistry.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/registry/RequirementRegistry.java deleted file mode 100644 index 03d0a5fa46a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/registry/RequirementRegistry.java +++ /dev/null @@ -1,2833 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.registry; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location.LocationRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.collection.LootRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.ConditionalRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.ShopRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.SpellbookRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.InventorySetupRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.RunePouchRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.LogicalRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.OrRequirement; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -import org.benf.cfr.reader.util.output.BytecodeDumpConsumer.Item; - -/** - * Enhanced requirement registry that manages all types of requirements with automatic - * uniqueness enforcement, consistency guarantees, and efficient lookup. - * - * This class solves the issues with the previous approach: - * - Eliminates duplication between specific collections and central registry - * - Enforces uniqueness automatically - * - Provides type-safe access while maintaining consistency - * - Simplifies requirement management - */ -@Slf4j -public class RequirementRegistry { - - /** - * Key class for ensuring requirement uniqueness. - * Two requirements are considered the same if they have the same type, - * schedule context, and core identity (defined by the requirement itself). - */ - public static class RequirementKey { - private final Class type; - private final TaskContext taskContext; - private final String identity; // Unique identifier from the requirement - - public RequirementKey(Requirement requirement) { - this.type = requirement.getClass(); - this.taskContext = requirement.hasTaskContext() ? requirement.getTaskContext() : null; - this.identity = requirement.getUniqueIdentifier(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - RequirementKey that = (RequirementKey) o; - return Objects.equals(type, that.type) && - Objects.equals(taskContext, that.taskContext) && - Objects.equals(identity, that.identity); - } - - @Override - public int hashCode() { - return Objects.hash(type, taskContext, identity); - } - - @Override - public String toString() { - return String.format("%s[%s:%s]", type.getSimpleName(), taskContext, identity); - } - } - - // Central storage - this is the single source of truth for standard requirements - private final Map requirements = new ConcurrentHashMap<>(); - - // Separate storage for externally added requirements to prevent mixing - private final Map externalRequirements = new ConcurrentHashMap<>(); - - // Standard requirements cached views for efficient access (rebuilt when requirements change) - private volatile Map>> equipmentItemsCache = new HashMap<>(); - private volatile Map>> inventorySlotRequirementsCache = new HashMap<>(); - private volatile Map> anyInventorySlotRequirementsCache = new HashMap<>(); - private volatile Map> shopRequirementsCache = new HashMap<>(); - private volatile Map> lootRequirementsCache = new HashMap<>(); - private volatile Map> conditionalItemRequirementsCache = new HashMap<>(); - - // External requirements cached views for efficient access (rebuilt when external requirements change) - private volatile Map>> externalEquipmentItemsCache = new HashMap<>(); - private volatile Map>> externalInventorySlotRequirementsCache = new HashMap<>(); - private volatile Map> externalAnyInventorySlotRequirementsCache = new HashMap<>(); - private volatile Map> externalShopRequirementsCache = new HashMap<>(); - private volatile Map> externalLootRequirementsCache = new HashMap<>(); - private volatile Map> externalIConditionalItemRequirementsCache = new HashMap<>(); - - // Single-instance requirements (enforced by registry) - @Getter - private volatile SpellbookRequirement preScheduleSpellbookRequirement = null; - @Getter - private volatile SpellbookRequirement postScheduleSpellbookRequirement = null; - @Getter - private volatile LocationRequirement preScheduleLocationRequirement = null; - @Getter - private volatile LocationRequirement postScheduleLocationRequirement = null; - @Getter - private volatile InventorySetupRequirement preScheduleInventorySetupRequirement = null; - @Getter - private volatile InventorySetupRequirement postScheduleInventorySetupRequirement = null; - - private volatile boolean cacheValid = false; - private volatile boolean externalCacheValid = false; - - /** - * Registers a requirement in the registry. - * Automatically handles uniqueness, categorization, and cache invalidation. - * Includes special validation for dummy items to ensure proper slot assignment. - * - * @param requirement The requirement to register - * @return true if the requirement was added (new), false if it replaced an existing one - */ - public boolean register(Requirement requirement) { - if (requirement == null) { - log.warn("Attempted to register null requirement"); - return false; - } - - // Special validation for dummy items - if (requirement instanceof ItemRequirement) { - ItemRequirement itemReq = (ItemRequirement) requirement; - if (itemReq.isDummyItemRequirement()) { - // Validate dummy item configuration - if (itemReq.getEquipmentSlot() == null && itemReq.getInventorySlot() == null) { - log.error("Dummy item requirement must specify either equipment slot or inventory slot"); - return false; - } - if (itemReq.getEquipmentSlot() != null && itemReq.getInventorySlot() != null && - itemReq.getInventorySlot() >= 0) { - log.error("Dummy item requirement cannot specify both equipment slot and specific inventory slot"); - return false; - } - log.debug("Registering dummy item requirement for slot: {} (equipment: {}, inventory: {})", - itemReq.getDescription(), itemReq.getEquipmentSlot(), itemReq.getInventorySlot()); - } - } - - RequirementKey key = new RequirementKey(requirement); - - // Special handling for single-instance requirements - if (requirement instanceof SpellbookRequirement) { - return registerSpellbookRequirement((SpellbookRequirement) requirement, key); - } else if (requirement instanceof LocationRequirement) { - return registerLocationRequirement((LocationRequirement) requirement, key); - } else if (requirement instanceof InventorySetupRequirement) { - return registerInventorySetupRequirement((InventorySetupRequirement) requirement, key); - } else if (requirement instanceof RunePouchRequirement) { - return registerRunePouchRequirement((RunePouchRequirement) requirement, key); - } - - // For multi-instance requirements, just add to central storage - Requirement previous = requirements.put(key, requirement); - invalidateCache(); - - if (previous != null) { - log.debug("Replaced existing requirement: {} -> {}", previous, requirement); - return false; - } else { - log.debug("Added new requirement: {}", requirement); - return true; - } - } - - /** - * Registers an externally added requirement in the registry. - * These requirements are tracked separately and fulfilled after all standard requirements. - * - * @param requirement The externally added requirement to register - * @return true if the requirement was added (new), false if it replaced an existing one - */ - public boolean registerExternal(Requirement requirement) { - if (requirement == null) { - log.warn("Attempted to register null external requirement"); - return false; - } - - RequirementKey key = new RequirementKey(requirement); - - // Store directly in external requirements map - Requirement previous = externalRequirements.put(key, requirement); - invalidateExternalCache(); - - if (previous != null) { - log.debug("Replaced external requirement: {} -> {}", previous.getDescription(), requirement.getDescription()); - return false; - } else { - log.debug("Registered new external requirement: {}", requirement.getDescription()); - return true; - } - } - - /** - * Gets all externally added requirements for a specific schedule context. - * These requirements should be fulfilled after all standard requirements. - * - * @param context The schedule context to filter by - * @return List of externally added requirements for the given context - */ - public List getExternalRequirements(TaskContext context) { - return externalRequirements.values().stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(Collectors.toList()); - } - - private boolean registerSpellbookRequirement(SpellbookRequirement requirement, RequirementKey key) { - boolean isPreSchedule = requirement.isPreSchedule(); - boolean isPostSchedule = requirement.isPostSchedule(); - - if (isPreSchedule && preScheduleSpellbookRequirement != null) { - log.warn("Replacing existing pre-schedule spellbook requirement: {} -> {}", - preScheduleSpellbookRequirement, requirement); - RequirementKey preScheduleKey = new RequirementKey(preScheduleSpellbookRequirement); - requirements.remove(preScheduleKey); // Remove old requirement to avoid duplicates - } - if (isPostSchedule && postScheduleSpellbookRequirement != null) { - log.warn("Replacing existing post-schedule spellbook requirement: {} -> {}", - postScheduleSpellbookRequirement, requirement); - RequirementKey postScheduleKey = new RequirementKey(postScheduleSpellbookRequirement); - requirements.remove(postScheduleKey); // Remove old requirement to avoid duplicates - } - - Requirement previous = requirements.put(key, requirement); - - if (isPreSchedule) { - preScheduleSpellbookRequirement = requirement; - } - if (isPostSchedule) { - postScheduleSpellbookRequirement = requirement; - } - - invalidateCache(); - return previous == null; - } - - private boolean registerLocationRequirement(LocationRequirement requirement, RequirementKey key) { - boolean isPreSchedule = requirement.isPreSchedule(); - boolean isPostSchedule = requirement.isPostSchedule(); - - if (isPreSchedule && preScheduleLocationRequirement != null) { - log.warn("Replacing existing pre-schedule location requirement: {} -> {}", - preScheduleLocationRequirement, requirement); - RequirementKey preRequirementKey = new RequirementKey(preScheduleLocationRequirement); - requirements.remove(preRequirementKey); // Remove old requirement to avoid duplicates - } - if (isPostSchedule && postScheduleLocationRequirement != null) { - log.warn("Replacing existing post-schedule location requirement: {} -> {}", - postScheduleLocationRequirement, requirement); - RequirementKey postRequirementKey = new RequirementKey(postScheduleLocationRequirement); - requirements.remove(postRequirementKey); // Remove old requirement to avoid duplicates - } - - Requirement previous = requirements.put(key, requirement); - - if (isPreSchedule) { - preScheduleLocationRequirement = requirement; - } - if (isPostSchedule) { - postScheduleLocationRequirement = requirement; - } - - invalidateCache(); - return previous == null; - } - - private boolean registerInventorySetupRequirement(InventorySetupRequirement requirement, RequirementKey key) { - boolean isPreSchedule = requirement.isPreSchedule(); - boolean isPostSchedule = requirement.isPostSchedule(); - - if (isPreSchedule && preScheduleInventorySetupRequirement != null) { - log.warn("Replacing existing pre-schedule inventory setup requirement: {} -> {}", - preScheduleInventorySetupRequirement, requirement); - RequirementKey preRequirementKey = new RequirementKey(preScheduleInventorySetupRequirement); - requirements.remove(preRequirementKey); // Remove old requirement to avoid duplicates - } - if (isPostSchedule && postScheduleInventorySetupRequirement != null) { - log.warn("Replacing existing post-schedule inventory setup requirement: {} -> {}", - postScheduleInventorySetupRequirement, requirement); - RequirementKey postRequirementKey = new RequirementKey(postScheduleInventorySetupRequirement); - requirements.remove(postRequirementKey); // Remove old requirement to avoid duplicates - } - - Requirement previous = requirements.put(key, requirement); - - if (isPreSchedule) { - preScheduleInventorySetupRequirement = requirement; - } - if (isPostSchedule) { - postScheduleInventorySetupRequirement = requirement; - } - - invalidateCache(); - return previous == null; - } - - private boolean registerRunePouchRequirement(RunePouchRequirement requirement, RequirementKey key) { - // Check if any RunePouchRequirement already exists - RunePouchRequirement existingRunePouchRequirement = requirements.values().stream() - .filter(r -> r instanceof RunePouchRequirement) - .map(r -> (RunePouchRequirement) r) - .findFirst() - .orElse(null); - - if (existingRunePouchRequirement != null) { - log.warn("Replacing existing rune pouch requirement: {} -> {}", - existingRunePouchRequirement, requirement); - RequirementKey existingKey = new RequirementKey(existingRunePouchRequirement); - requirements.remove(existingKey); // Remove old requirement to avoid duplicates - } - - Requirement previous = requirements.put(key, requirement); - invalidateCache(); - - log.debug("Registered rune pouch requirement: {}", requirement); - return previous == null; - } - - /** - * Removes a requirement from the registry. - * - * @param requirement The requirement to remove - * @return true if the requirement was removed, false if it wasn't found - */ - public boolean unregister(Requirement requirement) { - if (requirement == null) { - return false; - } - - RequirementKey key = new RequirementKey(requirement); - Requirement removed = requirements.remove(key); - - if (removed != null) { - // Clear single-instance references if applicable - if (removed == preScheduleSpellbookRequirement) { - preScheduleSpellbookRequirement = null; - } - if (removed == postScheduleSpellbookRequirement) { - postScheduleSpellbookRequirement = null; - } - if (removed == preScheduleLocationRequirement) { - preScheduleLocationRequirement = null; - } - if (removed == postScheduleLocationRequirement) { - postScheduleLocationRequirement = null; - } - if (removed == preScheduleInventorySetupRequirement) { - preScheduleInventorySetupRequirement = null; - } - if (removed == postScheduleInventorySetupRequirement) { - postScheduleInventorySetupRequirement = null; - } - - invalidateCache(); - log.debug("Removed requirement: {}", removed); - return true; - } - - return false; - } - - /** - * Gets all requirements of a specific type. - */ - @SuppressWarnings("unchecked") - public List getRequirements(Class clazz) { - return requirements.values().stream() - .filter(clazz::isInstance) - .map(req -> (T) req) - .collect(Collectors.toList()); - } - - /** - * Gets all requirements for a specific schedule context. - */ - public List getRequirements(TaskContext context) { - return requirements.values().stream() - .filter(req -> req.hasTaskContext() && - (req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH)) - .collect(Collectors.toList()); - } - - - /** - * Gets all standard (non-external) requirements of a specific type for a specific schedule context. - * This excludes externally added requirements to prevent double processing. - */ - @SuppressWarnings("unchecked") - public List getRequirements(Class clazz, TaskContext context) { - return requirements.values().stream() - .filter(clazz::isInstance) - .filter(req -> req.hasTaskContext() && - (req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH)) - .map(req -> (T) req) - .collect(Collectors.toList()); - } - - /** - * Gets all requirements in the registry. - */ - public LinkedHashSet getAllRequirements() { - return new LinkedHashSet<>(requirements.values()); - } - - /** - * Checks if a requirement exists in the registry. - */ - public boolean contains(Requirement requirement) { - if (requirement == null) { - return false; - } - return requirements.containsKey(new RequirementKey(requirement)); - } - - /** - * Gets the total number of requirements. - */ - public int size() { - return requirements.size(); - } - - /** - * Clears all requirements from the registry. - */ - public void clear() { - requirements.clear(); - preScheduleSpellbookRequirement = null; - postScheduleSpellbookRequirement = null; - preScheduleLocationRequirement = null; - postScheduleLocationRequirement = null; - preScheduleInventorySetupRequirement = null; - postScheduleInventorySetupRequirement = null; - invalidateCache(); - } - - /** - * Helper class to hold a complete set of caches returned by the unified rebuild method. - */ - private static class CacheSet { - final Map>> equipmentCache; - final Map>> inventorySlotCache; - final Map> anyInventorySlotCache; - final Map> shopCache; - final Map> lootCache; - final Map> conditionalCache; - - CacheSet(Map>> equipmentCache, - Map>> inventorySlotCache, - Map> anyInventorySlotCache, - Map> shopCache, - Map> lootCache, - Map> conditionalCache) { - this.equipmentCache = equipmentCache; - this.inventorySlotCache = inventorySlotCache; - this.anyInventorySlotCache = anyInventorySlotCache; - this.shopCache = shopCache; - this.lootCache = lootCache; - this.conditionalCache = conditionalCache; - } - } - - /** - * Unified cache rebuilding logic used by both standard and external cache rebuild methods. - * This method groups compatible requirements into logical requirements for better organization. - * - * @param requirementCollection The collection of requirements to process - * @param cacheType The type of cache being rebuilt ("standard" or "external") for logging - * @return A CacheSet containing all the rebuilt caches - */ - /** - * Unified cache rebuilding logic used by both standard and external cache rebuild methods. - * This method groups compatible requirements into logical requirements for better organization. - * - * @param requirementCollection The collection of requirements to process - * @param cacheType The type of cache being rebuilt ("standard" or "external") for logging - * @return A CacheSet containing all the rebuilt caches - */ - private CacheSet rebuildCacheUnified(Collection requirementCollection, String cacheType) { - // New caches with logical requirements - - - Map> newConditionalCache = new HashMap<>(); - - // Group requirements by schedule context FIRST, then by type and slot - Map>> newEquipmentCache = new HashMap<>(); - Map>> newInventorySlotCache = new HashMap<>(); - Map> newAnyInventorySlotCacheByContext = new HashMap<>(); - Map> newShopCache = new HashMap<>(); - Map> newLootCache = new HashMap<>(); - /*for (TaskContext context : TaskContext.values()) { - newEquipmentCache.put(context, new HashMap<>()); - newInventorySlotCache.put(context, new HashMap<>()); - newAnyInventorySlotCacheByContext.put(context, new ArrayList<>()); - newAnyInventorySlotCacheByContext.get(context).add(new OrRequirement( - RequirementPriority.MANDATORY, - 0, - "default mandatory any inventory slot requirement", - context, - ItemRequirement.class - )); - newAnyInventorySlotCacheByContext.get(context).add(new OrRequirement( - RequirementPriority.RECOMMENDED, - 0, - "default recommend any inventory slot requirement", - context, - ItemRequirement.class - )); - - newShopCache.put(context, new ArrayList<>()); - newShopCache.get(context).add(new OrRequirement( - RequirementPriority.MANDATORY, - 0, - "default mandatory shop requirement", - context, - ShopRequirement.class - )); - newShopCache.get(context).add(new OrRequirement( - RequirementPriority.RECOMMENDED, - 0, - "default recommended shop requirement", - context, - ShopRequirement.class - )); - newLootCache.put(context, new ArrayList<>()); - newLootCache.get(context).add(new OrRequirement( - RequirementPriority.MANDATORY, - 0, - "default mandatory loot requirement", - context, - LootRequirement.class - )); - newLootCache.get(context).add(new OrRequirement( - RequirementPriority.RECOMMENDED, - 0, - "default recommended loot requirement", - context, - LootRequirement.class - )); - newConditionalCache.put(context, new LinkedHashSet<>()); - - }*/ - // First pass: collect and group requirements by schedule context, then by slot - for (Requirement requirement : requirementCollection) { - if (requirement instanceof LogicalRequirement) { - // Handle existing LogicalRequirement - add it directly to appropriate cache based on its child requirements - LogicalRequirement logical = (LogicalRequirement) requirement; - TaskContext context = logical.getTaskContext(); - - if (logical.containsOnlyItemRequirements()) { - // Check if this is an OR requirement for flexible inventory items - List itemReqs = logical.getAllItemRequirements(); - if (!itemReqs.isEmpty()) { - // Check if all items are flexible inventory items (slot -1) - boolean allFlexible = itemReqs.stream() - .allMatch(item -> item.getRequirementType() == RequirementType.INVENTORY && - item.allowsAnyInventorySlot()); - - if (allFlexible && logical instanceof OrRequirement) { - // This is a flexible inventory OR requirement (like food) - add it directly - newAnyInventorySlotCacheByContext.computeIfAbsent(context, k -> new LinkedHashSet<>()) - .add((OrRequirement) logical); - log.debug("Added flexible inventory OR requirement: {} with {} items", - logical.getDescription(), itemReqs.size()); - } else { - // Mixed types, specific slots, or not an OrRequirement - decompose it - log.debug("Decomposing logical requirement: {}", logical.getDescription()); - decomposeLogicalRequirement(logical, newEquipmentCache, newInventorySlotCache, - newAnyInventorySlotCacheByContext, newShopCache, newLootCache); - } - } - } else { - // Non-item logical requirements - decompose them - log.debug("Decomposing non-item logical requirement: {}", logical.getDescription()); - decomposeLogicalRequirement(logical, newEquipmentCache, newInventorySlotCache, - newAnyInventorySlotCacheByContext, newShopCache, newLootCache); - } - } else if (requirement instanceof ConditionalRequirement) { - // Handle ConditionalRequirement - only cache if it contains only ItemRequirements - ConditionalRequirement conditionalReq = (ConditionalRequirement) requirement; - if (conditionalReq.containsOnlyItemRequirements()) { - newConditionalCache.computeIfAbsent(conditionalReq.getTaskContext(), k -> new LinkedHashSet<>()) - .add(conditionalReq); - // Log the caching of ConditionalRequirement with only ItemRequirements - log.debug("Cached ConditionalRequirement with only ItemRequirements: {}", conditionalReq.getName()); - } else { - log.debug("Skipped ConditionalRequirement with mixed requirement types for now: {}", conditionalReq.getName()); - } - } else if (requirement instanceof ItemRequirement) { - ItemRequirement itemReq = (ItemRequirement) requirement; - TaskContext context = itemReq.getTaskContext(); - int slot = -2; - switch (itemReq.getRequirementType()) { - case EQUIPMENT: - slot = itemReq.getInventorySlot(); - if( slot != -2) { - throw new IllegalArgumentException("Equipment requirement must not specify specific inventory slot"); - } - if (itemReq.getEquipmentSlot() != null) { - newEquipmentCache - .computeIfAbsent(context, k -> new HashMap<>()) - .computeIfAbsent(itemReq.getEquipmentSlot(), k -> new LinkedHashSet<>()) - .add(itemReq); - } - break; - case INVENTORY: - slot = itemReq.hasSpecificInventorySlot() ? itemReq.getInventorySlot() : -1; - if (slot != -1) { - newInventorySlotCache - .computeIfAbsent(context, k -> new HashMap<>()) - .computeIfAbsent(slot, k -> new LinkedHashSet<>()) - .add(itemReq); - } else { - OrRequirement orReq = new OrRequirement(itemReq.getPriority(), itemReq.getRating(), - itemReq.getName(), context, ItemRequirement.class); - orReq.addRequirement(itemReq); - newAnyInventorySlotCacheByContext.computeIfAbsent(context, k -> new LinkedHashSet<>()) - .add(orReq); - - } - break; - case EITHER: - slot = itemReq.hasSpecificInventorySlot() ? itemReq.getInventorySlot() : -1; - EquipmentInventorySlot equipmentSlot = itemReq.getEquipmentSlot(); - if (equipmentSlot== null || slot == -2) { - throw new IllegalArgumentException("Either requirement must specify either equipment slot or specific inventory slot"); - } - if (slot != -1) { - newInventorySlotCache - .computeIfAbsent(context, k -> new HashMap<>()) - .computeIfAbsent(slot, k -> new LinkedHashSet<>()) - .add(itemReq); - } else { - LinkedHashSet contextCache = newAnyInventorySlotCacheByContext.computeIfAbsent(context, k -> new LinkedHashSet<>()); - OrRequirement orReq = new OrRequirement(itemReq.getPriority(), itemReq.getRating(), - itemReq.getName(), context, ItemRequirement.class); - orReq.addRequirement(itemReq); - contextCache.add(orReq); - } - newEquipmentCache - .computeIfAbsent(context, k -> new HashMap<>()) - .computeIfAbsent(itemReq.getEquipmentSlot(), k -> new LinkedHashSet<>()) - .add(itemReq); - break; - case PLAYER_STATE: - case LOCATION: - case GAME_CONFIG: - // These are handled elsewhere - break; - case OR_LOGICAL: - log.warn("ItemRequirement with logical type: {}", itemReq); - break; - case SHOP: - case CONDITIONAL: - case LOOT: - case CUSTOM: - // These types are not expected for ItemRequirement - log.warn("Unexpected requirement type for ItemRequirement: {}", itemReq.getRequirementType()); - break; - } - } else if (requirement instanceof ShopRequirement) { - ShopRequirement shopReq = (ShopRequirement) requirement; - RequirementPriority priority = shopReq.getPriority(); - TaskContext context = shopReq.getTaskContext(); - - if( context == null) { - log.warn("ShopRequirement without a shop context: {}", shopReq); - continue; // Skip invalid shop requirements - } - - // Group by context and priority - LinkedHashSet contextCache = newShopCache.computeIfAbsent(context, k -> new LinkedHashSet<>()); - OrRequirement orReq = new OrRequirement(priority, 0, shopReq.getName(), context, ShopRequirement.class); - orReq.addRequirement(shopReq); - contextCache.add(orReq); - - } else if (requirement instanceof LootRequirement) { - LootRequirement lootReq = (LootRequirement) requirement; - RequirementPriority priority = lootReq.getPriority(); - TaskContext context = lootReq.getTaskContext(); - - if (context == null) { - log.warn("LootRequirement without a loot context: {}", lootReq); - continue; // Skip invalid loot requirements - } - - // Group by context and priority - LinkedHashSet contextCache = newLootCache.computeIfAbsent(context, k -> new LinkedHashSet<>()); - OrRequirement orReq = new OrRequirement(priority, 0, lootReq.getName(), context, LootRequirement.class); - orReq.addRequirement(lootReq); - contextCache.add(orReq); - } - } - - // Debug equipment grouping before creating logical requirements (only for standard cache) - if ("standard".equals(cacheType)) { - log.debug("=== EQUIPMENT GROUPING DEBUG ==="); - for (Map.Entry>> contextEntry : newEquipmentCache.entrySet()) { - TaskContext context = contextEntry.getKey(); - log.debug("Schedule Context: {}", context); - for (Map.Entry> slotEntry : contextEntry.getValue().entrySet()) { - EquipmentInventorySlot slot = slotEntry.getKey(); - LinkedHashSet slotItems = slotEntry.getValue(); - log.debug(" Slot {}: {} items", slot, slotItems.size()); - for (ItemRequirement item : slotItems) { - log.debug(" - {} (ID: {}, Priority: {}, Rating: {})", - item.getName(), item.getId(), item.getPriority(), item.getRating()); - } - } - } - log.debug("=== END EQUIPMENT GROUPING DEBUG ==="); - } - - // Second pass: create logical requirements from grouped items - //createLogicalRequirementsFromGroups(equipmentByContextAndSlot, inventoryByContextAndSlot, - // eitherByContext, shopByContext, lootByContext, newEquipmentCache, - // newShopCache, newLootCache, newInventorySlotCache); - - // Sort all caches - //sortAllCaches(newEquipmentCache, newShopCache, newLootCache, newInventorySlotCache); - - return new CacheSet(newEquipmentCache, newInventorySlotCache,newAnyInventorySlotCacheByContext,newShopCache, newLootCache, newConditionalCache); - } - - /** - * Invalidates cached views, forcing them to be rebuilt on next access. - */ - private void invalidateCache() { - cacheValid = false; - } - - /** - * Invalidates external cached views, forcing them to be rebuilt on next access. - */ - private void invalidateExternalCache() { - externalCacheValid = false; - } - - /** - * Rebuilds all cached views from the central requirements storage. - * This method groups compatible requirements into logical requirements for better organization. - * - * The new approach: - * - Equipment items for the same slot become OR requirements - * - Individual requirements are wrapped in logical requirements for consistency - * - Already logical requirements are categorized appropriately - */ - private synchronized void rebuildCache() { - if (cacheValid) { - return; // Another thread already rebuilt the cache - } - if(requirements ==null || requirements.isEmpty()) { - log.debug("No requirements to rebuild cache for - initializing empty caches"); - // Initialize empty caches when no requirements exist - equipmentItemsCache = new HashMap<>(); - shopRequirementsCache = new HashMap<>(); - lootRequirementsCache = new HashMap<>(); - inventorySlotRequirementsCache = new HashMap<>(); - anyInventorySlotRequirementsCache = new HashMap<>(); - conditionalItemRequirementsCache = new HashMap<>(); - - // Initialize empty collections for each context - for (TaskContext context : TaskContext.values()) { - equipmentItemsCache.put(context, new HashMap<>()); - shopRequirementsCache.put(context, new LinkedHashSet<>()); - lootRequirementsCache.put(context, new LinkedHashSet<>()); - inventorySlotRequirementsCache.put(context, new HashMap<>()); - anyInventorySlotRequirementsCache.put(context, new LinkedHashSet<>()); - conditionalItemRequirementsCache.put(context, new LinkedHashSet<>()); - } - - cacheValid = true; - return; - } - log.debug("Rebuilding requirement caches..."); - - // Use unified rebuild logic for standard requirements - CacheSet newCaches = rebuildCacheUnified(requirements.values(), "standard"); - - // Atomically update caches - equipmentItemsCache = newCaches.equipmentCache; - shopRequirementsCache = newCaches.shopCache; - lootRequirementsCache = newCaches.lootCache; - inventorySlotRequirementsCache = newCaches.inventorySlotCache; - anyInventorySlotRequirementsCache = newCaches.anyInventorySlotCache; - conditionalItemRequirementsCache = newCaches.conditionalCache; - - cacheValid = true; - log.debug("Rebuilt requirement caches with {} total requirements", requirements.size()); - } - - /** - * Decomposes a logical requirement into its child requirements and adds them to the appropriate maps - * for priority-based grouping. This method handles LogicalRequirements that cannot be kept as-is. - */ - private void decomposeLogicalRequirement(LogicalRequirement logical, - Map>> newEquipmentCache, - Map>> newInventorySlotCache, - Map> newAnyInventorySlotCacheByContext, - Map> newShopCache, - Map> newLootCache) { - - // Decompose the logical requirement's child requirements into individual requirements - for (Requirement child : logical.getChildRequirements()) { - if (child instanceof ItemRequirement) { - ItemRequirement itemReq = (ItemRequirement) child; - TaskContext context = itemReq.getTaskContext(); - int slot = -2; - - switch (itemReq.getRequirementType()) { - case EQUIPMENT: - slot = itemReq.getInventorySlot(); - if (slot != -2) { - throw new IllegalArgumentException("Equipment requirement must not specify specific inventory slot"); - } - if (itemReq.getEquipmentSlot() != null) { - newEquipmentCache - .computeIfAbsent(context, k -> new HashMap<>()) - .computeIfAbsent(itemReq.getEquipmentSlot(), k -> new LinkedHashSet<>()) - .add(itemReq); - } - log.debug("Decomposed EQUIPMENT requirement: {} for context {}", itemReq.getName(), context); - break; - case INVENTORY: - slot = itemReq.hasSpecificInventorySlot() ? itemReq.getInventorySlot() : -1; - if (slot != -1) { - newInventorySlotCache - .computeIfAbsent(context, k -> new HashMap<>()) - .computeIfAbsent(slot, k -> new LinkedHashSet<>()) - .add(itemReq); - } else { - // Flexible inventory item - wrap in OrRequirement - OrRequirement orReq = new OrRequirement(itemReq.getPriority(), itemReq.getRating(), - itemReq.getName(), context, ItemRequirement.class); - orReq.addRequirement(itemReq); - newAnyInventorySlotCacheByContext.computeIfAbsent(context, k -> new LinkedHashSet<>()) - .add(orReq); - } - break; - case EITHER: - slot = itemReq.hasSpecificInventorySlot() ? itemReq.getInventorySlot() : -1; - EquipmentInventorySlot equipmentSlot = itemReq.getEquipmentSlot(); - if (equipmentSlot == null && slot == -2) { - throw new IllegalArgumentException("Either requirement must specify either equipment slot or specific inventory slot"); - } - - // Add to equipment cache if equipment slot is specified - if (equipmentSlot != null) { - newEquipmentCache - .computeIfAbsent(context, k -> new HashMap<>()) - .computeIfAbsent(equipmentSlot, k -> new LinkedHashSet<>()) - .add(itemReq); - } - - // Add to inventory cache based on slot - if (slot != -1) { - newInventorySlotCache - .computeIfAbsent(context, k -> new HashMap<>()) - .computeIfAbsent(slot, k -> new LinkedHashSet<>()) - .add(itemReq); - } else { - // Flexible EITHER requirement - wrap in OrRequirement - OrRequirement orReq = new OrRequirement(itemReq.getPriority(), itemReq.getRating(), - itemReq.getName(), context, ItemRequirement.class); - orReq.addRequirement(itemReq); - newAnyInventorySlotCacheByContext.computeIfAbsent(context, k -> new LinkedHashSet<>()) - .add(orReq); - } - break; - default: - log.info("Skipping non-slot requirement type in decompose: {}", itemReq.getRequirementType()); - break; - } - } else if (child instanceof ShopRequirement) { - ShopRequirement shopReq = (ShopRequirement) child; - TaskContext context = shopReq.getTaskContext(); - - if (context == null) { - log.warn("ShopRequirement without context during decompose: {}", shopReq); - continue; - } - - OrRequirement orReq = new OrRequirement(shopReq.getPriority(), 0, - shopReq.getName(), context, ShopRequirement.class); - orReq.addRequirement(shopReq); - newShopCache.computeIfAbsent(context, k -> new LinkedHashSet<>()).add(orReq); - } else if (child instanceof LootRequirement) { - LootRequirement lootReq = (LootRequirement) child; - TaskContext context = lootReq.getTaskContext(); - - if (context == null) { - log.warn("LootRequirement without context during decompose: {}", lootReq); - continue; - } - - OrRequirement orReq = new OrRequirement(lootReq.getPriority(), 0, - lootReq.getName(), context, LootRequirement.class); - orReq.addRequirement(lootReq); - newLootCache.computeIfAbsent(context, k -> new LinkedHashSet<>()).add(orReq); - } else if (child instanceof LogicalRequirement) { - // Recursively decompose nested logical requirements - decomposeLogicalRequirement((LogicalRequirement) child, newEquipmentCache, - newInventorySlotCache, newAnyInventorySlotCacheByContext, newShopCache, newLootCache); - } - } - } - - /** - * Checks if an OR requirement contains only ItemRequirements. - * OR requirements created from ItemRequirement.createOrRequirement() have "total amount" semantics - * that should be preserved rather than being decomposed and regrouped. - * - * @param orReq The OR requirement to check - * @return true if the OR requirement contains only ItemRequirements, false otherwise - */ - private boolean isItemOnlyOrRequirement(OrRequirement orReq) { - for (Requirement child : orReq.getChildRequirements()) { - if (!(child instanceof ItemRequirement)) { - return false; - } - } - return true; - } - - /** - * Adds an OR requirement directly to the appropriate cache based on the type of items it contains. - * This preserves the original total amount semantics for OR requirements like "5 food from any combination". - * - * @param orReq The OR requirement to add directly to cache - * @param equipmentCache Equipment cache to update - * @param inventorySlotCache Inventory slot cache to update - */ - private void addOrRequirementDirectlyToCache(OrRequirement orReq, - Map> equipmentCache, - Map> inventorySlotCache) { - - // Determine the appropriate cache location based on the first child requirement - // All items in an OR requirement should target the same slot type - ItemRequirement firstItem = (ItemRequirement) orReq.getChildRequirements().get(0); - - switch (firstItem.getRequirementType()) { - case EQUIPMENT: - if (firstItem.getEquipmentSlot() != null) { - equipmentCache.computeIfAbsent(firstItem.getEquipmentSlot(), k -> new LinkedHashSet<>()).add(orReq); - log.debug("Added OR requirement directly to equipment slot {}: {}", - firstItem.getEquipmentSlot(), orReq.getName()); - } - break; - case INVENTORY: - int slot = firstItem.hasSpecificInventorySlot() ? firstItem.getInventorySlot() : -1; - inventorySlotCache.computeIfAbsent(slot, k -> new LinkedHashSet<>()).add(orReq); - log.debug("Added OR requirement directly to inventory slot {}: {}", - slot == -1 ? "any" : String.valueOf(slot), orReq.getName()); - break; - case EITHER: - // For EITHER requirements, add to both equipment and inventory slots as appropriate - if (firstItem.getEquipmentSlot() != null) { - equipmentCache.computeIfAbsent(firstItem.getEquipmentSlot(), k -> new LinkedHashSet<>()).add(orReq); - log.debug("Added EITHER OR requirement to equipment slot {}: {}", - firstItem.getEquipmentSlot(), orReq.getName()); - } - int invSlot = firstItem.hasSpecificInventorySlot() ? firstItem.getInventorySlot() : -1; - inventorySlotCache.computeIfAbsent(invSlot, k -> new LinkedHashSet<>()).add(orReq); - log.debug("Added EITHER OR requirement to inventory slot {}: {}", - invSlot == -1 ? "any" : String.valueOf(invSlot), orReq.getName()); - break; - default: - log.warn("Cannot add OR requirement to cache - unsupported requirement type: {}", - firstItem.getRequirementType()); - break; - } - } - - /** - * Wraps an individual ItemRequirement in an OrRequirement for consistency with cache structure. - * This allows all items in the cache to be treated uniformly as logical requirements. - * - * @param itemReq The ItemRequirement to wrap - * @return An OrRequirement containing the single ItemRequirement - */ - private OrRequirement wrapItemRequirementInOr(ItemRequirement itemReq) { - return new OrRequirement( - itemReq.getPriority(), - itemReq.getRating(), - itemReq.getDescription(), - itemReq.getTaskContext(), - itemReq - ); - } - - /** - * Legacy method - kept for potential future use but no longer used in main flow. - * Adds a logical requirement to the appropriate cache based on its content. - * This method analyzes the child requirements to determine the best placement. - */ - private void addLogicalRequirementToCache(LogicalRequirement logical, - Map> equipmentCache, - LinkedHashSet shopCache, - LinkedHashSet lootCache, - Map> inventorySlotCache) { - - // Analyze child requirements to determine placement - boolean hasEquipment = false; - boolean hasInventory = false; - boolean hasEither = false; - boolean hasShop = false; - boolean hasLoot = false; - EquipmentInventorySlot equipmentSlot = null; - Integer specificInventorySlot = null; - - for (Requirement child : logical.getChildRequirements()) { - if (child instanceof ItemRequirement) { - ItemRequirement item = (ItemRequirement) child; - switch (item.getRequirementType()) { - case EQUIPMENT: - hasEquipment = true; - if (equipmentSlot == null) { - equipmentSlot = item.getEquipmentSlot(); - } - break; - case INVENTORY: - hasInventory = true; - if (item.hasSpecificInventorySlot() && specificInventorySlot == null) { - specificInventorySlot = item.getInventorySlot(); - } - break; - case EITHER: - hasEither = true; - if (item.getEquipmentSlot() != null && equipmentSlot == null) { - equipmentSlot = item.getEquipmentSlot(); - } - if (item.hasSpecificInventorySlot() && specificInventorySlot == null) { - specificInventorySlot = item.getInventorySlot(); - } - break; - case PLAYER_STATE: - case LOCATION: - case GAME_CONFIG: - case OR_LOGICAL: - // These don't affect cache placement for items - break; - case SHOP: - case CONDITIONAL: - case LOOT: - case CUSTOM: - // These types are not expected for ItemRequirement - log.warn("Unexpected requirement type for ItemRequirement in logical: {}", item.getRequirementType()); - break; - } - } else if (child instanceof ShopRequirement) { - hasShop = true; - } else if (child instanceof LootRequirement) { - hasLoot = true; - } else if (child instanceof LogicalRequirement) { - // For nested logical requirements, recursively add them - addLogicalRequirementToCache((LogicalRequirement) child, equipmentCache, - shopCache, lootCache, inventorySlotCache); - return; // Don't add the parent logical requirement - } - } - - // Place in the most appropriate cache(s) - // EITHER items can be placed in multiple caches based on their capabilities - if (hasEquipment && equipmentSlot != null) { - equipmentCache.computeIfAbsent(equipmentSlot, k -> new LinkedHashSet<>()).add(logical); - } - - if (hasShop) { - shopCache.add(logical); - } else if (hasLoot) { - lootCache.add(logical); - } else if (hasInventory || hasEither) { - // Add to inventory slot cache (specific slot or -1 for any slot) - if (specificInventorySlot != null) { - // Add to specific slot cache - inventorySlotCache.computeIfAbsent(specificInventorySlot, k -> new LinkedHashSet<>()).add(logical); - } else { - // Add to any-slot cache (key = -1) - inventorySlotCache.computeIfAbsent(-1, k -> new LinkedHashSet<>()).add(logical); - } - } - } - - /** - * Creates logical requirements from grouped individual requirements organized by priority. - * This creates up to 2 OR requirements per slot: one for MANDATORY priority, one for RECOMMENDED priority. - */ - private void createLogicalRequirementsFromGroups( - Map>> equipmentByContextAndSlot, - Map>> inventoryByContextAndSlot, - Map> eitherByContext, - Map> shopByContext, - Map> lootByContext, - Map> equipmentCache, - LinkedHashSet shopCache, - LinkedHashSet lootCache, - Map> inventorySlotCache) { - - // Process equipment items: create consolidated PRE and POST OR requirements per slot - processEquipmentSlots(equipmentByContextAndSlot, eitherByContext, equipmentCache); - - // Process inventory items: create consolidated PRE and POST OR requirements per slot - processInventorySlots(inventoryByContextAndSlot, eitherByContext, inventorySlotCache); - - // Process shop and loot requirements (these don't need slot consolidation) - processShopAndLootRequirements(shopByContext, lootByContext, shopCache, lootCache); - } - - /** - * Processes equipment slots to create up to 2 OR requirements per slot (one for each priority level). - */ - private void processEquipmentSlots( - Map>> equipmentByContextAndSlot, - Map> eitherByContext, - Map> equipmentCache) { - - // Collect all equipment slots that have requirements - Set allEquipmentSlots = new HashSet<>(); - for (Map> slotMap : equipmentByContextAndSlot.values()) { - allEquipmentSlots.addAll(slotMap.keySet()); - } - - // Add slots from EITHER items - for (List eitherItems : eitherByContext.values()) { - for (ItemRequirement item : eitherItems) { - if (item.getEquipmentSlot() != null) { - allEquipmentSlots.add(item.getEquipmentSlot()); - } - } - } - - // For each equipment slot, create MANDATORY and RECOMMENDED OR requirements - for (EquipmentInventorySlot slot : allEquipmentSlots) { - // Collect items for MANDATORY priority (all schedule contexts) - List mandatoryItems = new ArrayList<>(); - addItemsForPriority(equipmentByContextAndSlot, slot, RequirementPriority.MANDATORY, mandatoryItems); - addEitherItemsForEquipmentSlotPriority(eitherByContext, slot, RequirementPriority.MANDATORY, mandatoryItems); - - // Collect items for RECOMMENDED priority (all schedule contexts) - List recommendedItems = new ArrayList<>(); - addItemsForPriority(equipmentByContextAndSlot, slot, RequirementPriority.RECOMMENDED, recommendedItems); - addEitherItemsForEquipmentSlotPriority(eitherByContext, slot, RequirementPriority.RECOMMENDED, recommendedItems); - - // Create OR requirements if items exist - LinkedHashSet slotRequirements = new LinkedHashSet<>(); - - if (!mandatoryItems.isEmpty()) { - OrRequirement mandatoryReq = createMergedOrRequirement(mandatoryItems, - slot.name() + " equipment (MANDATORY)", determineTaskContext(mandatoryItems)); - slotRequirements.add(mandatoryReq); - log.debug("Created MANDATORY equipment OR requirement for slot {}: {} with {} alternatives", - slot, mandatoryReq.getName(), mandatoryItems.size()); - } - - if (!recommendedItems.isEmpty()) { - OrRequirement recommendedReq = createMergedOrRequirement(recommendedItems, - slot.name() + " equipment (RECOMMENDED)", determineTaskContext(recommendedItems)); - slotRequirements.add(recommendedReq); - log.debug("Created RECOMMENDED equipment OR requirement for slot {}: {} with {} alternatives", - slot, recommendedReq.getName(), recommendedItems.size()); - } - - if (!slotRequirements.isEmpty()) { - equipmentCache.put(slot, slotRequirements); - } - } - } - - /** - * Processes inventory slots to create up to 2 OR requirements per slot (one for each priority level). - */ - private void processInventorySlots( - Map>> inventoryByContextAndSlot, - Map> eitherByContext, - Map> inventorySlotCache) { - - // Collect all inventory slots that have requirements - Set allInventorySlots = new HashSet<>(); - for (Map> slotMap : inventoryByContextAndSlot.values()) { - allInventorySlots.addAll(slotMap.keySet()); - } - - // Add slots from EITHER items - for (List eitherItems : eitherByContext.values()) { - for (ItemRequirement item : eitherItems) { - int invSlot = item.hasSpecificInventorySlot() ? item.getInventorySlot() : -1; - allInventorySlots.add(invSlot); - } - } - - // For each inventory slot, create MANDATORY and RECOMMENDED OR requirements - for (Integer slot : allInventorySlots) { - // Collect items for MANDATORY priority (all schedule contexts) - List mandatoryItems = new ArrayList<>(); - addInventoryItemsForPriority(inventoryByContextAndSlot, slot, RequirementPriority.MANDATORY, mandatoryItems); - addEitherItemsForInventorySlotPriority(eitherByContext, slot, RequirementPriority.MANDATORY, mandatoryItems); - - // Collect items for RECOMMENDED priority (all schedule contexts) - List recommendedItems = new ArrayList<>(); - addInventoryItemsForPriority(inventoryByContextAndSlot, slot, RequirementPriority.RECOMMENDED, recommendedItems); - addEitherItemsForInventorySlotPriority(eitherByContext, slot, RequirementPriority.RECOMMENDED, recommendedItems); - - // Create OR requirements if items exist - LinkedHashSet slotRequirements = new LinkedHashSet<>(); - String slotDescription = slot == -1 ? "any inventory slot" : "inventory slot " + slot; - - if (!mandatoryItems.isEmpty()) { - OrRequirement mandatoryReq = createMergedOrRequirement(mandatoryItems, - slotDescription + " (MANDATORY)", determineTaskContext(mandatoryItems)); - slotRequirements.add(mandatoryReq); - log.debug("Created MANDATORY inventory OR requirement for {}: {} with {} alternatives", - slotDescription, mandatoryReq.getName(), mandatoryItems.size()); - } - - if (!recommendedItems.isEmpty()) { - OrRequirement recommendedReq = createMergedOrRequirement(recommendedItems, - slotDescription + " (RECOMMENDED)", determineTaskContext(recommendedItems)); - slotRequirements.add(recommendedReq); - log.debug("Created RECOMMENDED inventory OR requirement for {}: {} with {} alternatives", - slotDescription, recommendedReq.getName(), recommendedItems.size()); - } - - if (!slotRequirements.isEmpty()) { - inventorySlotCache.put(slot, slotRequirements); - } - } - } - - // Helper methods for collecting items - private void addItemsForContext(Map>> equipmentByContextAndSlot, - EquipmentInventorySlot slot, TaskContext context, List targetList) { - Map> contextMap = equipmentByContextAndSlot.get(context); - if (contextMap != null) { - List items = contextMap.get(slot); - if (items != null) { - targetList.addAll(items); - } - } - } - - private void addInventoryItemsForContext(Map>> inventoryByContextAndSlot, - Integer slot, TaskContext context, List targetList) { - Map> contextMap = inventoryByContextAndSlot.get(context); - if (contextMap != null) { - List items = contextMap.get(slot); - if (items != null) { - targetList.addAll(items); - } - } - } - - private void addEitherItemsForEquipmentSlot(Map> eitherByContext, - EquipmentInventorySlot equipSlot, TaskContext context, List targetList) { - List eitherItems = eitherByContext.get(context); - if (eitherItems != null) { - for (ItemRequirement item : eitherItems) { - if (equipSlot.equals(item.getEquipmentSlot())) { - targetList.add(item); - } - } - } - } - - private void addEitherItemsForInventorySlot(Map> eitherByContext, - Integer invSlot, TaskContext context, List targetList) { - List eitherItems = eitherByContext.get(context); - if (eitherItems != null) { - for (ItemRequirement item : eitherItems) { - int itemInvSlot = item.hasSpecificInventorySlot() ? item.getInventorySlot() : -1; - if (invSlot.equals(itemInvSlot)) { - targetList.add(item); - } - } - } - } - - // Priority-based helper methods for collecting items - private void addItemsForPriority(Map>> equipmentByContextAndSlot, - EquipmentInventorySlot slot, RequirementPriority priority, List targetList) { - // Check all schedule contexts for items with the specified priority - for (Map> contextMap : equipmentByContextAndSlot.values()) { - List items = contextMap.get(slot); - if (items != null) { - for (ItemRequirement item : items) { - if (item.getPriority() == priority) { - targetList.add(item); - } - } - } - } - } - - private void addEitherItemsForEquipmentSlotPriority(Map> eitherByContext, - EquipmentInventorySlot equipSlot, RequirementPriority priority, List targetList) { - // Check all schedule contexts for either items with the specified priority - for (List eitherItems : eitherByContext.values()) { - if (eitherItems != null) { - for (ItemRequirement item : eitherItems) { - if (equipSlot.equals(item.getEquipmentSlot()) && item.getPriority() == priority) { - targetList.add(item); - } - } - } - } - } - - private TaskContext determineTaskContext(List items) { - // Determine the most appropriate TaskContext for a group of items - // Priority: PRE_SCHEDULE -> POST_SCHEDULE -> BOTH - boolean hasPre = false, hasPost = false, hasBoth = false; - - for (ItemRequirement item : items) { - switch (item.getTaskContext()) { - case PRE_SCHEDULE: - hasPre = true; - break; - case POST_SCHEDULE: - hasPost = true; - break; - case BOTH: - hasBoth = true; - break; - } - } - - // If all items have the same context, use that - if (hasPre && !hasPost && !hasBoth) return TaskContext.PRE_SCHEDULE; - if (hasPost && !hasPre && !hasBoth) return TaskContext.POST_SCHEDULE; - if (hasBoth && !hasPre && !hasPost) return TaskContext.BOTH; - - // Mixed contexts - default to BOTH (covers all cases) - return TaskContext.BOTH; - } - - // Priority-based helper methods for inventory items - private void addInventoryItemsForPriority(Map>> inventoryByContextAndSlot, - Integer slot, RequirementPriority priority, List targetList) { - // Check all schedule contexts for items with the specified priority - for (Map> contextMap : inventoryByContextAndSlot.values()) { - List items = contextMap.get(slot); - if (items != null) { - for (ItemRequirement item : items) { - if (item.getPriority() == priority) { - targetList.add(item); - } - } - } - } - } - - private void addEitherItemsForInventorySlotPriority(Map> eitherByContext, - Integer invSlot, RequirementPriority priority, List targetList) { - // Check all schedule contexts for either items with the specified priority - for (List eitherItems : eitherByContext.values()) { - if (eitherItems != null) { - for (ItemRequirement item : eitherItems) { - int itemInvSlot = item.hasSpecificInventorySlot() ? item.getInventorySlot() : -1; - if (invSlot.equals(itemInvSlot) && item.getPriority() == priority) { - targetList.add(item); - } - } - } - } - } - - /** - * Processes shop and loot requirements (these don't need slot-based consolidation). - */ - private void processShopAndLootRequirements( - Map> shopByContext, - Map> lootByContext, - LinkedHashSet shopCache, - LinkedHashSet lootCache) { - - // Process shop requirements grouped by schedule context - for (Map.Entry> contextEntry : shopByContext.entrySet()) { - TaskContext taskContext = contextEntry.getKey(); - List shopReqs = contextEntry.getValue(); - - for (ShopRequirement shop : shopReqs) { - OrRequirement orReq = new OrRequirement(shop.getPriority(), shop.getRating(), - shop.getDescription(), taskContext, shop); - shopCache.add(orReq); - } - } - - // Process loot requirements grouped by schedule context - for (Map.Entry> contextEntry : lootByContext.entrySet()) { - TaskContext taskContext = contextEntry.getKey(); - List lootReqs = contextEntry.getValue(); - - for (LootRequirement loot : lootReqs) { - OrRequirement orReq = new OrRequirement(loot.getPriority(), loot.getRating(), - loot.getDescription(), taskContext, loot); - lootCache.add(orReq); - } - } - } - - /** - * Creates a merged OR requirement from a list of competing items for the same slot. - * Implements proper rating calculation (sum) and priority selection (highest). - * - * @param items List of items competing for the same slot - * @param slotDescription Description of the slot for the OR requirement name - * @param TaskContext The schedule context (must be the same for all items) - * @return A merged OrRequirement with correct rating and priority - */ - private OrRequirement createMergedOrRequirement(List items, String slotDescription, TaskContext taskContext) { - if (items.isEmpty()) { - throw new IllegalArgumentException("Cannot create OR requirement from empty item list"); - } - - // Calculate merged rating as sum of all item ratings - int mergedRating = items.stream().mapToInt(Requirement::getRating).sum(); - - // Calculate merged priority as the highest priority (lowest ordinal value) - RequirementPriority mergedPriority = items.stream() - .map(Requirement::getPriority) - .min(RequirementPriority::compareTo) - .orElse(RequirementPriority.RECOMMENDED); - - // Verify all items have the same schedule context - boolean allSameContext = items.stream() - .map(Requirement::getTaskContext) - .allMatch(context -> context == taskContext); - - if (!allSameContext) { - log.warn("Items for {} have different schedule contexts, using {}", slotDescription, taskContext); - } - - // Create descriptive name showing alternatives count - String name = slotDescription + " options (" + items.size() + " alternatives, rating: " + mergedRating + ")"; - - return new OrRequirement(mergedPriority, mergedRating, name, taskContext, - items.toArray(new Requirement[0])); - } - - /** - * Sorts all cache collections by priority and rating. - */ - private void sortAllCaches( - Map> equipmentCache, - LinkedHashSet shopCache, - LinkedHashSet lootCache, - Map> inventorySlotCache) { - - // Sort equipment items within each slot - for (LinkedHashSet slotRequirements : equipmentCache.values()) { - sortLogicalRequirements(slotRequirements); - } - - // Sort inventory slot requirements - for (LinkedHashSet slotRequirements : inventorySlotCache.values()) { - sortLogicalRequirements(slotRequirements); - } - - // Sort other collections - sortLogicalRequirements(shopCache); - sortLogicalRequirements(lootCache); - } - - /** - * Sorts logical requirements by priority and rating. - */ - private void sortLogicalRequirements(LinkedHashSet requirements) { - List sorted = new ArrayList<>(requirements); - sorted.sort((a, b) -> { - // First sort by priority (MANDATORY > RECOMMENDED > OPTIONAL) - int priorityCompare = a.getPriority().compareTo(b.getPriority()); - if (priorityCompare != 0) { - return priorityCompare; - } - // Then sort by rating (higher rating is better) - return Integer.compare(b.getRating(), a.getRating()); - }); - - requirements.clear(); - requirements.addAll(sorted); - } - - - /** - * Gets equipment logical requirements cache for a specific schedule context, rebuilding if necessary. - * Returns only the LogicalRequirements that match the given context. - * - * @param context The schedule context to filter by (PRE_SCHEDULE or POST_SCHEDULE) - * @return Map of equipment slot to context-specific logical requirements - */ - /** - * Gets standard (non-external) inventory slot requirements cache, rebuilding if necessary. - * This excludes externally added requirements to prevent double processing. - */ - public Map> getInventoryRequirements(TaskContext context) { - if (!cacheValid) { - rebuildCache(); - } - - return inventorySlotRequirementsCache.getOrDefault(context, new LinkedHashMap<>()); - } - - /** - * Gets standard (non-external) inventory slot requirements cache, rebuilding if necessary. - * This excludes externally added requirements to prevent double processing. - */ - public Map getInventorySlotLogicalRequirements(TaskContext context) { - if (!cacheValid) { - rebuildCache(); - } - - // Convert LinkedHashSet to OrRequirement - Map result = new LinkedHashMap<>(); - Map> inventory = inventorySlotRequirementsCache.getOrDefault(context, new LinkedHashMap<>()); - for (Map.Entry> entry : inventory.entrySet()) { - if (!entry.getValue().isEmpty()) { - ItemRequirement first = entry.getValue().iterator().next(); - if (entry.getValue().size() == 1) { - OrRequirement orReq = new OrRequirement(first.getPriority(), first.getRating(), - "Inventory slot " + entry.getKey(), context, ItemRequirement.class); - orReq.addRequirement(first); - result.put(entry.getKey(), orReq); - } else { - OrRequirement orReq = new OrRequirement(first.getPriority(), first.getRating(), - "Inventory slot " + entry.getKey(), context, ItemRequirement.class); - for (ItemRequirement item : entry.getValue()) { - orReq.addRequirement(item); - } - result.put(entry.getKey(), orReq); - } - } - } - return result; - } - - /** - * Gets standard (non-external) inventory slot requirements cache as raw ItemRequirement sets. - * This excludes externally added requirements to prevent double processing. - */ - public Map> getInventorySlotRequirements(TaskContext context) { - if (!cacheValid) { - rebuildCache(); - } - - return inventorySlotRequirementsCache.getOrDefault(context, new LinkedHashMap<>()); - } - - /** - * Gets standard (non-external) equipment logical requirements cache, rebuilding if necessary. - * This excludes externally added requirements to prevent double processing. - */ - public Map> getEquipmentRequirements(TaskContext context) { - if (!cacheValid) { - rebuildCache(); - } - - return equipmentItemsCache.getOrDefault(context, new LinkedHashMap<>()); - } - - - - - - /** - * Gets standard (non-external) shop logical requirements cache, rebuilding if necessary. - * This excludes externally added requirements to prevent double processing. - */ - public LinkedHashSet getShopLogicalRequirements(TaskContext context) { - if (!cacheValid) { - rebuildCache(); - } - - - return shopRequirementsCache.getOrDefault(context, new LinkedHashSet<>()); - } - - /** - * Gets standard (non-external) shop requirements cache for a specific context. - */ - public LinkedHashSet getShopRequirements(TaskContext context) { - return getShopLogicalRequirements(context); - } - - /** - * Gets standard (non-external) loot logical requirements cache, rebuilding if necessary. - * This excludes externally added requirements to prevent double processing. - */ - public LinkedHashSet getLootLogicalRequirements(TaskContext context) { - if (!cacheValid) { - rebuildCache(); - } - - return lootRequirementsCache.getOrDefault(context, new LinkedHashSet<>()); - } - - /** - * Gets standard (non-external) loot requirements cache for a specific context. - */ - public LinkedHashSet getLootRequirements(TaskContext context) { - return getLootLogicalRequirements(context); - } - - /** - * Checks if a logical requirement contains only standard (non-external) child requirements. - * - * @param logical The logical requirement to check - * @return true if all child requirements are standard, false if any are external - */ - private boolean isLogicalRequirement(LogicalRequirement logical) { - for (Requirement child : logical.getChildRequirements()) { - RequirementKey childKey = new RequirementKey(child); - if (externalRequirements.containsKey(childKey)) { - return false; // Contains external requirement - } - // For nested logical requirements, check recursively - if (child instanceof LogicalRequirement) { - if (!isLogicalRequirement((LogicalRequirement) child)) { - return false; - } - } - } - return true; // All child requirements are standard - } - - - /** - * Validates the consistency of the registry. - * - * @return true if the registry is consistent, false otherwise - */ - public boolean validateConsistency() { - try { - // Ensure single-instance requirements are properly referenced - long preSpellbookCount = getRequirements(SpellbookRequirement.class, TaskContext.PRE_SCHEDULE).size(); - long postSpellbookCount = getRequirements(SpellbookRequirement.class, TaskContext.POST_SCHEDULE).size(); - long preLocationCount = getRequirements(LocationRequirement.class, TaskContext.PRE_SCHEDULE).size(); - long postLocationCount = getRequirements(LocationRequirement.class, TaskContext.POST_SCHEDULE).size(); - - if (preSpellbookCount > 1 || postSpellbookCount > 1) { - log.error("Multiple spellbook requirements detected: pre={}, post={}", preSpellbookCount, postSpellbookCount); - return false; - } - - if (preLocationCount > 1 || postLocationCount > 1) { - log.error("Multiple location requirements detected: pre={}, post={}", preLocationCount, postLocationCount); - return false; - } - - // Ensure cache consistency (if cache is valid) - if (cacheValid) { - // Count equipment items across all contexts and slots - int equipmentSize = equipmentItemsCache.values().stream() - .mapToInt(contextMap -> contextMap.values().stream().mapToInt(Set::size).sum()) - .sum(); - - // Count inventory slot items across all contexts and slots - int inventorySize = inventorySlotRequirementsCache.values().stream() - .mapToInt(contextMap -> contextMap.values().stream().mapToInt(Set::size).sum()) - .sum(); - - // Count any inventory slot, shop and loot items across all contexts - int anySlotSize = anyInventorySlotRequirementsCache.values().stream().mapToInt(Set::size).sum(); - int shopSize = shopRequirementsCache.values().stream().mapToInt(Set::size).sum(); - int lootSize = lootRequirementsCache.values().stream().mapToInt(Set::size).sum(); - - int cacheSize = equipmentSize + inventorySize + anySlotSize + shopSize + lootSize; - - // Note: EITHER items are now distributed to equipment and inventory caches, - // so we need to account for potential duplicates in the count - long actualItemCount = requirements.values().stream() - .filter(req -> req instanceof ItemRequirement || - req instanceof ShopRequirement || - req instanceof LootRequirement) - .count(); - - // For now, we'll log cache statistics but not fail validation - // since EITHER items are counted in multiple caches - log.debug("Cache statistics: cache={}, actual={}", cacheSize, actualItemCount); - } - - return true; - } catch (Exception e) { - log.error("Error during consistency validation", e); - return false; - } - } - - /** - * Gets debug information about the registry state. - */ - public String getDebugInfo() { - StringBuilder sb = new StringBuilder(); - sb.append("RequirementRegistry Debug Info:\n"); - sb.append(" Total requirements: ").append(requirements.size()).append("\n"); - sb.append(" Cache valid: ").append(cacheValid).append("\n"); - sb.append(" Pre-schedule spellbook: ").append(preScheduleSpellbookRequirement != null).append("\n"); - sb.append(" Post-schedule spellbook: ").append(postScheduleSpellbookRequirement != null).append("\n"); - sb.append(" Pre-schedule location: ").append(preScheduleLocationRequirement != null).append("\n"); - sb.append(" Post-schedule location: ").append(postScheduleLocationRequirement != null).append("\n"); - - Map, Long> typeCounts = requirements.values().stream() - .collect(Collectors.groupingBy(Object::getClass, Collectors.counting())); - - sb.append(" Requirements by type:\n"); - typeCounts.forEach((type, count) -> - sb.append(" ").append(type.getSimpleName()).append(": ").append(count).append("\n")); - - return sb.toString(); - } - - /** - * Gets a comprehensive validation summary of all requirements in the registry. - * This provides an overall evaluation of requirement fulfillment status organized by priority and context. - * - * @param context The schedule context to filter requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @return A formatted string containing the validation summary - */ - public String getValidationSummary(TaskContext context) { - StringBuilder sb = new StringBuilder(); - sb.append("=== Requirements Validation Summary ===\n"); - - // Get all requirements for the specified context - List contextRequirements = getAllRequirements().stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(Collectors.toList()); - - if (contextRequirements.isEmpty()) { - sb.append("No requirements found for context: ").append(context.name()).append("\n"); - return sb.toString(); - } - - // Group requirements by priority for better organization - Map> byPriority = contextRequirements.stream() - .collect(Collectors.groupingBy(Requirement::getPriority)); - - // Overall statistics - long totalRequirements = contextRequirements.size(); - long fulfilledCount = contextRequirements.stream().mapToLong(req -> req.isFulfilled() ? 1 : 0).sum(); - long notFulfilledCount = totalRequirements - fulfilledCount; - - sb.append("Context: ").append(context.name()).append("\n"); - sb.append("Total Requirements: ").append(totalRequirements).append("\n"); - sb.append("Fulfilled: ").append(fulfilledCount).append(" (") - .append(totalRequirements > 0 ? (fulfilledCount * 100 / totalRequirements) : 0).append("%)\n"); - sb.append("Not Fulfilled: ").append(notFulfilledCount).append(" (") - .append(totalRequirements > 0 ? (notFulfilledCount * 100 / totalRequirements) : 0).append("%)\n\n"); - - // Detailed breakdown by priority - for (RequirementPriority priority : RequirementPriority.values()) { - List priorityRequirements = byPriority.getOrDefault(priority, Collections.emptyList()); - if (priorityRequirements.isEmpty()) { - continue; - } - - long priorityFulfilled = priorityRequirements.stream().mapToLong(req -> req.isFulfilled() ? 1 : 0).sum(); - long priorityNotFulfilled = priorityRequirements.size() - priorityFulfilled; - - sb.append("--- ").append(priority.name()).append(" Requirements ---\n"); - sb.append("Total: ").append(priorityRequirements.size()).append(" | "); - sb.append("Fulfilled: ").append(priorityFulfilled).append(" | "); - sb.append("Not Fulfilled: ").append(priorityNotFulfilled).append("\n"); - - // Group by requirement type for better organization - Map> byType = priorityRequirements.stream() - .collect(Collectors.groupingBy(Requirement::getRequirementType)); - - for (Map.Entry> typeEntry : byType.entrySet()) { - RequirementType type = typeEntry.getKey(); - List typeRequirements = typeEntry.getValue(); - - long typeFulfilled = typeRequirements.stream().mapToLong(req -> req.isFulfilled() ? 1 : 0).sum(); - - sb.append(" ").append(type.name()).append(": ") - .append(typeFulfilled).append("/").append(typeRequirements.size()) - .append(" (").append(typeRequirements.size() > 0 ? (typeFulfilled * 100 / typeRequirements.size()) : 0).append("% fulfilled)\n"); - - // Show validation status for each requirement in this type - typeRequirements.forEach(req -> { - String status = req.isFulfilled() ? "✓" : "✗"; - sb.append(" ").append(status).append(" ") - .append("Rating: ").append(req.getRating()).append("/10") - .append(" | Type: ").append(req.getRequirementType().name()).append("\n"); - }); - } - sb.append("\n"); - } - - // Critical validation status - List mandatoryNotFulfilled = contextRequirements.stream() - .filter(req -> req.getPriority() == RequirementPriority.MANDATORY && !req.isFulfilled()) - .collect(Collectors.toList()); - - if (!mandatoryNotFulfilled.isEmpty()) { - sb.append("⚠ïļ CRITICAL: ").append(mandatoryNotFulfilled.size()) - .append(" mandatory requirements are not fulfilled!\n"); - } else if (byPriority.containsKey(RequirementPriority.MANDATORY)) { - sb.append("✓ All mandatory requirements are fulfilled\n"); - } - - // External requirements summary - List externalContextRequirements = getExternalRequirements(context); - if (!externalContextRequirements.isEmpty()) { - long externalFulfilled = externalContextRequirements.stream().mapToLong(req -> req.isFulfilled() ? 1 : 0).sum(); - sb.append("\n--- External Requirements ---\n"); - sb.append("Total: ").append(externalContextRequirements.size()).append(" | "); - sb.append("Fulfilled: ").append(externalFulfilled).append(" | "); - sb.append("Not Fulfilled: ").append(externalContextRequirements.size() - externalFulfilled).append("\n"); - } - - return sb.toString(); - } - - /** - * Gets a concise validation status summary for quick overview. - * - * @param context The schedule context to evaluate - * @return A brief status summary string - */ - public String getValidationStatusSummary(TaskContext context) { - List contextRequirements = getAllRequirements().stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(Collectors.toList()); - - if (contextRequirements.isEmpty()) { - return "No requirements for " + context.name(); - } - - long totalRequirements = contextRequirements.size(); - long fulfilledCount = contextRequirements.stream().mapToLong(req -> req.isFulfilled() ? 1 : 0).sum(); - long mandatoryCount = contextRequirements.stream().mapToLong(req -> req.getPriority() == RequirementPriority.MANDATORY ? 1 : 0).sum(); - long mandatoryFulfilled = contextRequirements.stream() - .filter(req -> req.getPriority() == RequirementPriority.MANDATORY) - .mapToLong(req -> req.isFulfilled() ? 1 : 0).sum(); - - String mandatoryStatus = mandatoryCount > 0 ? - String.format(" | Mandatory: %d/%d", mandatoryFulfilled, mandatoryCount) : ""; - - return String.format("Requirements [%s]: %d/%d fulfilled (%.0f%%)%s", - context.name(), fulfilledCount, totalRequirements, - totalRequirements > 0 ? (fulfilledCount * 100.0 / totalRequirements) : 0.0, - mandatoryStatus); - } - - /** - * Recursively extracts ItemRequirements from a LogicalRequirement. - */ - private void extractItemRequirements(LogicalRequirement logical, LinkedHashSet items) { - for (Requirement child : logical.getChildRequirements()) { - if (child instanceof ItemRequirement) { - items.add((ItemRequirement) child); - } else if (child instanceof LogicalRequirement) { - extractItemRequirements((LogicalRequirement) child, items); - } - } - } - - /** - * Gets requirements for a specific inventory slot. - * - * @param slot The inventory slot (0-27) - * @return Logical requirements for the specified slot - */ - public LinkedHashSet getInventorySlotRequirement(TaskContext context, int slot) { - if (!cacheValid) { - rebuildCache(); - } - return inventorySlotRequirementsCache.getOrDefault(context, new HashMap<>()).get(slot); - } - - /** - * Gets all inventory slot requirements for a specific schedule context. - * Returns only the LogicalRequirements that match the given context. - * - * @param context The schedule context to filter by (PRE_SCHEDULE or POST_SCHEDULE) - * @return Map of slot to context-specific logical requirements - */ - public Map> getInventorySlotsRequirements(TaskContext context) { - if (!cacheValid) { - rebuildCache(); - } - return inventorySlotRequirementsCache.getOrDefault(context, new HashMap<>()); - } - - - - /** - * Gets equipment items from slot-based cache, extracting ItemRequirements from LogicalRequirements. - * - * @return Map of equipment slot to ItemRequirements - */ - public Map> getEquipmentSlotItems(TaskContext context) { - ensureCacheValid(); - return this.equipmentItemsCache.getOrDefault(context, new HashMap<>()); - } - - /** - * Gets inventory items from slot-based cache, extracting ItemRequirements from LogicalRequirements. - * - * @return Set of inventory ItemRequirements (from slot -1 which represents "any slot") - */ - public Map> getInventorySlotItems(TaskContext context) { - ensureCacheValid(); - return this.inventorySlotRequirementsCache.getOrDefault(context, new HashMap<>()); - } - - /** - * Gets inventory items from slot-based cache, extracting ItemRequirements from LogicalRequirements. - * - * @return Set of inventory ItemRequirements (from slot -1 which represents "any slot") - */ - public LinkedHashSet getAnyInventorySlotItems(TaskContext context) { - ensureCacheValid(); - LinkedHashSet inventoryItems = new LinkedHashSet<>(); - LinkedHashSet logicalReqs = this.anyInventorySlotRequirementsCache.getOrDefault(context, new LinkedHashSet<>()); - - for (LogicalRequirement logical : logicalReqs) { - extractItemRequirements(logical, inventoryItems); - } - - return inventoryItems; - } - - /** - * Gets any-slot logical requirements (OrRequirements) for the InventorySetupPlanner. - * These represent flexible inventory items that can go in any inventory slot. - * - * @param context The schedule context to filter by - * @return LinkedHashSet of OrRequirements for flexible inventory placement - */ - public LinkedHashSet getAnySlotLogicalRequirements(TaskContext context) { - ensureCacheValid(); - return anyInventorySlotRequirementsCache.getOrDefault(context, new LinkedHashSet<>()); - } - - /** - * Gets all EITHER items by aggregating from both equipment and inventory slot caches. - * Since EITHER requirements are now distributed across caches, we need to collect them. - * - * @return Set of all EITHER ItemRequirements - */ - public LinkedHashSet getAllEitherItems(TaskContext context) { - LinkedHashSet eitherItems = new LinkedHashSet<>(); - - // Check equipment slots for EITHER items - Map> equipmentItems = getEquipmentSlotItems(context); - for (LinkedHashSet slotItems : equipmentItems.values()) { - for (ItemRequirement item : slotItems) { - if (RequirementType.EITHER.equals(item.getRequirementType())) { - eitherItems.add(item); - } - } - } - - // Check inventory slot cache for EITHER items - Map> inventoryItems = getInventorySlotItems(context); - for (LinkedHashSet slotItems : inventoryItems.values()) { - for (ItemRequirement item : slotItems) { - if (RequirementType.EITHER.equals(item.getRequirementType())) { - eitherItems.add(item); - } - } - } - // Check any inventory slot cache for EITHER items - LinkedHashSet anyInventoryItems = getAnyInventorySlotItems(context); - for (ItemRequirement item : anyInventoryItems) { - if (RequirementType.EITHER.equals(item.getRequirementType())) { - eitherItems.add(item); - } - } - - return eitherItems; - } - - // === UNIFIED ACCESS API === - - /** - * Gets all logical requirements (equipment + inventory) for a specific schedule context. - * This provides a unified view of all item-related requirements. - * - * @param context The schedule context to filter by (PRE_SCHEDULE or POST_SCHEDULE) - * @return List of all logical requirements for the given context - */ - public LinkedHashSet getAllItemRequirements(TaskContext context) { - LinkedHashSet allItemReqs = new LinkedHashSet<>(); - ensureCacheValid(); - - // Add equipment requirements (flatten the LinkedHashSet collections) - Map> equipmentCache = getEquipmentRequirements(context); - for (LinkedHashSet itemSet : equipmentCache.values()) { - allItemReqs.addAll(itemSet); - } - - // Add inventory slot requirements (flatten the LinkedHashSet collections) - Map> inventoryCache = getInventoryRequirements(context); - for (LinkedHashSet itemSet : inventoryCache.values()) { - allItemReqs.addAll(itemSet); - } - - // Add any inventory slot requirements (these are OrRequirements, extract their ItemRequirements) - LinkedHashSet anySlotCache = anyInventorySlotRequirementsCache.getOrDefault(context, new LinkedHashSet<>()); - for (OrRequirement orReq : anySlotCache) { - for (Object child : orReq.getChildRequirements()) { - if (child instanceof ItemRequirement) { - allItemReqs.add((ItemRequirement) child); - } - } - } - - return allItemReqs; - } - - /** - * Gets the total count of all logical requirements (equipment + inventory) for a specific schedule context. - * This is a convenience method for UI components that need to display counts. - * - * @param context The schedule context to filter by (PRE_SCHEDULE or POST_SCHEDULE) - * @return Total count of logical requirements for the given context - */ - public int getItemCount(TaskContext context) { - int count = 0; - // Count all equipment items per slot - for (LinkedHashSet items : getEquipmentRequirements(context).values()) { - count += items.size(); - } - // Count all inventory items per slot - for (LinkedHashSet items : getInventoryRequirements(context).values()) { - count += items.size(); - } - // Count any-slot inventory items (from OrRequirements) - LinkedHashSet anySlotOrs = anyInventorySlotRequirementsCache.getOrDefault(context, new LinkedHashSet<>()); - for (OrRequirement orReq : anySlotOrs) { - for (Requirement req : orReq.getChildRequirements()) { - if (req instanceof ItemRequirement) { - count++; - } - } - } - return count; - } - - - - - - /** - * Ensures the cache is valid, rebuilding if necessary. - */ - private void ensureCacheValid() { - if (!cacheValid) { - rebuildCache(); - } - } - - /** - * Ensures the external cache is valid, rebuilding if necessary. - */ - private void ensureExternalCacheValid() { - if (!externalCacheValid) { - rebuildExternalCache(); - } - } - - // =============================== - // EXTERNAL REQUIREMENTS METHODS - // =============================== - - /** - * Gets all external requirements of a specific type for a specific schedule context. - */ - @SuppressWarnings("unchecked") - public List getExternalRequirements(Class clazz, TaskContext context) { - return externalRequirements.values().stream() - .filter(clazz::isInstance) - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .map(req -> (T) req) - .collect(Collectors.toList()); - } - - /** - * Gets external equipment logical requirements cache, rebuilding if necessary. - */ - public Map>> getExternalEquipmentLogicalRequirements() { - ensureExternalCacheValid(); - return externalEquipmentItemsCache; - } - - /** - * Gets external inventory logical requirements cache, rebuilding if necessary. - */ - public Map>> getExternalInventoryLogicalRequirements() { - ensureExternalCacheValid(); - return externalInventorySlotRequirementsCache; - } - - /** - * Gets external shop logical requirements cache, rebuilding if necessary. - */ - public Map> getExternalShopLogicalRequirements() { - ensureExternalCacheValid(); - return externalShopRequirementsCache; - } - - /** - * Gets external loot logical requirements cache, rebuilding if necessary. - */ - public Map> getExternalLootLogicalRequirements() { - ensureExternalCacheValid(); - return externalLootRequirementsCache; - } - - /** - * Gets conditional item requirements cache, rebuilding if necessary. - */ - public Map> getConditionalItemRequirements() { - ensureCacheValid(); - return conditionalItemRequirementsCache; - } - - /** - * Gets external conditional item requirements cache, rebuilding if necessary. - */ - public Map> getExternalConditionalItemRequirements() { - ensureExternalCacheValid(); - return externalIConditionalItemRequirementsCache; - } - - /** - * Gets conditional item requirements for a specific schedule context. - * - * @param context The schedule context to filter by - * @return List of conditional requirements for the given context - */ - public List getConditionalItemRequirements(TaskContext context) { - ensureCacheValid(); - return conditionalItemRequirementsCache.getOrDefault(context, new LinkedHashSet<>()).stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(Collectors.toList()); - } - - /** - * Gets external conditional item requirements for a specific schedule context. - * - * @param context The schedule context to filter by - * @return List of external conditional requirements for the given context - */ - public List getExternalConditionalItemRequirements(TaskContext context) { - ensureExternalCacheValid(); - return externalIConditionalItemRequirementsCache.getOrDefault(context, new LinkedHashSet<>()).stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(Collectors.toList()); - } - - /** - * Rebuilds external requirement caches from the external requirements storage. - */ - private synchronized void rebuildExternalCache() { - if (externalCacheValid) { - return; // Another thread already rebuilt the cache - } - - if(externalRequirements == null || externalRequirements.isEmpty()) { - log.debug("No external requirements to rebuild cache for - initializing empty external caches"); - // Initialize empty external caches when no requirements exist - externalEquipmentItemsCache = new HashMap<>(); - externalShopRequirementsCache = new HashMap<>(); - externalLootRequirementsCache = new HashMap<>(); - externalInventorySlotRequirementsCache = new HashMap<>(); - externalAnyInventorySlotRequirementsCache = new HashMap<>(); - externalIConditionalItemRequirementsCache = new HashMap<>(); - - // Initialize empty collections for each context - for (TaskContext context : TaskContext.values()) { - externalEquipmentItemsCache.put(context, new HashMap<>()); - externalShopRequirementsCache.put(context, new LinkedHashSet<>()); - externalLootRequirementsCache.put(context, new LinkedHashSet<>()); - externalInventorySlotRequirementsCache.put(context, new HashMap<>()); - externalAnyInventorySlotRequirementsCache.put(context, new LinkedHashSet<>()); - externalIConditionalItemRequirementsCache.put(context, new LinkedHashSet<>()); - } - - externalCacheValid = true; - return; - } - - log.debug("Rebuilding external requirement caches..."); - - // Use unified rebuild logic for external requirements - CacheSet newCaches = rebuildCacheUnified(externalRequirements.values(), "external"); - - // Atomically update external caches - this.externalEquipmentItemsCache = newCaches.equipmentCache; - this.externalShopRequirementsCache = newCaches.shopCache; - this.externalLootRequirementsCache = newCaches.lootCache; - this.externalInventorySlotRequirementsCache = newCaches.inventorySlotCache; - this.externalAnyInventorySlotRequirementsCache = newCaches.anyInventorySlotCache; - this.externalIConditionalItemRequirementsCache = newCaches.conditionalCache; - - externalCacheValid = true; - log.debug("Rebuilt external requirement caches with {} external requirements", externalRequirements.size()); - } - - /** - * Gets item requirements separated into equipment and inventory categories with detailed slot breakdowns. - * This method uses the processed logical requirements from the caches to provide accurate counts - * that reflect OR requirement grouping (e.g., pickaxes grouped as one requirement). - * - * @param context The schedule context to filter by - * @return A breakdown containing detailed slot-by-slot requirement analysis - */ - public RequirementBreakdown getItemRequirementBreakdown(TaskContext context) { - // Ensure caches are valid - ensureCacheValid(); - - Map> equipmentSlotBreakdown = new LinkedHashMap<>(); - Map> inventorySlotBreakdown = new LinkedHashMap<>(); - - // Process equipment logical requirements by slot - Map> contextEquipmentCache = equipmentItemsCache.get(context); - if (contextEquipmentCache != null) { - for (Map.Entry> entry : contextEquipmentCache.entrySet()) { - EquipmentInventorySlot slot = entry.getKey(); - LinkedHashSet logicalReqs = entry.getValue(); - - Map slotCounts = new EnumMap<>(RequirementPriority.class); - slotCounts.put(RequirementPriority.MANDATORY, 0); - slotCounts.put(RequirementPriority.RECOMMENDED, 0); - slotCounts.put(RequirementPriority.RECOMMENDED, 0); - - // Count logical requirements (not individual items) by priority - for (ItemRequirement logicalReq : logicalReqs) { - if (logicalReq.getTaskContext() == context || logicalReq.getTaskContext() == TaskContext.BOTH) { - RequirementPriority priority = logicalReq.getPriority(); - slotCounts.put(priority, slotCounts.get(priority) + 1); - } - } - - // Only add slots that have requirements - if (slotCounts.values().stream().anyMatch(count -> count > 0)) { - equipmentSlotBreakdown.put(slot, slotCounts); - } - } - } - - // Process inventory logical requirements by slot (EXCLUDE -1 slot for "any slot" items) - Map> contextInventoryCache = inventorySlotRequirementsCache.get(context); - if (contextInventoryCache != null) { - for (Map.Entry> entry : contextInventoryCache.entrySet()) { - int slot = entry.getKey(); - LinkedHashSet logicalReqs = entry.getValue(); - - // Skip -1 slot (any slot items) as requested - if (slot == -1) { - continue; - } - - Map slotCounts = new EnumMap<>(RequirementPriority.class); - slotCounts.put(RequirementPriority.MANDATORY, 0); - slotCounts.put(RequirementPriority.RECOMMENDED, 0); - - // Count logical requirements (not individual items) by priority - for (ItemRequirement itemReq : logicalReqs) { - if (itemReq.getTaskContext() == context || itemReq.getTaskContext() == TaskContext.BOTH) { - RequirementPriority priority = itemReq.getPriority(); - slotCounts.put(priority, slotCounts.get(priority) + 1); - } - } - - // Only add slots that have requirements - if (slotCounts.values().stream().anyMatch(count -> count > 0)) { - inventorySlotBreakdown.put(slot, slotCounts); - } - } - } - - return new RequirementBreakdown(equipmentSlotBreakdown, inventorySlotBreakdown, getConditionalItemRequirements(context)); - } - - /** - * Counts requirements by priority for a specific type and context. - * - * @param clazz The requirement class to count - * @param context The schedule context to filter by - * @return A map of Priority to count - */ - public Map countRequirementsByPriority(Class clazz, TaskContext context) { - return getRequirements(clazz, context).stream() - .collect(Collectors.groupingBy( - Requirement::getPriority, - Collectors.counting() - )); - } - - /** - * Helper class to hold detailed slot-by-slot requirement breakdowns with priority counts. - */ - public static class RequirementBreakdown { - private final Map> equipmentSlotBreakdown; - private final Map> inventorySlotBreakdown; - private final List conditionalRequirements; - - public RequirementBreakdown( - Map> equipmentSlotBreakdown, - Map> inventorySlotBreakdown, - List conditionalRequirements) { - this.equipmentSlotBreakdown = new LinkedHashMap<>(equipmentSlotBreakdown); - this.inventorySlotBreakdown = new LinkedHashMap<>(inventorySlotBreakdown); - this.conditionalRequirements = conditionalRequirements != null ? new ArrayList<>(conditionalRequirements) : List.of(); - } - - /** - * Gets the detailed breakdown of equipment slots. - * @return Map of equipment slot to priority counts - */ - public Map> getEquipmentSlotBreakdown() { - return new LinkedHashMap<>(equipmentSlotBreakdown); - } - - /** - * Gets the detailed breakdown of inventory slots. - * @return Map of inventory slot to priority counts - */ - public Map> getInventorySlotBreakdown() { - return new LinkedHashMap<>(inventorySlotBreakdown); - } - - /** - * Gets total count of equipment requirements by priority across all slots. - */ - public long getEquipmentCount(RequirementPriority priority) { - return equipmentSlotBreakdown.values().stream() - .mapToLong(slotCounts -> slotCounts.getOrDefault(priority, 0)) - .sum(); - } - - /** - * Gets total count of inventory requirements by priority across all slots. - */ - public long getInventoryCount(RequirementPriority priority) { - return inventorySlotBreakdown.values().stream() - .mapToLong(slotCounts -> slotCounts.getOrDefault(priority, 0)) - .sum(); - } - - /** - * Gets total count of all equipment requirements across all slots. - */ - public long getTotalEquipmentCount() { - return equipmentSlotBreakdown.values().stream() - .mapToLong(slotCounts -> slotCounts.values().stream().mapToInt(Integer::intValue).sum()) - .sum(); - } - - /** - * Gets total count of all inventory requirements across all slots. - */ - public long getTotalInventoryCount() { - return inventorySlotBreakdown.values().stream() - .mapToLong(slotCounts -> slotCounts.values().stream().mapToInt(Integer::intValue).sum()) - .sum(); - } - - /** - * Gets a detailed string representation of the breakdown showing slot-by-slot details, including conditional requirements. - */ - public String getDetailedBreakdownString() { - StringBuilder sb = new StringBuilder(); - sb.append("=== DETAILED REQUIREMENT BREAKDOWN ===\n"); - - if (!equipmentSlotBreakdown.isEmpty()) { - sb.append("Equipment Slots:\n"); - for (Map.Entry> entry : equipmentSlotBreakdown.entrySet()) { - EquipmentInventorySlot slot = entry.getKey(); - Map counts = entry.getValue(); - sb.append(String.format(" %s: M=%d, R=%d\n", - slot.name(), - counts.getOrDefault(RequirementPriority.MANDATORY, 0), - counts.getOrDefault(RequirementPriority.RECOMMENDED, 0))); - } - } - - if (!inventorySlotBreakdown.isEmpty()) { - sb.append("Inventory Slots:\n"); - for (Map.Entry> entry : inventorySlotBreakdown.entrySet()) { - Integer slot = entry.getKey(); - Map counts = entry.getValue(); - sb.append(String.format(" Slot %d: M=%d, R=%d\n", - slot, - counts.getOrDefault(RequirementPriority.MANDATORY, 0), - counts.getOrDefault(RequirementPriority.RECOMMENDED, 0))); - } - } - - // Conditional requirements statistics - if (!conditionalRequirements.isEmpty()) { - sb.append("\nConditional Requirements Statistics:\n"); - int totalSteps = 0; - int totalItemReqs = 0; - int totalLogicalReqs = 0; - for (ConditionalRequirement conditionalReq : conditionalRequirements) { - sb.append(" ").append(conditionalReq.getName()).append(":\n"); - int stepIdx = 0; - for (var step : conditionalReq.getSteps()) { - totalSteps++; - sb.append(String.format(" Step %d: %s\n", stepIdx++, step.getDescription())); - var req = step.getRequirement(); - if (req instanceof LogicalRequirement) { - totalLogicalReqs++; - sb.append(" LogicalRequirement\n"); - } else if (req instanceof ItemRequirement) { - totalItemReqs++; - sb.append(" ItemRequirement\n"); - } else { - sb.append(" OtherRequirement\n"); - } - } - } - sb.append(String.format(" Total Conditional Steps: %d\n", totalSteps)); - sb.append(String.format(" Total LogicalRequirements: %d\n", totalLogicalReqs)); - sb.append(String.format(" Total ItemRequirements: %d\n", totalItemReqs)); - } - - return sb.toString(); - } - - /** - * Gets count of requirements for a specific equipment slot and priority. - */ - public int getEquipmentSlotCount(EquipmentInventorySlot slot, RequirementPriority priority) { - return equipmentSlotBreakdown.getOrDefault(slot, Map.of()).getOrDefault(priority, 0); - } - - /** - * Gets count of requirements for a specific inventory slot and priority. - */ - public int getInventorySlotCount(int slot, RequirementPriority priority) { - return inventorySlotBreakdown.getOrDefault(slot, Map.of()).getOrDefault(priority, 0); - } - public boolean isEmpty() { - return equipmentSlotBreakdown.isEmpty() && inventorySlotBreakdown.isEmpty(); - } - } - - - - /** - * Provides a comprehensive string representation of the entire registry. - * Shows all requirements organized by type, slot, and schedule context with proper formatting. - * - * @return A detailed string representation of the registry - */ - @Override - public String toString() { - return getDetailedRegistryString(); - } - - /** - * Gets a detailed string representation of the entire registry. - * Organizes requirements by type and provides clear structure with proper indentation. - * - * @return A comprehensive string showing all registered requirements - */ - public String getDetailedRegistryString() { - StringBuilder sb = new StringBuilder(); - - sb.append("=== Requirement Registry Summary ===\n"); - sb.append("Total Requirements: ").append(requirements.size()).append("\n"); - sb.append("Cache Valid: ").append(cacheValid).append("\n\n"); - - if (requirements.isEmpty()) { - sb.append("No requirements registered.\n"); - return sb.toString(); - } - - // Ensure cache is rebuilt for accurate display - ensureCacheValid(); - // Add detailed breakdown for conditional requirements just before returning - if (!conditionalItemRequirementsCache.isEmpty()) { - sb.append("\n=== CONDITIONAL REQUIREMENTS BREAKDOWN ===\n"); - for (Map.Entry> contextEntry : conditionalItemRequirementsCache.entrySet()) { - if (!contextEntry.getValue().isEmpty()) { - sb.append("Context: ").append(contextEntry.getKey()).append("\n"); - for (ConditionalRequirement conditionalReq : contextEntry.getValue()) { - sb.append(" ").append(formatConditionalRequirement(conditionalReq, " ")).append("\n"); - } - } - } - } - - // Display Equipment Requirements by Slot (per context) - sb.append("=== EQUIPMENT REQUIREMENTS BY SLOT ===\n"); - if (equipmentItemsCache.isEmpty()) { - sb.append("\tNo equipment requirements registered.\n"); - } else { - for (Map.Entry>> contextEntry : equipmentItemsCache.entrySet()) { - if (!contextEntry.getValue().isEmpty()) { - sb.append("\tContext: ").append(contextEntry.getKey()).append("\n"); - for (Map.Entry> slotEntry : contextEntry.getValue().entrySet()) { - sb.append("\t\t").append(slotEntry.getKey().name()).append(":\n"); - for (ItemRequirement itemReq : slotEntry.getValue()) { - sb.append("\t\t\t").append(itemReq.getName()).append(" (ID: ").append(itemReq.getId()).append(")\n"); - } - } - } - } - } - sb.append("\n"); - - // Display Inventory Slot Requirements - sb.append("=== INVENTORY SLOT REQUIREMENTS (PRE-Schedule)===\n"); - Map inventorySlotCache = getInventorySlotLogicalRequirements(TaskContext.PRE_SCHEDULE); - if (inventorySlotCache.isEmpty()) { - sb.append("\tNo specific inventory slot requirements registered.\n"); - } else { - for (Map.Entry entry : inventorySlotCache.entrySet()) { - String slotName = entry.getKey() == -1 ? "ANY_SLOT" : "SLOT_" + entry.getKey(); - sb.append("\t").append(slotName).append(":\n"); - sb.append("\t\t").append(formatLogicalRequirement(entry.getValue(), "\t\t")).append("\n"); - } - } - sb.append("\n"); - - // Display Shop Requirements - sb.append("=== SHOP REQUIREMENTS ===\n"); - boolean hasShop = false; - for (Map.Entry> contextEntry : shopRequirementsCache.entrySet()) { - if (!contextEntry.getValue().isEmpty()) { - hasShop = true; - sb.append("\tContext: ").append(contextEntry.getKey()).append("\n"); - for (OrRequirement orReq : contextEntry.getValue()) { - sb.append("\t\t").append(formatLogicalRequirement(orReq, "\t\t")).append("\n"); - } - } - } - if (!hasShop) { - sb.append("\tNo shop requirements registered.\n"); - } - sb.append("\n"); - - // Display Loot Requirements - sb.append("=== LOOT REQUIREMENTS ===\n"); - boolean hasLoot = false; - for (Map.Entry> contextEntry : lootRequirementsCache.entrySet()) { - if (!contextEntry.getValue().isEmpty()) { - hasLoot = true; - sb.append("\tContext: ").append(contextEntry.getKey()).append("\n"); - for (OrRequirement orReq : contextEntry.getValue()) { - sb.append("\t\t").append(formatLogicalRequirement(orReq, "\t\t")).append("\n"); - } - } - } - if (!hasLoot) { - sb.append("\tNo loot requirements registered.\n"); - } - sb.append("\n"); - - // Display Conditional Requirements - sb.append("=== CONDITIONAL REQUIREMENTS ===\n"); - boolean hasConditional = false; - for (Map.Entry> contextEntry : conditionalItemRequirementsCache.entrySet()) { - if (!contextEntry.getValue().isEmpty()) { - hasConditional = true; - sb.append("\tContext: ").append(contextEntry.getKey()).append("\n"); - for (ConditionalRequirement conditionalReq : contextEntry.getValue()) { - sb.append("\t\t").append(formatConditionalRequirement(conditionalReq, "\t\t")).append("\n"); - } - } - } - if (!hasConditional) { - sb.append("\tNo conditional requirements registered.\n"); - } - sb.append("\n"); - - // Display Single-Instance Requirements - sb.append("=== SINGLE-INSTANCE REQUIREMENTS ===\n"); - sb.append("\tPre-Schedule Spellbook: ").append(preScheduleSpellbookRequirement != null ? - preScheduleSpellbookRequirement.getName() : "None").append("\n"); - sb.append("\tPost-Schedule Spellbook: ").append(postScheduleSpellbookRequirement != null ? - postScheduleSpellbookRequirement.getName() : "None").append("\n"); - sb.append("\tPre-Schedule Location: ").append(preScheduleLocationRequirement != null ? - preScheduleLocationRequirement.getName() : "None").append("\n"); - sb.append("\tPost-Schedule Location: ").append(postScheduleLocationRequirement != null ? - postScheduleLocationRequirement.getName() : "None").append("\n"); - - return sb.toString(); - } - - - - - - -/** - * Gets a detailed string representation of cached requirements for a specific schedule context. - * This shows only the processed logical requirements from the cache for the given context, - * providing a focused view of what will actually be fulfilled. - * - * @param context The schedule context to display (PRE_SCHEDULE or POST_SCHEDULE) - * @return A formatted string showing cached requirements for the context - */ - public String getDetailedCacheStringForContext(TaskContext context) { - StringBuilder sb = new StringBuilder(); - if (context == null) { - sb.append("Invalid context provided.\n"); - return sb.toString(); - } - sb.append("=== Cached Requirements for Context: ").append(context.name()).append(" ===\n"); - - - - // Ensure cache is rebuilt for accurate display - ensureCacheValid(); - - boolean hasAnyRequirements = false; - - // Display Equipment Requirements by Slot for this context - sb.append("=== EQUIPMENT REQUIREMENTS BY SLOT ===\n"); - Map> equipmentCache = getEquipmentRequirements(context); - if (equipmentCache.isEmpty()) { - sb.append("\tNo equipment requirements for context: ").append(context.name()).append("\n"); - } else { - hasAnyRequirements = true; - for (Map.Entry> entry : equipmentCache.entrySet()) { - sb.append("\t").append(entry.getKey().name()).append(":\n"); - for (ItemRequirement itemReq : entry.getValue()) { - sb.append("\t\t").append(itemReq.getName()).append(" (ID: ").append(itemReq.getId()).append(")\n"); - } - } - } - sb.append("\n"); - - // Display Inventory Slot Requirements for this context - sb.append("=== INVENTORY SLOT REQUIREMENTS ===\n"); - Map inventorySlotCache = getInventorySlotLogicalRequirements(context); - if (inventorySlotCache.isEmpty()) { - sb.append("\tNo inventory slot requirements for context: ").append(context.name()).append("\n"); - } else { - hasAnyRequirements = true; - for (Map.Entry entry : inventorySlotCache.entrySet()) { - String slotName = entry.getKey() == -1 ? "ANY_SLOT" : "SLOT_" + entry.getKey(); - sb.append("\t").append(slotName).append(":\n"); - sb.append("\t\t").append(formatLogicalRequirement(entry.getValue(), "\t\t")).append("\n"); - } - } - sb.append("\n"); - - // Display Shop Requirements for this context - sb.append("=== SHOP REQUIREMENTS ===\n"); - LinkedHashSet shopCache = getShopRequirements(context); - if (shopCache.isEmpty()) { - sb.append("\tNo shop requirements for context: ").append(context.name()).append("\n"); - } else { - hasAnyRequirements = true; - for (OrRequirement logicalReq : shopCache) { - sb.append("\t").append(formatLogicalRequirement(logicalReq, "\t")).append("\n"); - } - } - sb.append("\n"); - - // Display Loot Requirements for this context - sb.append("=== LOOT REQUIREMENTS ===\n"); - LinkedHashSet lootCache = getLootRequirements(context); - if (lootCache.isEmpty()) { - sb.append("\tNo loot requirements for context: ").append(context.name()).append("\n"); - } else { - hasAnyRequirements = true; - for (OrRequirement logicalReq : lootCache) { - sb.append("\t").append(formatLogicalRequirement(logicalReq, "\t")).append("\n"); - } - } - sb.append("\n"); - - // Display Conditional Requirements for this context - sb.append("=== CONDITIONAL REQUIREMENTS ===\n"); - List conditionalReqs = getConditionalItemRequirements(context); - if (conditionalReqs.isEmpty()) { - sb.append("\tNo conditional requirements for context: ").append(context.name()).append("\n"); - } else { - hasAnyRequirements = true; - for (ConditionalRequirement conditionalReq : conditionalReqs) { - sb.append(formatConditionalRequirement(conditionalReq, "\t")).append("\n"); - } - } - sb.append("\n"); - - - - // Display Single-Instance Requirements for this context - sb.append("=== SINGLE-INSTANCE REQUIREMENTS ===\n"); - boolean hasSpellbook = false; - boolean hasLocation = false; - - if (context == TaskContext.PRE_SCHEDULE || context == TaskContext.BOTH) { - if (preScheduleSpellbookRequirement != null) { - sb.append("\tPre-Schedule Spellbook: ").append(preScheduleSpellbookRequirement.getName()).append("\n"); - hasSpellbook = true; - hasAnyRequirements = true; - } - if (preScheduleLocationRequirement != null) { - sb.append("\tPre-Schedule Location: ").append(preScheduleLocationRequirement.getName()).append("\n"); - hasLocation = true; - hasAnyRequirements = true; - } - } - - if (context == TaskContext.POST_SCHEDULE || context == TaskContext.BOTH) { - if (postScheduleSpellbookRequirement != null) { - sb.append("\tPost-Schedule Spellbook: ").append(postScheduleSpellbookRequirement.getName()).append("\n"); - hasSpellbook = true; - hasAnyRequirements = true; - } - if (postScheduleLocationRequirement != null) { - sb.append("\tPost-Schedule Location: ").append(postScheduleLocationRequirement.getName()).append("\n"); - hasLocation = true; - hasAnyRequirements = true; - } - } - - if (!hasSpellbook && !hasLocation) { - sb.append("\tNo single-instance requirements for context: ").append(context.name()).append("\n"); - } - sb.append("\n"); - - // Summary - if (!hasAnyRequirements) { - sb.append("=== SUMMARY ===\n"); - sb.append("No cached requirements found for context: ").append(context.name()).append("\n"); - } - - return sb.toString(); - } - - /** - * Formats a ConditionalRequirement for display, including all steps, their conditions, and requirements. - * @param conditionalReq The ConditionalRequirement to format - * @param indent The indentation prefix - * @return A formatted string representation - */ - private String formatConditionalRequirement(ConditionalRequirement conditionalReq, String indent) { - StringBuilder sb2 = new StringBuilder(); - sb2.append(conditionalReq.getName()) - .append(" [Parallel: ").append(conditionalReq.isAllowParallelExecution()) - .append("]\n"); - int stepIdx = 0; - for (var step : conditionalReq.getSteps()) { - boolean conditionMet = false; - try { conditionMet = step.needsExecution(); } catch (Throwable t) { conditionMet = false; } - sb2.append(indent).append("Step ").append(stepIdx++).append(": ") - .append(step.getDescription()) - .append(" [ConditionMet: ").append(conditionMet) - .append(", Optional: ").append(step.isOptional()).append("]\n"); - // Show the requirement for this step - Requirement req = step.getRequirement(); - if (req instanceof LogicalRequirement) { - sb2.append(indent).append(" ").append(formatLogicalRequirement((LogicalRequirement)req, indent + " ")).append("\n"); - } else { - sb2.append(indent).append(" ").append(formatSingleRequirement(req)).append("\n"); - } - } - return sb2.toString(); - } - /** - * Formats a logical requirement for display with proper indentation. - * - * @param logicalReq The logical requirement to format - * @param indent The indentation prefix - * @return A formatted string representation - */ - private String formatLogicalRequirement(LogicalRequirement logicalReq, String indent) { - StringBuilder sb = new StringBuilder(); - sb.append(logicalReq.getClass().getSimpleName()).append(": "); - - if (logicalReq instanceof OrRequirement) { - OrRequirement orReq = (OrRequirement) logicalReq; - sb.append("(").append(orReq.getChildRequirements().size()).append(" options)"); - for (Requirement childReq : orReq.getChildRequirements()) { - sb.append("\n").append(indent).append("\t- ").append(formatSingleRequirement(childReq)); - } - } else { - sb.append(formatSingleRequirement(logicalReq)); - } - - return sb.toString(); - } - - /** - * Formats a single requirement for display. - * - * @param req The requirement to format - * @return A formatted string representation - */ - private String formatSingleRequirement(Requirement req) { - if (req instanceof ItemRequirement) { - ItemRequirement itemReq = (ItemRequirement) req; - return String.format("%s (id:%d amount:%d)[%s, Rating: %d] - %s", - itemReq.getName(), - itemReq.getId(), - itemReq.getAmount(), - itemReq.getPriority().name(), - itemReq.getRating(), - itemReq.getTaskContext().name()); - } - return String.format("%s [%s, Rating: %d] - %s", - req.getName(), - req.getPriority().name(), - req.getRating(), - req.getTaskContext().name()); - } - - /** - * Gets all ConditionalRequirements for a specific schedule context that are NOT just item requirements alone. - * This is used to process only 'mixed' or complex conditional requirements (not simple item wrappers). - * - * @param context The schedule context to filter by - * @return List of ConditionalRequirements that are not just item requirements - */ - public List getMixedConditionalRequirements(TaskContext context) { - List all = getRequirements(ConditionalRequirement.class, context); - List mixed = new ArrayList<>(); - for (ConditionalRequirement req : all) { - if (!req.containsOnlyItemRequirements()) { - mixed.add(req); - } - } - return mixed; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/InventorySetupRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/InventorySetupRequirement.java deleted file mode 100644 index e406784321f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/InventorySetupRequirement.java +++ /dev/null @@ -1,172 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.util.Rs2InventorySetup; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -import java.util.Collections; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Delayed; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; -/** - * Inventory setup requirement that loads a specific inventory setup using Rs2InventorySetup. - * This allows plugins to use predefined inventory configurations instead of progressive equipment management. - */ -@Slf4j -public class InventorySetupRequirement extends Requirement { - - private final String inventorySetupName; - private final boolean bankItemsNotInSetup; - /** - * Creates an inventory setup requirement. - * - * @param inventorySetupName Name of the inventory setup to load - * @param taskContext When to apply this requirement (PRE_SCHEDULE, POST_SCHEDULE, BOTH) - * @param priority Priority level (MANDATORY, RECOMMENDED, OPTIONAL) - * @param rating Effectiveness rating 1-10 - * @param description Human-readable description - */ - public InventorySetupRequirement(String inventorySetupName, TaskContext taskContext, - RequirementPriority priority, int rating, String description, boolean bankItemsNotInSetup) { - super(RequirementType.CUSTOM, priority, rating, description, Collections.emptyList(), taskContext); - this.inventorySetupName = inventorySetupName; - this.bankItemsNotInSetup = bankItemsNotInSetup; - - } - - @Override - public String getName() { - return "Inventory Setup: " + inventorySetupName; - } - - @Override - public boolean fulfillRequirement(CompletableFuture scheduledFuture) { - try { - if (inventorySetupName == null || inventorySetupName.trim().isEmpty()) { - log.warn("Inventory setup name is empty, skipping requirement"); - return !isMandatory(); // fail only if mandatory - } - - log.info("Loading inventory setup: {}", inventorySetupName); - // check if setup was successful by waiting for completion - // rs2inventorysetup handles its own validation and timeout - return execute(scheduledFuture); - - } catch (Exception e) { - log.error("Failed to load inventory setup '{}': {}", inventorySetupName, e.getMessage()); - return !isMandatory(); // only fail if mandatory - } - } - private boolean execute(CompletableFuture scheduledFuture){ - try { - log.info("\n\t-Executing plan using Rs2InventorySetup approach: {}", inventorySetupName); - - // Convert CompletableFuture to ScheduledFuture (simplified conversion) - ScheduledFuture mainScheduler = new ScheduledFuture() { - @Override - public long getDelay(TimeUnit unit) { return 0; } - @Override - public int compareTo(Delayed o) { return 0; } - @Override - public boolean cancel(boolean mayInterruptIfRunning) { return scheduledFuture.cancel(mayInterruptIfRunning); } - @Override - public boolean isCancelled() { return scheduledFuture.isCancelled(); } - @Override - public boolean isDone() { return scheduledFuture.isDone(); } - @Override - public Object get() throws InterruptedException, ExecutionException { return scheduledFuture.get(); } - @Override - public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { return scheduledFuture.get(timeout, unit); } - }; - if (!Rs2InventorySetup.isInventorySetup(inventorySetupName)) { - log.error("Failed to create Rs2InventorySetup"); - return false; - } - Rs2InventorySetup rs2Setup = new Rs2InventorySetup(inventorySetupName, mainScheduler); - - - if(rs2Setup.doesEquipmentMatch() && rs2Setup.doesInventoryMatch()){ - log.info("Plan already matches current inventory and equipment setup, skipping execution"); - return true; // No need to execute if already matches - } - if (!Rs2Bank.isOpen()) { - if (!Rs2Bank.walkToBankAndUseBank() && !Rs2Player.isInteracting() && !Rs2Player.isMoving()) { - log.error("\n\tFailed to open bank for comprehensive item management"); - } - boolean openBank= sleepUntil(() -> Rs2Bank.isOpen(), 5000); - if (!openBank) { - log.error("\n\tFailed to open bank within timeout period,for invntory setup execution \"{}\"", inventorySetupName); - return false; - } - } - - // Bank items not in setup first if requested (excludes teleport items) - if (bankItemsNotInSetup) { - log.info("Banking items not in setup (excluding teleport items) before setting up: {}", inventorySetupName); - if (!rs2Setup.bankAllItemsNotInSetup(true)) { - log.warn("Failed to bank all items not in setup, continuing with setup anyway"); - } - } - // Use existing Rs2InventorySetup methods to fulfill the requirements - boolean equipmentSuccess = rs2Setup.loadEquipment(); - if (!equipmentSuccess) { - log.error("Failed to load equipment using Rs2InventorySetup"); - return false; - } - - boolean inventorySuccess = rs2Setup.loadInventory(); - if (!inventorySuccess) { - log.error("Failed to load inventory using Rs2InventorySetup"); - return false; - } - - // Verify the setup matches - boolean equipmentMatches = rs2Setup.doesEquipmentMatch(); - boolean inventoryMatches = rs2Setup.doesInventoryMatch(); - - if (equipmentMatches && inventoryMatches) { - log.info("Successfully executed plan using Rs2InventorySetup: {}", inventorySetupName); - return true; - } else { - log.warn("Plan execution completed but setup verification failed. Equipment matches: {}, Inventory matches: {}", - equipmentMatches, inventoryMatches); - return false; - } - - } catch (Exception e) { - log.error("Failed to execute plan using Rs2InventorySetup: {}", e.getMessage(), e); - return false; - } - } - - @Override - public boolean isFulfilled() { - // inventory setup is more of an action than a state to check - // we consider it fulfilled if the setup name is valid - return inventorySetupName != null && !inventorySetupName.trim().isEmpty(); - } - - @Override - public String getUniqueIdentifier() { - return String.format("%s:INVENTORY_SETUP:%s", - requirementType.name(), - inventorySetupName != null ? inventorySetupName : "null"); - } - - /** - * Gets the inventory setup name for this requirement. - * - * @return The inventory setup name - */ - public String getInventorySetupName() { - return inventorySetupName; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/Requirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/Requirement.java deleted file mode 100644 index 5bf9907e999..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/Requirement.java +++ /dev/null @@ -1,235 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement; - -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; - -import java.util.List; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -/** - * Abstract base class for all requirement types in the plugin scheduler system. - * This class defines common properties and behaviors for all requirements. - */ -@Getter -@AllArgsConstructor -@Slf4j -public abstract class Requirement implements Comparable { - - /** - * The type of requirement (equipment, inventory, player state, etc.) - */ - protected final RequirementType requirementType; - - /** - * Priority level of this requirement for plugin functionality. - */ - protected final RequirementPriority priority; - - /** - * Effectiveness rating from 1-10 (10 being most effective). - * Used for comparison when multiple valid options are available. - */ - protected int rating; - - /** - * Human-readable description explaining the purpose and effectiveness. - * Should include context about why this requirement is useful for the specific plugin/activity. - */ - protected final String description; - - /** - * List of identifiers for this requirement. - * These could be item IDs, NPC IDs, object IDs, varbit IDs, etc. depending on the requirement type. - * For items, this can represent multiple alternative item IDs that satisfy the same requirement. - * Can be empty if not applicable. - */ - protected List ids; - - /** - * Context for when this requirement should be fulfilled. - * PRE_SCHEDULE means before script execution, POST_SCHEDULE means after completion. - * Can be null for requirements that don't have schedule-specific behavior. - */ - @Setter - protected TaskContext taskContext; - - - /** - * Gets the human-readable name of this requirement. - * This is used for display purposes in overlays and logging. - * - * @return The name of this requirement - */ - public abstract String getName(); - - /** - * Abstract method to fulfill this requirement. - * Each requirement type implements its own fulfillment logic. - * - * @param scheduledFuture The CompletableFuture for cancellation support - * @return true if the requirement was fulfilled successfully, false otherwise - */ - public abstract boolean fulfillRequirement(CompletableFuture scheduledFuture); - /** - * Executes this step's requirement with timeout support. - * - * @param scheduledFuture The CompletableFuture for cancellation support - * @param timeoutSeconds Maximum time allowed for this step - * @return true if successfully fulfilled, false otherwise - */ - public boolean fulfillRequirementWithTimeout(CompletableFuture scheduledFuture, long timeoutSeconds) { - try { - log.debug("Executing ordered step with {}s timeout: {}", timeoutSeconds, description); - - CompletableFuture stepFuture = CompletableFuture.supplyAsync(() -> - fulfillRequirement(scheduledFuture) - ); - - return stepFuture.get(timeoutSeconds, TimeUnit.SECONDS); - } catch (TimeoutException e) { - log.error("Step '{}' timed out after {} seconds", description, timeoutSeconds); - return !isMandatory(); - } catch (Exception e) { - log.error("Error executing ordered step '{}': {}", description, e.getMessage()); - return !isMandatory(); - } - } - /** - * Checks if this requirement is currently fulfilled. - * This is a convenience method that calls fulfillRequirement() for consistency - * with the condition system and logical requirements. - * - * @return true if the requirement is fulfilled, false otherwise - */ - public abstract boolean isFulfilled(); - - - /** - * Compare requirements based on priority first, then rating. - * This allows sorting requirements by importance. - * - * @param other The requirement to compare with - * @return A negative value if this requirement is more important, zero if equally important, - * or a positive value if less important - */ - @Override - public int compareTo(Requirement other) { - // First compare by priority (MANDATORY > RECOMMENDED > OPTIONAL) - int priorityComparison = other.getPriority().ordinal() - this.getPriority().ordinal(); - if (priorityComparison != 0) { - return priorityComparison; - } - - // If same priority, compare by rating (higher rating is better) - return other.getRating() - this.getRating(); - } - - /** - * Check if this is a mandatory requirement. - * - * @return true if this requirement has MANDATORY priority, false otherwise - */ - public boolean isMandatory() { - return priority == RequirementPriority.MANDATORY; - } - - /** - * Check if this is a recommended requirement. - * - * @return true if this requirement has RECOMMENDED priority, false otherwise - */ - public boolean isRecommended() { - return priority == RequirementPriority.RECOMMENDED; - } - - - - /** - * Check if this requirement should be fulfilled before script execution. - * - * @return true if this requirement has PRE_SCHEDULE or BOTH context, false otherwise - */ - public boolean isPreSchedule() { - return taskContext == TaskContext.PRE_SCHEDULE || taskContext == TaskContext.BOTH; - } - - /** - * Check if this requirement should be fulfilled after script completion. - * - * @return true if this requirement has POST_SCHEDULE or BOTH context, false otherwise - */ - public boolean isPostSchedule() { - return taskContext == TaskContext.POST_SCHEDULE || taskContext == TaskContext.BOTH; - } - - /** - * Check if this requirement has a specific schedule context task set. - * Since TaskContext is never null (defaults to BOTH), this always returns true. - * - * @return true always, as all requirements have a schedule context - */ - public boolean hasTaskContext() { - return taskContext != null; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || getClass() != obj.getClass()) return false; - - Requirement that = (Requirement) obj; - return rating == that.rating && - Objects.equals(requirementType, that.requirementType) && - Objects.equals(priority, that.priority) && - Objects.equals(description, that.description) && - Objects.equals(ids, that.ids) && - Objects.equals(taskContext, that.taskContext); - } - - @Override - public int hashCode() { - return Objects.hash(requirementType, priority, rating, description, ids, taskContext); - } - - /** - * Returns a multi-line display string with detailed information about this requirement. - * Uses StringBuilder with tabs for proper formatting. - * - * @return A formatted string containing requirement details - */ - public String displayString() { - StringBuilder sb = new StringBuilder(); - sb.append("=== Requirement Details ===\n"); - sb.append("Name:\t\t").append(getName()).append("\n"); - sb.append("Type:\t\t").append(requirementType.name()).append("\n"); - sb.append("Priority:\t").append(priority.name()).append("\n"); - sb.append("Rating:\t\t").append(rating).append("/10\n"); - sb.append("Schedule:\t").append(taskContext.name()).append("\n"); - sb.append("IDs:\t\t").append(ids != null ? ids.toString() : "[]").append("\n"); - sb.append("Description:\t").append(description != null ? description : "No description").append("\n"); - return sb.toString(); - } - - /** - * Gets a unique identifier for this requirement. - * This is used by the RequirementRegistry to ensure uniqueness. - * The default implementation uses the requirement type, description, and IDs. - * Subclasses can override this for more specific uniqueness logic. - * - * @return A unique identifier string for this requirement - */ - public String getUniqueIdentifier() { - return String.format("%s:%s:%s", - requirementType.name(), - description != null ? description.hashCode() : "null", - ids != null ? ids.hashCode() : "null"); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/SpellbookRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/SpellbookRequirement.java deleted file mode 100644 index fd990e07028..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/SpellbookRequirement.java +++ /dev/null @@ -1,520 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import lombok.EqualsAndHashCode; -import net.runelite.api.gameval.ItemID; -import net.runelite.api.gameval.VarbitID; -import net.runelite.api.Quest; -import net.runelite.api.QuestState; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.shortestpath.Transport; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.magic.Rs2Spellbook; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; -import net.runelite.client.plugins.microbot.util.walker.TransportRouteAnalysis; - -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -import org.slf4j.event.Level; - -/** - * Enhanced SpellbookRequirement that extends from the base Requirement class. - * Integrates with the RequirementCollection system and provides comprehensive - * spellbook switching functionality. - * - * This requirement manages spellbook switching operations including: - * - Checking current spellbook state - * - Determining if switching is required before/after script execution - * - Handling different switching methods (altar prayer, NPC dialogue, Magic cape) - * - Managing travel to switching locations - * - Restoring original spellbook after completion - */ -@Slf4j -@EqualsAndHashCode(callSuper = true) -public class SpellbookRequirement extends Requirement { - - - - @Getter - private final Rs2Spellbook requiredSpellbook; // Spellbook required for this task - - - /** - * Full constructor for SpellbookRequirement - * - * @param requiredSpellbook The spellbook required for optimal plugin performance - * @param TaskContext When this spellbook requirement should be applied (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param priority Priority level of this requirement - * @param rating Effectiveness rating (1-10, 10 being most effective) - * @param description Human-readable description of the requirement - */ - public SpellbookRequirement( - Rs2Spellbook requiredSpellbook, - TaskContext taskContext, - RequirementPriority priority, - int rating, - String description) { - - super(RequirementType.GAME_CONFIG, - priority, - rating, - description != null ? description : generateDefaultDescription(requiredSpellbook, taskContext), - requiredSpellbook != null ? Collections.singletonList(requiredSpellbook.getValue()) : Collections.emptyList(), - taskContext); - - this.requiredSpellbook = requiredSpellbook; - - } - - /** - * Simplified constructor with default settings (applies to both pre and post schedule) - * - * @param requiredSpellbook The spellbook required for the task - * @param priority Priority level of this requirement - * @param rating Effectiveness rating (1-10) - */ - public SpellbookRequirement(Rs2Spellbook requiredSpellbook, TaskContext taskContext, RequirementPriority priority, int rating) { - this(requiredSpellbook, taskContext, priority, rating, null); - } - - /** - * Constructor for specific schedule context - * - * @param requiredSpellbook The spellbook required for the task - * @param TaskContext When this requirement should be applied - */ - public SpellbookRequirement(Rs2Spellbook requiredSpellbook, TaskContext taskContext) { - this(requiredSpellbook, taskContext, RequirementPriority.MANDATORY, 10, null); - } - - - - @Override - public String getName() { - return requiredSpellbook != null ? requiredSpellbook.name() + " Spellbook" : "Unknown Spellbook"; - } - - /** - * Generate a default description based on the required spellbook and schedule context - * - * @param spellbook The required spellbook - * @param context When the requirement applies - * @return A descriptive string explaining the requirement - */ - private static String generateDefaultDescription(Rs2Spellbook spellbook, TaskContext context) { - if (spellbook == null) { - return "No specific spellbook required"; - } - - String contextDescription = ""; - switch (context) { - case PRE_SCHEDULE: - contextDescription = "before script execution"; - break; - case POST_SCHEDULE: - contextDescription = "after script completion"; - break; - case BOTH: - contextDescription = "for optimal plugin performance"; - break; - } - - return String.format("Requires %s spellbook %s. %s", - spellbook.name(), - contextDescription, - spellbook.getDescription()); - } - - /** - * Checks if the player is using the required spellbook. - * - * @return true if no spellbook is required or the player is using the required spellbook, - * false otherwise - */ - public boolean hasRequiredSpellbook() { - if (requiredSpellbook == null) { - return true; // No spellbook requirement - } - - return Rs2Spellbook.getCurrentSpellbook() == requiredSpellbook; - } - public boolean isFulfilled() { - // Check if the required spellbook is currently active - return hasRequiredSpellbook(); - } - - /** - * Checks if the required spellbook is available to the player (unlocked). - * - * @return true if the required spellbook is available, false otherwise - */ - public boolean isRequiredSpellbookAvailable() { - if (requiredSpellbook == null) { - return true; // No spellbook requirement - } - - return requiredSpellbook.isUnlocked(); - } - - /** - * Attempts to switch to the required spellbook if needed. - * This method should be called before starting the script if TaskContext includes PRE_SCHEDULE. - * - * @return true if the switch was successful or no switch was needed, false otherwise - */ - private boolean switchToRequiredSpellbook(CompletableFuture scheduledFuture) { - if (requiredSpellbook == null ){ - return true; // No switch needed - } - - if (hasRequiredSpellbook()) { - return true; // Already using required spellbook - } - - if (!isRequiredSpellbookAvailable()) { - log.error("Required spellbook {} is not unlocked, cannot switch", requiredSpellbook.name()); - return false; - } - if (!travelToSwitchLocation(requiredSpellbook)) { - log.error("Failed to travel to {} spellbook switch location at location {}", requiredSpellbook.name(), requiredSpellbook.getSwitchLocation()); - return false; - } - log.info("Switching to required spellbook: {}....", requiredSpellbook.name()); - // Use the enhanced spellbook switching functionality - return requiredSpellbook.switchTo(); - } - - /** - * Switches the player back to their original spellbook after the script completes. - * This method should be called after the script finishes if TaskContext includes POST_SCHEDULE. - * - * @return true if the switch was successful or no switch was needed, false otherwise - */ - public static boolean switchBackToSpellbook(Rs2Spellbook originalSpellbook) { - if (Microbot.getClient() == null) { - return false; - } - if (Microbot.getClient().isClientThread()) { - log.info("Please run fulfillRequirement() on a non-client thread."); - return false; - } - if (originalSpellbook == null) { - return true; // No switch needed - } - - if (Rs2Spellbook.getCurrentSpellbook() == originalSpellbook) { - return true; // Already using original spellbook - } - if (!originalSpellbook.isUnlocked()) { - log.error("Original spellbook {} is not unlocked, cannot switch back", originalSpellbook.name()); - return false; - } - if (!travelToSwitchLocation(originalSpellbook)) { - log.error("Failed to travel to {} spellbook switch location at location {}", originalSpellbook.name(), originalSpellbook.getSwitchLocation()); - return false; - } - log.info("Switching back to original spellbook: {}....", originalSpellbook.name()); - // Use the enhanced spellbook switching functionality - return originalSpellbook.switchTo(); - } - - /** - * Helper method to travel to the spellbook switching location. - * Uses intelligent pathfinding to determine whether to go directly or via bank first - * for transport items. Analyzes transport requirements and route efficiency. - * Special handling for Lunar Isle access requirements. - * - * @param targetSpellbook The spellbook to switch to - * @return true if travel was successful, false otherwise - */ - private static boolean travelToSwitchLocation(Rs2Spellbook targetSpellbook) { - WorldPoint location = targetSpellbook.getSwitchLocation(); - - if (location == null) { - Microbot.status = "No switch location defined for " + targetSpellbook.name() + " spellbook"; - return false; - } - - // Check if we're already at or very close to the target location - WorldPoint currentLocation = Rs2Player.getWorldLocation(); - if (currentLocation != null && currentLocation.distanceTo(location) <= 3) { - Microbot.status = "Already near " + targetSpellbook.name() + " spellbook switch location"; - return true; - } - - // Special handling for Lunar Isle access (Lunar spellbook) - if (targetSpellbook == Rs2Spellbook.LUNAR) { - log.info("Handling Lunar Isle access for Lunar spellbook switching"); - if (!ensureLunarIsleAccess()) { - log.error("Failed to ensure Lunar Isle access - cannot travel to Lunar spellbook location"); - return false; - } - } - - try { - // Use intelligent transport strategy to determine best route - Microbot.status = "Analyzing route to " + targetSpellbook.name() + " spellbook location..."; - - // Analyze transport requirements for the destination - List missingTransports = Rs2Walker.getTransportsForDestination(location, true); - List missingItemIds = Rs2Walker.getMissingTransportItemIds(missingTransports); - - if (!missingItemIds.isEmpty()) { - Microbot.status = String.format("Found %d missing transport items for %s spellbook location", - missingItemIds.size(), targetSpellbook.name()); - - // Compare direct vs banking routes - TransportRouteAnalysis comparison = Rs2Walker.compareRoutes(location); - - if (comparison.isDirectIsFaster()) { - Microbot.status = String.format("Direct route to %s location is faster (%s)", - targetSpellbook.name(), comparison.getAnalysis()); - } else { - Microbot.status = String.format("Banking route to %s location is more efficient (%s)", - targetSpellbook.name(), comparison.getAnalysis()); - } - } else { - Microbot.status = "No transport items needed, traveling directly to " + targetSpellbook.name() + " location"; - } - - // Execute the travel using intelligent strategy - if (!Rs2Walker.walkWithBankedTransports(location, false)) { - Microbot.status = "Failed to initiate travel to " + targetSpellbook.name() + " spellbook location"; - return false; - } - - Microbot.status = "Traveling to " + targetSpellbook.name() + " spellbook switch location"; - return true; - - } catch (Exception e) { - Microbot.status = "Error during travel planning to " + targetSpellbook.name() + " location: " + e.getMessage(); - log.warn("Error in travelToSwitchLocation for {}: {}", targetSpellbook.name(), e.getMessage()); - - // Fallback to simple walkTo - if (!Rs2Walker.walkTo(location)) { - Microbot.status = "Fallback travel failed to " + targetSpellbook.name() + " spellbook location"; - return false; - } - - return true; - } - } - - /** - * Ensures proper access to Lunar Isle for Lunar spellbook switching. - * Handles the seal of passage requirement and Fremennik Diary elite tier exception. - * - * @return true if Lunar Isle access is ensured, false otherwise - */ - private static boolean ensureLunarIsleAccess() { - try { - // check if lunar diplomacy quest is completed (prerequisite for lunar isle access) - if (Rs2Player.getQuestState(Quest.LUNAR_DIPLOMACY) != QuestState.FINISHED) { - log.error("Lunar Diplomacy quest not completed - cannot access Lunar Isle for spellbook switching"); - return false; - } - - // check fremennik elite diary completion (removes seal of passage requirement) - boolean hasFremennikElite = Microbot.getVarbitValue(VarbitID.FREMENNIK_DIARY_ELITE_COMPLETE) == 1; - if (hasFremennikElite) { - log.info("Fremennik Elite diary completed - seal of passage not required for Lunar Isle access"); - return true; // elite diary completed, no seal needed - } - - log.info("Fremennik Elite diary not completed - checking seal of passage requirement"); - - // check if seal of passage is already equipped - if (Rs2Equipment.isWearing(ItemID.LUNAR_SEAL_OF_PASSAGE)) { - log.info("Seal of passage already equipped - Lunar Isle access confirmed"); - return true; - } - - // check if seal of passage is in inventory - if (Rs2Inventory.hasItem(ItemID.LUNAR_SEAL_OF_PASSAGE)) { - log.info("Seal of passage in inventory, equipping it for Lunar Isle access"); - // equip the seal of passage - if (Rs2Inventory.interact(ItemID.LUNAR_SEAL_OF_PASSAGE, "Wear")) { - boolean equipped = sleepUntil(() -> Rs2Equipment.isWearing(ItemID.LUNAR_SEAL_OF_PASSAGE), 3000); - if (equipped) { - log.info("Successfully equipped seal of passage for Lunar Isle access"); - return true; - } else { - log.error("Failed to equip seal of passage within timeout"); - } - } else { - log.error("Failed to interact with seal of passage to equip"); - } - } - - log.info("Seal of passage not found in inventory, checking bank"); - - // try to get seal of passage from bank - if (!Rs2Bank.isOpen()) { - log.info("Opening bank to retrieve seal of passage"); - if (!Rs2Bank.walkToBankAndUseBank()) { - log.error("Failed to walk to bank and open it"); - return false; - } - - // wait for bank to open - boolean bankOpened = sleepUntil(() -> Rs2Bank.isOpen(), 5000); - if (!bankOpened) { - log.error("Failed to open bank within timeout"); - return false; - } - } - - // check if seal of passage is in bank - if (!Rs2Bank.hasItem(ItemID.LUNAR_SEAL_OF_PASSAGE)) { - log.error("Seal of passage not found in bank - cannot access Lunar Isle without Fremennik Elite diary"); - return false; - } - - log.info("Withdrawing seal of passage from bank"); - - // withdraw seal of passage - if (Rs2Bank.withdrawAllAndEquip(ItemID.LUNAR_SEAL_OF_PASSAGE)) { - if (Rs2Equipment.isWearing(ItemID.LUNAR_SEAL_OF_PASSAGE)){ - log.info("Seal of passage already equipped after withdrawal"); - Rs2Bank.closeBank(); - sleepUntil(() -> !Rs2Bank.isOpen(), 2000); - return true; - } - // wait for withdrawal to complete - boolean withdrawn = sleepUntil(() -> Rs2Inventory.hasItem(ItemID.LUNAR_SEAL_OF_PASSAGE), 3000); - if (!withdrawn) { - log.error("Failed to withdraw seal of passage from bank within timeout"); - return false; - } - - log.info("Successfully withdrew seal of passage, now equipping it"); - - // close bank first - Rs2Bank.closeBank(); - sleepUntil(() -> !Rs2Bank.isOpen(), 2000); - - // equip the seal of passage - if (Rs2Inventory.interact(ItemID.LUNAR_SEAL_OF_PASSAGE, "Wear")) { - boolean equipped = sleepUntil(() -> Rs2Equipment.isWearing(ItemID.LUNAR_SEAL_OF_PASSAGE), 3000); - if (equipped) { - log.info("Successfully equipped seal of passage for Lunar Isle access"); - return true; - } else { - log.error("Failed to equip seal of passage within timeout after withdrawal"); - } - } else { - log.error("Failed to interact with seal of passage to equip after withdrawal"); - } - } else { - log.error("Failed to withdraw seal of passage from bank"); - } - - return false; - - } catch (Exception e) { - log.error("Error ensuring Lunar Isle access: {}", e.getMessage(), e); - return false; - } - } - - - - /** - * Implements the abstract fulfillRequirement method from the base Requirement class. - * Attempts to fulfill this spellbook requirement by switching spellbooks as needed. - * - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if the requirement was successfully fulfilled, false otherwise - */ - @Override - public boolean fulfillRequirement(CompletableFuture scheduledFuture) { - try { - if (Microbot.getClient() == null) { - return false; - } - if (Microbot.getClient().isClientThread()) { - log.error("Please run fulfillRequirement() on a non-client thread."); - return false; - } - // Check if the requirement is already fulfilled - if (hasRequiredSpellbook()) { - return true; - } - - // Check if the required spellbook is available to the player - if (!isRequiredSpellbookAvailable()) { - if (isMandatory()) { - log.error("MANDATORY spellbook requirement cannot be fulfilled: " + getName() + " - Spellbook not unlocked"); - return false; - } else { - log.warn("RECOMMENDED spellbook requirement skipped: " + getName() + " - Spellbook not unlocked"); - return true; // Non-mandatory requirements return true if spellbook isn't available - } - } - - // Determine action based on schedule context - boolean success = false; - success = switchToRequiredSpellbook(scheduledFuture); - - - if (!success && isMandatory()) { - Microbot.log("MANDATORY spellbook requirement failed: " + getName()); - return false; - } - - return true; - - } catch (Exception e) { - Microbot.log("Error fulfilling spellbook requirement " + getName() + ": " + e.getMessage()); - return !isMandatory(); // Don't fail mandatory requirements due to exceptions - } - } - - /** - * Check if the required spellbook is currently available (unlocked via quest completion) - * - * @return true if the spellbook is unlocked and can be used - */ - public boolean isSpellbookUnlocked() { - return requiredSpellbook.isUnlocked(); - } - - /** - * Get the quest required to unlock this spellbook (if any) - * - * @return Quest required for spellbook access, or null if no quest required - */ - public Quest getRequiredQuest() { - return requiredSpellbook.getRequiredQuest(); - } - - /** - * Get the location where spellbook switching can be performed - * - * @return WorldPoint of the spellbook switching location - */ - public WorldPoint getSwitchLocation() { - return requiredSpellbook.getSwitchLocation(); - } - - /** - * Get a description of the spellbook switching method and location - * - * @return String describing how to switch to this spellbook - */ - public String getSwitchDescription() { - return requiredSpellbook.getDescription(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/collection/LootRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/collection/LootRequirement.java deleted file mode 100644 index efaaf6d6f1e..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/collection/LootRequirement.java +++ /dev/null @@ -1,600 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.collection; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import lombok.EqualsAndHashCode; -import net.runelite.api.Constants; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.Microbot; - -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -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.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItem; -import net.runelite.client.plugins.microbot.util.grounditem.models.Rs2SpawnLocation; -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.walker.Rs2Walker; - -import static net.runelite.client.plugins.microbot.util.Global.sleep; -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import org.slf4j.event.Level; - -/** - * Represents an item requirement that can be fulfilled by looting an item from a spawn location. - * Extends the Requirement class directly to add loot-specific functionality. - */ -@Getter -@Slf4j -@EqualsAndHashCode(callSuper = true) -public class LootRequirement extends Requirement { - - /** - * Default item count for this requirement. - * This can be overridden if the plugin requires a specific count. - */ - private final Map amounts; - private final Map collectedAmounts = new HashMap<>();; - - /** - * The spawn locations for this item requirement. - */ - private final Rs2SpawnLocation spawnLocation; - - - /** - * Maximum time to wait for collecting items in milliseconds. - */ - private final Duration timeout ; - /** - * define how far apart spawn locations can be to be considered part of the same cluster. - */ - private final int clusterProximity; - - - /** - * Represents a cluster of nearby spawn locations. - */ - private static class SpawnCluster { - List locations; - WorldPoint center; - double averageDistance; - boolean reachable; - - SpawnCluster(List locations) { - this.locations = locations; - this.center = calculateCenter(locations); - this.averageDistance = calculateAverageDistance(); - this.reachable = false; - } - - private WorldPoint calculateCenter(List points) { - int sumX = points.stream().mapToInt(WorldPoint::getX).sum(); - int sumY = points.stream().mapToInt(WorldPoint::getY).sum(); - int sumPlane = points.stream().mapToInt(WorldPoint::getPlane).sum(); - - return new WorldPoint( - sumX / points.size(), - sumY / points.size(), - sumPlane / points.size() - ); - } - - private double calculateAverageDistance() { - WorldPoint playerLocation = Rs2Player.getWorldLocation(); - if (playerLocation == null) { - return Double.MAX_VALUE; - } - return locations.stream() - .mapToInt(location -> location.distanceTo(playerLocation)) - .average() - .orElse(Double.MAX_VALUE); - } - } - public String getName() { - // Use the first item ID as the name, or "Unknown Item" if no IDs are provided - return (ids.isEmpty() || spawnLocation == null) ? "Unknown Item" : spawnLocation.getItemName(); - } - - /** - * Returns a multi-line display string with detailed loot requirement information. - * Uses StringBuilder with tabs for proper formatting. - * - * @return A formatted string containing loot requirement details - */ - public String displayString() { - StringBuilder sb = new StringBuilder(); - sb.append("=== Loot Requirement Details ===\n"); - sb.append("Name:\t\t\t").append(getName()).append("\n"); - sb.append("Type:\t\t\t").append(getRequirementType().name()).append("\n"); - sb.append("Priority:\t\t").append(getPriority().name()).append("\n"); - sb.append("Rating:\t\t\t").append(getRating()).append("/10\n"); - sb.append("Amounts per id:\t\t\t").append(amounts).append("\n"); - sb.append("Amounts Collected per id:\t").append(collectedAmounts).append("\n"); - sb.append("Item IDs:\t\t").append(getIds().toString()).append("\n"); - sb.append("clusterProximity:\t").append(clusterProximity).append(" tiles\n"); - sb.append("Timeout:\t\t").append(timeout.toSeconds()).append(" seconds\n"); - sb.append("Description:\t\t").append(getDescription() != null ? getDescription() : "No description").append("\n"); - - if (spawnLocation != null) { - sb.append("\n--- Spawn Location Information ---\n"); - sb.append(spawnLocation.displayString()); - - // Add availability check - sb.append("Currently Available:\t").append(isSpawnAvailable() ? "Yes" : "No").append("\n"); - } else { - sb.append("Spawn Location:\t\tNot specified\n"); - } - - return sb.toString(); - } - - /** - * Full constructor for a loot item requirement with schedule context. - */ - public LootRequirement( - List itemIds, - int amount, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext, - Rs2SpawnLocation spawnLocation, - int clusterProximity, - Duration timeout) { - - super(RequirementType.INVENTORY, priority, rating, description, - itemIds, taskContext); - - // Create amounts map with the same amount for all item IDs - Map amountsBuilder = new HashMap<>(itemIds.size()); - for (Integer itemId : itemIds) { - amountsBuilder.put(itemId, amount); - this.collectedAmounts.put(itemId, 0); // Initialize collected amounts to 0 - } - this.amounts = Map.copyOf(amountsBuilder); - this.spawnLocation = spawnLocation; - this.clusterProximity = clusterProximity; - this.timeout = timeout; - } - - /** - * Full constructor for a loot item requirement. - * Defaults to TaskContext.BOTH for backwards compatibility. - */ - public LootRequirement( - List itemIds, - int amount, - RequirementPriority priority, - int rating, - String description, - Rs2SpawnLocation spawnLocation, - int clusterProximity, - Duration timeout) { - - super(RequirementType.INVENTORY, priority, rating, description, - itemIds, TaskContext.BOTH); // Default to BOTH for backwards compatibility - - // Create amounts map with the same amount for all item IDs - Map amountsBuilder = new HashMap<>(); - for (Integer itemId : itemIds) { - amountsBuilder.put(itemId, amount); - collectedAmounts.put(itemId, 0); // Initialize collected amounts to 0 - } - this.amounts = Map.copyOf(amountsBuilder); - this.spawnLocation = spawnLocation; - this.clusterProximity = clusterProximity; - this.timeout = timeout; - } - - /** - * Simple constructor for a loot item requirement with mandatory priority. - */ - public LootRequirement( - int itemId, - int amount, - String description, - Rs2SpawnLocation spawnLocation) { - this( - Arrays.asList(itemId), - amount, - RequirementPriority.MANDATORY, - 5, - description, - spawnLocation, - 20, // Default cluster proximity of 20 tiles - Duration.of(2, java.time.temporal.ChronoUnit.MINUTES) // Default timeout of 2 minutes - ); - - } - - - - /** - * Gets the primary item ID (first in the list). - * Used for backward compatibility with code that expects a single item ID. - * - * @return The primary item ID, or -1 if there are no item IDs - */ - public int getPrimaryItemId() { - return ids.isEmpty() ? -1 : ids.get(0); - } - - /** - * Check if this requirement accepts a specific item ID. - * - * @param itemId The item ID to check - * @return true if this requirement can be fulfilled by the specified item ID, false otherwise - */ - public boolean acceptsItemId(int itemId) { - return ids.contains(itemId); - } - - /** - * Gets the list of item IDs for this requirement. - * Provided for backward compatibility. - * - * @return The list of item IDs - */ - public List getItemIds() { - return ids; - } - - - - /** - * Main loot collection method with configurable cluster proximity. - * Finds the best reachable cluster of spawn locations and efficiently collects items. - * - * @param clusterProximity The maximum distance between spawn locations to be considered part of the same cluster - * @return true if the required amount was successfully collected, false otherwise - */ - private boolean collectLootItems(CompletableFuture scheduledFuture) { - - - if (isFulfilled()) { - return true; - } - - if (spawnLocation == null || spawnLocation.getLocations() == null || spawnLocation.getLocations().isEmpty()) { - log.error("No spawn locations defined for loot requirement: " + getName()); - return false; - } - - try { - // Find the best reachable cluster - SpawnCluster bestCluster = findBestReachableCluster(clusterProximity); - if (bestCluster == null) { - log.error("No reachable spawn clusters found for loot requirement: " + getName()); - return false; - } - log.info("Found cluster with {} spawn locations for {}", bestCluster.locations.size(), getName()); - - // Move to the cluster center - if (!moveToCluster(bestCluster)) { - log.error("Failed to reach cluster center for loot requirement: " + getName()); - return false; - } - - // Collect items from the cluster - return collectFromCluster(scheduledFuture, bestCluster); - - } catch (Exception e) { - log.error("Exception during loot collection for " + getName() + ": " + e.getMessage()); - return false; - } - } - - - - /** - * Finds the best reachable cluster of spawn locations. - */ - private SpawnCluster findBestReachableCluster(int clusterProximity) { - List allLocations = spawnLocation.getLocations(); - List clusters = new ArrayList<>(); - Set processedLocations = new HashSet<>(); - - // Create clusters by grouping nearby locations - for (WorldPoint location : allLocations) { - if (processedLocations.contains(location)) continue; - - List clusterLocations = new ArrayList<>(); - clusterLocations.add(location); - processedLocations.add(location); - - // Find all locations within cluster proximity - for (WorldPoint otherLocation : allLocations) { - if (processedLocations.contains(otherLocation)) continue; - - boolean isNearCluster = clusterLocations.stream() - .anyMatch(clusterLoc -> clusterLoc.distanceTo(otherLocation) <= clusterProximity); - - if (isNearCluster) { - clusterLocations.add(otherLocation); - processedLocations.add(otherLocation); - } - } - - clusters.add(new SpawnCluster(clusterLocations)); - } - - // Check reachability and find the best cluster - SpawnCluster bestCluster = null; - double bestScore = Double.MAX_VALUE; - - for (SpawnCluster cluster : clusters) { - // Check if the cluster center is reachable - cluster.reachable = Rs2Walker.canReach(cluster.center); - - if (cluster.reachable) { - // Score based on distance and cluster size (prefer closer clusters with more spawns) - double score = cluster.averageDistance / Math.max(1, cluster.locations.size()); - - if (score < bestScore) { - bestScore = score; - bestCluster = cluster; - } - } - } - - return bestCluster; - } - - /** - * Moves the player to the cluster center. - */ - private boolean moveToCluster(SpawnCluster cluster) { - WorldPoint currentPosition = Rs2Player.getWorldLocation(); - if (currentPosition == null) { - log.error("Player location is unknown, cannot move to cluster for " + getName()); - return false; - } - // Check if we're already near the cluster - boolean nearCluster = cluster.locations.stream() - .anyMatch(location -> currentPosition.distanceTo(location) <= 15); - - if (nearCluster) { - return true; - } - - // Walk to the cluster center - log.info("Walking to cluster center at {} for {}", cluster.center, getName()); - if (!Rs2Walker.walkTo(cluster.center)) { - return false; - } - - // Wait for arrival - return sleepUntil(() -> { - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - return playerLoc!=null && cluster.locations.stream() - .anyMatch(location -> playerLoc.distanceTo(location) <= 15); - }, 30000); - } - - /** - * Collects items from the cluster with proper banking and respawn handling. - */ - private boolean collectFromCluster(CompletableFuture scheduledFuture,SpawnCluster cluster) { - long startTime = System.currentTimeMillis(); - long lastItemFoundTime = System.currentTimeMillis(); - - while (!isFulfilled() && - (System.currentTimeMillis() - startTime) < timeout.toMillis()) { - if (scheduledFuture != null && (scheduledFuture.isCancelled() || scheduledFuture.isDone())) { - log.error("Loot collection cancelled or completed prematurely: " + getName()); - return false; // Stop if the scheduled future is cancelled or done - } - // Check inventory space and bank if needed - if (Rs2Inventory.isFull() ) { - if (!handleBanking(cluster.center)) { - log.error("Banking failed while collecting loot requirement: " + getName()); - return false; - } - startTime = System.currentTimeMillis(); // Reset start time after banking - - continue; - } - - // Try to loot items in the cluster area - boolean itemFound = false; - for (int itemId : getItemIds()) { - int requiredAmount = amounts.get(itemId); - int currentOverallAmountCollected = collectedAmounts.get(itemId);; //not only in the inventory over the whole collection session with banking - log.info("Looking for item {} in cluster for {} (collected {}/{})", itemId, getName(), currentOverallAmountCollected, requiredAmount); - // Check if we have enough - if (currentOverallAmountCollected >= requiredAmount) { - log.info("Successfully collected required amount of {} for {} already fulfilled", getName(), itemId); - continue; - } - - // Check for items within the cluster area - if (Rs2GroundItem.exists(itemId, 25)) { - final int currentInventoryCount = Rs2Inventory.itemQuantity(itemId); - if (Rs2GroundItem.loot(itemId, 25)) { - // Wait for inventory update - sleepUntil(() -> Rs2Inventory.itemQuantity(itemId) > currentInventoryCount, 3000); - final int gained = Math.max(0, Rs2Inventory.itemQuantity(itemId) - currentInventoryCount); - if (gained <= 0) { - log.warn("Failed to confirm loot of item {} for {}", itemId, getName()); - continue; - } - itemFound = true; - lastItemFoundTime = System.currentTimeMillis(); - currentOverallAmountCollected += gained; // Increment count after successful loot - log.info("Looted item {} for {}, gained {} (collected {}/{})", itemId, getName(),gained, currentOverallAmountCollected, requiredAmount); - collectedAmounts.put(itemId, currentOverallAmountCollected); - break; - } - } - } - - if (itemFound) { - lastItemFoundTime = System.currentTimeMillis(); // Reset last found time - } else { - // No items found, wait for respawn - long timeSinceLastItem = System.currentTimeMillis() - lastItemFoundTime; - Duration itemRespwanTime = spawnLocation.getRespawnTime() != null ? spawnLocation.getRespawnTime() : Duration.ofSeconds(30); - // If we haven't found any items for too long, the cluster might be depleted - if (timeSinceLastItem > 30000) { // 30 seconds - log.info("No items found in cluster for 30s, checking other areas..."); - - // Try checking a bit further from cluster center - boolean foundNearby = false; - for (WorldPoint location : cluster.locations) { - if (Rs2Player.getWorldLocation()!=null && Rs2Player.getWorldLocation().distanceTo(location) <= 30) { - for (int itemId : getItemIds()) { - if (Rs2GroundItem.exists(itemId, 15)) { - Rs2Walker.walkTo(location); - sleepUntil(() -> Rs2Player.getWorldLocation().distanceTo(location) <= 10, 15000); - foundNearby = true; - break; - } - } - if (foundNearby) break; - } - } - - if (!foundNearby) { - log.warn("Cluster appears depleted, waiting for respawn..."); - } - - } - - log.info("Waiting for items to respawn for {} (waited {}s)", getName(), timeSinceLastItem / 1000); - int respawnMs = Math.max(Constants.GAME_TICK_LENGTH*2,(int) itemRespwanTime.toMillis()); - int minMs = Math.max(Constants.GAME_TICK_LENGTH, (int)(itemRespwanTime.toMillis()/2.0)); - sleep(Rs2Random.between(minMs, respawnMs)); - } - } - - // Final check - return isFulfilled(); - } - - /** - * Handles banking when inventory is full. - */ - private boolean handleBanking(WorldPoint returnLocation) { - - try { - final WorldPoint localReturnLocation = returnLocation != null ? returnLocation : Rs2Player.getWorldLocation(); - - - // Find and use nearest bank - if (!Rs2Bank.walkToBankAndUseBank()) { - return false; - } - - //Rs2Bank.depositAllExcept();// transportation related items... we need to impplement these in the future - Rs2Bank.depositAll((item)->getIds().contains(item.getId()) );// transportation related items... we need to impplement these in the future - sleepUntil(() -> getItemIds().stream().allMatch(id -> !Rs2Inventory.hasItem(id) ), 5000); // Wait until all items are deposited - // use the rs2transport or nviation or we have called it to get transportation items to the spwan location .. sowe we dont deposit it. - Rs2Bank.closeBank(); - sleepUntil( () -> !Rs2Bank.isOpen(), 5000); // Wait until bank is closed - // Return to collection area - Rs2Walker.walkTo(localReturnLocation); - return sleepUntil(() -> Rs2Player.getWorldLocation().distanceTo(localReturnLocation) <= 10, 30000); - - } catch (Exception e) { - Microbot.logStackTrace("LootItemRequirement.handleBanking", e); - return false; - } - } - - - - /** - * Checks if this item is currently available to loot. - * - * @return true if the item is available to loot, false otherwise - */ - private boolean isSpawnAvailable() { - if (spawnLocation == null || spawnLocation.getLocations() == null || spawnLocation.getLocations().isEmpty()) { - return false; - } - - // Check if we're near any spawn location - WorldPoint currentPosition = Rs2Player.getWorldLocation(); - if (currentPosition == null) { - return false; - } - for (WorldPoint location : spawnLocation.getLocations()) { - if (location.distanceTo(currentPosition) <= 20) { - // Check if any of our target items are available to loot - for (int itemId : getItemIds()) { - if (Rs2GroundItem.exists(itemId, 15)) { - return true; - } - } - } - } - - return false; - } - - /** - * Implements the abstract fulfillRequirement method from the base Requirement class. - * Attempts to fulfill this loot requirement by collecting items from spawn locations. - * - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if the requirement was successfully fulfilled, false otherwise - */ - @Override - public boolean fulfillRequirement(CompletableFuture scheduledFuture) { - if (Microbot.getClient().isClientThread()) { - Microbot.log("Please run fulfillRequirement() on a non-client thread.", Level.ERROR); - return false; - } - try { - // Check if the requirement is already fulfilled - if (isFulfilled()) { - return true; - } - - // Attempt to collect the required items - boolean success = collectLootItems(scheduledFuture); - - if (!success && isMandatory()) { - Microbot.log("MANDATORY loot requirement failed: " + getName()); - return false; - } - - return true; - - } catch (Exception e) { - Microbot.log("Error fulfilling loot requirement " + getName() + ": " + e.getMessage()); - return !isMandatory(); // Don't fail mandatory requirements due to exceptions - } - } - - /** - * Checks if we already have the required amount of items. - * - * @return true if we have enough items, false otherwise - */ - public boolean isFulfilled() { - boolean hasRequiredAmount = collectedAmounts.entrySet().stream() - .allMatch(entry -> entry.getValue() >= amounts.getOrDefault(entry.getKey(), 0)); - if (hasRequiredAmount) { - log.info("Already have all required items for " + getName()); - return true; - } - return false; - } - - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/conditional/ConditionalRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/conditional/ConditionalRequirement.java deleted file mode 100644 index f70fd245773..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/conditional/ConditionalRequirement.java +++ /dev/null @@ -1,522 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -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.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.LogicalRequirement; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.function.BooleanSupplier; - -/** - * A conditional requirement that executes requirements in sequence based on conditions. - * This addresses the preparation workflows where order matters: - * - * Examples: - * - "If we don't have lunar spellbook AND magic level >= 65, switch to lunar" - * - "First ensure we're at bank, then shop for items, then loot materials, then equip gear" - * - "If missing rune pouches, shop for them, then ensure NPC contact runes" - * - * This is much more powerful than simple AND/OR logic because: - * - Handles temporal dependencies (sequence matters) - * - Represents real OSRS preparation workflows - * - Provides conditional logic based on game state - * - Allows for complex decision trees - */ -@Slf4j -@EqualsAndHashCode(callSuper = true) -public class ConditionalRequirement extends Requirement { - - /** - * Represents a single conditional step in the sequence. - */ - @Getter - public static class ConditionalStep { - private final BooleanSupplier condition; - private final Requirement requirement; - private final String description; - private final boolean isOptional; - - /** - * Creates a mandatory conditional step. - * - * @param condition The condition to check (e.g., () -> !Rs2Player.hasLunarSpellbook()) - * @param requirement The requirement to fulfill if condition is true - * @param description Human-readable description of this step - */ - public ConditionalStep(BooleanSupplier condition, Requirement requirement, String description) { - this(condition, requirement, description, false); - } - - /** - * Creates a conditional step. - * - * @param condition The condition to check - * @param requirement The requirement to fulfill if condition is true - * @param description Human-readable description of this step - * @param isOptional Whether this step can be skipped if it fails - */ - public ConditionalStep(BooleanSupplier condition, Requirement requirement, String description, boolean isOptional) { - this.condition = condition; - this.requirement = requirement; - this.description = description; - this.isOptional = isOptional; - } - - /** - * Checks if this step's condition is met and needs execution. - * - * @return true if the condition is true (step needs to be executed) - */ - public boolean needsExecution() { - try { - return condition.getAsBoolean(); - } catch (Exception e) { - log.warn("Error checking condition for step '{}': {}", description, e.getMessage()); - return false; - } - } - - /** - * Executes this step's requirement. - * - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if successfully fulfilled, false otherwise - */ - public boolean execute(CompletableFuture scheduledFuture) { - try { - log.debug("Executing conditional step: {}", description); - return requirement.fulfillRequirement(scheduledFuture); - } catch (Exception e) { - log.error("Error executing conditional step '{}': {}", description, e.getMessage()); - return isOptional; // Optional steps return true on error, mandatory steps return false - } - } - } - - @Getter - private final List steps = new ArrayList<>(); - - @Getter - private final boolean allowParallelExecution; - - // Execution state tracking - private volatile int currentStepIndex = 0; - private volatile boolean allStepsCompleted = false; - private volatile String lastFailureReason = null; - - /** - * Creates a conditional requirement with sequential execution. - * - * @param priority Priority level for this conditional requirement - * @param rating Effectiveness rating (1-10) - * @param description Human-readable description - * @param TaskContext When this requirement should be fulfilled - * @param stopOnFirstFailure Whether to stop execution on first failure - */ - public ConditionalRequirement(RequirementPriority priority, int rating, String description, - TaskContext taskContext) { - this(priority, rating, description, taskContext, false); - } - - /** - * Creates a conditional requirement. - * - * @param priority Priority level for this conditional requirement - * @param rating Effectiveness rating (1-10) - * @param description Human-readable description - * @param TaskContext When this requirement should be fulfilled - * @param stopOnFirstFailure Whether to stop execution on first failure - * @param allowParallelExecution Whether steps can be executed in parallel (when conditions don't depend on each other) - */ - public ConditionalRequirement(RequirementPriority priority, int rating, String description, - TaskContext taskContext, - boolean allowParallelExecution) { - super(RequirementType.CONDITIONAL, priority, rating, description, List.of(), taskContext); - this.allowParallelExecution = allowParallelExecution; - } - - /** - * Adds a conditional step to this requirement. - * Steps are executed in the order they are added. - * - * @param condition The condition to check - * @param requirement The requirement to fulfill if condition is true - * @param description Description of this step - * @return This ConditionalRequirement for method chaining - */ - public ConditionalRequirement addStep(BooleanSupplier condition, Requirement requirement, String description) { - return addStep(condition, requirement, description, false); - } - - /** - * Adds a conditional step to this requirement. - * - * @param condition The condition to check - * @param requirement The requirement to fulfill if condition is true - * @param description Description of this step - * @param isOptional Whether this step can be skipped if it fails - * @return This ConditionalRequirement for method chaining - */ - public ConditionalRequirement addStep(BooleanSupplier condition, Requirement requirement, - String description, boolean isOptional) { - steps.add(new ConditionalStep(condition, requirement, description, isOptional)); - return this; - } - - /** - * Adds a step that always executes (unconditional). - * - * @param requirement The requirement to always fulfill - * @param description Description of this step - * @return This ConditionalRequirement for method chaining - */ - public ConditionalRequirement addAlwaysStep(Requirement requirement, String description) { - return addStep(() -> true, requirement, description, false); - } - - /** - * Adds a step that executes only if a condition is NOT met. - * - * @param condition The condition to check (step executes if this is false) - * @param requirement The requirement to fulfill if condition is false - * @param description Description of this step - * @return This ConditionalRequirement for method chaining - */ - public ConditionalRequirement addIfNotStep(BooleanSupplier condition, Requirement requirement, String description) { - return addStep(() -> !condition.getAsBoolean(), requirement, description, false); - } - - @Override - public String getName() { - return "Conditional: " + getDescription(); - } - - @Override - public boolean fulfillRequirement(CompletableFuture scheduledFuture) { - log.debug("Starting conditional requirement fulfillment: {}", getName()); - - // Reset state - currentStepIndex = 0; - allStepsCompleted = false; - lastFailureReason = null; - if (steps.isEmpty()) { - log.warn("No steps defined for conditional requirement: {}", getName()); - return true; // Empty requirement is considered fulfilled - } - - // Execute steps in sequence - for (int i = 0; i < steps.size(); i++) { - if( scheduledFuture != null && (scheduledFuture.isCancelled() || scheduledFuture.isDone())) { - log.warn("Conditional requirement execution cancelled or completed prematurely: {}", getName()); - return false; // Stop if the scheduled future is cancelled or done - } - currentStepIndex = i; - ConditionalStep step = steps.get(i); - - // Check if this step needs execution - if (!step.needsExecution()) { - log.debug("Skipping step {} (condition not met): {}", i, step.getDescription()); - continue; - } - - // Execute the step - log.debug("Executing step {}: {}", i, step.getDescription()); - boolean success = step.execute(scheduledFuture); - - if (!success) { - lastFailureReason = "Step " + i + " failed: " + step.getDescription(); - log.warn("Conditional requirement step failed: {}", lastFailureReason); - - if (!step.isOptional()) { - log.error("Stopping conditional requirement due to mandatory step failure: {}", lastFailureReason); - return false; - } - - } - } - - allStepsCompleted = true; - log.debug("Conditional requirement completed successfully: {}", getName()); - return true; - } - - /** - * Checks if this conditional requirement is currently fulfilled. - * This checks if all mandatory steps have been executed successfully. - */ - @Override - public boolean isFulfilled() { - // If we haven't started or haven't completed all steps, not fulfilled - if (!allStepsCompleted) { - return false; - } - - // Check if any mandatory steps still need execution - for (ConditionalStep step : steps) { - if (step.needsExecution() && !step.isOptional()) { - return false; - } - } - - return true; - } - - /** - * Gets the current execution progress as a percentage. - * - * @return Progress from 0.0 to 1.0 - */ - public double getExecutionProgress() { - if (steps.isEmpty()) { - return 1.0; - } - return (double) currentStepIndex / steps.size(); - } - - /** - * Gets the current step being executed. - * - * @return The current step, or null if not started or completed - */ - public ConditionalStep getCurrentStep() { - if (currentStepIndex >= 0 && currentStepIndex < steps.size()) { - return steps.get(currentStepIndex); - } - return null; - } - /** - * Gets all requirements that are currently active and need to be fulfilled. - * - * This method is crucial for the @InventorySetupPlanner integration. For example, we have a - * conditional requirement with 2 steps, but only one of the steps is relevant at any given time. - * - * Example use case: Items for alching - we need either fire runes in inventory OR any kind of fire staff - * (normal, lava, battlestaff, mystic) equipped or in inventory. - * - * Setup: - * - Step 1: ItemRequirement for fire runes, with condition: - * () -> (!Rs2Equipment.hasFireStaff() && !Rs2Inventory.hasFireStaff() && !Rs2Bank.hasFireStaff()) - * This step is active when we don't have any fire staff available - * - * - Step 2: OrRequirement with multiple ItemRequirements for different fire staffs, - * with condition: () -> Rs2Equipment.hasFireStaff() || Rs2Inventory.hasFireStaff() || Rs2Bank.hasFireStaff() - * This step is active when we have a fire staff available - * - * The RequirementRegistry caching system should: - * 1. Detect ConditionalRequirements that have only ItemRequirement steps (or LogicalRequirements composed of ItemRequirements) - * 2. Create separate cache entries for ConditionalRequirements to enable efficient active requirement lookup - * 3. Use containsOnlyItemRequirements() method from LogicalRequirement to validate cache compatibility - * 4. Save the allowed child type information to enable type-safe caching optimizations - * - * TODO: Implement ConditionalRequirement handling in: - * - RequirementSelector (to process active requirements) - * - RequirementRegistry (separate cache for conditional requirements with type tracking) - * - InventorySetupPlanner (to use getActiveRequirements() for planning) - * - * @return List of requirements that are currently active and need fulfillment - */ - public List getActiveRequirements() { - List activeRequirements = new ArrayList<>(); - for (ConditionalStep step : steps) { - if (step.needsExecution()) { - activeRequirements.add(step.getRequirement()); - } - } - return activeRequirements; - } - - /** - * Gets all ItemRequirements from currently active steps only. - * This method extracts ItemRequirements from steps that currently need execution, - * flattening any nested LogicalRequirements that contain ItemRequirements. - * - * This is essential for InventorySetupPlanner integration, as it needs to know - * which specific items are required for the current conditional state. - * - * Example: If we have a conditional with two steps: - * - Step 1 (active): Requires fire runes - * - Step 2 (inactive): Requires fire staff - * - * This method will return only the fire runes ItemRequirement since - * that's the only currently active step. - * - * @return List of ItemRequirements from currently active conditional steps - */ - public List getActiveItemRequirements() { - List activeItemRequirements = new ArrayList<>(); - - for (ConditionalStep step : steps) { - if (step.needsExecution()) { - Requirement stepRequirement = step.getRequirement(); - - if (stepRequirement instanceof ItemRequirement) { - activeItemRequirements.add((ItemRequirement) stepRequirement); - } else if (stepRequirement instanceof LogicalRequirement) { - LogicalRequirement logicalReq = (LogicalRequirement) stepRequirement; - activeItemRequirements.addAll(logicalReq.getAllItemRequirements()); - } - } - } - - return activeItemRequirements; - } - - /** - * Checks if this conditional requirement contains only ItemRequirements (or LogicalRequirements that contain only ItemRequirements). - * This is useful for RequirementRegistry caching to identify conditional requirements that can be processed - * alongside other item-based requirements for inventory planning. - * - * @return true if all steps in this conditional requirement contain only ItemRequirements - */ - public boolean containsOnlyItemRequirements() { - for (ConditionalStep step : steps) { - Requirement stepRequirement = step.getRequirement(); - - if (stepRequirement instanceof ItemRequirement) { - // ItemRequirement is allowed - continue; - } else if (stepRequirement instanceof LogicalRequirement) { - // Check if LogicalRequirement contains only ItemRequirements - LogicalRequirement logicalReq = (LogicalRequirement) stepRequirement; - if (!logicalReq.containsOnlyItemRequirements()) { - return false; - } - } else { - // Any other requirement type means this is not an item-only conditional - return false; - } - } - - return true; - } - - /** - * Gets all ItemRequirements from all steps in this conditional requirement, flattening any nested logical requirements. - * This is useful for RequirementRegistry to extract all potential item requirements for caching purposes. - * - * @return List of all ItemRequirements across all steps - */ - public List getAllItemRequirements() { - List allItemRequirements = new ArrayList<>(); - - for (ConditionalStep step : steps) { - Requirement stepRequirement = step.getRequirement(); - - if (stepRequirement instanceof ItemRequirement) { - allItemRequirements.add((ItemRequirement) stepRequirement); - } else if (stepRequirement instanceof LogicalRequirement) { - LogicalRequirement logicalReq = (LogicalRequirement) stepRequirement; - allItemRequirements.addAll(logicalReq.getAllItemRequirements()); - } - } - - return allItemRequirements; - } - - /** - * Gets the reason for the last failure, if any. - * - * @return Failure reason or null if no failure - */ - public String getLastFailureReason() { - return lastFailureReason; - } - - /** - * Gets a detailed status string for display/debugging. - * - * @return Status string with progress and current step info - */ - public String getDetailedStatus() { - StringBuilder sb = new StringBuilder(); - sb.append(getName()).append(" - "); - - if (allStepsCompleted) { - sb.append("Completed"); - } else { - sb.append("Progress: ").append(String.format("%.0f%%", getExecutionProgress() * 100)); - - ConditionalStep currentStep = getCurrentStep(); - if (currentStep != null) { - sb.append(" - Current: ").append(currentStep.getDescription()); - } - } - - if (lastFailureReason != null) { - sb.append(" - Last failure: ").append(lastFailureReason); - } - - return sb.toString(); - } - - @Override - public String toString() { - return "ConditionalRequirement{" + - "name='" + getName() + '\'' + - ", steps=" + steps.size() + - ", progress=" + String.format("%.0f%%", getExecutionProgress() * 100) + - ", completed=" + allStepsCompleted + - '}'; - } - - /** - * Returns a detailed display string with conditional requirement information. - * - * @return A formatted string containing conditional requirement details - */ - @Override - public String displayString() { - StringBuilder sb = new StringBuilder(); - sb.append("=== Conditional Requirement Details ===\n"); - sb.append("Name:\t\t\t").append(getName()).append("\n"); - sb.append("Type:\t\t\t").append(getRequirementType().name()).append("\n"); - sb.append("Priority:\t\t").append(getPriority().name()).append("\n"); - sb.append("Rating:\t\t\t").append(getRating()).append("/10\n"); - sb.append("Schedule Context:\t").append(getTaskContext().name()).append("\n"); - sb.append("Parallel Execution:\t").append(allowParallelExecution ? "Yes" : "No").append("\n"); - sb.append("Total Steps:\t\t").append(steps.size()).append("\n"); - sb.append("Progress:\t\t").append(String.format("%.1f%%", getExecutionProgress() * 100)).append("\n"); - sb.append("Completed:\t\t").append(allStepsCompleted ? "Yes" : "No").append("\n"); - sb.append("Description:\t\t").append(getDescription() != null ? getDescription() : "No description").append("\n"); - - // Add step details - sb.append("\n--- Steps Details ---\n"); - for (int i = 0; i < steps.size(); i++) { - ConditionalStep step = steps.get(i); - sb.append("Step ").append(i + 1).append(":\t\t").append(step.getDescription()).append("\n"); - sb.append("\t\t\tOptional: ").append(step.isOptional() ? "Yes" : "No").append("\n"); - sb.append("\t\t\tRequirement: ").append(step.getRequirement().getName()).append("\n"); - if (i < currentStepIndex) { - sb.append("\t\t\tStatus: Completed\n"); - } else if (i == currentStepIndex) { - sb.append("\t\t\tStatus: Current Step\n"); - } else { - sb.append("\t\t\tStatus: Pending\n"); - } - } - - if (lastFailureReason != null) { - sb.append("\n--- Last Failure ---\n"); - sb.append("Reason:\t\t\t").append(lastFailureReason).append("\n"); - } - - return sb.toString(); - } - - /** - * Enhanced toString method that uses displayString for comprehensive output. - * - * @return A comprehensive string representation of this conditional requirement - */ - public String toDetailedString() { - return displayString(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/conditional/OrderedRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/conditional/OrderedRequirement.java deleted file mode 100644 index 2d0b8ed4f37..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/conditional/OrderedRequirement.java +++ /dev/null @@ -1,427 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ScheduledExecutorService; - -/** - * An ordered requirement that executes requirements in strict sequence. - * Unlike ConditionalRequirement which uses conditions, this executes ALL steps in order. - * - * Perfect for workflows where order matters: - * - First shop for supplies - * - Then loot materials - * - Then equip gear - * - Then go to location - * - * Each step must complete successfully before proceeding to the next. - */ -@Slf4j -@EqualsAndHashCode(callSuper = true) -public class OrderedRequirement extends Requirement { - - /** - * Represents a single ordered step in the sequence. - */ - @Getter - public static class OrderedStep { - private final Requirement requirement; - private final String description; - private final boolean isMandatory; - - /** - * Creates a mandatory ordered step. - * - * @param requirement The requirement to fulfill - * @param description Human-readable description of this step - */ - public OrderedStep(Requirement requirement, String description) { - this(requirement, description, true); - } - - /** - * Creates an ordered step. - * - * @param requirement The requirement to fulfill - * @param description Human-readable description of this step - * @param isMandatory Whether this step must succeed for the sequence to continue - */ - public OrderedStep(Requirement requirement, String description, boolean isMandatory) { - this.requirement = requirement; - this.description = description; - this.isMandatory = isMandatory; - } - - /** - * Executes this step's requirement. - * - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if successfully fulfilled, false otherwise - */ - public boolean execute(CompletableFuture scheduledFuture) { - try { - log.debug("Executing ordered step: {}", description); - return requirement.fulfillRequirement(scheduledFuture); - } catch (Exception e) { - log.error("Error executing ordered step '{}': {}", description, e.getMessage()); - return false; // Defer optional skip policy to the caller (allowSkipOptional) - } - } - - /** - * Checks if this step is currently fulfilled. - * - * @return true if the requirement is fulfilled - */ - public boolean isFulfilled() { - try { - return requirement.isFulfilled(); - } catch (Exception e) { - log.warn("Error checking fulfillment for step '{}': {}", description, e.getMessage()); - return false; - } - } - } - - @Getter - private final List steps = new ArrayList<>(); - - @Getter - private final boolean allowSkipOptional; - - @Getter - private final boolean resumeFromLastFailed; - - // Execution state tracking - private volatile int currentStepIndex = 0; - private volatile int lastCompletedStep = -1; - private volatile boolean allStepsCompleted = false; - private volatile String lastFailureReason = null; - - /** - * Creates an ordered requirement. - * - * @param priority Priority level for this ordered requirement - * @param rating Effectiveness rating (1-10) - * @param description Human-readable description - * @param TaskContext When this requirement should be fulfilled - * @param allowSkipOptional Whether optional steps can be skipped on failure - * @param resumeFromLastFailed Whether to resume from the last failed step or restart - */ - public OrderedRequirement(RequirementPriority priority, int rating, String description, - TaskContext taskContext, boolean allowSkipOptional, - boolean resumeFromLastFailed) { - super(RequirementType.CONDITIONAL, priority, rating, description, List.of(), taskContext); - this.allowSkipOptional = allowSkipOptional; - this.resumeFromLastFailed = resumeFromLastFailed; - } - - /** - * Creates an ordered requirement with default settings. - * - * @param priority Priority level for this ordered requirement - * @param rating Effectiveness rating (1-10) - * @param description Human-readable description - * @param TaskContext When this requirement should be fulfilled - */ - public OrderedRequirement(RequirementPriority priority, int rating, String description, TaskContext taskContext) { - this(priority, rating, description, taskContext, true, true); - } - - /** - * Adds a mandatory step to this ordered requirement. - * Steps are executed in the order they are added. - * - * @param requirement The requirement to fulfill - * @param description Description of this step - * @return This OrderedRequirement for method chaining - */ - public OrderedRequirement addStep(Requirement requirement, String description) { - return addStep(requirement, description, true); - } - - /** - * Adds an optional step to this ordered requirement. - * - * @param requirement The requirement to fulfill - * @param description Description of this step - * @return This OrderedRequirement for method chaining - */ - public OrderedRequirement addOptionalStep(Requirement requirement, String description) { - return addStep(requirement, description, false); - } - - /** - * Adds a step to this ordered requirement. - * - * @param requirement The requirement to fulfill - * @param description Description of this step - * @param isMandatory Whether this step must succeed - * @return This OrderedRequirement for method chaining - */ - public OrderedRequirement addStep(Requirement requirement, String description, boolean isMandatory) { - steps.add(new OrderedStep(requirement, description, isMandatory)); - return this; - } - - @Override - public String getName() { - return "Ordered: " + getDescription(); - } - - @Override - public boolean fulfillRequirement(CompletableFuture scheduledFuture) { - log.debug("Starting ordered requirement fulfillment: {}", getName()); - - // Determine starting point - int startIndex = resumeFromLastFailed ? Math.max(0, lastCompletedStep + 1) : 0; - - if (startIndex == 0) { - // Fresh start - reset state - currentStepIndex = 0; - lastCompletedStep = -1; - allStepsCompleted = false; - lastFailureReason = null; - } - - if (steps.isEmpty()) { - log.warn("No steps defined for ordered requirement: {}", getName()); - return true; // Empty requirement is considered fulfilled - } - - // Execute steps in strict order starting from determined index - for (int i = startIndex; i < steps.size(); i++) { - if( scheduledFuture!= null && (scheduledFuture.isCancelled() || scheduledFuture.isDone())) { - log.warn("Ordered requirement execution cancelled or completed prematurely: {}", getName()); - return false; // Stop if the scheduled future is cancelled or done - } - currentStepIndex = i; - OrderedStep step = steps.get(i); - - log.debug("Executing ordered step {}: {}", i, step.getDescription()); - boolean success = step.execute(scheduledFuture); - - if (success) { - lastCompletedStep = i; - log.debug("Completed step {}: {}", i, step.getDescription()); - } else { - lastFailureReason = "Step " + i + " failed: " + step.getDescription(); - log.warn("Ordered requirement step failed: {}", lastFailureReason); - - if (step.isMandatory()) { - log.error("Stopping ordered requirement due to mandatory step failure: {}", lastFailureReason); - return false; - } else if (!allowSkipOptional) { - log.error("Stopping ordered requirement due to optional step failure (skip not allowed): {}", lastFailureReason); - return false; - } else { - log.warn("Skipping optional step failure: {}", lastFailureReason); - lastCompletedStep = i; // Mark as completed even though it failed (optional) - } - } - } - - allStepsCompleted = true; - log.debug("Ordered requirement completed successfully: {}", getName()); - return true; - } - - /** - * Checks if this ordered requirement is currently fulfilled. - * This checks if all mandatory steps have been completed. - */ - @Override - public boolean isFulfilled() { - // If we haven't completed all steps, check current state - if (!allStepsCompleted) { - return false; - } - - // Check if all mandatory steps are still fulfilled - for (OrderedStep step : steps) { - if (step.isMandatory() && !step.isFulfilled()) { - return false; - } - } - - return true; - } - - /** - * Gets the current execution progress as a percentage. - * - * @return Progress from 0.0 to 1.0 - */ - public double getExecutionProgress() { - if (steps.isEmpty()) { - return 1.0; - } - return (double) (lastCompletedStep + 1) / steps.size(); - } - - /** - * Gets the current step being executed. - * - * @return The current step, or null if not started or completed - */ - public OrderedStep getCurrentStep() { - if (currentStepIndex >= 0 && currentStepIndex < steps.size()) { - return steps.get(currentStepIndex); - } - return null; - } - - /** - * Gets the next step to be executed. - * - * @return The next step, or null if all steps completed - */ - public OrderedStep getNextStep() { - int nextIndex = lastCompletedStep + 1; - if (nextIndex >= 0 && nextIndex < steps.size()) { - return steps.get(nextIndex); - } - return null; - } - - /** - * Gets the number of completed steps. - * - * @return Number of successfully completed steps - */ - public int getCompletedStepCount() { - return lastCompletedStep + 1; - } - - /** - * Gets the number of remaining steps. - * - * @return Number of steps still to be executed - */ - public int getRemainingStepCount() { - return Math.max(0, steps.size() - getCompletedStepCount()); - } - - /** - * Resets the execution state to start from the beginning. - */ - public void reset() { - currentStepIndex = 0; - lastCompletedStep = -1; - allStepsCompleted = false; - lastFailureReason = null; - log.debug("Reset ordered requirement: {}", getName()); - } - - /** - * Gets the reason for the last failure, if any. - * - * @return Failure reason or null if no failure - */ - public String getLastFailureReason() { - return lastFailureReason; - } - - /** - * Gets a detailed status string for display/debugging. - * - * @return Status string with progress and current step info - */ - public String getDetailedStatus() { - StringBuilder sb = new StringBuilder(); - sb.append(getName()).append(" - "); - - if (allStepsCompleted) { - sb.append("Completed (").append(steps.size()).append("/").append(steps.size()).append(" steps)"); - } else { - sb.append("Progress: ").append(getCompletedStepCount()).append("/").append(steps.size()).append(" steps"); - - OrderedStep nextStep = getNextStep(); - if (nextStep != null) { - sb.append(" - Next: ").append(nextStep.getDescription()); - } - } - - if (lastFailureReason != null) { - sb.append(" - Last failure: ").append(lastFailureReason); - } - - return sb.toString(); - } - - @Override - public String toString() { - return "OrderedRequirement{" + - "name='" + getName() + '\'' + - ", steps=" + steps.size() + - ", completed=" + getCompletedStepCount() + - ", allCompleted=" + allStepsCompleted + - '}'; - } - - /** - * Returns a detailed display string with ordered requirement information. - * - * @return A formatted string containing ordered requirement details - */ - @Override - public String displayString() { - StringBuilder sb = new StringBuilder(); - sb.append("=== Ordered Requirement Details ===\n"); - sb.append("Name:\t\t\t").append(getName()).append("\n"); - sb.append("Type:\t\t\t").append(getRequirementType().name()).append("\n"); - sb.append("Priority:\t\t").append(getPriority().name()).append("\n"); - sb.append("Rating:\t\t\t").append(getRating()).append("/10\n"); - sb.append("Schedule Context:\t").append(getTaskContext().name()).append("\n"); - sb.append("Allow Skip Optional:\t").append(allowSkipOptional ? "Yes" : "No").append("\n"); - sb.append("Resume from Failed:\t").append(resumeFromLastFailed ? "Yes" : "No").append("\n"); - sb.append("Total Steps:\t\t").append(steps.size()).append("\n"); - sb.append("Completed Steps:\t").append(getCompletedStepCount()).append("/").append(steps.size()).append("\n"); - sb.append("Progress:\t\t").append(String.format("%.1f%%", getExecutionProgress() * 100)).append("\n"); - sb.append("All Completed:\t\t").append(allStepsCompleted ? "Yes" : "No").append("\n"); - sb.append("Description:\t\t").append(getDescription() != null ? getDescription() : "No description").append("\n"); - - // Add step details - sb.append("\n--- Steps Details ---\n"); - for (int i = 0; i < steps.size(); i++) { - OrderedStep step = steps.get(i); - sb.append("Step ").append(i + 1).append(":\t\t").append(step.getDescription()).append("\n"); - sb.append("\t\t\tMandatory: ").append(step.isMandatory() ? "Yes" : "No").append("\n"); - sb.append("\t\t\tRequirement: ").append(step.getRequirement().getName()).append("\n"); - - // Show execution status - if (i < currentStepIndex) { - sb.append("\t\t\tStatus: Completed\n"); - } else if (i == currentStepIndex && !allStepsCompleted) { - sb.append("\t\t\tStatus: Current Step\n"); - } else { - sb.append("\t\t\tStatus: Pending\n"); - } - } - - if (lastFailureReason != null) { - sb.append("\n--- Last Failure ---\n"); - sb.append("Reason:\t\t\t").append(lastFailureReason).append("\n"); - } - - return sb.toString(); - } - - /** - * Enhanced toString method that uses displayString for comprehensive output. - * - * @return A comprehensive string representation of this ordered requirement - */ - public String toDetailedString() { - return displayString(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/InventorySetupPlanner.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/InventorySetupPlanner.java deleted file mode 100644 index 556340c3622..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/InventorySetupPlanner.java +++ /dev/null @@ -1,3097 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item; - -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.api.Skill; -import net.runelite.client.plugins.microbot.inventorysetups.InventorySetup; -import net.runelite.client.plugins.microbot.inventorysetups.InventorySetupsItem; -import net.runelite.client.plugins.microbot.inventorysetups.InventorySetupsStackCompareID; -import net.runelite.client.plugins.microbot.inventorysetups.InventorySetupsVariationMapping; -import net.runelite.client.plugins.microbot.inventorysetups.MInventorySetupsPlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.LogicalRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.OrRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.ConditionalRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.OrRequirementMode; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.registry.RequirementRegistry; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.util.RequirementSelector; -import net.runelite.client.plugins.microbot.util.Rs2InventorySetup; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Delayed; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.stream.Collectors; -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -/** - * Represents a comprehensive inventory and equipment layout plan. - * This class manages the optimal placement of items across equipment slots and inventory slots, - * considering constraints, priorities, and availability. - */ -@Slf4j -@Getter -public class InventorySetupPlanner { - - // Equipment slot assignments (fixed positions) - private final Map equipmentAssignments = new HashMap<>(); - - // Specific inventory slot assignments (0-27) - private final Map inventorySlotAssignments = new HashMap<>(); - - // Flexible inventory items that can be placed in any available slot - private final List flexibleInventoryItems = new ArrayList<>(); - - // Missing mandatory items that could not be fulfilled - private final List missingMandatoryItems = new ArrayList<>(); - - // Missing mandatory equipment slots - private final Map> missingMandatoryEquipment = new HashMap<>(); - - // Optional: Requirements planning support (for new planning approach) - private RequirementRegistry registry; - private TaskContext taskContext; - private OrRequirementMode orRequirementMode = OrRequirementMode.ANY_COMBINATION; // Default mode - - // Flag to control whether to log recommended/optional missing items in comprehensive analysis - private boolean logOptionalMissingItems = true; // Default to true for backward compatibility - - /** - * Default constructor for backward compatibility. - */ - public InventorySetupPlanner() { - // Default constructor - uses existing functionality - } - - /** - * Enhanced constructor for requirements-based planning. - * - * @param registry The requirement registry containing all requirements - * @param TaskContext The schedule context (PRE_SCHEDULE or POST_SCHEDULE) - * @param orRequirementMode How to handle OR requirements (ANY_COMBINATION or SINGLE_TYPE) - */ - public InventorySetupPlanner(RequirementRegistry registry, TaskContext taskContext, OrRequirementMode orRequirementMode) { - this.registry = registry; - this.taskContext = taskContext; - this.orRequirementMode = orRequirementMode; - this.logOptionalMissingItems = true; // Default to logging optional items - } - - /** - * Enhanced constructor for requirements-based planning with optional item logging control. - * - * @param registry The requirement registry containing all requirements - * @param TaskContext The schedule context (PRE_SCHEDULE or POST_SCHEDULE) - * @param orRequirementMode How to handle OR requirements (ANY_COMBINATION or SINGLE_TYPE) - * @param logOptionalMissingItems Whether to log recommended/optional missing items in comprehensive analysis - */ - public InventorySetupPlanner(RequirementRegistry registry, TaskContext taskContext, OrRequirementMode orRequirementMode, boolean logOptionalMissingItems) { - this.registry = registry; - this.taskContext = taskContext; - this.orRequirementMode = orRequirementMode; - this.logOptionalMissingItems = logOptionalMissingItems; - } - - /** - * Adds an equipment slot assignment. - */ - public void addEquipmentSlotAssignment(EquipmentInventorySlot slot, ItemRequirement item) { - equipmentAssignments.put(slot, item); - log.debug("Assigned {} to equipment slot {}", item.getName(), slot); - } - - /** - * Adds a specific inventory slot assignment with stackability validation. - */ - public void addInventorySlotAssignment(int slot, ItemRequirement item) { - if (slot < 0 || slot > 27) { - throw new IllegalArgumentException("Invalid inventory slot: " + slot); - } - - // Validate stackability constraints - if (item.getAmount() > 1 && !item.isStackable()) { - log.warn("Cannot assign non-stackable item {} with amount {} to single slot {}", - item.getName(), item.getAmount(), slot); - // For non-stackable items with amount > 1, we need to handle differently - // This should be caught earlier in the planning process - return; - } - - inventorySlotAssignments.put(slot, item); - log.info("Assigned {} (amount: {}, stackable: {}) to inventory slot {}", - item.getName(), item.getAmount(), item.isStackable(), slot); - } - - /** - * Adds a flexible inventory item with stackability considerations. - */ - public void addFlexibleInventoryItem(ItemRequirement item) { - // Validate that the item can fit in inventory - if (!item.canFitInInventory()) { - log.warn("Item {} requires {} slots but inventory is full", - item.getName(), item.getRequiredInventorySlots()); - if (item.isMandatory()) { - addMissingMandatoryInventoryItem(item); - } - return; - } - - flexibleInventoryItems.add(item); - log.debug("Added flexible inventory item: {} (requires {} slots)", - item.getName(), item.getRequiredInventorySlots()); - } - - /** - * Adds a missing mandatory item. - */ - public void addMissingMandatoryInventoryItem(ItemRequirement item) { - missingMandatoryItems.add(item); - log.debug("Missing mandatory item: {}", item.getName()); - } - - /** - * Adds a missing mandatory equipment slot. - */ - public void addMissingMandatoryEquipment(EquipmentInventorySlot slot, ItemRequirement item) { - missingMandatoryEquipment.computeIfAbsent(slot, k -> new ArrayList<>()).add(item); - log.warn("Missing mandatory equipment for slot: {}", slot); - } - - /** - * Checks if an equipment slot is already occupied in this plan. - */ - public boolean isEquipmentSlotOccupied(EquipmentInventorySlot slot) { - return equipmentAssignments.containsKey(slot); - } - - /** - * Checks if an inventory slot is already occupied in this plan. - */ - public boolean isInventorySlotOccupied(int slot) { - return inventorySlotAssignments.containsKey(slot); - } - - /** - * Checks if the plan is feasible (no missing mandatory items). - */ - public boolean isFeasible() { - return missingMandatoryItems.isEmpty() && missingMandatoryEquipment.isEmpty(); - } - - /** - * Gets the total number of inventory slots that will be occupied. - * Properly accounts for stackability and amounts. - */ - public int getTotalInventorySlotsNeeded() { - int slotsNeeded = inventorySlotAssignments.size(); - - // Calculate slots needed for flexible items considering stackability - for (ItemRequirement item : flexibleInventoryItems) { - slotsNeeded += item.getRequiredInventorySlots(); - } - - return slotsNeeded; - } - - /** - * Checks if the plan fits within inventory capacity (28 slots). - */ - public boolean fitsInInventory() { - return getTotalInventorySlotsNeeded() <= 28; - } - - /** - * Gets all occupied inventory slots. - */ - public Set getOccupiedInventorySlots() { - Set occupied = new HashSet<>(inventorySlotAssignments.keySet()); - - // For flexible items, we'd need to simulate placement - // For now, just assume they'll fit in remaining slots - int flexibleItemsPlaced = 0; - for (int slot = 0; slot < 28 && flexibleItemsPlaced < flexibleInventoryItems.size(); slot++) { - if (!occupied.contains(slot)) { - occupied.add(slot); - flexibleItemsPlaced++; - } - } - - return occupied; - } - - /** - * Optimizes the placement of flexible inventory items. - * This method attempts to find the best slots for items that don't have specific slot requirements. - * Considers stackability, space constraints, and item consolidation opportunities. - */ - public void optimizeFlexibleItemPlacement() { - if (flexibleInventoryItems.isEmpty()) { - return; - } - - // First, try to consolidate stackable items of the same type - consolidateStackableItems(); - - // Sort flexible items by priority and rating - flexibleInventoryItems.sort((a, b) -> { - int priorityCompare = a.getPriority().compareTo(b.getPriority()); - if (priorityCompare != 0) { - return priorityCompare; - } - return Integer.compare(b.getRating(), a.getRating()); - }); - - // Check if we have enough space - if (!fitsInInventory()) { - log.warn("Not enough inventory space for all items. Need {} slots, but only 28 available.", - getTotalInventorySlotsNeeded()); - - // Attempt space optimization before removing items - if (attemptSpaceOptimization()) { - log.info("Space optimization successful - all items now fit"); - } else { - // Remove lowest priority items until we fit, considering stackability - removeItemsUntilFit(); - } - } - - log.info("Optimized placement for {} flexible inventory items (total slots needed: {})", - flexibleInventoryItems.size(), getTotalInventorySlotsNeeded()); - } - - /** - * Consolidates stackable items of the same type to save space. - */ - private void consolidateStackableItems() { - Map, ItemRequirement> stackableItems = new HashMap<>(); - List toRemove = new ArrayList<>(); - List toAdd = new ArrayList<>(); - - for (ItemRequirement item : flexibleInventoryItems) { - if (item.isStackable()) { - List itemIds = item.getIds(); - if (stackableItems.containsKey(itemIds)) { - // Found another item of the same type - consolidate - ItemRequirement existing = stackableItems.get(itemIds); - int newAmount = existing.getAmount() + item.getAmount(); - - // Create consolidated item - ItemRequirement consolidated = existing.copyWithAmount(newAmount); - - // Mark items for replacement - toRemove.add(existing); - toRemove.add(item); - toAdd.add(consolidated); - - // Update the map - stackableItems.put(itemIds, consolidated); - - log.info("Consolidated stackable items: {} + {} = {} (total: {})", - existing.getName(), item.getName(), consolidated.getName(), newAmount); - } else { - stackableItems.put(itemIds, item); - } - } - } - - // Apply consolidation - flexibleInventoryItems.removeAll(toRemove); - flexibleInventoryItems.addAll(toAdd); - } - - /** - * Attempts various space optimization strategies. - * @return true if optimization freed enough space - */ - private boolean attemptSpaceOptimization() { - int originalSlotsNeeded = getTotalInventorySlotsNeeded(); - - // Strategy 1: Look for items that could be moved to specific slots to free flexible space - optimizeSlotUtilization(); - - // Strategy 2: Consolidate any remaining stackable items - consolidateStackableItems(); - - int newSlotsNeeded = getTotalInventorySlotsNeeded(); - boolean improved = newSlotsNeeded < originalSlotsNeeded; - - if (improved) { - log.info("Space optimization reduced slot usage from {} to {}", originalSlotsNeeded, newSlotsNeeded); - } - - return fitsInInventory(); - } - - /** - * Optimizes slot utilization by moving flexible items to specific slots when beneficial. - */ - private void optimizeSlotUtilization() { - // Find unused specific slots that could accommodate flexible items - Set usedSlots = new HashSet<>(inventorySlotAssignments.keySet()); - List availableSlots = new ArrayList<>(); - - for (int slot = 0; slot < 28; slot++) { - if (!usedSlots.contains(slot)) { - availableSlots.add(slot); - } - } - - // Try to move single-slot flexible items to specific slots - Iterator flexIterator = flexibleInventoryItems.iterator(); - while (flexIterator.hasNext() && !availableSlots.isEmpty()) { - ItemRequirement item = flexIterator.next(); - - // Only move items that need exactly 1 slot - if (item.getRequiredInventorySlots() == 1) { - int targetSlot = availableSlots.remove(0); - - // Create slot-specific copy - ItemRequirement slotSpecific = item.copyWithSpecificSlot(targetSlot); - - // Move to specific slot assignment - inventorySlotAssignments.put(targetSlot, slotSpecific); - flexIterator.remove(); - - log.info("Moved flexible item {} to specific slot {} for optimization", - item.getName(), targetSlot); - } - } - } - - /** - * Removes lowest priority items until the plan fits in inventory. - * CRITICAL: Never removes MANDATORY items - they are protected from removal. - */ - private void removeItemsUntilFit() { - List toRemove = new ArrayList<>(); - - // Sort flexible items by priority (mandatory items should not be removed) - flexibleInventoryItems.sort((a, b) -> { - // Mandatory items always stay (higher priority) - int priorityCompare = a.getPriority().compareTo(b.getPriority()); - if (priorityCompare != 0) { - return priorityCompare; - } - // For same priority, prefer higher rating - return Integer.compare(b.getRating(), a.getRating()); - }); - - while (!fitsInInventory() && !flexibleInventoryItems.isEmpty()) { - // Find the lowest priority non-mandatory item to remove - ItemRequirement itemToRemove = null; - for (int i = flexibleInventoryItems.size() - 1; i >= 0; i--) { - ItemRequirement item = flexibleInventoryItems.get(i); - if (!item.isMandatory()) { - itemToRemove = item; - break; - } - } - - if (itemToRemove != null) { - flexibleInventoryItems.remove(itemToRemove); - toRemove.add(itemToRemove); - log.info("Removed optional item due to space constraints: {} (needs {} slots)", - itemToRemove.getName(), itemToRemove.getRequiredInventorySlots()); - } else { - // All remaining items are mandatory - cannot remove any more - log.error("Cannot fit all mandatory items in inventory. Need {} slots but only 28 available.", - getTotalInventorySlotsNeeded()); - - // Mark remaining mandatory items as missing if they don't fit - for (ItemRequirement mandatoryItem : flexibleInventoryItems) { - if (mandatoryItem.isMandatory()) { - missingMandatoryItems.add(mandatoryItem); - log.error("Cannot fit mandatory item: {} (needs {} slots)", - mandatoryItem.getName(), mandatoryItem.getRequiredInventorySlots()); - } - } - - // Clear all remaining flexible items since they can't fit - flexibleInventoryItems.clear(); - break; - } - } - - if (!toRemove.isEmpty()) { - log.info("Removed {} optional items to fit inventory constraints", toRemove.size()); - } - } - - /** - * Gets a summary of the layout plan. - */ - public String getSummary() { - StringBuilder sb = new StringBuilder(); - sb.append("=== Inventory Layout Plan Summary ===\n"); - sb.append("Equipment assignments: ").append(equipmentAssignments.size()).append("\n"); - sb.append("Specific inventory slots: ").append(inventorySlotAssignments.size()).append("\n"); - sb.append("Flexible inventory items: ").append(flexibleInventoryItems.size()).append("\n"); - sb.append("Total inventory slots needed: ").append(getTotalInventorySlotsNeeded()).append("/28\n"); - sb.append("Plan feasible: ").append(isFeasible()).append("\n"); - sb.append("Fits in inventory: ").append(fitsInInventory()).append("\n"); - - if (!missingMandatoryItems.isEmpty()) { - sb.append("Missing mandatory items: "); - missingMandatoryItems.forEach(item -> sb.append(item.getName()).append(", ")); - sb.append("\n"); - } - - if (!missingMandatoryEquipment.isEmpty()) { - sb.append("Missing mandatory equipment slots: "); - missingMandatoryEquipment.forEach((slot, items) -> sb.append("\n\t"+slot.name()+": ") - .append(items.stream().map(ItemRequirement::getName).collect(Collectors.joining(", "))) - .append("")); - sb.append("\n"); - } - - return sb.toString(); - } - - /** - * Gets a detailed string representation of the inventory setup plan. - * Shows equipment assignments, inventory slot assignments, and flexible items. - * - * @return A comprehensive string describing the planned setup - */ - public String getDetailedPlanString() { - StringBuilder sb = new StringBuilder(); - sb.append("=== Inventory Setup Plan Details ===\n"); - sb.append("Plan Feasible:\t\t").append(isFeasible() ? "Yes" : "No").append("\n"); - sb.append("Fits in Inventory:\t").append((getTotalInventorySlotsNeeded() <= 28) ? "Yes" : "No").append("\n"); - sb.append("Total Slots Needed:\t").append(getTotalInventorySlotsNeeded()).append("/28\n\n"); - - // Equipment assignments - sb.append("=== EQUIPMENT ASSIGNMENTS ===\n"); - if (equipmentAssignments.isEmpty()) { - sb.append("\tNo equipment assignments.\n"); - } else { - for (Map.Entry entry : equipmentAssignments.entrySet()) { - sb.append("\t").append(entry.getKey().name()).append(":\t"); - sb.append(formatItemRequirement(entry.getValue())).append("\n"); - } - } - sb.append("\n"); - - // Specific inventory slot assignments - sb.append("=== SPECIFIC INVENTORY SLOT ASSIGNMENTS ===\n"); - if (inventorySlotAssignments.isEmpty()) { - sb.append("\tNo specific slot assignments.\n"); - } else { - for (int slot = 0; slot < 28; slot++) { - if (inventorySlotAssignments.containsKey(slot)) { - ItemRequirement item = inventorySlotAssignments.get(slot); - sb.append("\tSlot ").append(slot).append(":\t\t"); - sb.append(formatItemRequirement(item)).append("\n"); - } - } - } - sb.append("\n"); - - // Flexible inventory items - sb.append("=== FLEXIBLE INVENTORY ITEMS ===\n"); - if (flexibleInventoryItems.isEmpty()) { - sb.append("\tNo flexible items.\n"); - } else { - for (int i = 0; i < flexibleInventoryItems.size(); i++) { - ItemRequirement item = flexibleInventoryItems.get(i); - sb.append("\t").append(i + 1).append(". ").append(formatItemRequirement(item)); - sb.append(" (needs ").append(item.getRequiredInventorySlots()).append(" slots)\n"); - } - } - sb.append("\n"); - - // Missing mandatory items - if (!missingMandatoryItems.isEmpty()) { - sb.append("=== MISSING MANDATORY ITEMS ===\n"); - for (int i = 0; i < missingMandatoryItems.size(); i++) { - ItemRequirement item = missingMandatoryItems.get(i); - sb.append("\t").append(i + 1).append(". ").append(formatItemRequirement(item)).append("\n"); - } - sb.append("\n"); - } - - // Missing mandatory equipment - if (!missingMandatoryEquipment.isEmpty()) { - sb.append("=== MISSING MANDATORY EQUIPMENT SLOTS ===\n"); - missingMandatoryEquipment.forEach((slot, items) -> { - sb.append("\t").append(slot.name()).append(": "); - if (items != null && !items.isEmpty()) { - sb.append(items.stream() - .map(ItemRequirement::getName) - .collect(Collectors.joining(", "))); - } else { - sb.append("No items listed"); - } - sb.append("\n"); - }); - sb.append("\n"); - } - - return sb.toString(); - } - - /** - * Formats an ItemRequirement for display. - * - * @param item The item requirement to format - * @return A formatted string representation - */ - private String formatItemRequirement(ItemRequirement item) { - StringBuilder sb = new StringBuilder(); - sb.append(item.getName()); - if (item.getAmount() > 1) { - sb.append(" x").append(item.getAmount()); - } - sb.append(" [").append(item.getPriority().name()).append(", Rating: ").append(item.getRating()).append("]"); - if (item.isStackable()) { - sb.append(" (stackable)"); - } - return sb.toString(); - } - - /** - * Gets a summary of occupied slots for logging. - */ - public String getOccupiedSlotsSummary() { - StringBuilder sb = new StringBuilder(); - sb.append("=== SLOT UTILIZATION SUMMARY ===\n"); - - // Equipment slots - sb.append("Equipment slots occupied: ").append(equipmentAssignments.size()).append("\n"); - if (!equipmentAssignments.isEmpty()) { - sb.append(" Slots: ").append(equipmentAssignments.keySet().stream() - .map(Object::toString) - .collect(Collectors.joining(", "))).append("\n"); - } - - // Specific inventory slots - sb.append("Specific inventory slots: ").append(inventorySlotAssignments.size()).append("\n"); - if (!inventorySlotAssignments.isEmpty()) { - sb.append(" Slots: ").append(inventorySlotAssignments.keySet().stream() - .sorted() - .map(Object::toString) - .collect(Collectors.joining(", "))).append("\n"); - } - - // Available inventory slots for flexible items - Set occupiedSlots = new HashSet<>(inventorySlotAssignments.keySet()); - int availableSlots = 28 - occupiedSlots.size(); - int flexibleSlotsNeeded = flexibleInventoryItems.stream() - .mapToInt(ItemRequirement::getRequiredInventorySlots) - .sum(); - - sb.append("Available inventory slots: ").append(availableSlots).append("\n"); - sb.append("Flexible items slots needed: ").append(flexibleSlotsNeeded).append("\n"); - sb.append("Slot surplus/deficit: ").append(availableSlots - flexibleSlotsNeeded).append("\n"); - - return sb.toString(); - } - - /** - * Enhanced toString that provides detailed plan information. - */ - @Override - public String toString() { - return getDetailedPlanString(); - } - - // ========== REQUIREMENTS PLANNING METHODS ========== - - /** - * Creates an optimized inventory setup plan from a requirement registry. - * This is the main entry point for the new requirements-based planning approach. - * - * @return true if planning was successful, false if mandatory requirements could not be fulfilled - */ - public boolean createPlanFromRequirements() { - if (registry == null || taskContext == null) { - throw new IllegalStateException("Cannot create plan from requirements: registry or TaskContext is null"); - } - - StringBuilder planningLog = new StringBuilder(); - planningLog.append("Starting comprehensive requirement analysis for context: ").append(taskContext) - .append(" with OR mode: ").append(orRequirementMode).append("\n"); - - // Track already planned items to avoid double-processing EITHER requirements - Set alreadyPlanned = new HashSet<>(); - - // Get all requirements filtered by context using new context-aware methods - Map> equipmentReqs = - registry.getEquipmentRequirements(taskContext); - Map> slotSpecificReqs = - registry.getInventoryRequirements(taskContext); - LinkedHashSet anySlotReqs = registry.getAnySlotLogicalRequirements(taskContext); - // Get conditional requirements that contain only ItemRequirements - List conditionalReqs = registry.getConditionalItemRequirements(taskContext); - List externalConditionalReqs = registry.getExternalConditionalItemRequirements(taskContext); - - // Process conditional requirements to get active ItemRequirements and merge them - integrateConditionalRequirements(conditionalReqs, externalConditionalReqs, equipmentReqs, slotSpecificReqs,anySlotReqs); - - // Step 1: Plan equipment slots (these have fixed positions) - planningLog.append("\n=== STEP 1: EQUIPMENT PLANNING ===\n"); - if (!planEquipmentSlotsFromCache(equipmentReqs, alreadyPlanned)) { - planningLog.append("FAILED: Mandatory equipment cannot be fulfilled\n"); - log.debug(planningLog.toString()); - return false; // Early exit if mandatory equipment cannot be fulfilled - } - - // Log status after equipment planning - planningLog.append("Equipment assignments: ").append(equipmentAssignments.size()).append("\n"); - planningLog.append("Items in alreadyPlanned: ").append(alreadyPlanned.size()).append("\n"); - planningLog.append("Missing mandatory items so far: ").append(missingMandatoryItems.size()).append("\n"); - - for (ItemRequirement planned : alreadyPlanned) { - planningLog.append(" - Already planned: ").append(planned.getName()) - .append(" (IDs: ").append(planned.getIds()).append(")\n"); - } - - // Step 2: Plan specific inventory slots (0-27, excluding items already planned in equipment) - planningLog.append("\n=== STEP 2: SPECIFIC SLOT PLANNING ===\n"); - planSpecificInventorySlots(slotSpecificReqs, alreadyPlanned); - - // Log status after specific slot planning - planningLog.append("Specific inventory slots: ").append(inventorySlotAssignments.size()).append("\n"); - planningLog.append("Items in alreadyPlanned: ").append(alreadyPlanned.size()).append("\n"); - planningLog.append("Missing mandatory items so far: ").append(missingMandatoryItems.size()).append("\n"); - - // Step 3: Plan flexible inventory items (any slot allowed, from the any-slot cache) - planningLog.append("\n=== STEP 3: FLEXIBLE PLANNING===\n"); - planFlexibleInventoryItems(anySlotReqs, alreadyPlanned); - - // Log status after flexible planning - planningLog.append("Flexible inventory items: ").append(flexibleInventoryItems.size()).append("\n"); - planningLog.append("Items in alreadyPlanned: ").append(alreadyPlanned.size()).append("\n"); - planningLog.append("Missing mandatory items final: ").append(missingMandatoryItems.size()).append("\n"); - - for (ItemRequirement missing : missingMandatoryItems) { - planningLog.append(" - Missing: ").append(missing.getName()) - .append(" (IDs: ").append(missing.getIds()) - .append(", Priority: ").append(missing.getPriority()).append(")\n"); - } - - // Step 4: Analyze and log comprehensive requirement status - planningLog.append(getComprehensiveRequirementAnalysis(true)); - - // Step 5: Optimize and validate the entire plan - planningLog.append("\n=== STEP 4: OPTIMIZATION AND VALIDATION ===\n"); - boolean planValid = optimizeAndValidatePlan(this); - - if (!planValid) { - planningLog.append("FAILED: Plan optimization and validation failed - see comprehensive analysis above for details\n"); - log.error(planningLog.toString()); - return false; - } - - planningLog.append("SUCCESS: Created and validated optimal layout plan for context: ").append(taskContext).append("\n"); - - // Output all planning logs at once - log.info(planningLog.toString()); - - return true; - } - - /** - * Integrates conditional requirements into the equipment and slot-specific requirements. - * This method evaluates active conditional requirements and merges their ItemRequirements - * into the appropriate LogicalRequirements for each slot. - * - * @param conditionalReqs Standard conditional requirements containing only ItemRequirements - * @param externalConditionalReqs External conditional requirements containing only ItemRequirements - * @param equipmentReqs Equipment requirements map to be updated - * @param slotSpecificReqs Slot-specific requirements map to be updated - */ - private void integrateConditionalRequirements( - List conditionalReqs, - List externalConditionalReqs, - Map> equipmentReqs, - Map> slotSpecificReqs, - LinkedHashSet anySlotReqs - ) { - - log.debug("Integrating {} standard and {} external conditional requirements", - conditionalReqs.size(), externalConditionalReqs.size()); - - // Process standard conditional requirements - for (ConditionalRequirement conditionalReq : conditionalReqs) { - processConditionalRequirement(conditionalReq, equipmentReqs, slotSpecificReqs, anySlotReqs,"standard"); - } - - // Process external conditional requirements - for (ConditionalRequirement conditionalReq : externalConditionalReqs) { - processConditionalRequirement(conditionalReq, equipmentReqs, slotSpecificReqs,anySlotReqs, "external"); - } - - log.debug("Completed integration of conditional requirements"); - } - - /** - * Processes a single conditional requirement and integrates its active ItemRequirements - * into the appropriate LogicalRequirements. - * - * @param conditionalReq The conditional requirement to process - * @param equipmentReqs Equipment requirements map to be updated - * @param slotSpecificReqs Slot-specific requirements map to be updated - * @param source Source type for logging ("standard" or "external") - */ - private void processConditionalRequirement( - ConditionalRequirement conditionalReq, - Map> equipmentReqs, - Map> slotSpecificReqs, - LinkedHashSet anySlotReqs, - String source) { - - try { - // Get active requirements from the conditional requirement - List activeRequirements = conditionalReq.getActiveRequirements(); - - if (activeRequirements.isEmpty()) { - log.debug("No active requirements for {} conditional requirement: {}", source, conditionalReq.getName()); - return; - } - - log.info("Processing {} active requirements from {} conditional requirement: {}", - activeRequirements.size(), source, conditionalReq.getName()); - - // Process each active requirement - for (Requirement activeReq : activeRequirements) { - if (activeReq instanceof ItemRequirement) { - ItemRequirement itemReq = (ItemRequirement) activeReq; - log.debug("Integrating active ItemRequirement: {}", itemReq.getName()); - integrateActiveItemRequirement(itemReq, equipmentReqs, slotSpecificReqs,anySlotReqs); - } else if (activeReq instanceof LogicalRequirement) { - LogicalRequirement logicalReq = (LogicalRequirement) activeReq; - if (logicalReq.containsOnlyItemRequirements()) { - // Extract all ItemRequirements from the LogicalRequirement - List itemRequirements = logicalReq.getAllItemRequirements(); - for (ItemRequirement itemReq : itemRequirements) { - log.debug("Integrating ItemRequirement from LogicalRequirement: {}", itemReq.getName()); - integrateActiveItemRequirement(itemReq, equipmentReqs, slotSpecificReqs,anySlotReqs); - } - } else { - log.error("\n\tSkipping LogicalRequirement with mixed requirement types in conditional: {} - consider correct impllementation of the condtional requirement: {}", - conditionalReq.getName()); - } - } else { - log.warn("Unexpected requirement type in conditional requirement: {} (type: {})", - activeReq.getClass().getSimpleName(), activeReq.getName()); - } - } - - } catch (Exception e) { - log.error("Error processing {} conditional requirement '{}': {}", - source, conditionalReq.getName(), e.getMessage(), e); - } - } - - /** - * Integrates a single active ItemRequirement into the appropriate slot requirements. - * - * @param itemReq The active ItemRequirement to integrate - * @param equipmentReqs Equipment requirements map to be updated - * @param slotSpecificReqs Slot-specific requirements map to be updated - */ - private void integrateActiveItemRequirement( - ItemRequirement itemReq, - Map> equipmentReqs, - Map> slotSpecificReqs, - LinkedHashSet anyslotSpecificReqs - ) { - - try { - switch (itemReq.getRequirementType()) { - case EQUIPMENT: - if (itemReq.getEquipmentSlot() != null) { - integrateIntoEquipmentSlot(itemReq, equipmentReqs); - } - break; - - case INVENTORY: - int slot = itemReq.hasSpecificInventorySlot() ? itemReq.getInventorySlot() : -1; - if (slot!=-1){ - integrateIntoInventorySlot(itemReq, slotSpecificReqs, slot); - }else{ - // Flexible inventory item, add to anyslotSpecificReqs - log.debug("Adding flexible ItemRequirement '{}' to anyslotSpecificReqs", itemReq.getName()); - anyslotSpecificReqs.add(new OrRequirement( - itemReq.getPriority(), - itemReq.getRating(), - "Flexible requirement for inventory. based on conditional requirement: " + itemReq.getName(), - itemReq.getTaskContext(), - ItemRequirement.class, - itemReq - )); - } - break; - - case EITHER: - // For EITHER requirements, we can choose the best placement - // For now, prefer equipment slot if available, otherwise flexible inventory - OrRequirement newOrReq = new OrRequirement( - itemReq.getPriority(), - itemReq.getRating(), - "Conditional requirement for inventory flexible", - itemReq.getTaskContext(), - ItemRequirement.class, - itemReq - ); - anyslotSpecificReqs.add(newOrReq); - break; - - default: - log.debug("Skipping ItemRequirement with non-slot type: {} ({})", - itemReq.getName(), itemReq.getRequirementType()); - break; - } - - } catch (Exception e) { - log.error("Error integrating active ItemRequirement '{}': {}", itemReq.getName(), e.getMessage(), e); - } - } - - /** - * Integrates an ItemRequirement into an equipment slot. - * Properly merges conditional requirements with existing OR requirements. - * - * @param itemReq The ItemRequirement to integrate - * @param equipmentReqs Equipment requirements map to be updated - */ - private void integrateIntoEquipmentSlot( - ItemRequirement itemReq, - Map> equipmentReqs) { - - EquipmentInventorySlot slot = itemReq.getEquipmentSlot(); - LinkedHashSet existingLogical = equipmentReqs.getOrDefault(slot , new LinkedHashSet<>()); - - - // If existing is not an OrRequirement or we couldn't merge, try direct addition - try { - existingLogical.add(itemReq); - log.debug("Added conditional ItemRequirement '{}' to existing equipment slot {}", - itemReq.getName(), slot); - } catch (IllegalArgumentException e) { - log.error("Cannot integrate conditional ItemRequirement '{}' into equipment slot {} - incompatible types: {}", - itemReq.getName(), slot, e.getMessage()); - // This should not happen in a well-designed system, but we log the error and continue - throw e; // Rethrow to indicate failure - } - equipmentReqs.put(slot, existingLogical); - - } - - /** - * Integrates an ItemRequirement into an inventory slot. - * Properly merges conditional requirements with existing OR requirements. - * - * @param itemReq The ItemRequirement to integrate - * @param slotSpecificReqs Slot-specific requirements map to be updated - * @param slot The inventory slot (-1 for flexible, 0-27 for specific) - */ - private void integrateIntoInventorySlot( - ItemRequirement itemReq, - Map> slotSpecificReqs, - int slot) { - - LinkedHashSet existingLogical = slotSpecificReqs.getOrDefault(slot , new LinkedHashSet<>()); - - if (existingLogical != null) { - - // If we can't merge into existing requirement, try to add directly - try { - existingLogical.add(itemReq); - log.debug("Added conditional ItemRequirement '{}' to existing inventory slot {}", - itemReq.getName(), slot == -1 ? "flexible" : String.valueOf(slot)); - } catch (IllegalArgumentException e) { - log.error("Cannot integrate conditional ItemRequirement '{}' into inventory slot {} - incompatible types: {}", - itemReq.getName(), slot == -1 ? "flexible" : String.valueOf(slot), e.getMessage()); - // This should not happen in a well-designed system, but we log the error and continue - throw e; // Rethrow to indicate failure - } - }else { - throw new IllegalArgumentException("No existing logical requirement for inventory slot " + - (slot == -1 ? "flexible" : String.valueOf(slot))); - } - slotSpecificReqs.put(slot, existingLogical); - } - - /** - * Plans equipment slots from the new cache structure (LinkedHashSet per slot). - * Treats multiple ItemRequirements in the same slot as alternatives (OR logic). - * - * @param equipmentReqs Map of equipment slot to set of ItemRequirements (alternatives for that slot) - * @param alreadyPlanned Set to track already planned items - * @return true if successful, false if mandatory equipment cannot be fulfilled - */ - private boolean planEquipmentSlotsFromCache(Map> equipmentReqs, - Set alreadyPlanned) { - for (Map.Entry> entry : equipmentReqs.entrySet()) { - EquipmentInventorySlot slot = entry.getKey(); - LinkedHashSet slotItems = entry.getValue(); - - if (slotItems.isEmpty()) { - log.debug("No requirements for equipment slot {}", slot); - continue; - } - - log.debug("Planning equipment slot {} with {} alternatives", slot, slotItems.size()); - - // Convert to list for compatibility with existing selector logic - List itemList = new ArrayList<>(slotItems); - - // Use enhanced item selection for equipment slots with proper slot and skill validation - ItemRequirement bestItem = RequirementSelector.findBestAvailableItemForEquipmentSlot( - itemList, slot, alreadyPlanned); - - if (bestItem != null) { - addEquipmentSlotAssignment(slot, bestItem); - alreadyPlanned.addAll(slotItems); // Mark as planned to avoid double-processing - - log.info("Assigned {} (type: {}) to equipment slot {}", - bestItem.getName(), bestItem.getRequirementType(), slot); - } else { - // Check if any requirement was mandatory - boolean hasMandatory = slotItems.stream().anyMatch(ItemRequirement::isMandatory); - - if (hasMandatory) { - for (ItemRequirement item : slotItems) { - if (item.isMandatory()) { - addMissingMandatoryEquipment(slot, item); - } - } - //addMissingMandatoryEquipment(slot); - log.warn("Cannot fulfill mandatory equipment requirement for slot {}", slot); - log.error("Planning failed: Missing mandatory equipment for slot {}", slot); - return false; // Early exit for mandatory equipment failure - } else { - log.debug("Optional equipment not available for slot {}", slot); - } - } - } - return true; // All mandatory equipment successfully planned - } - - - /** - * Plans specific inventory slots from context-filtered logical requirements. - * With the new cache structure, each slot has exactly one LogicalRequirement for the given context. - * - * @param slotSpecificReqs Slot-specific requirements (one LogicalRequirement per slot) - * @param alreadyPlanned Set to track already planned items - */ - private void planSpecificInventorySlots(Map> slotSpecificReqs, - Set alreadyPlanned) { - for (Map.Entry> entry : slotSpecificReqs.entrySet()) { - int slot = entry.getKey(); - LinkedHashSet itemSlotReq = entry.getValue(); - - // Skip the "any slot" entry (-1) as we'll handle it in planFlexibleInventoryItems - if (slot == -1) { - continue; - } - - - - ItemRequirement bestItem = RequirementSelector.findBestAvailableItemNotAlreadyPlannedForInventory( - itemSlotReq, this); - - if (bestItem != null) { - // Enhanced validation for slot assignment - if (!ItemRequirement.canAssignToSpecificSlot(bestItem, slot)) { - log.warn("Cannot assign item {} to slot {} due to constraints. Moving to flexible items.", - bestItem.getName(), slot); - throw new IllegalArgumentException( - "Item " + bestItem.getName() + " cannot be assigned to specific slot " + slot); - // Handle the item as flexible instead - //handleItemAsFlexible(bestItem, this, alreadyPlanned); - } else { - // Item can be assigned to specific slot - ItemRequirement slotSpecificItem = bestItem.copyWithSpecificSlot(slot); - addInventorySlotAssignment(slot, slotSpecificItem); - alreadyPlanned.addAll(itemSlotReq); // Mark all alternatives as planned - //for (ItemRequirement item : itemSlotReq) { - - - //} - log.info("Assigned {} to specific slot {} (amount: {}, stackable: {})", - bestItem.getName(), slot, bestItem.getAmount(), bestItem.isStackable()); - } - } else { - // Handle missing mandatory items - convert LinkedHashSet to OrRequirement - if (!itemSlotReq.isEmpty()) { - ItemRequirement firstItem = itemSlotReq.iterator().next(); - OrRequirement slotOrRequirement = new OrRequirement( - firstItem.getPriority(), - firstItem.getRating(), - "Slot " + slot + " requirement alternatives", - firstItem.getTaskContext(), - ItemRequirement.class, - itemSlotReq.toArray(new ItemRequirement[0]) - ); - handleMissingMandatoryItem(Collections.singletonList(slotOrRequirement), this, "inventory slot " + slot); - } - } - } - } - - /** - * Plans flexible inventory items from the any-slot cache (new cache structure). - * These items can be placed in any available inventory slot. - * - * @param anySlotReqs Set of OrRequirements for flexible inventory placement - * @param alreadyPlanned Set to track already planned items - */ - private void planFlexibleInventoryItems(LinkedHashSet anySlotReqs, - Set alreadyPlanned) { - if (anySlotReqs == null || anySlotReqs.isEmpty()) { - log.debug("No flexible inventory requirements to plan"); - return; - } - - log.debug("Planning {} flexible inventory OrRequirements", anySlotReqs.size()); - - for (OrRequirement orReq : anySlotReqs) { - log.debug("Planning flexible OR requirement: {}", orReq.getName()); - - // Extract ItemRequirements from the OrRequirement - List orItems = LogicalRequirement.extractItemRequirementsFromLogical(orReq); - - if (orItems.isEmpty()) { - log.warn("OrRequirement has no ItemRequirements: {}", orReq.getName()); - continue; - } - - // Check if this OR requirement has already been satisfied by equipment or specific slots - int alreadySatisfiedAmount = calculateAlreadySatisfiedAmount(orItems, alreadyPlanned); - int totalNeeded = orItems.get(0).getAmount(); // All items in OR should have same amount - - if (alreadySatisfiedAmount >= totalNeeded) { - log.debug("OR requirement already fully satisfied by equipment/specific slots: {} satisfied out of {} needed", - alreadySatisfiedAmount, totalNeeded); - continue; // Skip this OR requirement as it's already satisfied - } - - // Calculate remaining amount needed for inventory - int remainingAmountNeeded = totalNeeded - alreadySatisfiedAmount; - log.debug("OR requirement needs additional {} items for inventory (total needed: {}, already satisfied: {})", - remainingAmountNeeded, totalNeeded, alreadySatisfiedAmount); - // Plan remaining amount needed for inventory (pass remaining amount to avoid double calculation) - List plannedOrItems = planOrRequirement(orItems, remainingAmountNeeded, alreadyPlanned); - - // Check if the OR requirement is fully satisfied after planning - int totalPlannedInventory = plannedOrItems.stream().mapToInt(ItemRequirement::getAmount).sum(); - int totalPlanned = totalPlannedInventory + alreadySatisfiedAmount; - - if (totalPlanned < totalNeeded) { - if (orReq.getPriority() == RequirementPriority.MANDATORY) { - log.warn("Mandatory flexible OR requirement not fully satisfied: {} planned out of {} needed", - totalPlanned, totalNeeded); - missingMandatoryItems.addAll(orItems); - } else { - log.debug("Optional flexible OR requirement partially satisfied: {} planned out of {} needed", - totalPlanned, totalNeeded); - } - } else { - log.debug("Flexible OR requirement fully satisfied: {} planned (including {} already satisfied)", - totalPlanned, alreadySatisfiedAmount); - } - } - - log.debug("Finished planning flexible inventory items. Total flexible items: {}", flexibleInventoryItems.size()); - } - - - /** - * Plans an OR requirement by selecting the best combination of available items to fulfill the specified amount needed. - * This handles OR requirements according to the configured mode (ANY_COMBINATION or SINGLE_TYPE). - * - * @param orItems All items in the OR requirement group - * @param amountNeeded Amount still required (after accounting for equipment assignments) - * @param alreadyPlanned Set of already planned items to avoid conflicts - * @return List of ItemRequirements that were successfully planned - */ - private List planOrRequirement(List orItems, int amountNeeded, Set alreadyPlanned) { - if (orItems.isEmpty()) { - return new ArrayList<>(); - } - - log.debug("Planning OR requirement: {} amount needed from {} alternatives using mode: {}", - amountNeeded, orItems.size(), orRequirementMode); - - // If no amount needed, requirement is already satisfied - if (amountNeeded <= 0) { - log.info("OR requirement already fully satisfied - no additional inventory items needed"); - return new ArrayList<>(); - } - - switch (orRequirementMode) { - case SINGLE_TYPE: - return planOrRequirementSingleType(orItems, amountNeeded, alreadyPlanned); - case ANY_COMBINATION: - default: - return planOrRequirementAnyCombination(orItems, amountNeeded, alreadyPlanned); - } - //log.debug("OR requirement planning completed with allready planned items: {}", alreadyPlanned.size()); - } - - /** - * Calculates how much of an OR requirement has already been satisfied by equipment assignments or already planned items. - * This prevents double-counting when an item can be equipped but also fulfill inventory requirements. - * - * @param orItems All items in the OR requirement group - * @param alreadyPlanned Set of already planned items - * @return The amount already satisfied (0 if none) - */ - private int calculateAlreadySatisfiedAmount(List orItems, Set alreadyPlanned) { - int satisfiedAmount = 0; - - log.debug("Calculating already satisfied amount for OR group with {} items", orItems.size()); - log.debug("Current equipment assignments: {}", equipmentAssignments.size()); - log.debug("AlreadyPlanned set size: {}", alreadyPlanned.size()); - - // Check if any item from the OR group has been assigned to equipment - for (ItemRequirement orItem : orItems) { - log.debug("Checking OR item: {} (IDs: {})", orItem.getName(), orItem.getIds()); - - // Check if this specific item is in already planned (marked during equipment assignment) - if (alreadyPlanned.contains(orItem)) { - satisfiedAmount += orItem.getAmount(); - log.debug("Found OR item {} already planned with amount {}", orItem.getName(), orItem.getAmount()); - continue; - } - - // Also check if any equipment assignment matches this item by ID - for (Map.Entry equipEntry : equipmentAssignments.entrySet()) { - ItemRequirement equippedItem = equipEntry.getValue(); - log.debug("Comparing with equipped item: {} (IDs: {}) in slot {}", - equippedItem.getName(), equippedItem.getIds(), equipEntry.getKey().name()); - - // Check if the equipped item has the same ID as any of the OR alternatives - if (orItem.getIds().stream().anyMatch(id -> equippedItem.getIds().contains(id))) { - satisfiedAmount += equippedItem.getAmount(); - log.debug("MATCH FOUND! OR requirement satisfied by equipment: {} in slot {} with amount {}", - equippedItem.getName(), equipEntry.getKey().name(), equippedItem.getAmount()); - break; // Only count once per OR item - } else { - log.debug("No match: OR item IDs {} vs equipped item IDs {}", orItem.getIds(), equippedItem.getIds()); - } - } - } - log.info("Total satisfied amount calculated: {}", satisfiedAmount); - return satisfiedAmount; - } - - /** - * Checks if an item has already been planned in equipment slots, specific inventory slots, or the alreadyPlanned set. - * This prevents double-planning the same item in flexible inventory when it's already assigned elsewhere. - * - * @param item The item to check - * @param alreadyPlanned Set of items already marked as planned - * @return true if the item is already planned elsewhere, false otherwise - */ - private boolean isItemAlreadyPlannedElsewhere(ItemRequirement item, Set alreadyPlanned) { - // Check if it's in the alreadyPlanned set - if (alreadyPlanned.contains(item)) { - return true; - } - - // Check if any of the item's IDs match equipment assignments - for (ItemRequirement equippedItem : equipmentAssignments.values()) { - if (item.getIds().stream().anyMatch(id -> equippedItem.getIds().contains(id))) { - return true; - } - } - - // Check if any of the item's IDs match specific inventory slot assignments - for (ItemRequirement slotItem : inventorySlotAssignments.values()) { - if (item.getIds().stream().anyMatch(id -> slotItem.getIds().contains(id))) { - return true; - } - } - - return false; - } - - /** - * Plans an OR requirement using SINGLE_TYPE mode - must fulfill with exactly one item type. - * - * @param orItems All items in the OR requirement group - * @param amountNeeded Amount still required (after accounting for equipment assignments) - * @param alreadyPlanned Set of already planned items - * @return List of planned items (will contain at most one item type) - */ - private List planOrRequirementSingleType(List orItems, - int amountNeeded, - Set alreadyPlanned) { - List plannedItems = new ArrayList<>(); - - // If no amount needed, requirement is already satisfied - if (amountNeeded <= 0) { - return plannedItems; - } - - // Calculate what we have available for each item type - Map availableCounts = new HashMap<>(); - for (ItemRequirement item : orItems) { - if (alreadyPlanned.contains(item)) { - continue; // Skip already planned items - } - - int inventoryQuantity= Rs2Inventory.itemQuantity(item.getId()); - int bankCount = Rs2Bank.count(item.getUnNotedId()); - int totalAvailable = inventoryQuantity + bankCount; - - if (totalAvailable >= amountNeeded) { - availableCounts.put(item, totalAvailable); - } - } - - if (availableCounts.isEmpty()) { - log.warn("SINGLE_TYPE mode: No single item type has enough quantity ({} needed)", amountNeeded); - return plannedItems; - } - - // Sort available items by preference (priority, then rating, then amount available) - List> sortedAvailable = availableCounts.entrySet().stream() - .sorted((a, b) -> { - ItemRequirement itemA = a.getKey(); - ItemRequirement itemB = b.getKey(); - - // First by priority (MANDATORY > RECOMMENDED > OPTIONAL) - int priorityCompare = itemA.getPriority().compareTo(itemB.getPriority()); - if (priorityCompare != 0) { - return priorityCompare; - } - - // Then by rating (higher is better) - int ratingCompare = Integer.compare(itemB.getRating(), itemA.getRating()); - if (ratingCompare != 0) { - return ratingCompare; - } - - // Finally by available amount (more is better) - return Integer.compare(b.getValue(), a.getValue()); - }) - .collect(Collectors.toList()); - - // Select the best single item type that can fulfill the entire requirement - Map.Entry bestChoice = sortedAvailable.get(0); - ItemRequirement chosenItem = bestChoice.getKey(); - - // Create a copy with the exact amount needed - ItemRequirement plannedItem = chosenItem.copyWithAmount(amountNeeded); - handleItemAsFlexible(plannedItem, this, alreadyPlanned); - plannedItems.add(plannedItem); // Keep for tracking, but avoid duplicate addition later - - log.info("SINGLE_TYPE mode: Selected {} x{} (available: {}) for OR requirement", - chosenItem.getName(), amountNeeded, bestChoice.getValue()); - - return plannedItems; - } - - /** - * Plans an OR requirement using ANY_COMBINATION mode - can fulfill with any combination of items. - * This is the original behavior from PrePostScheduleRequirements. - * - * @param orItems All items in the OR requirement group - * @param amountNeeded Amount still required (after accounting for equipment assignments) - * @param alreadyPlanned Set of already planned items - * @return List of planned items (can be multiple types) - */ - private List planOrRequirementAnyCombination(List orItems, - int amountNeeded, - Set alreadyPlanned) { - List plannedItems = new ArrayList<>(); - - // If no amount needed, requirement is already satisfied - if (amountNeeded <= 0) { - return plannedItems; - } - - // Calculate what we have available for each item type - Map availableCounts = new HashMap<>(); - for (ItemRequirement item : orItems) { - if (alreadyPlanned.contains(item)) { - continue; // Skip already planned items - } - - int inventoryQuantity = Rs2Inventory.itemQuantity(item.getId()); - int bankCount = Rs2Bank.count(item.getUnNotedId()); - int totalAvailable = inventoryQuantity + bankCount; - - if (totalAvailable > 0) { - availableCounts.put(item, totalAvailable); - } - } - - // Sort available items by preference (priority, then rating, then amount available) - List> sortedAvailable = availableCounts.entrySet().stream() - .sorted((a, b) -> { - ItemRequirement itemA = a.getKey(); - ItemRequirement itemB = b.getKey(); - - // First by priority (MANDATORY > RECOMMENDED > OPTIONAL) - int priorityCompare = itemA.getPriority().compareTo(itemB.getPriority()); - if (priorityCompare != 0) { - return priorityCompare; - } - - // Then by rating (higher is better) - int ratingCompare = Integer.compare(itemB.getRating(), itemA.getRating()); - if (ratingCompare != 0) { - return ratingCompare; - } - - // Finally by available amount (more is better) - return Integer.compare(b.getValue(), a.getValue()); - }) - .collect(Collectors.toList()); - log.debug("ANY_COMBINATION mode: Sorted available items by preference ({} total)", sortedAvailable.size()); - log.debug("Sorted available items: {}", sortedAvailable.stream() - .map(e -> String.format("%s (available: %d)", e.getKey().getName(), e.getValue())) - .collect(Collectors.joining(", "))); - // Select items to fulfill the total amount needed (or as much as possible) - int remainingNeeded = amountNeeded; - - for (Map.Entry entry : sortedAvailable) { - if (remainingNeeded <= 0) { - break; - } - - ItemRequirement item = entry.getKey(); - int available = entry.getValue(); - int amountToTake = Math.min(remainingNeeded, available); - - // Create a copy of the item with the actual amount we're planning to take - ItemRequirement plannedItem = item.copyWithAmount(amountToTake); - - handleItemAsFlexible(plannedItem, this, alreadyPlanned); - plannedItems.add(plannedItem); // Keep for tracking, but remove duplicate addition later - - remainingNeeded -= amountToTake; - - log.debug("ANY_COMBINATION mode: Planned {} x{} for OR requirement (remaining needed: {})\n\t item: \n\t\t{}", - item.getName(), amountToTake, remainingNeeded,item); - } - - // Log the result - if (remainingNeeded > 0) { - log.warn("ANY_COMBINATION mode: OR requirement partially satisfied - planned {}/{} items from available options", - amountNeeded - remainingNeeded, amountNeeded); - - // Add a single "collective shortage" item to represent the unmet need - if (!plannedItems.isEmpty()) { - // Create a special shortage indicator using the first item as template - ItemRequirement firstItem = orItems.get(0); - String shortageDescription = String.format("OR requirement shortage: need %d more from any of %d item types", - remainingNeeded, orItems.size()); - - //ItemRequirement shortageItem = new ItemRequirement( - // -2, // Special shortage indicator ID - // remainingNeeded, - // firstItem.getEquipmentSlot(), - // firstItem.getInventorySlot(), - // firstItem.getPriority(), - // firstItem.getRating(), - // shortageDescription, - // firstItem.getTaskContext() - //); - - //addMissingMandatoryInventoryItem(shortageItem); - } - } else { - log.debug("ANY_COMBINATION mode: Successfully planned OR requirement: {} items from {} alternatives", - amountNeeded, plannedItems.size()); - } - - return plannedItems; - } - - /** - * Determines whether a missing item should be logged in the comprehensive analysis based on its priority and the flag. - * - * @param item The item requirement to check - * @return true if the item should be logged, false otherwise - */ - private boolean shouldLogMissingItem(ItemRequirement item) { - // Always log mandatory items - if (item.getPriority() == RequirementPriority.MANDATORY) { - return true; - } - - // Log recommended/optional items only if the flag is enabled - return logOptionalMissingItems; - } - - /** - * Logs a comprehensive analysis of all requirements including quantities, availability, and missing items. - * This provides a single, detailed summary instead of multiple scattered log messages. - */ - private String getComprehensiveRequirementAnalysis(boolean verbose) { - StringBuilder analysis = new StringBuilder(); - analysis.append("\n").append("=".repeat(80)); - analysis.append("\n\tCOMPREHENSIVE REQUIREMENT ANALYSIS - ").append(taskContext); - analysis.append("\n\tOR Requirement Mode: ").append(orRequirementMode); - analysis.append("\n").append("=".repeat(80)); - - // Equipment Analysis - int plannedEquipment = equipmentAssignments.size(); - int missingEquipment = missingMandatoryEquipment.size(); - analysis.append("\n\nðŸ“Ķ EQUIPMENT ANALYSIS:"); - analysis.append("\n ✓ Successfully planned: ").append(plannedEquipment).append(" slots"); - if (missingEquipment > 0) { - analysis.append("\n ❌ Missing mandatory: ").append(missingEquipment).append(" slots"); - for (Map.Entry> entry : missingMandatoryEquipment.entrySet()) { - EquipmentInventorySlot slot = entry.getKey(); - List items = entry.getValue(); - if (items != null && !items.isEmpty()) { - analysis.append("\n - Slot: ").append(slot.name()).append(" (missing "); - for (ItemRequirement item : items) { - if (verbose) { - analysis.append("\n ").append(item.displayString()); - } else { - int available = 0; - try { - available = Rs2Inventory.itemQuantity(item.getId()) + Rs2Bank.count(item.getUnNotedId()); - } catch (Exception e) { - // ignore, just show 0 - } - analysis.append(item.getName()) - .append(" [id:").append(item.getId()).append("]") - .append(", need: ").append(item.getAmount()) - .append(", available: ").append(available); - if (item.isStackable()) analysis.append(", stackable"); - if (item.getEquipmentSlot() != null) - analysis.append(", slot: ").append(item.getEquipmentSlot().name()); - if (item.getRequirementType() == RequirementType.EITHER) - analysis.append(", flexible"); - analysis.append("; "); - } - } - analysis.append(")\n"); - } - } - } - - // Inventory Analysis with quantities and availability - int plannedSpecificSlots = inventorySlotAssignments.size(); - int plannedFlexibleItems = flexibleInventoryItems.size(); - int missingMandatoryItems = this.missingMandatoryItems.size(); - int totalPlannedInventory = plannedSpecificSlots + plannedFlexibleItems; - - analysis.append("\n\n🎒 INVENTORY ANALYSIS:"); - analysis.append("\n ✓ Successfully planned: ").append(totalPlannedInventory).append(" items"); - analysis.append("\n - Specific slots: ").append(plannedSpecificSlots); - analysis.append("\n - Flexible items: ").append(plannedFlexibleItems); - - // DEBUG: Show what flexible items are planned - if (!flexibleInventoryItems.isEmpty()) { - analysis.append("\n 📋 DEBUG - Flexible items planned:"); - for (ItemRequirement flexItem : flexibleInventoryItems) { - analysis.append("\n - ").append(flexItem.getName()).append(" (IDs: ").append(flexItem.getIds()).append(", Priority: ").append(flexItem.getPriority()).append(")"); - } - } - - if (missingMandatoryItems > 0) { - analysis.append("\n ❌ Missing mandatory items: ").append(missingMandatoryItems); - - // Group missing items by their logical requirement to handle OR requirements properly - Map> groupedMissingItems = new HashMap<>(); - - for (ItemRequirement missingItem : this.missingMandatoryItems) { - // Try to find the logical requirement this item belongs to - String groupKey = findLogicalRequirementGroupKey(missingItem); - groupedMissingItems.computeIfAbsent(groupKey, k -> new ArrayList<>()).add(missingItem); - } - - // Analyze each group separately - for (Map.Entry> group : groupedMissingItems.entrySet()) { - String groupKey = group.getKey(); - List groupItems = group.getValue(); - - if (groupKey.startsWith("OR:")) { - // Handle OR requirement group - analyze as collective need - // Check if we should log this based on priority and flag - if (shouldLogMissingItem(groupItems.get(0))) { - analyzeOrRequirementGroup(analysis, groupKey, groupItems); - } - } else { - // Handle individual requirements - for (ItemRequirement missingItem : groupItems) { - if (shouldLogMissingItem(missingItem)) { - analyzeIndividualRequirement(analysis, missingItem); - } - } - } - } - } - - analysis.append("\n\n📊 SUMMARY:"); - analysis.append("\n Plan Feasible: ").append(isFeasible() ? "✅ YES" : "❌ NO"); - analysis.append("\n Fits in Inventory: ").append(fitsInInventory() ? "✅ YES" : "❌ NO"); - analysis.append("\n Total Slots Needed: ").append(getTotalInventorySlotsNeeded()).append("/28"); - analysis.append("\n").append("=".repeat(80)); - - return analysis.toString(); - } - - /** - * Finds the logical requirement group key for a given item requirement. - * This helps identify if an item belongs to an OR requirement group. - * - * @param item The item requirement to analyze - * @return A group key string for grouping related requirements - */ - private String findLogicalRequirementGroupKey(ItemRequirement item) { - if (registry == null) { - return "INDIVIDUAL:" + item.getName(); - } - - // Search through current context logical requirements to find which OR group this item belongs to - Map slotSpecificReqs = registry.getInventorySlotLogicalRequirements(taskContext); - - for (Map.Entry entry : slotSpecificReqs.entrySet()) { - OrRequirement logicalReq = entry.getValue(); - - if (logicalReq instanceof OrRequirement) { - List orItems = OrRequirement.extractItemRequirementsFromLogical(logicalReq); - - // Check if this item belongs to this OR requirement - for (ItemRequirement orItem : orItems) { - if (orItem.getIds().equals(item.getIds()) && - orItem.getAmount() == item.getAmount() && - Objects.equals(orItem.getEquipmentSlot(), item.getEquipmentSlot()) && - Objects.equals(orItem.getInventorySlot(), item.getInventorySlot())) { - return "OR:" + logicalReq.getDescription(); - } - } - } - } - - return "INDIVIDUAL:" + item.getName(); - } - - /** - * Analyzes an OR requirement group to calculate total available vs. total required items. - * This provides a more accurate analysis for OR requirements like "5 food items from any combination". - * Now includes detailed skill requirement checking and usability analysis. - * - * @param analysis The analysis string builder - * @param groupKey The group key identifying the OR requirement - * @param groupItems All items in the OR requirement group - */ - private void analyzeOrRequirementGroup(StringBuilder analysis, String groupKey, List groupItems) { - String orDescription = groupKey.substring(3); // Remove "OR:" prefix - - analysis.append("\n 🍎 OR Requirement Group: ").append(orDescription); - analysis.append("\n Mode: ").append(orRequirementMode); - - // Calculate total required amount (should be same for all items in OR requirement) - int totalRequired = groupItems.get(0).getAmount(); - analysis.append("\n Total Required: ").append(totalRequired); - - if (orRequirementMode == OrRequirementMode.SINGLE_TYPE) { - analysis.append(" (exactly ").append(totalRequired).append(" of ONE type)"); - } else { - analysis.append(" (any combination)"); - } - - // Calculate total available and usable items across all types in the OR requirement - int totalInventoryQuantity = 0; - int totalBankCount = 0; - int totalUsableCount = 0; - Map itemAnalysis = new HashMap<>(); - - for (ItemRequirement item : groupItems) { - int inventoryQuantity = 0; - int inventoryCount = 0; - int bankCount = 0; - - try { - // Safely get inventory and bank counts with error handling - inventoryQuantity = Rs2Inventory.itemQuantity(item.getId()); - bankCount = Rs2Bank.count(item.getUnNotedId()); - inventoryCount = Rs2Inventory.count(item.getId()); - } catch (ArrayIndexOutOfBoundsException e) { - log.warn("ArrayIndexOutOfBoundsException when counting item '{}' (ID: {}): {}", - item.getName(), item.getId(), e.getMessage()); - // Continue with 0 counts to avoid crashing - inventoryQuantity = 0; - inventoryCount = 0; - bankCount = 0; - } catch (Exception e) { - log.warn("Unexpected error when counting item '{}' (ID: {}): {}", - item.getName(), item.getId(), e.getMessage()); - inventoryQuantity = 0; - inventoryCount = 0; - bankCount = 0; - } - - int totalCount = inventoryQuantity + bankCount; - - totalInventoryQuantity += inventoryQuantity; - totalBankCount += bankCount; - - if (totalCount > 0) { - // Analyze usability based on skill requirements and requirement type - boolean canUse = checkItemUsability(item, inventoryQuantity, bankCount); - boolean meetsSkillReqs = item.meetsSkillRequirements(); - - ItemAvailabilityInfo info = new ItemAvailabilityInfo( - inventoryQuantity,inventoryCount, bankCount, canUse, meetsSkillReqs, item.getRequirementType(), - item.getSkillToUse(), item.getMinimumLevelToUse(), - item.getSkillToEquip(), item.getMinimumLevelToEquip() - ); - - itemAnalysis.put(item.getName(), info); - - if (canUse) { - totalUsableCount += totalCount; - } - } - } - - int totalAvailable = totalInventoryQuantity + totalBankCount; - analysis.append("\n Total Available: ").append(totalAvailable).append(" (Inventory: ").append(totalInventoryQuantity).append(", Bank: ").append(totalBankCount).append(")"); - analysis.append("\n Total Usable: ").append(totalUsableCount).append(" (considering skill requirements)"); - - // Show detailed breakdown of available items with usability analysis - if (!itemAnalysis.isEmpty()) { - analysis.append("\n Detailed Item Analysis:"); - for (Map.Entry entry : itemAnalysis.entrySet()) { - String itemName = entry.getKey(); - ItemAvailabilityInfo info = entry.getValue(); - - analysis.append("\n 🔍 ").append(itemName).append(": ") - .append(info.inventoryQuantity + info.bankCount).append(" total") - .append(" (Inv: ").append(info.inventoryQuantity).append("("+info.inventoryCount+")").append(", Bank: ").append(info.bankCount).append(")"); - - if (info.canUse) { - analysis.append(" ✅ USABLE"); - } else { - analysis.append(" ❌ NOT USABLE"); - - if (!info.meetsSkillRequirements) { - analysis.append(" - Skill requirements not met:"); - - if (info.skillToUse != null && info.minimumLevelToUse != null) { - int currentLevel = Rs2Player.getRealSkillLevel(info.skillToUse); - if (currentLevel < info.minimumLevelToUse) { - analysis.append(" Need ").append(info.skillToUse.getName()) - .append(" ").append(info.minimumLevelToUse) - .append(" (current: ").append(currentLevel).append(")"); - } - } - - if (info.skillToEquip != null && info.minimumLevelToEquip != null) { - int currentLevel = Rs2Player.getRealSkillLevel(info.skillToEquip); - if (currentLevel < info.minimumLevelToEquip) { - analysis.append(" Need ").append(info.skillToEquip.getName()) - .append(" ").append(info.minimumLevelToEquip) - .append(" to equip (current: ").append(currentLevel).append(")"); - } - } - } - - // Add requirement type context - if (info.requirementType == RequirementType.EQUIPMENT && info.inventoryCount > 0 && info.bankCount == 0) { - analysis.append(" - Item in inventory but requirement needs it equipped"); - } else if (info.requirementType == RequirementType.INVENTORY && info.inventoryCount == 0 && info.bankCount > 0) { - analysis.append(" - Item in bank but requirement needs it in inventory"); - } - } - } - } - - // Calculate shortage properly for OR requirements based on mode and usability - boolean isSufficient = false; - if (orRequirementMode == OrRequirementMode.SINGLE_TYPE) { - // Check if any single item type has enough usable items - boolean anySingleTypeHasEnoughUsable = itemAnalysis.values().stream() - .anyMatch(info -> info.canUse && (info.inventoryCount + info.bankCount) >= totalRequired); - - if (anySingleTypeHasEnoughUsable) { - analysis.append("\n Status: ✅ SUFFICIENT (at least one usable type has ").append(totalRequired).append("+ items)"); - isSufficient = true; - } else { - int maxUsableSingleType = itemAnalysis.values().stream() - .filter(info -> info.canUse) - .mapToInt(info -> info.inventoryCount + info.bankCount) - .max() - .orElse(0); - int shortage = totalRequired - maxUsableSingleType; - analysis.append("\n Status: ❌ INSUFFICIENT (need ").append(shortage).append(" more usable items of any single type)"); - } - } else { - // ANY_COMBINATION mode - consider only usable items - if (totalUsableCount >= totalRequired) { - analysis.append("\n Status: ✅ SUFFICIENT (have ").append(totalUsableCount).append(" usable, need ").append(totalRequired).append(")"); - isSufficient = true; - } else { - int shortage = totalRequired - totalUsableCount; - analysis.append("\n Status: ❌ INSUFFICIENT (need ").append(shortage).append(" more usable items of any type)"); - } - } - - analysis.append("\n Priority: ").append(groupItems.get(0).getPriority()); - - if (orRequirementMode == OrRequirementMode.SINGLE_TYPE) { - analysis.append("\n Note: Must have exactly ").append(totalRequired).append(" of ONE usable item type"); - } else { - analysis.append("\n Note: Any combination of usable items can fulfill this requirement"); - } - - // Add debugging hint if insufficient - if (!isSufficient) { - analysis.append("\n ðŸ’Ą Debug Hint: Check if items exist but skill requirements aren't met, or items are in wrong location (bank vs inventory)"); - } - } - - /** - * Checks if an item is usable based on its requirement type and skill requirements. - * - * @param item The item requirement to check - * @param inventoryCount Number of items in inventory - * @param bankCount Number of items in bank - * @return true if the item can be used to fulfill the requirement - */ - private boolean checkItemUsability(ItemRequirement item, int inventoryCount, int bankCount) { - // First check if we have the item available - if (inventoryCount + bankCount == 0) { - return false; - } - - // Check skill requirements - if (!item.meetsSkillRequirements()) { - return false; - } - - // Check requirement type constraints - RequirementType reqType = item.getRequirementType(); - switch (reqType) { - case INVENTORY: - // Must be available (can withdraw from bank if needed) - return true; - case EQUIPMENT: - // Must be available and equippable (can equip from inventory or bank) - return true; - case EITHER: - // Can be in inventory or equipped (most flexible) - return true; - default: - return true; - } - } - - /** - * Helper class to store item availability and usability information. - */ - private static class ItemAvailabilityInfo { - final int inventoryCount; - final int inventoryQuantity; - final int bankCount; - final boolean canUse; - final boolean meetsSkillRequirements; - final RequirementType requirementType; - final Skill skillToUse; - final Integer minimumLevelToUse; - final Skill skillToEquip; - final Integer minimumLevelToEquip; - - ItemAvailabilityInfo(int inventoryCount, int inventoryQuantity, int bankCount, boolean canUse, boolean meetsSkillRequirements, - RequirementType requirementType, Skill skillToUse, Integer minimumLevelToUse, - Skill skillToEquip, Integer minimumLevelToEquip) { - this.inventoryCount = inventoryCount; - this.inventoryQuantity = inventoryQuantity; - this.bankCount = bankCount; - this.canUse = canUse; - this.meetsSkillRequirements = meetsSkillRequirements; - this.requirementType = requirementType; - this.skillToUse = skillToUse; - this.minimumLevelToUse = minimumLevelToUse; - this.skillToEquip = skillToEquip; - this.minimumLevelToEquip = minimumLevelToEquip; - } - } - - /** - * Analyzes an individual item requirement (not part of an OR group). - * Now includes detailed skill requirement checking and usability analysis. - * - * @param analysis The analysis string builder - * @param missingItem The individual item requirement to analyze - */ - private void analyzeIndividualRequirement(StringBuilder analysis, ItemRequirement missingItem) { - analysis.append("\n 📋 Item: ").append(missingItem.getName()); - analysis.append("\n Required: ").append(missingItem.getAmount()); - - // Check current inventory and bank for availability with error handling - int getInventoryQuantity = 0; - int bankCount = 0; - - try { - // Safely get inventory and bank counts with error handling - getInventoryQuantity = Rs2Inventory.itemQuantity(missingItem.getId()); - bankCount = Rs2Bank.count(missingItem.getUnNotedId()); - } catch (ArrayIndexOutOfBoundsException e) { - log.warn("ArrayIndexOutOfBoundsException when counting individual item '{}' (ID: {}): {}", - missingItem.getName(), missingItem.getId(), e.getMessage()); - // Continue with 0 counts to avoid crashing - analysis.append("\n ⚠ïļ Error accessing item data - using 0 counts"); - } catch (Exception e) { - log.warn("Unexpected error when counting individual item '{}' (ID: {}): {}", - missingItem.getName(), missingItem.getId(), e.getMessage()); - analysis.append("\n ⚠ïļ Error accessing item data - using 0 counts"); - } - - int totalAvailable = getInventoryQuantity + bankCount; - - analysis.append("\n Available: ").append(totalAvailable).append(" (Inventory: ").append(getInventoryQuantity).append(", Bank: ").append(bankCount).append(")"); - - // Analyze usability based on skill requirements and requirement type - boolean canUse = checkItemUsability(missingItem, getInventoryQuantity, bankCount); - boolean meetsSkillReqs = missingItem.meetsSkillRequirements(); - boolean hasEnoughQuantity = totalAvailable >= missingItem.getAmount(); - - // Enhanced status analysis considering both quantity and usability - if (hasEnoughQuantity && canUse) { - analysis.append("\n Status: ✅ SUFFICIENT AND USABLE"); - analysis.append("\n ðŸ’Ą Debug Hint: Item is available and usable but wasn't selected - check planning logic"); - } else if (hasEnoughQuantity && !canUse) { - analysis.append("\n Status: ⚠ïļ AVAILABLE BUT NOT USABLE"); - - if (!meetsSkillReqs) { - analysis.append("\n ðŸšŦ Skill Requirements NOT Met:"); - - // Check skill to use requirements - if (missingItem.getSkillToUse() != null && missingItem.getMinimumLevelToUse() != null) { - int currentLevel = Rs2Player.getRealSkillLevel(missingItem.getSkillToUse()); - if (currentLevel < missingItem.getMinimumLevelToUse()) { - analysis.append("\n - Need ").append(missingItem.getSkillToUse().getName()) - .append(" ").append(missingItem.getMinimumLevelToUse()) - .append(" to use (current: ").append(currentLevel).append(")"); - } - } - - // Check skill to equip requirements - if (missingItem.getSkillToEquip() != null && missingItem.getMinimumLevelToEquip() != null) { - int currentLevel = Rs2Player.getRealSkillLevel(missingItem.getSkillToEquip()); - if (currentLevel < missingItem.getMinimumLevelToEquip()) { - analysis.append("\n - Need ").append(missingItem.getSkillToEquip().getName()) - .append(" ").append(missingItem.getMinimumLevelToEquip()) - .append(" to equip (current: ").append(currentLevel).append(")"); - } - } - } - - // Add requirement type context - RequirementType reqType = missingItem.getRequirementType(); - if (reqType == RequirementType.EQUIPMENT && getInventoryQuantity > 0 && bankCount == 0) { - analysis.append("\n 📍 Location Issue: Item in inventory but requirement needs it equipped"); - } else if (reqType == RequirementType.INVENTORY && getInventoryQuantity == 0 && bankCount > 0) { - analysis.append("\n 📍 Location Issue: Item in bank but requirement needs it in inventory"); - } - } else { - int shortage = missingItem.getAmount() - totalAvailable; - analysis.append("\n Status: ❌ INSUFFICIENT (need ").append(shortage).append(" more)"); - } - - // Add detailed item properties for debugging - analysis.append("\n Properties:"); - analysis.append("\n - Name: ").append(missingItem.getName()); - analysis.append("\n - ID: ").append(missingItem.getId()); - analysis.append("\n - noted ID: ").append(missingItem.getNotedId()); - analysis.append("\n - is noted item: ").append(missingItem.getNotedId() == missingItem.getId() ); - analysis.append("\n - Amount: ").append(missingItem.getAmount()); - analysis.append("\n - Stackable: ").append(missingItem.isStackable() ? "Yes" : "No"); - analysis.append("\n - Priority: ").append(missingItem.getPriority()); - analysis.append("\n - Requirement Type: ").append(missingItem.getRequirementType()); - - if (missingItem.getEquipmentSlot() != null) { - analysis.append("\n - Equipment Slot: ").append(missingItem.getEquipmentSlot()); - } - - if (missingItem.getInventorySlot() >= 0) { - analysis.append("\n - Specific Slot: ").append(missingItem.getInventorySlot()); - } - - // Add skill requirements summary if present - if (missingItem.getSkillToUse() != null || missingItem.getSkillToEquip() != null) { - analysis.append("\n Skill Requirements:"); - - if (missingItem.getSkillToUse() != null && missingItem.getMinimumLevelToUse() != null) { - int currentUseLevel = Rs2Player.getRealSkillLevel(missingItem.getSkillToUse()); - String useStatus = currentUseLevel >= missingItem.getMinimumLevelToUse() ? "✅" : "❌"; - analysis.append("\n - Use: ").append(useStatus) - .append(" ").append(missingItem.getSkillToUse().getName()) - .append(" ").append(missingItem.getMinimumLevelToUse()) - .append(" (current: ").append(currentUseLevel).append(")"); - } - - if (missingItem.getSkillToEquip() != null && missingItem.getMinimumLevelToEquip() != null) { - int currentEquipLevel = Rs2Player.getRealSkillLevel(missingItem.getSkillToEquip()); - String equipStatus = currentEquipLevel >= missingItem.getMinimumLevelToEquip() ? "✅" : "❌"; - analysis.append("\n - Equip: ").append(equipStatus) - .append(" ").append(missingItem.getSkillToEquip().getName()) - .append(" ").append(missingItem.getMinimumLevelToEquip()) - .append(" (current: ").append(currentEquipLevel).append(")"); - } - } - } - - // ========== PLAN EXECUTION METHODS ========== - - /** - * Banks all equipped and inventory items that are not part of this planned loadout. - * This ensures a clean slate before executing the optimal layout plan. - * - * @return true if banking was successful, false otherwise - */ - public boolean bankItemsNotInPlan(CompletableFuture scheduledFuture) { - try { - log.info("Analyzing current equipment and inventory state..."); - - // Quick check: if plan is empty, bank everything - boolean hasEquipmentPlan = !equipmentAssignments.isEmpty(); - boolean hasInventoryPlan = !inventorySlotAssignments.isEmpty() || !flexibleInventoryItems.isEmpty(); - - if (!hasEquipmentPlan && !hasInventoryPlan) { - log.info("Plan is empty - banking all equipment and inventory items"); - } else { - log.info("Plan has {} equipment assignments, {} specific inventory slots, {} flexible items", - equipmentAssignments.size(), - inventorySlotAssignments.size(), - flexibleInventoryItems.size()); - } - - // Ensure bank is open - if (!Rs2Bank.isOpen()) { - log.warn("Bank is not open for cleanup banking. Opening bank..."); - if (!Rs2Bank.walkToBankAndUseBank()) { - log.error("Failed to open bank for cleanup banking"); - return false; - } - sleepUntil(() -> Rs2Bank.isOpen(), 5000); - } - - boolean bankingSuccess = true; - int itemsBanked = 0; - - // Step 1: Bank equipped items not in the plan - log.info("Banking equipped items not in planned loadout..."); - for (EquipmentInventorySlot equipmentSlot : EquipmentInventorySlot.values()) { - if (scheduledFuture!=null && scheduledFuture.isCancelled()) { - log.info("Banking task cancelled, stopping equipment cleanup."); - return false; - } - // Check if this slot is planned to have an item - ItemRequirement plannedItem = equipmentAssignments.get(equipmentSlot); - - // Get currently equipped item in this slot - Rs2ItemModel equippedItem = Rs2Equipment.get(equipmentSlot); - - if (equippedItem != null) { // Something is equipped in this slot - boolean shouldKeepEquipped = false; - - if (plannedItem != null) { - // Check if the currently equipped item matches the planned item - shouldKeepEquipped = plannedItem.getId() == equippedItem.getId(); - } - - if (!shouldKeepEquipped) { - // Unequip the item (this will move it to inventory, then we can bank it) - log.info("Unequipping item from slot {}: {}", equipmentSlot.name(), equippedItem.getName()); - - if (Rs2Equipment.interact(item -> item.getSlot() == equipmentSlot.getSlotIdx(), "remove")) { - sleepUntil(() -> Rs2Equipment.get(equipmentSlot) == null, 3000); - - // Now bank the item from inventory - if (Rs2Inventory.hasItem(equippedItem.getId())) { - Rs2Bank.depositOne(equippedItem.getId()); - sleepUntil(() -> !Rs2Inventory.hasItem(equippedItem.getId()), 3000); - itemsBanked++; - } - } else { - log.warn("Failed to unequip item from slot {}", equipmentSlot.name()); - bankingSuccess = false; - } - } else { - log.info("Keeping equipped item in slot {}: matches planned loadout", equipmentSlot.name()); - } - } - } - - // Step 2: Bank inventory items not in the plan - log.info("Banking inventory items not in planned loadout..."); - - // Collect all planned item IDs for quick lookup - Set plannedItemIds = new HashSet<>(); - - // Add planned equipment item IDs - for (ItemRequirement equippedItem : equipmentAssignments.values()) { - plannedItemIds.add(equippedItem.getId()); - } - - // Add planned specific slot item IDs - for (ItemRequirement slotItem : inventorySlotAssignments.values()) { - plannedItemIds.add(slotItem.getId()); - } - - // Add planned flexible item IDs - for (ItemRequirement flexibleItem : flexibleInventoryItems) { - plannedItemIds.add(flexibleItem.getId()); - } - - // Check each inventory slot - for (int inventorySlot = 0; inventorySlot < 28; inventorySlot++) { - final int currentSlot = inventorySlot; // Make it effectively final for lambda - Rs2ItemModel currentItem = Rs2Inventory.getItemInSlot(inventorySlot); - if (currentItem != null) { - boolean shouldKeepInInventory = false; - - // Check if this item is part of the planned loadout - if (plannedItemIds.contains(currentItem.getId())) { - // Further check: is this item in the right place according to the plan? - shouldKeepInInventory = isItemInCorrectPlanPosition(currentItem, inventorySlot); - } - - if (!shouldKeepInInventory) { - // Bank the item - log.info("Banking inventory item from slot {}: {} x{}", - inventorySlot, currentItem.getName(), currentItem.getQuantity()); - Rs2Bank.depositOne(currentItem.getId()); - if ( sleepUntil(() -> Rs2Inventory.getItemInSlot(currentSlot) == null || - Rs2Inventory.getItemInSlot(currentSlot).getId() != currentItem.getId(), 3000)) { - itemsBanked++; - } else { - log.warn("Failed to bank item from inventory slot {}: {}", inventorySlot, currentItem.getName()); - bankingSuccess = false; - } - } else { - log.info("Keeping inventory item in slot {}: {} (matches planned position)", - inventorySlot, currentItem.getName()); - } - } - } - - if (bankingSuccess) { - log.info("Successfully banked {} items not in planned loadout", itemsBanked); - } else { - log.warn("Banking completed with some failures. {} items banked.", itemsBanked); - } - - return bankingSuccess; - - } catch (Exception e) { - log.error("Error banking items not in plan: {}", e.getMessage(), e); - return false; - } - } - - /** - * Checks if an item is in the correct position according to this plan. - * This considers both specific slot assignments and flexible item allowances. - * - * @param currentItem The item currently in inventory - * @param currentSlot The slot where the item is currently located - * @return true if the item should stay in its current position, false if it should be banked - */ - private boolean isItemInCorrectPlanPosition(Rs2ItemModel currentItem, int currentSlot) { - int itemId = currentItem.getId(); - int itemQuantity = currentItem.getQuantity(); - - // Check if this slot has a specific assignment that matches - ItemRequirement specificSlotItem = inventorySlotAssignments.get(currentSlot); - if (specificSlotItem != null) { - // Check if the current item matches the planned item for this slot - if (specificSlotItem.getId() == itemId) { - // Check quantity requirements - if (specificSlotItem.getAmount() <= itemQuantity) { - log.info("Item {} in slot {} matches specific slot assignment", currentItem.getName(), currentSlot); - return true; - } else { - log.info("Item {} in slot {} matches ID but insufficient quantity: {} < {}", - currentItem.getName(), currentSlot, itemQuantity, specificSlotItem.getAmount()); - return false; - } - } else { - log.info("Item {} in slot {} doesn't match specific slot assignment", currentItem.getName(), currentSlot); - return false; - } - } - - // Check if this item is among the flexible items - for (ItemRequirement flexibleItem : flexibleInventoryItems) { - if (flexibleItem.getId() == itemId) { - log.info("Item {} in slot {} is a planned flexible item", currentItem.getName(), currentSlot); - return true; // Flexible items can be anywhere - } - } - - // Not found in any planned positions - log.info("Item {} in slot {} is not part of the planned loadout", currentItem.getName(), currentSlot); - return false; - } - - /** - * Executes this inventory and equipment layout plan. - * Withdraws and equips items according to the optimal layout. - * - * @return true if execution was successful - */ - public boolean executePlan(CompletableFuture scheduledFuture) { - try { - log.info("\n--- Executing Inventory and Equipment Layout Plan ---"); - - // Ensure bank is open - if (!Rs2Bank.isOpen()) { - if (!Rs2Bank.walkToBankAndUseBank()) { - log.error("Failed to open bank for plan execution"); - return false; - } - sleepUntil(() -> Rs2Bank.isOpen(), 5000); - } - - boolean success = true; - - // Step 1: Handle equipment assignments - if (!equipmentAssignments.isEmpty()) { - log.info("Executing equipment assignments ({} items)...", equipmentAssignments.size()); - for (Map.Entry entry : equipmentAssignments.entrySet()) { - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.info("Plan execution cancelled, stopping equipment assignments."); - return false; - } - EquipmentInventorySlot slot = entry.getKey(); - ItemRequirement item = entry.getValue(); - - if (!handleEquipmentAssignment(slot, item)) { - log.error("Failed to fulfill equipment assignment: {} -> {}", slot.name(), item.getName()); - success = false; - } - } - } - - // Step 2: Handle specific inventory slot assignments - if (!inventorySlotAssignments.isEmpty()) { - log.info("Executing specific inventory slot assignments ({} items)...", inventorySlotAssignments.size()); - for (Map.Entry entry : inventorySlotAssignments.entrySet()) { - Integer slot = entry.getKey(); - ItemRequirement item = entry.getValue(); - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.info("Plan execution cancelled, stopping inventory slot assignments."); - return false; - } - if (!handleInventorySlotAssignment(slot, item)) { - log.error("Failed to fulfill inventory slot assignment: slot {} -> {}", slot, item.getName()); - success = false; - } - } - } - - // Step 3: Handle flexible inventory items - if (!flexibleInventoryItems.isEmpty()) { - log.info("Executing flexible inventory items ({} items)...", flexibleInventoryItems.size()); - for (ItemRequirement item : flexibleInventoryItems) { - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.info("Plan execution cancelled, stopping flexible inventory items."); - return false; - } - if (!handleFlexibleInventoryItem(item)) { - log.error("Failed to fulfill flexible inventory item: {}", item.getName()); - // Don't mark as failed for optional items - if (item.isMandatory()) { - success = false; - } - } - } - } - - if (success) { - log.info("Successfully executed all plan assignments"); - } else { - log.error("Some plan assignments failed to execute"); - } - - return success; - - } catch (Exception e) { - log.error("Error executing plan: {}", e.getMessage(), e); - return false; - } - } - - /** - * Handles a single equipment assignment. - */ - private boolean handleEquipmentAssignment(EquipmentInventorySlot slot, ItemRequirement item) { - log.info("Handling equipment assignment: {} -> {}", slot.name(), item.getName()); - - // Check if already equipped correctly (considering fuzzy matching for variations) - Rs2ItemModel currentEquipped = Rs2Equipment.get(slot); - if (currentEquipped != null) { - if (item.isFuzzy()) { - // For fuzzy items, check if any variation is equipped - Collection variations = InventorySetupsVariationMapping.getVariations(item.getId()); - if (variations.contains(currentEquipped.getId())) { - log.info("Item {} (or variation) already equipped in slot {}", item.getName(), slot.name()); - return true; - } - } else { - // Exact ID match - if (item.getId() == currentEquipped.getId()) { - log.info("Item {} already equipped in slot {}", item.getName(), slot.name()); - return true; - } - } - } - - // Try to withdraw and equip the item if available - if (item.isAvailableInBank()) { - int itemId = item.getId(); - Rs2Bank.withdrawAndEquip(itemId); - if (sleepUntil(() -> Rs2Equipment.get(slot) != null && - Rs2Equipment.get(slot).getId() == itemId, 5000)) { - log.info("Withdrew {} for equipment slot {}", item.getName(), slot.name()); - log.info("Successfully equipped {} in slot {}", item.getName(), slot.name()); - return true; - } - } - - log.error("Failed to equip {} in slot {}", item.getName(), slot.name()); - return false; - } - - /** - * Handles a single inventory slot assignment. - */ - private boolean handleInventorySlotAssignment(int slot, ItemRequirement item) { - log.info("Handling inventory slot assignment: slot {} -> {}", slot, item.getName()); - - // Use the static utility method from ItemRequirement - return ItemRequirement.withdrawAndPlaceInSpecificSlot(slot, item); - } - - /** - * Handles a single flexible inventory item. - */ - private boolean handleFlexibleInventoryItem(ItemRequirement item) { - log.info("Handling flexible inventory item: {}", item.getName()); - - // Check if item is already in inventory with sufficient amount (handles fuzzy matching) - if (item.isAvailableInInventory()) { - log.info("Flexible item {} already in inventory with sufficient amount", item.getName()); - return true; - } - - // Try to withdraw the item if available in bank - if (item.isAvailableInBank()) { - int itemId = item.getId(); - if (Rs2Bank.withdrawX(itemId, item.getAmount())) { - log.info("Withdrew flexible item: {} x{}", item.getName(), item.getAmount()); - return true; - } else { - log.warn("Failed to withdraw flexible item: {} x{}", item.getName(), item.getAmount()); - } - } - - log.error("Could not withdraw flexible item: {}", item.getName()); - return false; - } - - // ========== VALIDATION AND ANALYSIS METHODS ========== - - /** - * Optimizes and validates the entire plan with comprehensive checks. - * This includes space optimization, conflict resolution, feasibility checking, and comprehensive mandatory validation. - * - * @param plan The inventory setup plan to optimize and validate - * @return true if the plan is valid and feasible, false otherwise - */ - public static boolean optimizeAndValidatePlan(InventorySetupPlanner plan) { - // Step 1: Optimize flexible item placement - plan.optimizeFlexibleItemPlacement(); - - // Step 2: Validate plan feasibility - if (!plan.isFeasible()) { - log.info("Plan is not feasible - has missing mandatory items:"); - plan.getMissingMandatoryItems().forEach(item -> - log.info(" - Missing: {}", item.getName())); - plan.getMissingMandatoryEquipment().forEach((slot,items) -> - log.info(" - Missing equipment slot: {}", slot+ " with items: {}", - items.stream().map(ItemRequirement::getName).collect(Collectors.joining(", ")))); - return false; // Early exit if plan is not feasible - } - - // Step 3: Comprehensive mandatory requirements validation - boolean allMandatorySatisfied = validateAllMandatoryRequirementsSatisfied(plan); - if (!allMandatorySatisfied) { - log.error("CRITICAL: Final validation failed - not all mandatory requirements are satisfied in the plan"); - return false; // Early exit if mandatory requirements are not satisfied - } - - // Step 4: Validate inventory capacity - if (!plan.fitsInInventory()) { - log.warn("Plan does not fit in inventory - needs {} slots but only 28 available", - plan.getTotalInventorySlotsNeeded()); - return false; // Early exit if plan exceeds inventory capacity - } else { - log.info("Plan successfully created - uses {}/28 inventory slots", - plan.getTotalInventorySlotsNeeded()); - } - - // Step 5: Log summary for debugging - if (log.isDebugEnabled()) { - log.info("Plan summary:\n{}", plan.getSummary()); - } - return true; // Plan is valid and feasible - } - - /** - * Silent version of optimizeAndValidatePlan that suppresses logging. - * Used when detailed analysis is already logged elsewhere. - * - * @param plan The inventory setup plan to optimize and validate - * @return true if the plan is valid and feasible, false otherwise - */ - public static boolean optimizeAndValidatePlanSilent(InventorySetupPlanner plan) { - // Step 1: Optimize flexible item placement - plan.optimizeFlexibleItemPlacement(); - - // Step 2: Validate plan feasibility (silent) - if (!plan.isFeasible()) { - return false; // Early exit if plan is not feasible - } - - // Step 3: Comprehensive mandatory requirements validation (silent) - boolean allMandatorySatisfied = validateAllMandatoryRequirementsSatisfied(plan); - if (!allMandatorySatisfied) { - return false; // Early exit if mandatory requirements are not satisfied - } - - // Step 4: Validate inventory capacity (silent) - if (!plan.fitsInInventory()) { - return false; // Early exit if plan exceeds inventory capacity - } - - return true; // Plan is valid and feasible - } - - /** - * Validates that all mandatory requirements are actually satisfied in the final plan. - * This is a comprehensive check that goes beyond just checking for "missing" items. - * - * @param plan The inventory setup plan to validate - * @return true if all mandatory requirements are satisfied, false otherwise - */ - public static boolean validateAllMandatoryRequirementsSatisfied(InventorySetupPlanner plan) { - // This would need access to the registry, so we'll keep this method simpler - // and focus on validating the plan itself rather than cross-referencing requirements - - // Check if all mandatory items in the plan are actually present - boolean allSatisfied = true; - List unsatisfiedRequirements = new ArrayList<>(); - - // Validate equipment assignments - for (Map.Entry entry : plan.getEquipmentAssignments().entrySet()) { - ItemRequirement item = entry.getValue(); - if (item.isMandatory()) { - // Use ItemRequirement's own availability checking which handles fuzzy matching and amounts - if (!item.isAvailableInInventoryOrBank()) { - unsatisfiedRequirements.add(item.getName() + " (equipment slot: " + entry.getKey() + ")"); - allSatisfied = false; - } - } - } - - // Validate inventory assignments - for (ItemRequirement item : plan.getInventorySlotAssignments().values()) { - if (item.isMandatory()) { - // Use ItemRequirement's own availability checking which handles fuzzy matching and amounts - if (!item.isAvailableInInventoryOrBank()) { - unsatisfiedRequirements.add(item.getName() + " (inventory)"); - allSatisfied = false; - } - } - } - - // Validate flexible items - for (ItemRequirement item : plan.getFlexibleInventoryItems()) { - if (item.isMandatory()) { - // Use ItemRequirement's own availability checking which handles fuzzy matching and amounts - if (!item.isAvailableInInventoryOrBank()) { - unsatisfiedRequirements.add("(flexible inventory): "+ item.displayString() ); - allSatisfied = false; - } - } - } - - if (!allSatisfied) { - StringBuilder unsatisfiedRequirementsBuilder = new StringBuilder(); - unsatisfiedRequirementsBuilder.append(String.format("\nFinal validation failed. {} mandatory requirements not satisfied:", unsatisfiedRequirements.size())); - unsatisfiedRequirements.forEach(req -> unsatisfiedRequirementsBuilder.append(String.format("\n - {}", req))); - log.error(unsatisfiedRequirementsBuilder.toString()); - } else { - log.info("Final validation passed - all mandatory requirements in plan are satisfied"); - } - - return allSatisfied; - } - - /** - * Handles an item as flexible inventory item with proper conflict checking. - * - * @param bestItem The item to handle as flexible - * @param plan The inventory setup plan - * @param alreadyPlanned Set of already planned items - */ - public static void handleItemAsFlexible(ItemRequirement bestItem, InventorySetupPlanner plan, Set alreadyPlanned) { - plan.addFlexibleInventoryItem(bestItem); - alreadyPlanned.add(bestItem); - - log.info("Added {} as flexible inventory item (amount: {}, stackable: {})", - bestItem.getName(), bestItem.getAmount(), bestItem.isStackable()); - } - - /** - * Handles missing mandatory items by adding them to the appropriate missing lists. - * - * @param contextReqs The requirements that couldn't be fulfilled - * @param plan The inventory setup plan - * @param location Description of where the item was needed (for logging) - */ - public static void handleMissingMandatoryItem(List contextReqs, InventorySetupPlanner plan, String location) { - // Check if any requirement was mandatory - boolean hasMandatory = LogicalRequirement.hasMandatoryItems(contextReqs); - - if (hasMandatory) { - List mandatoryItems = LogicalRequirement.extractMandatoryItemRequirements(contextReqs); - for (ItemRequirement mandatoryItem : mandatoryItems) { - plan.addMissingMandatoryInventoryItem(mandatoryItem); - log.info("Cannot fulfill mandatory item requirement for {}: {}", location, mandatoryItem.getName()); - } - } - } - - /** - * Converts this plan to an InventorySetup that can be used with Rs2InventorySetup. - * This allows reusing the existing inventory setup loading functionality. - * - * @param setupName The name for the generated setup - * @return InventorySetup generated from this plan, or null if conversion failed - */ - public InventorySetup convertToInventorySetup(String setupName) { - return convertToInventorySetup(setupName, java.awt.Color.RED, true, null, true, false, 0, false, -1); - } - - /** - * Converts this plan to an InventorySetup with customizable configuration. - * This allows reusing the existing inventory setup loading functionality. - * - * @param setupName The name for the generated setup - * @param highlightColor The highlight color for differences - * @param highlightDifference Whether to highlight differences - * @param displayColor The display color (can be null) - * @param filterBank Whether to filter bank - * @param unorderedHighlight Whether to highlight unordered differences - * @param spellbook The spellbook setting - * @param favorite Whether the setup is marked as favorite - * @param iconID The icon ID for the setup - * @return InventorySetup generated from this plan, or null if conversion failed - */ - public InventorySetup convertToInventorySetup(String setupName, java.awt.Color highlightColor, - boolean highlightDifference, java.awt.Color displayColor, boolean filterBank, - boolean unorderedHighlight, int spellbook, boolean favorite, int iconID) { - try { - log.debug("Converting InventorySetupPlanner to InventorySetup: {}", setupName); - - // Create inventory items list - List inventoryItems = - createInventoryItemsList(); - - // Create equipment items list - List equipmentItems = - createEquipmentItemsList(); - - // Create empty containers for special items (rune pouch, bolt pouch, quiver) - List runePouchItems = - createRunePouchItemsList(); - - List boltPouchItems = - createBoltPouchItemsList(); - - List quiverItems = - createQuiverItemsList(); - - // Create the inventory setup using the same pattern as addInventorySetup - return createInventorySetupFromLists( - setupName, - inventoryItems, - equipmentItems, - runePouchItems, - boltPouchItems, - quiverItems, - highlightColor, - highlightDifference, - displayColor, - filterBank, - unorderedHighlight, - spellbook, - favorite, - iconID - ); - - } catch (Exception e) { - log.error("Failed to convert InventorySetupPlanner to InventorySetup: {}", e.getMessage(), e); - return null; - } - } - - /** - * Creates the inventory items list from the plan. - * Fills all 28 slots, using dummy items for empty slots. - */ - private List createInventoryItemsList() { - List inventoryItems = new ArrayList<>(); - - // Initialize all 28 slots with dummy items - for (int i = 0; i < 28; i++) { - inventoryItems.add(InventorySetupsItem.getDummyItem()); - } - - // Fill specific slot assignments - for (Map.Entry entry : inventorySlotAssignments.entrySet()) { - int slot = entry.getKey(); - ItemRequirement item = entry.getValue(); - - if (slot >= 0 && slot < 28) { - inventoryItems.set(slot, createInventorySetupsItem(item, slot)); - log.debug("\n\t Added specific slot assignment: {} -> slot {}", item.getName(), slot); - } - } - - // Fill flexible items in available slots - int currentSlot = 0; - for (ItemRequirement item : flexibleInventoryItems) { - // Find next available slot - while (currentSlot < 28 && !InventorySetupsItem.itemIsDummy(inventoryItems.get(currentSlot))) { - currentSlot++; - } - - if (currentSlot < 28) { - inventoryItems.set(currentSlot, createInventorySetupsItem(item, currentSlot)); - log.debug("\n\t -Added flexible item: {} -> slot {}", item.getName(), currentSlot); - currentSlot++; - } else { - log.warn("No available inventory slot for flexible item: {}", item.getName()); - } - } - - return inventoryItems; - } - - /** - * Creates the equipment items list from the plan. - * Fills all 14 equipment slots, using dummy items for empty slots. - */ - private List createEquipmentItemsList() { - List equipmentItems = new ArrayList<>(); - - // Initialize all 14 equipment slots with dummy items - for (int i = 0; i < 14; i++) { - equipmentItems.add(InventorySetupsItem.getDummyItem()); - } - - // Fill equipment assignments - for (Map.Entry entry : equipmentAssignments.entrySet()) { - EquipmentInventorySlot slot = entry.getKey(); - ItemRequirement item = entry.getValue(); - - int slotIndex = slot.getSlotIdx(); - if (slotIndex >= 0 && slotIndex < 14) { - equipmentItems.set(slotIndex, createInventorySetupsItem(item, slotIndex)); - log.debug("Added equipment assignment: {} -> slot {}", item.getName(), slot.name()); - } - } - - return equipmentItems; - } - - /** - * Creates the rune pouch items list from the plan. - * Detects RunePouchRequirement objects and converts their required runes to InventorySetupsItem objects. - */ - private List createRunePouchItemsList() { - List runePouchItems = new ArrayList<>(); - - // Search for RunePouchRequirement in both inventory slot assignments and flexible items - List runePouchRequirements = new ArrayList<>(); - - // Check inventory slot assignments for RunePouchRequirement instances - for (ItemRequirement item : inventorySlotAssignments.values()) { - if (item instanceof RunePouchRequirement) { - runePouchRequirements.add((RunePouchRequirement) item); - } - } - - // Check flexible inventory items for RunePouchRequirement instances - for (ItemRequirement item : flexibleInventoryItems) { - if (item instanceof RunePouchRequirement) { - runePouchRequirements.add((RunePouchRequirement) item); - } - } - - // Convert rune requirements to InventorySetupsItem objects - if (!runePouchRequirements.isEmpty()) { - log.debug("Found {} RunePouchRequirement(s) in plan", runePouchRequirements.size()); - - // Collect all required runes from all RunePouchRequirements - Map allRequiredRunes = new HashMap<>(); - - for (RunePouchRequirement runePouchReq : runePouchRequirements) { - // Merge rune requirements (taking the maximum quantity for each rune type) - for (Map.Entry entry : runePouchReq.getRequiredRunes().entrySet()) { - net.runelite.client.plugins.microbot.util.magic.Runes rune = entry.getKey(); - int requiredAmount = entry.getValue(); - allRequiredRunes.merge(rune, requiredAmount, Integer::max); - } - } - - // Convert runes to InventorySetupsItem objects - // Rune pouch has 4 slots (0-3), but we'll use all available slots - int slotIndex = 0; - for (Map.Entry entry : allRequiredRunes.entrySet()) { - if (slotIndex >= 4) { - log.warn("Rune pouch can only hold 4 types of runes, skipping extra runes"); - break; - } - - net.runelite.client.plugins.microbot.util.magic.Runes rune = entry.getKey(); - int quantity = entry.getValue(); - - // Create InventorySetupsItem for this rune - InventorySetupsItem runeItem = new InventorySetupsItem( - rune.getItemId(), // itemID - rune.name() + " Rune", // name - quantity, // quantity - false, // fuzzy (runes don't have variations) - InventorySetupsStackCompareID.None, // stackCompare - false, // locked - slotIndex // slot in rune pouch - ); - - runePouchItems.add(runeItem); - slotIndex++; - - log.debug("Added rune to pouch setup: {} x{} in slot {}", - rune.name(), quantity, slotIndex - 1); - } - } - - // Fill remaining slots with dummy items if needed (rune pouch has 4 slots) - while (runePouchItems.size() < 4) { - InventorySetupsItem dummyItem = new InventorySetupsItem( - -1, // dummy item ID - "", // empty name - 0, // no quantity - false, // not fuzzy - InventorySetupsStackCompareID.None, // no stack compare - false, // not locked - runePouchItems.size() // slot index - ); - runePouchItems.add(dummyItem); - } - - log.debug("Created rune pouch items list with {} items", runePouchItems.size()); - return runePouchItems; - } - - /** - * Creates the bolt pouch items list from the plan. - * TODO: Currently returns empty list - can be enhanced to detect bolt pouch requirements - */ - private List createBoltPouchItemsList() { - // For now, return empty list - can be enhanced later to handle bolt pouch requirements - return new ArrayList<>(); - } - - /** - * Creates the quiver items list from the plan. - * TODO: Currently returns empty list - can be enhanced to detect quiver requirements - */ - private List createQuiverItemsList() { - // For now, return empty list - can be enhanced later to handle quiver requirements - return new ArrayList<>(); - } - - /** - * Creates an InventorySetupsItem from an ItemRequirement. - * Uses the same constructor pattern as found in MInventorySetupsPlugin. - */ - private InventorySetupsItem createInventorySetupsItem(ItemRequirement item, int slot) { - // Use fuzzy matching for items that have multiple variations - boolean fuzzy = item.isStackable() || hasItemVariations(item.getId()); - - // Default stack compare type - could be enhanced based on item type - InventorySetupsStackCompareID stackCompare = - InventorySetupsStackCompareID.None; - - // Item is not locked by default - could be enhanced based on requirements - boolean locked = false; - - return new InventorySetupsItem( - item.getId(), - item.getName(), - item.getAmount(), - fuzzy, - stackCompare, - locked, - slot - ); - } - - /** - * Checks if an item has known variations (e.g., degraded equipment). - */ - private boolean hasItemVariations(int itemId) { - // Use the existing variation mapping to check for variations - try { - Collection variations = - InventorySetupsVariationMapping.getVariations(itemId); - return variations.size() > 1; - } catch (Exception e) { - // If variation mapping fails, default to false - return false; - } - } - - /** - * Creates an InventorySetup from item lists, reusing the same pattern as MInventorySetupsPlugin.addInventorySetup. - */ - private InventorySetup createInventorySetupFromLists( - String setupName, - List inventoryItems, - List equipmentItems, - List runePouchItems, - List boltPouchItems, - List quiverItems) { - - // Default settings - could be enhanced to be configurable - java.awt.Color highlightColor = java.awt.Color.RED; - boolean highlightDifference = true; - java.awt.Color displayColor = null; - boolean filterBank = true; - boolean unorderedHighlight = false; - int spellbook = 0; // Standard spellbook - boolean favorite = false; - int iconID = -1; - - return createInventorySetupFromLists(setupName, inventoryItems, equipmentItems, - runePouchItems, boltPouchItems, quiverItems, highlightColor, highlightDifference, - displayColor, filterBank, unorderedHighlight, spellbook, favorite, iconID); - } - - /** - * Creates an InventorySetup from item lists with configurable parameters. - */ - private InventorySetup createInventorySetupFromLists( - String setupName, - List inventoryItems, - List equipmentItems, - List runePouchItems, - List boltPouchItems, - List quiverItems, - java.awt.Color highlightColor, - boolean highlightDifference, - java.awt.Color displayColor, - boolean filterBank, - boolean unorderedHighlight, - int spellbook, - boolean favorite, - int iconID) { - - // Create the inventory setup using the same constructor as in addInventorySetup - return new InventorySetup( - inventoryItems, - equipmentItems, - runePouchItems, - boltPouchItems, - quiverItems, - new java.util.HashMap<>(), // additionalFilteredItems - setupName, - "automatically Generated by the InventorySetupPlanner", // notes - highlightColor, - highlightDifference, - displayColor, - filterBank, - unorderedHighlight, - spellbook, - favorite, - iconID - ); - } - - /** - * Adds this plan as an InventorySetup to the MInventorySetupsPlugin with default settings. - * Returns the created InventorySetup if successful, null otherwise. - */ - public InventorySetup addToInventorySetupsPlugin(String setupName) { - java.awt.Color highlightColor = java.awt.Color.RED; - boolean highlightDifference = true; - java.awt.Color displayColor = null; - boolean filterBank = true; - boolean unorderedHighlight = false; - int spellbook = 0; // Standard spellbook - boolean favorite = false; - int iconID = -1; - - return addToInventorySetupsPlugin(setupName, highlightColor, highlightDifference, - displayColor, filterBank, unorderedHighlight, spellbook, favorite, iconID); - } - - /** - * Adds this plan as an InventorySetup to the MInventorySetupsPlugin with full configuration. - * Returns the created InventorySetup if successful, null otherwise. - */ - public InventorySetup addToInventorySetupsPlugin(String setupName, java.awt.Color highlightColor, - boolean highlightDifference, java.awt.Color displayColor, boolean filterBank, - boolean unorderedHighlight, int spellbook, boolean favorite, int iconID) { - try { - int MAX_SETUP_NAME_LENGTH = MInventorySetupsPlugin.MAX_SETUP_NAME_LENGTH; - if( setupName.length() > MAX_SETUP_NAME_LENGTH) { - // Trim the setup name to the maximum allowed length - setupName = setupName.substring(0, MAX_SETUP_NAME_LENGTH); - } - // Convert this plan to an InventorySetup with all configuration parameters - InventorySetup inventorySetup = convertToInventorySetup(setupName, highlightColor, - highlightDifference, displayColor, filterBank, unorderedHighlight, spellbook, favorite, iconID); - - if (inventorySetup == null) { - log.error("Failed to convert plan to InventorySetup"); - return null; - } - - // Update or add the setup using the same logic as Rs2InventorySetup - updateSetup(inventorySetup); - - log.debug("Successfully added InventorySetup '{}' to MInventorySetupsPlugin", setupName); - return inventorySetup; - - } catch (Exception e) { - log.error("Failed to add plan to MInventorySetupsPlugin: {}", e.getMessage(), e); - return null; - } - } - - /** - * Updates an existing setup or adds a new one if it doesn't exist. - * Uses the same logic as Rs2InventorySetup.updateSetup. - * - * @param newSetup The setup to update/add - */ - private void updateSetup(InventorySetup newSetup) { - InventorySetup existingSetup = getInventorySetup(newSetup.getName()); - if (existingSetup != null) { - MInventorySetupsPlugin.getInventorySetups().remove(existingSetup); - MInventorySetupsPlugin plugin = getMInventorySetupsPlugin(); - if (plugin != null) { - plugin.getCache().removeSetup(existingSetup); - } - } - addSetupToPlugin(newSetup); - } - - /** - * Adds a setup to the plugin's setup list and cache. - * Uses the same logic as Rs2InventorySetup.addSetupToPlugin. - * - * @param setup The setup to add - */ - private void addSetupToPlugin(InventorySetup setup) { - MInventorySetupsPlugin plugin = getMInventorySetupsPlugin(); - log.debug("\n\t Adding setup '{}' (name length{}) to MInventorySetupsPlugin", setup.getName() ,setup.getName().length() ); - if (plugin != null) { - plugin.addInventorySetup(setup); - - Rs2InventorySetup.isInventorySetup(setup.getName()); // Ensure setup is recognized as an inventory setup - sleepUntil( () -> Rs2InventorySetup.isInventorySetup(setup.getName()), 5000); - - //plugin.getCache().addSetup(setup); - //MInventorySetupsPlugin.getInventorySetups().add(setup); - //plugin.getDataManager().updateConfig(true, false); - //Layout setupLayout = plugin.getLayoutUtilities().createSetupLayout(setup); - //plugin.getLayoutManager().saveLayout(setupLayout); - //plugin.getTagManager().setHidden(setupLayout.getTag(), true); - //SwingUtilities.invokeLater(() -> plugin.getPan().redrawOverviewPanel(false)); - } - } - - /** - * Helper method to get an inventory setup by name. - * - * @param setupName The name of the setup to find - * @return The InventorySetup if found, null otherwise - */ - private InventorySetup getInventorySetup(String setupName) { - return MInventorySetupsPlugin.getInventorySetups().stream() - .filter(setup -> setup.getName().equalsIgnoreCase(setupName)) - .findFirst() - .orElse(null); - } - - /** - * Helper method to get the MInventorySetupsPlugin instance. - * - * @return MInventorySetupsPlugin instance or null if not available - */ - private MInventorySetupsPlugin getMInventorySetupsPlugin() { - return (MInventorySetupsPlugin) net.runelite.client.plugins.microbot.Microbot.getPlugin( - MInventorySetupsPlugin.class.getName()); - } - - /** - * Creates an Rs2InventorySetup instance from this planner for execution using existing Rs2 utilities. - * This allows leveraging the existing loadInventory(), loadEquipment(), etc. methods. - * - * @param setupName The name of the setup to create - * @param mainScheduler The scheduler to monitor for cancellation - * @return Rs2InventorySetup instance, or null if creation failed - */ - public Rs2InventorySetup createRs2InventorySetup(String setupName, ScheduledFuture mainScheduler) { - try { - // First, add this plan to the MInventorySetupsPlugin - InventorySetup createdSetup = addToInventorySetupsPlugin(setupName); - if (createdSetup == null) { - log.error("Failed to add inventory setup to plugin"); - return null; - } - - // Create Rs2InventorySetup using the setup name - Rs2InventorySetup rs2Setup = - new Rs2InventorySetup(createdSetup.getName(), mainScheduler); - - log.info("\n\t-Successfully created Rs2InventorySetup from planner: {}", createdSetup.getName()); - return rs2Setup; - - } catch (Exception e) { - log.error("Failed to create Rs2InventorySetup from planner: {}", e.getMessage(), e); - return null; - } - } - - /** - * Executes this plan using the Rs2InventorySetup approach. - * This provides a more integrated solution that reuses existing bank and equipment management. - * - * @param scheduledFuture The future to monitor for cancellation - * @param setupName The name for the temporary setup - * @return true if execution was successful - */ - public boolean executeUsingRs2InventorySetup(CompletableFuture scheduledFuture, String setupName) { - return executeUsingRs2InventorySetup(scheduledFuture, setupName, false); - } - - /** - * Executes this plan using the Rs2InventorySetup approach with optional banking of items not in setup. - * This provides a more integrated solution that reuses existing bank and equipment management. - * - * @param scheduledFuture The future to monitor for cancellation - * @param setupName The name for the temporary setup - * @param bankItemsNotInSetup Whether to bank items not in the setup first (excludes teleport items) - * @return true if execution was successful - */ - public boolean executeUsingRs2InventorySetup(CompletableFuture scheduledFuture, String setupName, boolean bankItemsNotInSetup) { - try { - log.info("\n\t-Executing plan using Rs2InventorySetup approach: {}", setupName); - - // Convert CompletableFuture to ScheduledFuture (simplified conversion) - ScheduledFuture mainScheduler = new ScheduledFuture() { - @Override - public long getDelay(TimeUnit unit) { return 0; } - @Override - public int compareTo(Delayed o) { return 0; } - @Override - public boolean cancel(boolean mayInterruptIfRunning) { return scheduledFuture.cancel(mayInterruptIfRunning); } - @Override - public boolean isCancelled() { return scheduledFuture.isCancelled(); } - @Override - public boolean isDone() { return scheduledFuture.isDone(); } - @Override - public Object get() throws InterruptedException, ExecutionException { return scheduledFuture.get(); } - @Override - public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { return scheduledFuture.get(timeout, unit); } - }; - - // Create Rs2InventorySetup from this plan - Rs2InventorySetup rs2Setup = - createRs2InventorySetup(setupName, mainScheduler); - - if (rs2Setup == null) { - log.error("Failed to create Rs2InventorySetup"); - return false; - } - if(rs2Setup.doesEquipmentMatch() && rs2Setup.doesInventoryMatch()){ - log.info("Plan already matches current inventory and equipment setup, skipping execution"); - return true; // No need to execute if already matches - } - if (!Rs2Bank.isOpen()) { - if (!Rs2Bank.walkToBankAndUseBank() && !Rs2Player.isInteracting() && !Rs2Player.isMoving()) { - log.error("\n\tFailed to open bank for comprehensive item management"); - } - boolean openBank= sleepUntil(() -> Rs2Bank.isOpen(), 5000); - if (!openBank) { - log.error("\n\tFailed to open bank within timeout period,for invntory setup execution \"{}\"", setupName); - return false; - } - } - - // Bank items not in setup first if requested (excludes teleport items) - if (bankItemsNotInSetup) { - log.info("Banking items not in setup (excluding teleport items) before setting up: {}", setupName); - if (!rs2Setup.bankAllItemsNotInSetup(true)) { - log.warn("Failed to bank all items not in setup, continuing with setup anyway"); - } - } - // Use existing Rs2InventorySetup methods to fulfill the requirements - boolean equipmentSuccess = rs2Setup.loadEquipment(); - if (!equipmentSuccess) { - log.error("Failed to load equipment using Rs2InventorySetup"); - return false; - } - - boolean inventorySuccess = rs2Setup.loadInventory(); - if (!inventorySuccess) { - log.error("Failed to load inventory using Rs2InventorySetup"); - return false; - } - - // Verify the setup matches - boolean equipmentMatches = rs2Setup.doesEquipmentMatch(); - boolean inventoryMatches = rs2Setup.doesInventoryMatch(); - - if (equipmentMatches && inventoryMatches) { - log.info("Successfully executed plan using Rs2InventorySetup: {}", setupName); - return true; - } else { - log.warn("Plan execution completed but setup verification failed. Equipment matches: {}, Inventory matches: {}", - equipmentMatches, inventoryMatches); - return false; - } - - } catch (Exception e) { - log.error("Failed to execute plan using Rs2InventorySetup: {}", e.getMessage(), e); - return false; - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/ItemRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/ItemRequirement.java deleted file mode 100644 index 5fc977a84ab..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/ItemRequirement.java +++ /dev/null @@ -1,1947 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item; - -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.api.ItemComposition; -import net.runelite.api.Skill; -import net.runelite.client.game.ItemEquipmentStats; -import net.runelite.client.game.ItemStats; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -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.tasks.requirements.requirement.logical.OrRequirement; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; -import net.runelite.client.plugins.microbot.util.inventory.Rs2FuzzyItem; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import lombok.Getter; -import lombok.EqualsAndHashCode; -import lombok.extern.slf4j.Slf4j; - -import java.util.List; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.concurrent.CompletableFuture; -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -/** - * Enhanced item recommendation that supports multiple item IDs, different requirement types, - * and priority-based selection logic. Generalized to handle both equipment and inventory requirements. - * - * Supports both equipment requirements (must be equipped) and inventory requirements (must be in inventory). - * Also includes charge detection for items like rings of dueling, binding necklaces, etc. - */ -@Getter -@EqualsAndHashCode(callSuper = true) -@Slf4j -public class ItemRequirement extends Requirement { - - - /** - * Default item count for this requirement. - * This can be overridden if the plugin requires a specific count. - */ - private final int amount; - - /** - * The equipment slot this item should occupy (only relevant for EQUIPMENT and EITHER requirement types). - * Null for pure inventory items. - */ - private final EquipmentInventorySlot equipmentSlot; - - /** - * The specific inventory slot this item should occupy (0-27). - * -1 means any available slot. - * Only relevant for INVENTORY and EITHER requirement types. - * For EITHER requirements, this represents the preferred inventory slot if not equipped. - */ - @Getter - private final Integer inventorySlot; - - /** - * Skill name for the minimum level requirement to use this item (e.g., "Attack", "Runecraft"). - * Null if no level requirement. - */ - private final Skill skillToUse; - - /** - * Minimum player level required to use this item, if applicable. - * Used for items that have a level requirement to be used effectively. - * - * Null if no level requirement. - */ - private final Integer minimumLevelToUse; - - /** - * Skill name for the minimum level requirement (e.g., "Attack", "Runecraft"). - * Null if no level requirement. - */ - private final Skill skillToEquip; - - /** - * Minimum player level required to use the highest priority item in the list. - * Used for level-based item selection (e.g., attack level for weapons). - * Null if no level requirement. - */ - private final Integer minimumLevelToEquip; - - /** - * Whether to use fuzzy matching for this item requirement. - * When fuzzy is true, the requirement will match any variation of the same item. - * For example, different charge states of an item (ring of dueling(8), ring of dueling(7), etc.) - * or different variants of the same item (graceful pieces in different colors). - * Uses InventorySetupsVariationMapping under the hood to map between equivalent item IDs. - */ - private final boolean fuzzy; - private ItemComposition itemComposition = null; // Cached item composition for performance - - /** - * Full constructor for item requirement with schedule context and inventory slot support. - * The RequirementType is automatically inferred from the slot parameters: - * - INVENTORY: equipmentSlot is null and inventorySlot is provided (not -1) - * - EQUIPMENT: equipmentSlot is provided and inventorySlot is -1 - * - EITHER: both equipmentSlot and inventorySlot are provided (not -1) - */ - public ItemRequirement( - int itemId, - int amount, - EquipmentInventorySlot equipmentSlot, - Integer inventorySlot, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext, - Skill skillToUse, - Integer minimumLevelToUse, - Skill skillToEquip, - Integer minimumLevelToEquip, - boolean fuzzy) { - - // Call super constructor with inferred RequirementType and resolved item ID - super(inferRequirementType(equipmentSlot, inventorySlot), priority, rating, description, - Arrays.asList(resolveOptimalItemId(itemId, amount)), taskContext); - - // Only override amount for equipment items (not inventory or ammo) - if (equipmentSlot != null && equipmentSlot != EquipmentInventorySlot.AMMO) { - amount = 1; // For non-ammo equipment items, amount is typically 1 - } - this.amount = amount; - this.equipmentSlot = equipmentSlot; - this.inventorySlot = inventorySlot; - this.skillToUse = skillToUse; - this.minimumLevelToUse = minimumLevelToUse; - this.skillToEquip = skillToEquip; - this.minimumLevelToEquip = minimumLevelToEquip; - if (isNoted()){ - this.fuzzy = true; - }else if(Rs2FuzzyItem.isChargedItem(itemId)){ - this.fuzzy = fuzzy; // it can be desired to use the exact item.. - }else{ - this.fuzzy = fuzzy; - } - - // Validate slot assignments - validateSlotAssignments(); - } - - // Convenience constructors - - /** - * Constructor for single item ID with equipment requirement. - */ - public ItemRequirement(int itemId, int amount, EquipmentInventorySlot equipmentSlot, - RequirementPriority priority, int rating, String description, TaskContext taskContext) { - this(itemId, amount, equipmentSlot, -2, priority, rating, description, - taskContext, null, null, null, null, false); - } - - /** - * Constructor for single item ID with equipment requirement with default amount of 1. - */ - public ItemRequirement(int itemId, EquipmentInventorySlot equipmentSlot, - RequirementPriority priority, int rating, String description, TaskContext taskContext) { - this(itemId, 1, equipmentSlot, -2, priority, rating, description, - taskContext, null, null, null, null, false); - } - - /** - * Constructor for single item ID with equipment requirement and skill requirements for use only. - */ - public ItemRequirement(int itemId, int amount, EquipmentInventorySlot equipmentSlot, Integer inventorySlot, - RequirementPriority priority, int rating, String description, TaskContext taskContext, - Skill skillToUse, Integer minimumLevelToUse) { - this(itemId, amount, equipmentSlot, inventorySlot, priority, rating, description, - taskContext, skillToUse, minimumLevelToUse, null, null, false); - } - - /** - * Constructor for single item ID with equipment requirement and skill requirements for use only. - */ - public ItemRequirement(int itemId, int amount, EquipmentInventorySlot equipmentSlot, - RequirementPriority priority, int rating, String description, TaskContext taskContext, - Skill skillToUse, Integer minimumLevelToUse) { - this(itemId, amount, equipmentSlot, -2, priority, rating, description, - taskContext, skillToUse, minimumLevelToUse, null, null, false); - } - - /** - * Constructor for single item ID with equipment requirement and both skill requirements (use and equip). - */ - public ItemRequirement(int itemId, int amount, EquipmentInventorySlot equipmentSlot, Integer inventorySlot, - RequirementPriority priority, int rating, String description, TaskContext taskContext, - Skill skillToUse, Integer minimumLevelToUse, Skill skillToEquip, Integer minimumLevelToEquip) { - this(itemId, amount, equipmentSlot, inventorySlot, priority, rating, description, - taskContext, skillToUse, minimumLevelToUse, skillToEquip, minimumLevelToEquip, false); - } - - /** - * Constructor for single item ID with equipment requirement and both skill requirements (use and equip). - */ - public ItemRequirement(int itemId, int amount, EquipmentInventorySlot equipmentSlot, - RequirementPriority priority, int rating, String description, TaskContext taskContext, - Skill skillToUse, Integer minimumLevelToUse, Skill skillToEquip, Integer minimumLevelToEquip) { - this(itemId, amount, equipmentSlot, -2, priority, rating, description, - taskContext, skillToUse, minimumLevelToUse, skillToEquip, minimumLevelToEquip, false); - } - - /** - * Constructor for single item ID with fuzzy option. - */ - public ItemRequirement(int itemId, EquipmentInventorySlot equipmentSlot, - RequirementPriority priority, int rating, String description, TaskContext taskContext, boolean fuzzy) { - this(itemId, 1, equipmentSlot, -2, priority, rating, description, - taskContext, null, null, null, null, fuzzy); - } - - /** - * Additional constructors for inventory slot specification - */ - - /** - * Constructor for single item ID with specific inventory slot. - */ - public ItemRequirement(int itemId, int amount, Integer inventorySlot, - RequirementPriority priority, int rating, String description, TaskContext taskContext) { - this(itemId, amount, null, inventorySlot, priority, rating, description, - taskContext, null, null, null, null, false); - } - - /** - * Constructor for EITHER requirement with both equipment and inventory slot specification. - */ - public ItemRequirement(int itemId, int amount, EquipmentInventorySlot equipmentSlot, Integer inventorySlot, - RequirementPriority priority, int rating, String description, TaskContext taskContext) { - this(itemId, amount, equipmentSlot, inventorySlot, priority, rating, description, - taskContext, null, null, null, null, false); - } - - @Override - public String getName() { - if (this.itemComposition == null) { - // Lazy load item composition if not already set - setItemComp(getId()); - } - if(isFuzzy()){ - if(this.itemComposition != null){ - boolean isCharged = Rs2FuzzyItem.isChargedItem(getId()); - if(isCharged){ - return Rs2FuzzyItem.getBaseItemNameFromString( this.itemComposition.getName()); - } - return this.itemComposition.getName(); - }else{ - return "Unknown Item (Fuzzy)"; - } - - } - // Use the single item ID as the name - return this.itemComposition != null ? this.itemComposition.getName() : "Unknown Item"; - } - private void setItemComp(int itemId) { - this.itemComposition = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemComposition(itemId)) .orElse(null); - } - - /** - * Checks if this is a dummy item requirement. - * Dummy item requirements have ID of -1 and are used to block inventory/equipment slots. - * Dummy items serve several purposes: - * 1. They represent empty slots in containers - * 2. They maintain consistent array sizes for comparison - * 3. They can be used as placeholders for required "empty" slots - * 4. They're automatically considered "fulfilled" in requirement checking - * - * @return true if this is a dummy item requirement (itemId == -1) - */ - public boolean isDummyItemRequirement() { - return getId() == -1; - } - - /** - * Creates a dummy item requirement for blocking an equipment slot. - * Dummy requirements have MANDATORY priority, rating 10, itemId -1, amount -1, and no skill requirements. - * They are used to reserve slots without specifying actual items. - * - * @param equipmentSlot The equipment slot to block - * @param TaskContext When this requirement applies - * @param description Description for the dummy requirement - * @return A dummy ItemRequirement for the specified equipment slot - */ - public static ItemRequirement createDummyEquipmentRequirement( - EquipmentInventorySlot equipmentSlot, - TaskContext taskContext, - String description) { - if (equipmentSlot == null) { - throw new IllegalArgumentException("Equipment slot cannot be null for dummy equipment requirement"); - } - - return new ItemRequirement( - -1, // dummy item ID - -1, // dummy amount - equipmentSlot, - -2, // equipment-only slot indicator - RequirementPriority.MANDATORY, - 10, // rating - description, - taskContext, - null, // skillToUse - -1, // minimumLevelToUse - null, // skillToEquip - -1, // minimumLevelToEquip - false // fuzzy - ); - } - - /** - * Creates a dummy item requirement for blocking an inventory slot. - * Dummy requirements have MANDATORY priority, rating 10, itemId -1, amount -1, and no skill requirements. - * They are used to reserve slots without specifying actual items. - * - * @param inventorySlot The inventory slot to block (0-27) - * @param TaskContext When this requirement applies - * @param description Description for the dummy requirement - * @return A dummy ItemRequirement for the specified inventory slot - * @throws IllegalArgumentException if inventorySlot is not between 0 and 27 - */ - public static ItemRequirement createDummyInventoryRequirement( - int inventorySlot, - TaskContext taskContext, - String description) { - if (inventorySlot < 0 || inventorySlot > 27) { - throw new IllegalArgumentException("Inventory slot must be between 0 and 27, got: " + inventorySlot); - } - - return new ItemRequirement( - -1, // dummy item ID - -1, // dummy amount - null, // equipmentSlot - inventorySlot, - RequirementPriority.MANDATORY, - 10, // rating - description, - taskContext, - null, // skillToUse - -1, // minimumLevelToUse - null, // skillToEquip - -1, // minimumLevelToEquip - false // fuzzy - ); - } - - // ========== EXISTING FACTORY METHODS ========== - - /** - * Factory method to create an OrRequirement when multiple item IDs are provided. - * Use this instead of ItemRequirement constructors with List when you have multiple alternative items. - * - * @param itemIds List of alternative item IDs - * @param amount Amount required for each item - * @param equipmentSlot Equipment slot if this is equipment - * @param inventorySlot Inventory slot if specific slot required - * @param priority Priority level - * @param rating Effectiveness rating - * @param description Description for the OR requirement - * @param TaskContext When to fulfill this requirement - * @param skillToUse Skill required to use the items - * @param minimumLevelToUse Minimum level to use - * @param skillToEquip Skill required to equip - * @param minimumLevelToEquip Minimum level to equip - * @param fuzzy Whether to prefer lower charge variants - * @return OrRequirement containing individual ItemRequirements for each ID - */ - public static OrRequirement createOrRequirement( - List itemIds, - int amount, - EquipmentInventorySlot equipmentSlot, - Integer inventorySlot, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext, - Skill skillToUse, - Integer minimumLevelToUse, - Skill skillToEquip, - Integer minimumLevelToEquip, - boolean fuzzy) { - - if (itemIds.size() <= 1) { - throw new IllegalArgumentException("Use regular ItemRequirement constructor for single item ID"); - } - - // Create individual ItemRequirements for each ID - ItemRequirement[] requirements = new ItemRequirement[itemIds.size()]; - for (int i = 0; i < itemIds.size(); i++) { - int itemId = itemIds.get(i); - String itemName = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemComposition(itemId).getName()).orElse("Unknown Item"); - String itemDescription = description + " (" + itemName + ")"; - - requirements[i] = new ItemRequirement( - itemId, amount, equipmentSlot, inventorySlot, - priority, rating, itemDescription, taskContext, - skillToUse, minimumLevelToUse, skillToEquip, minimumLevelToEquip, - fuzzy - ); - } - - return new OrRequirement(priority, rating, description, taskContext, ItemRequirement.class,requirements); - } - - /** - * Factory method for equipment OR requirements with default parameters. - */ - public static OrRequirement createOrRequirement( - List itemIds, - EquipmentInventorySlot equipmentSlot, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext) { - return createOrRequirement(itemIds, 1, equipmentSlot, -2, priority, rating, description, - taskContext, null, null, null, null, false); - } - /** - * Factory method for inventory OR requirements with default parameters. - */ - public static OrRequirement createOrRequirement( - List itemIds, - int amount, - Integer inventorySlot, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext) { - return createOrRequirement(itemIds, amount, null, inventorySlot, priority, rating, description, - taskContext, null, null, null, null, false); - } - /** - * Factory method for inventory OR requirements with default parameters. - */ - public static OrRequirement createOrRequirement( - List itemIds, - int amount, - EquipmentInventorySlot equipmentSlot, - Integer inventorySlot, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext) { - return createOrRequirement(itemIds, amount, equipmentSlot, inventorySlot, priority, rating, description, - taskContext, null, null, null, null, false); - } - - - /** - * Factory method for inventory OR requirements with default parameters. - */ - public static OrRequirement createOrRequirement( - List itemIds, - int amount, - EquipmentInventorySlot equipmentSlot, - Integer inventorySlot, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext, - Skill skillToUse, - Integer minimumLevelToUse, - Skill skillToEquip, - Integer minimumLevelToEquip - ) { - return createOrRequirement(itemIds, amount, equipmentSlot, inventorySlot, priority, rating, description, - taskContext, skillToUse, minimumLevelToUse, skillToEquip, minimumLevelToEquip, false); - } - - /** - * Creates a copy of this ItemRequirement with a specific inventory slot. - * Useful for slot-specific placement during layout planning. - * - * @param targetSlot The target inventory slot (0-27) - * @return A new ItemRequirement with the specified slot - */ - public ItemRequirement copyWithSpecificSlot(int targetSlot) { - if (targetSlot < 0 || targetSlot > 27) { - throw new IllegalArgumentException("Invalid inventory slot: " + targetSlot); - } - - return new ItemRequirement( - getId(), - amount, - equipmentSlot, - targetSlot, - priority, - rating, - description, - taskContext, - skillToUse, - minimumLevelToUse, - skillToEquip, - minimumLevelToEquip, - fuzzy - ); - } - - /** - * Creates a copy of this ItemRequirement with a different amount. - * Useful for partial fulfillment scenarios. - * - * @param newAmount The new amount for the requirement - * @return A new ItemRequirement with the specified amount - */ - public ItemRequirement copyWithAmount(int newAmount) { - return new ItemRequirement( - getId(), - newAmount, - equipmentSlot, - inventorySlot, - priority, - rating, - description, - taskContext, - skillToUse, - minimumLevelToUse, - skillToEquip, - minimumLevelToEquip, - fuzzy - ); - } - - /** - * Retrieves the wiki URL for this item based on the URL suffix or item id. - * - * @return the wiki URL as a {@link String}, or {@code null} if not available - */ - // TODO when implented the wikiscrapper - /**@Nullable - public String getWikiUrl() - { - if (getUrlSuffix() != null) { - return "https://oldschool.runescape.wiki/w/" + getUrlSuffix(); - } - - if (getId() != -1) { - return "https://oldschool.runescape.wiki/w/Special:Lookup?type=item&id=" + getId(); - } - - return null; - }**/ - - /** - * Infers the RequirementType from the provided slot parameters. - * - * @param equipmentSlot The equipment slot (can be null) - * @param inventorySlot The inventory slot (-1 for any slot) - * @return The inferred RequirementType - */ - private static RequirementType inferRequirementType(EquipmentInventorySlot equipmentSlot, Integer inventorySlot) { - boolean hasEquipmentSlot = equipmentSlot != null; // null indicates, we dont allow the item placed into any equipment slot, - boolean hasInventorySlot = inventorySlot != -2 || inventorySlot==null; //-2 indicates we dont allow to be in inventory, -1 indicates any slot, 0-27 indicates specific slot - - if (hasEquipmentSlot && hasInventorySlot) { - return RequirementType.EITHER; - } else if (hasEquipmentSlot) { - return RequirementType.EQUIPMENT; - } else { - return RequirementType.INVENTORY; - } - } - - /** - * Checks if this item is available in either the player's inventory or bank. - * Now properly checks the required amount. - * - * @return true if the item is available with sufficient quantity, false otherwise - */ - public boolean isAvailableInInventoryOrBank() { - return getTotalAvailableQuantity() >= amount; - } - - /** - * Checks if this item is available in the player's inventory. - * Now properly checks the required amount. - * - * @return true if the item is in inventory with sufficient quantity, false otherwise - */ - public boolean isAvailableInInventory() { - return getInventoryQuantity() >= amount; - } - - /** - * Checks if this item is available in the player's bank. - * Now properly checks the required amount. - * - * @return true if the item is in bank with sufficient quantity, false otherwise - */ - public boolean isAvailableInBank() { - return getBankCount() >= amount; - } - - public boolean canBeUsed() { - // Check if the item is available in inventory or bank - if (isAvailableInInventoryOrBank()) { - - // Check skill to use if specified - if (skillToUse != null && minimumLevelToUse != null) { - int currentLevel = Rs2Player.getRealSkillLevel(skillToUse); - if (currentLevel < minimumLevelToUse) { - return false; - } - } - - }else{ - return false; - } - return true; - - } - public boolean canBeEquipped() { - // Check if the item is available in inventory or bank - if (isAvailableInInventoryOrBank()) { - // Check skill to equip if specified - if (skillToEquip != null && minimumLevelToEquip != null) { - int currentLevel = Rs2Player.getRealSkillLevel(skillToEquip); - if (currentLevel < minimumLevelToEquip) { - return false; - } - } - - }else{ - return false; - } - return isEquipment(); - - } - /** - * Checks if the player meets the skill requirements to use this item. - * - * @return true if the player meets skill requirements, false otherwise - */ - public boolean meetsSkillRequirements() { - // Check skill to equip if specified - if (skillToEquip != null && minimumLevelToEquip != null) { - int currentLevel = Rs2Player.getRealSkillLevel(skillToEquip); - if (currentLevel < minimumLevelToEquip) { - return false; - } - } - - // Check skill to use if specified - if (skillToUse != null && minimumLevelToUse != null) { - int currentLevel = Rs2Player.getRealSkillLevel(skillToUse); - if (currentLevel < minimumLevelToUse) { - return false; - } - } - - return true; - } - // TODO implement meets requirements --- has the item availble in inventory or bank, and has the required skill level to use it, and for equipment items, has the required skill level to equip it - - - /** - * Attempts to equip this item from inventory or bank with improved logic. - * Handles charged items and proper equipment verification. - * - * @return true if successfully equipped, false otherwise - */ - private boolean equip() { - if (Microbot.getClient().isClientThread()) { - log.error("Please run equip() on a non-client thread."); - return false; - } - - if (equipmentSlot == null) { - return false; // Not an equipment item - } - - if (!meetsSkillRequirements()) { - log.error("Skill requirements not met for " + getDescription()); - return false; - } - - // Check if already equipped - if (hasRequiredItemEquipped()) { - return true; - } - - // Try to equip from inventory first - if (tryEquipFromInventory()) { - return true; - } - - // Try to withdraw from bank and equip - return tryEquipFromBank(); - } - - /** - * Helper method to try equipping from inventory. - */ - private boolean tryEquipFromInventory() { - int itemId = getId(); - return tryEquipSingleItem(itemId); - } - - /** - * Helper method to try equipping from bank. - */ - private boolean tryEquipFromBank() { - // Ensure bank is open - if (!Rs2Bank.isOpen()) { - if (!Rs2Bank.openBank()) { - return false; - } - sleepUntil(()->Rs2Bank.isOpen(), 3000); - if(!Rs2Bank.isOpen()) { - log.error("Failed to open bank for equipping item: " + getDescription()); - return false; - } - } - - int itemId = getId(); - return tryEquipFromBankSingle(itemId); - } - - /** - * Helper method to try equipping a single item from inventory. - */ - private boolean tryEquipSingleItem(Integer itemId) { - if (Rs2Inventory.hasItem(itemId)) { - Rs2ItemModel item = Rs2Inventory.get(itemId); - if (item != null) { - // Check for common equip actions: "wear", "wield", "equip" - List actionList = new ArrayList<>(Arrays.asList(item.getInventoryActions())); - for (String action : actionList) { - if (action != null && (action.equalsIgnoreCase("wear") || - action.equalsIgnoreCase("wield") || - action.equalsIgnoreCase("equip"))) { - Rs2Inventory.interact(item.getSlot(), action); - if (sleepUntil(() -> Rs2Equipment.isWearing(itemId), 1800)) { - return true; - } - break; - } - } - } - } - return false; - } - - /** - * Helper method to try equipping a single item from bank. - */ - private boolean tryEquipFromBankSingle(Integer itemId) { - if (Rs2Bank.hasItem(itemId)) { - Rs2Bank.withdrawAndEquip(itemId); - return sleepUntil(() -> Rs2Equipment.isWearing(itemId), 3000); - } - return false; - } - - /** - * Attempts to withdraw this item from the bank. - * - * @return true if successfully withdrawn, false otherwise - */ - /** - * Attempts to withdraw this item from the bank with improved logic. - * Handles charged items and proper quantity calculation. - * - * @return true if successfully withdrawn, false otherwise - */ - private boolean withdrawFromBank(CompletableFuture scheduledFuture) { - if (Microbot.getClient().isClientThread()) { - log.error("Please run withdrawFromBank() on a non-client thread."); - return false; - } - - if (!meetsSkillRequirements()) { - log.error("Skill requirements not met for " + getDescription()); - return false; - } - - // Check if already have enough in inventory - if (hasRequiredItemInInventory()) { - return true; - } - - // Ensure we're near and have bank open - if (!Rs2Bank.isNearBank(6)) { - log.error("Not near a bank, cannot withdraw item: " + getDescription()); - Rs2Bank.walkToBank(); - } - - if (!Rs2Bank.isOpen()) { - if (!Rs2Bank.openBank()) { - return false; - } - sleepUntil(()->Rs2Bank.isOpen(), 3000); - } - - // Handle charged items with preference logic - since we now only have one item, check if it's charged - int itemId = getId(); - return tryWithdrawItem(itemId, scheduledFuture); - } - - - /** - * Helper method to attempt withdrawing a specific item. - */ - private boolean tryWithdrawItem(Integer itemId, CompletableFuture scheduledFuture) { - - - if (Rs2Bank.hasItem(itemId)) { - int quantity = getOptimalQuantity(itemId); - if (quantity > 0) { - Rs2Bank.withdrawX(itemId, quantity); - } else if (quantity < 0) { - Rs2Bank.depositX(itemId, Math.abs(quantity)); - } - - return sleepUntil(() -> isAvailableInInventory(), 3000); - } - return false; - } - - - - /** - * Checks if this requirement specifies a particular inventory slot. - * - * @return true if a specific inventory slot is specified (0-27), false if any slot (-1) - */ - public boolean hasSpecificInventorySlot() { - return inventorySlot >= 0 && inventorySlot <= 27; - } - - /** - * Checks if this requirement allows any inventory slot. - * - * @return true if any slot is allowed (-1), false if a specific slot is required - */ - public boolean allowsAnyInventorySlot() { - return inventorySlot == -1; - } - - /** - * Checks if this item requirement can be fulfilled by placing the item in the given inventory slot. - * - * @param slot The inventory slot to check (0-27) - * @return true if the item can be placed in this slot, false otherwise - */ - public boolean canBePlacedInInventorySlot(int slot) { - if (slot < 0 || slot > 27) { - return false; - } - - // If requirement type is EQUIPMENT only, it cannot be placed in inventory - if (requirementType == RequirementType.EQUIPMENT) { - return false; - } - - // If specific slot required, check if it matches - if (hasSpecificInventorySlot()) { - return inventorySlot == slot; - } - - // Otherwise, any slot is fine - return true; - } - - /** - * Gets the total count of this item across inventory, equipment, and bank. - * Properly handles fuzzy matching for charged items and item variations. - * - * @return the total available count - */ - public int getTotalAvailableQuantity() { - if (fuzzy) { - // For fuzzy matching, check all variations of the item - return getFuzzyInventoryQuantity() + getFuzzyBankCount() + getFuzzyEquippedCount(); - } else { - // Exact item ID matching - int itemId = getId(); - return Rs2Inventory.itemQuantity(itemId) + Rs2Bank.count(getUnNotedId()) + getEquippedCount(); - } - } - public int getTotalAvailableCount() { - if (fuzzy) { - // For fuzzy matching, check all variations of the item - return getFuzzyInventoryCount() + getFuzzyBankCount() + getFuzzyEquippedCount(); - } else { - // Exact item ID matching - int itemId = getId(); - return Rs2Inventory.count(itemId) + Rs2Bank.count(getBankCount()) + getEquippedCount(); - } - } - - - /** - * Gets the count of this item currently in inventory. - * Properly handles fuzzy matching for charged items and item variations. - * - * @return the inventory count - */ - public int getInventoryQuantity() { - if (fuzzy) { - return getFuzzyInventoryQuantity(); - } else { - return Rs2Inventory.itemQuantity(getId()); - } - } - - /** - * Gets the count of this item currently in bank. - * Properly handles fuzzy matching for charged items and item variations. - * - * @return the bank count - */ - public int getBankCount() { - if (fuzzy) { - return getFuzzyBankCount(); - } else { - return Rs2Bank.count(getUnNotedId()); - } - } - - /** - * Gets the fuzzy count of this item in inventory (includes all variations). - * Uses Rs2FuzzyItem for comprehensive fuzzy matching including ID-based variations - * and name-based charged item variants. - * - * @return the fuzzy inventory count - */ - private int getFuzzyInventoryCount() { - return Rs2FuzzyItem.getFuzzyInventoryCount(getId(), true); - } - private int getFuzzyInventoryQuantity() { - return Rs2FuzzyItem.getFuzzyInventoryQuantity(getId(), true); - } - - - /** - * Gets the fuzzy count of this item in bank (includes all variations). - * Uses Rs2FuzzyItem for comprehensive fuzzy matching including ID-based variations - * and name-based charged item variants. - * - * @return the fuzzy bank count - */ - private int getFuzzyBankCount() { - return Rs2FuzzyItem.getFuzzyBankCount(getId(), true); - } - - /** - * Gets the fuzzy count of this item currently equipped (includes all variations). - * Uses Rs2FuzzyItem for comprehensive fuzzy matching including ID-based variations - * and name-based charged item variants. - * - * @return the fuzzy equipped count (0 or 1 for most items) - */ - private int getFuzzyEquippedCount() { - if (equipmentSlot != null) { - return Rs2FuzzyItem.getFuzzyEquippedCount(getId(), true); - } - return 0; - } - - /** - * Gets the count of this item currently equipped. - * - * @return the equipped count (0 or 1 for most items) - */ - private int getEquippedCount() { - if (equipmentSlot != null) { - return Rs2Equipment.isWearing(getId()) ? 1 : 0; - } - return 0; - } - - /** - * Gets the optimal quantity to withdraw/deposit for this item. - * Considers current inventory amount and desired amount. - * - * @param itemId The specific item ID to calculate for - * @return Positive for withdraw, negative for deposit, 0 for no action needed - */ - private int getOptimalQuantity(Integer itemId) { - int currentAmount = Rs2Inventory.count(itemId); - int targetAmount = this.amount > 0 ? this.amount : 1; - - return targetAmount - currentAmount; - } - - /** - * Gets the primary item ID for this requirement. - * This is usually the first ID in the list, which typically represents the best option. - * - /** - * Gets the resolved item ID for this requirement. - * This returns the auto-resolved ID (potentially noted variant) that should be used for all operations. - * - * @return the resolved item ID for this requirement - */ - public int getId() { - if (ids.isEmpty()) { - throw new IllegalStateException("ItemRequirement must have exactly one item ID"); - } - if (ids.size() > 1) { - throw new IllegalStateException("ItemRequirement has multiple IDs, use createOrRequirement() factory method instead"); - } - return ids.get(0); - } - - - /** - * Checks if the player meets the skill requirements to equip a specific item. - * Validates both the minimum level and required skill for equipping. - * - * @param item The ItemRequirement to check skill requirements for - * @return true if player can equip the item, false if skill requirements aren't met - */ - public static boolean canPlayerEquipItem(ItemRequirement item) { - // Check if item has skill requirements for equipping - if (item.getSkillToEquip() != null && item.getMinimumLevelToEquip() > 0) { - int playerLevel = Rs2Player.getRealSkillLevel(item.getSkillToEquip()); - - if (playerLevel < item.getMinimumLevelToEquip()) { - log.debug("Player " + item.getSkillToEquip().getName() + " level (" + playerLevel + - ") insufficient to equip item requiring level " + item.getMinimumLevelToEquip()); - return false; - } - } - - return true; - } - - /** - * Checks if the player meets the skill requirements to use a specific item. - * Validates both the minimum level and required skill for using. - * - * @param item The ItemRequirement to check skill requirements for - * @return true if player can use the item, false if skill requirements aren't met - */ - public static boolean canPlayerUseItem(ItemRequirement item) { - // Check if item has skill requirements for using - if (item.getSkillToUse() != null && item.getMinimumLevelToUse() > 0) { - int playerLevel = Rs2Player.getRealSkillLevel(item.getSkillToUse()); - - if (playerLevel < item.getMinimumLevelToUse()) { - log.debug("Player " + item.getSkillToUse().getName() + " level (" + playerLevel + - ") insufficient to use item requiring level " + item.getMinimumLevelToUse()); - return false; - } - } - - return true; - } - - /** - * Returns a multi-line display string with detailed item requirement information. - * Uses StringBuilder with tabs for proper formatting. - * - * @return A formatted string containing item requirement details - */ - public String displayString() { - StringBuilder sb = new StringBuilder(); - sb.append("=== Item Requirement Details ===\n"); - sb.append(" -Name:\t\t\t").append(getName()).append("\n"); - sb.append(" -Type:\t\t\t").append(getRequirementType().name()).append("\n"); - sb.append(" -Priority:\t\t").append(getPriority().name()).append("\n"); - sb.append(" -Rating:\t\t\t").append(getRating()).append("/10\n"); - sb.append(" -Description:\t").append(getDescription()).append("\n"); - sb.append(" -Schedule Context:\t").append(getTaskContext().name()).append("\n"); - sb.append(" -Item ID:\t\t").append(getId()).append("\n"); - sb.append(" -Item unnoted ID:\t\t").append(getUnNotedId()).append("\n"); - sb.append(" -ItemModel unnoted ID:\t\t").append(Rs2ItemModel.getUnNotedId(getId())).append("\n"); - sb.append(" -Item noted ID:\t\t").append(getNotedId()).append("\n"); - sb.append(" -ItemModel noted ID:\t\t").append(Rs2ItemModel.getNotedId(getId())).append("\n"); - sb.append(" -Item linked ID:\t").append( getUnNotedId()).append("\n"); - sb.append(" -Amount:\t\t\t").append(amount).append("\n"); - - if (equipmentSlot != null) { - sb.append(" -Equipment Slot:\t").append(equipmentSlot.name()).append("\n"); - } - - if (inventorySlot != null && inventorySlot >= 0) { - sb.append(" -Inventory Slot:\t").append(inventorySlot).append("\n"); - } - - if (skillToUse != null) { - sb.append(" -Skill to Use:\t").append(skillToUse.getName()).append("\n"); - sb.append(" -Min Level to Use:\t").append(minimumLevelToUse != null ? minimumLevelToUse : "N/A").append("\n"); - } - - if (skillToEquip != null) sb.append(" -Skill to Equip:\t").append(skillToEquip.getName()).append("\n"); - if (minimumLevelToEquip != null) sb.append(" -Min Level to Equip:\t").append(minimumLevelToEquip).append("\n"); - - sb.append(" -Fuzzy Charge:\t").append(fuzzy).append("\n"); - sb.append(" -Is Available in Inventory:\t").append(isAvailableInInventory()).append("\n"); - sb.append(" -Is Available in Bank:\t").append(isAvailableInBank()).append("\n"); - sb.append(" -Total Available Count:\t").append(getTotalAvailableCount()).append("\n"); - sb.append(" - Banked: ").append(getBankCount()).append(" Inventory:").append(getFuzzyInventoryCount()).append("\n"); - sb.append(" -Total Available Quantity:\t").append(getTotalAvailableQuantity()).append("\n"); - sb.append(" - Banked: ").append(getBankCount()).append(" Inventory:").append(getInventoryQuantity()).append("\n"); - sb.append(" -Can be Used:\t\t").append(canBeUsed()).append("\n"); - sb.append(" -Can be Equipped:\t").append(canBeEquipped()).append("\n"); - sb.append(" -Meets Skill Req.:\t").append(meetsSkillRequirements()).append("\n"); - sb.append(" -Is Dummy Item:\t\t").append(isDummyItemRequirement()).append("\n"); - - return sb.toString(); - } - - - - /** - * Enhanced toString method that uses displayString for comprehensive output. - * - * @return A comprehensive string representation of this item requirement - */ - @Override - public String toString() { - return displayString(); - } - - /** - * Implements the abstract fulfillRequirement method from the base Requirement class. - * Attempts to fulfill this item requirement by checking availability and managing inventory/equipment. - * - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if the requirement was successfully fulfilled, false otherwise - */ - @Override - public boolean fulfillRequirement(CompletableFuture scheduledFuture) { - try { - if (Microbot.getClient().isClientThread()) { - log.error("Please run fulfillRequirement() on a non-client thread."); - return false; - } - // Dummy items are always considered "fulfilled" since they're just slot placeholders - if (isDummyItemRequirement()) { - log.debug("Dummy item requirement automatically fulfilled: {}", getDescription()); - return true; - } - // Check if the requirement is already fulfilled - if (isRequirementAlreadyFulfilled()) { - return true; - } - - // Check if the item is available in inventory or bank - if (!isAvailableInInventoryOrBank()) { - if (isMandatory()) { - log.error("MANDATORY item requirement cannot be fulfilled: " + getName() + " - Item not available"); - return false; - } else { - log.error("OPTIONAL/RECOMMENDED item requirement skipped: " + getName() + " - Item not available"); - return true; // Non-mandatory requirements return true if item isn't available - } - } - - // Handle equipment requirements - if (requirementType == RequirementType.EQUIPMENT || requirementType == RequirementType.EITHER) { - if (!fulfillEquipmentRequirement(scheduledFuture)) { - return !isMandatory(); // Return false only for mandatory requirements - } - } - - // Handle inventory requirements - if (requirementType == RequirementType.INVENTORY || requirementType == RequirementType.EITHER) { - if (!fulfillInventoryRequirement(scheduledFuture)) { - return !isMandatory(); // Return false only for mandatory requirements - } - } - - return true; - - } catch (Exception e) { - log.error("Error fulfilling item requirement " + getName() + ": " + e.getMessage()); - return !isMandatory(); // Don't fail mandatory requirements due to exceptions - } - } - - /** - * Checks if this requirement is currently fulfilled without attempting to fulfill it. - * This is more efficient than fulfillRequirement() for status checking. - * - * @return true if the requirement is already met, false otherwise - */ - @Override - public boolean isFulfilled() { - return isRequirementAlreadyFulfilled(); - } - - /** - * Checks if this requirement is already fulfilled based on the requirement type. - * - * @return true if the requirement is already met, false otherwise - */ - private boolean isRequirementAlreadyFulfilled() { - switch (requirementType) { - case EQUIPMENT: - return hasRequiredItemEquipped(); - case INVENTORY: - return hasRequiredItemInInventory(); - case EITHER: - return hasRequiredItemEquipped() || hasRequiredItemInInventory(); - default: - return false; - } - } - - /** - * Attempts to fulfill an equipment requirement by equipping the required item. - * - * @param scheduledFuture The CompletableFuture to monitor for cancellation - * @return true if the equipment requirement was fulfilled, false otherwise - */ - private boolean fulfillEquipmentRequirement(CompletableFuture scheduledFuture) { - - return equip(); - } - - /** - * Attempts to fulfill an inventory requirement by ensuring the required item is in inventory. - * Handles specific slot requirements and proper quantity management. - * - * @param scheduledFuture The CompletableFuture to monitor for cancellation - * @return true if the inventory requirement was fulfilled, false otherwise - */ - private boolean fulfillInventoryRequirement(CompletableFuture scheduledFuture) { - - if (hasSpecificInventorySlot()) { - return withdrawAndPlaceInSpecificSlot(scheduledFuture); - } else { - return withdrawFromBank(scheduledFuture); - } - } - - /** - * Withdraws the item and places it in the specific inventory slot if required. - * Creates a proper copy of the requirement with the target slot. - * - * @param scheduledFuture The CompletableFuture to monitor for cancellation - * @return true if successfully placed in the specific slot, false otherwise - */ - private boolean withdrawAndPlaceInSpecificSlot(CompletableFuture scheduledFuture) { - if (Microbot.getClient().isClientThread()) { - log.error("Please run withdrawAndPlaceInSpecificSlot() on a non-client thread."); - return false; - } - - // Check if already have the item in the correct slot - if (hasItemInSpecificSlot(inventorySlot)) { - return true; - } - - // Ensure we have the item available - if (!isAvailableInInventoryOrBank()) { - return false; - } - - // Handle case where item is already in inventory but wrong slot - if (isAvailableInInventory()) { - return moveToSpecificSlot(); - } - - // Withdraw from bank to specific slot - return withdrawFromBankToSpecificSlot(); - } - - /** - * Checks if the required item is in the specific inventory slot with correct amount. - * - * @param slot The inventory slot to check - * @return true if the item is in the slot with sufficient quantity - */ - private boolean hasItemInSpecificSlot(int slot) { - Rs2ItemModel item = Rs2Inventory.get(slot); - if (item == null) { - return false; - } - - for (Integer itemId : ids) { - if (item.getId() == itemId && item.getQuantity() >= amount) { - return true; - } - } - return false; - } - - /** - * Moves the item from its current inventory position to the specific slot. - * - * @return true if successfully moved, false otherwise - */ - private boolean moveToSpecificSlot() { - // Find the item in inventory - for (Integer itemId : ids) { - Rs2ItemModel item = Rs2Inventory.get(itemId); - if (item != null && item.getQuantity() >= amount) { - // Use the available moveItemToSlot method - return Rs2Inventory.moveItemToSlot(item, inventorySlot); - } - } - return false; - } - - /** - * Withdraws the item from bank directly to the specific inventory slot. - * Since withdrawToSlot doesn't exist, we'll withdraw and then move. - * - * @return true if successfully withdrawn to slot, false otherwise - */ - private boolean withdrawFromBankToSpecificSlot() { - if (!Rs2Bank.isOpen()) { - if (!Rs2Bank.openBank()) { - return false; - } - sleepUntil(() -> Rs2Bank.isOpen(), 3000); - } - - // Find best available item in bank - int itemId = getUnNotedId(); - if (Rs2Bank.count(itemId) >= amount) { - // Clear target slot if needed by depositing the item there - Rs2ItemModel targetSlotItem = Rs2Inventory.get(inventorySlot); - if (targetSlotItem != null) { - Rs2Bank.depositOne(targetSlotItem.getId()); - sleepUntil(() -> Rs2Inventory.get(inventorySlot) == null, 2000); - } - - // Withdraw the item (it will go to any available slot) - Rs2Bank.withdrawX(itemId, amount); - sleepUntil(() -> Rs2Inventory.itemQuantity(itemId) >= amount, 3000); - - // Now move to the specific slot - Rs2ItemModel withdrawnItem = Rs2Inventory.get(itemId); - if (withdrawnItem != null) { - return Rs2Inventory.moveItemToSlot(withdrawnItem, inventorySlot); - } - } - - return false; - } - - /** - * Checks if the required item is equipped in the correct slot (if specified). - * - * @return true if the required item is equipped, false otherwise - */ - private boolean hasRequiredItemEquipped() { - int itemId = getId(); - if (equipmentSlot != null) { - return Rs2Equipment.isWearing(itemId); - } else { - return Rs2Equipment.isWearing(itemId); - } - } - - /** - * Checks if the required item is in inventory with the correct amount. - * - * @return true if the required item is in inventory with sufficient quantity, false otherwise - */ - private boolean hasRequiredItemInInventory() { - return Rs2Inventory.count(getId()) >= amount; - } - - /** - * Validates slot assignments to ensure consistency between requirement type and slot specifications. - * Throws IllegalArgumentException if the configuration is invalid. - */ - private void validateSlotAssignments() { - // Validate inventory slot range - if (inventorySlot < -2 || inventorySlot > 27) { - throw new IllegalArgumentException("Inventory slot must be between -1 (any slot) and 27, got: " + inventorySlot); - } - - // Validate requirement type and slot consistency - switch (requirementType) { - case EQUIPMENT: - if (equipmentSlot == null || inventorySlot!= -2) { - throw new IllegalArgumentException("EQUIPMENT requirement must specify an equipment slot"); - } - break; - case INVENTORY: - if (equipmentSlot != null) { - throw new IllegalArgumentException("INVENTORY requirement should not specify an equipment slot"); - } - if( inventorySlot != -1) { - if(!isStackable() && amount > 1) { - throw new IllegalArgumentException("INVENTORY requirement with non-stackable items must have inventory slot -1 (any slot) or amount 1. Item ID: " + getId() + ", Amount: " + amount); - } - } - break; - case EITHER: - if ( !(equipmentSlot != null || inventorySlot>=-1) ) { - throw new IllegalArgumentException("EITHER requirement must specify at least one of equipment or inventory slot"); - } - // EITHER requirements can have equipment slot specified (preferred) and optional inventory slot - break; - default: - throw new IllegalArgumentException("Unsupported requirement type: " + requirementType); - } - } - - /** - * Resolves the optimal item ID for the given amount, automatically detecting when noted variants should be used. - * This method is called during construction to auto-resolve noted items for stackable requirements. - * - * @param originalItemId The original item ID provided to the constructor - * @param amount The amount required - * @return The optimal item ID (potentially noted variant) to use - */ - private static int resolveOptimalItemId(int originalItemId, int amount) { - if (amount <= 1) { - return originalItemId; // Single items don't need noting - } - - try { - ItemComposition composition = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemComposition(originalItemId) - ).orElse(null); - - if (composition == null) { - log.warn("Could not get item composition for item ID: {}, using original ID", originalItemId); - return originalItemId; - } - - if (composition.isStackable()) { - return originalItemId; // Already stackable, no need to change - } - - // Check if this item has a noted variant - int linkedNoteId = composition.getLinkedNoteId(); - if (linkedNoteId != originalItemId) { - ItemComposition notedComposition = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemComposition(linkedNoteId) - ).orElse(null); - - if (notedComposition != null && notedComposition.isStackable()) { - log.debug("Auto-resolved {} (ID: {}) to noted variant {} (ID: {}) for amount {}", - composition.getName(), originalItemId, - notedComposition.getName(), linkedNoteId, amount); - return linkedNoteId; // Use noted version - } - } - - // If we reach here, item is not stackable and has no noted variant - log.debug("Item {} (ID: {}) with amount {} has no stackable noted variant, keeping original", - composition.getName(), originalItemId, amount); - return originalItemId; - - } catch (Exception e) { - log.error("Error resolving optimal item ID for {} with amount {}: {}", - originalItemId, amount, e.getMessage()); - return originalItemId; // Fall back to original on error - } - } - - private static int getNotedItemId(ItemComposition composition) { - try { - - if (composition == null) { - return -1; - } - - int itemId = composition.getId(); - // If already stackable, return original ID - if (composition.isStackable()) { - return itemId; - } - // Check if this item has a noted variant - int linkedNoteId = composition.getLinkedNoteId(); - ItemComposition linkedComposition = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemComposition(linkedNoteId) - ).orElse(null); - - if (linkedComposition.isStackable() && !composition.isStackable()) { - log.debug("Found noted variant for {} (ID: {}) -> {} (ID: {})", - composition.getName(), itemId, - linkedComposition.getName(), linkedNoteId); - return linkedNoteId; // Use noted version - - } - if (linkedComposition.isStackable() && composition.isStackable()) { - log.debug("Item {} (ID: {}) has only a stackable variant", - composition.getName(), itemId, - linkedComposition.getName(), linkedNoteId); - return linkedNoteId < itemId ? linkedNoteId : itemId; // Use noted version if it has lower ID - } - // If we reach here, item is not stackable and has no noted variant - log.debug("Item {} (ID: {}) has no stackable noted variant", - composition.getName(), itemId); - return itemId; - - } catch (Exception e) { - //log.error("Error getting noted item ID for {}: {}", itemId, e.getMessage()); - return -1; // Fall back to original on error - } - } - private static int getUnNotedId(ItemComposition composition) { - try { - - if (composition == null) { - log.warn("Could not get item composition for item ID, returning original ID"); - return -1; - } - int itemId = composition.getId(); - // Check if this item has a noted variant - int linkedNoteId = composition.getLinkedNoteId(); - if (linkedNoteId == -1){ - log.debug("Item {} (ID: {}) has no noted variant, returning original ID", - composition.getName(), itemId); - return itemId; // No noted variant, return original ID - } - ItemComposition linkedComposition = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemComposition(linkedNoteId) - ).orElse(null); - - if (!linkedComposition.isStackable() && composition.isStackable()) { - log.debug("Found unnoted variant for {} (ID: {}) -> {} (ID: {})", - composition.getName(), itemId, - linkedComposition.getName(), linkedNoteId); - return linkedNoteId; // Use noted version - - } - if (linkedComposition.isStackable() && composition.isStackable()) { - log.debug("Item {} (ID: {}) has only a stackable variant", - composition.getName(), itemId, - linkedComposition.getName(), linkedNoteId); - - return linkedNoteId < itemId ? linkedNoteId : itemId; // Use noted version if it has lower ID - } - // If we reach here, item is not stackable and has no noted variant - log.debug("Item {} (ID: {}) has no non stackable variant", - composition.getName(), itemId); - return itemId; - - } catch (Exception e) { - log.error("Error getting unnoted item ID:{}", e.getMessage()); - return -1; // Fall back to original on error - } - } - private static int getLinkedItemId(ItemComposition composition) { - try { - if (composition == null) { - log.warn("no item composition for item ID, returning -1"); - return -1; - } - - - // Check if this item has a noted variant - return composition.getLinkedNoteId(); - - - } catch (Exception e) { - log.error("Error getting linked item ID: {}", e.getMessage()); - return -1; // Fall back to original on error - } - } - - - /** - * Checks if this item is stackable using the resolved item ID. - * - * @return true if this item is stackable, false otherwise - */ - public boolean isStackable() { - int itemId = getId(); - - try { - if (itemComposition == null) { - this.setItemComp(itemId); - } - return itemComposition !=null ? itemComposition.isStackable(): false; - } catch (Exception e) { - log.error("Error checking if item " + itemId + " is stackable: " + e.getMessage()); - return false; - } - } - - /** - * Gets the noted variant of an item ID if it exists and is stackable. - * Returns the original ID if the item is already stackable or has no noted variant. - * Uses lazy loading pattern for ItemComposition. - * - * @param itemId The original item ID - * @return The noted item ID if available and stackable, otherwise the original item ID - */ - public static int getNotedId(int itemId) { - ItemComposition composition = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemComposition(itemId) - ).orElse(null); - return getNotedItemId(composition); - } - public boolean isNoted() { - try { - if (itemComposition == null) { - this.setItemComp(getId()); - } - return itemComposition.getNote() == 799 && itemComposition.isStackable() && itemComposition.getLinkedNoteId() != -1; - } catch (Exception e) { - log.error("Error getting noted ID for item " + getId() + ": " + e.getMessage()); - return false; - } - } - public int getNotedId(){ - int itemId = getId(); - try { - if (itemComposition == null) { - this.setItemComp(itemId); - } - return getNotedItemId( itemComposition); - } catch (Exception e) { - log.error("Error getting noted ID for item " + itemId + ": " + e.getMessage()); - return itemId; // Fallback to original ID on error - } - } - public int getUnNotedId(){ - int itemId = getId(); - try { - if (itemComposition == null) { - this.setItemComp(itemId); - } - return getUnNotedId(itemComposition); - } catch (Exception e) { - log.error("Error getting noted ID for item " + itemId + ": " + e.getMessage()); - return itemId; // Fallback to original ID on error - } - } - public int getLinkedId(){ - int itemId = getId(); - try { - if (itemComposition == null) { - this.setItemComp(itemId); - } - return getLinkedItemId(itemComposition); - } catch (Exception e) { - log.error("Error getting linked ID for item " + itemId + ": " + e.getMessage()); - return itemId; // Fallback to original ID on error - } - } - public boolean isEquipment() { - int itemId = getId(); - try { - if (itemComposition == null) { - this.setItemComp(itemId); - } - - final ItemStats itemStats = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemStats(itemId) - ).orElse(null); - - if (itemStats == null || !itemStats.isEquipable()) { - return false; - } - final ItemEquipmentStats equipmentStats = itemStats.getEquipment(); - if (equipmentStats == null) { - return false; - } - return true; - - } catch (Exception e) { - log.error("Error checking if item " + itemId + " is equipped: " + e.getMessage()); - return false; - } - } - - /** - * Gets the number of inventory slots this requirement would occupy. - * For stackable items with any amount, this is 1 slot. - * For non-stackable items, this equals the amount required. - * - * @return the number of inventory slots needed - */ - public int getRequiredInventorySlots() { - if (requirementType == RequirementType.EQUIPMENT) { - return 0; // Equipment items don't occupy inventory slots when equipped - } - - if (isStackable()) { - return 1; // Stackable items only need one slot regardless of amount - } else { - return amount; // Non-stackable items need one slot per item - } - } - - /** - * Checks if this item requirement can be placed in inventory considering stackability and amount. - * Non-stackable items with amount > 1 need multiple slots. - * - * @return true if the item can be placed in inventory, false if not enough slots - */ - public boolean canFitInInventory() { - return canFitInInventory(Rs2Inventory.emptySlotCount()); - } - - /** - * Checks if this item requirement can fit in the given number of available slots. - * - * @param availableSlots The number of available inventory slots - * @return true if the item can fit, false otherwise - */ - public boolean canFitInInventory(int availableSlots) { - return getRequiredInventorySlots() <= availableSlots; - } - - /** - * Checks if this requirement meets all conditions (availability, skill requirements, etc.). - * - * @return true if all requirements are met, false otherwise - */ - public boolean meetsAllRequirements() { - return isAvailableInInventoryOrBank() && meetsSkillRequirements(); - } - - - // ========== STATIC UTILITY METHODS ========== - - /** - * Checks if an item is present in a specific inventory slot. - * - * @param slot The inventory slot to check (0-27) - * @param item The ItemRequirement to check for - * @return true if the item is in the specified slot - */ - public static boolean hasItemInSpecificSlot(int slot, ItemRequirement item) { - if (slot < 0 || slot > 27) { - log.warn("Invalid inventory slot: {}", slot); - return false; - } - - Rs2ItemModel slotItem = Rs2Inventory.getItemInSlot(slot); - if (slotItem == null) { - return false; - } - - // Check if the slot contains the item's ID - if (slotItem.getId() == item.getId()) { - log.debug("Found {} in slot {}", item.getName(), slot); - return true; - } - - return false; - } - - /** - * Withdraws an item and places it in a specific inventory slot. - * - * @param slot The target inventory slot (0-27) - * @param item The ItemRequirement to withdraw and place - * @return true if successful - */ - public static boolean withdrawAndPlaceInSpecificSlot(int slot, ItemRequirement item) { - if (slot < 0 || slot > 27) { - log.error("Invalid inventory slot: {}", slot); - return false; - } - - log.debug("Attempting to withdraw {} and place in slot {}", item.displayString(), slot); - - // Check if item is already in the correct slot - if (hasItemInSpecificSlot(slot, item)) { - log.debug("Item {} already in slot {}", item.getName(), slot); - return true; - } - - // Ensure slot is empty or contains the same item - Rs2ItemModel currentSlotItem = Rs2Inventory.getItemInSlot(slot); - if (currentSlotItem != null) { - if (currentSlotItem.getId() != item.getId()) { - log.warn("Slot {} contains different item: {}", slot, currentSlotItem.getName()); - return false; - } - } - - // Try to withdraw the item - boolean withdrawSuccess = false; - int itemId = item.getId(); - if (Rs2Bank.hasItem(itemId)) { - if (Rs2Bank.withdrawX(itemId, item.getAmount())) { - withdrawSuccess = true; - log.debug("Successfully withdrew {} (ID: {})", item.getName(), itemId); - } else { - log.warn("Failed to withdraw {} (ID: {})", item.getName(), itemId); - } - } - - if (!withdrawSuccess) { - log.error("Could not withdraw any variant of {}", item.getName()); - return false; - } - - // Wait for withdrawal to complete - sleepUntil(() -> Rs2Inventory.hasItemAmount(item.getId(), item.getAmount(), false), 3000); - - // Verify the item is now in inventory and in the correct slot if needed - if (hasItemInSpecificSlot(slot, item)) { - log.debug("Successfully placed {} in slot {}", item.getName(), slot); - return true; - } else { - log.warn("Item {} not found in expected slot {} after withdrawal", item.getName(), slot); - // Check if it's anywhere in inventory - if (Rs2Inventory.hasItem(item.getId())) { - log.debug("Item {} found in inventory but not in expected slot", item.getName()); - return true; // Close enough for now - } - return false; - } - } - - /** - * Checks if an item can be assigned to a specific slot based on its constraints. - * - * @param item The ItemRequirement to check - * @param slot The target slot number - * @return true if the item can be assigned to the slot - */ - public static boolean canAssignToSpecificSlot(ItemRequirement item, int slot) { - // For inventory slots (0-27), check if the item can fit in inventory - if (slot >= 0 && slot <= 27) { - return item.getRequirementType() != RequirementType.EQUIPMENT && - (item.getInventorySlot() == null || item.getInventorySlot() == -1 || item.getInventorySlot() == slot); - } - - // For equipment slots, check if the item can be equipped - return item.getRequirementType() != RequirementType.INVENTORY && - (item.getEquipmentSlot() != null && item.getEquipmentSlot().getSlotIdx() == slot); - } - - /** - * Validates that an item meets all suitability requirements for use. - * - * @param item The ItemRequirement to validate - * @return true if the item is suitable for use - */ - public static boolean validateItemSuitability(ItemRequirement item) { - log.debug("Validating suitability for item: {}", item.displayString()); - - // Check skill requirements for usage - if (item.getSkillToUse() != null && item.getMinimumLevelToUse() > 0) { - int currentLevel = Rs2Player.getRealSkillLevel(item.getSkillToUse()); - if (currentLevel < item.getMinimumLevelToUse()) { - log.warn("Insufficient {} level for {}: {} < {}", - item.getSkillToUse().getName(), item.getName(), - currentLevel, item.getMinimumLevelToUse()); - return false; - } - } - - // Check skill requirements for equipping - if (item.getSkillToEquip() != null && item.getMinimumLevelToEquip() > 0) { - int currentLevel = Rs2Player.getRealSkillLevel(item.getSkillToEquip()); - if (currentLevel < item.getMinimumLevelToEquip()) { - log.warn("Insufficient {} level to equip {}: {} < {}", - item.getSkillToEquip().getName(), item.getName(), - currentLevel, item.getMinimumLevelToEquip()); - return false; - } - } - - log.debug("Item {} passes all suitability checks", item.getName()); - return true; - } - - // Advanced fuzzy item utility methods - - /** - * Gets the total charges available for this item across all locations. - * Only applicable for charged items. - * - * @return total charges available, or 0 if not a charged item - */ - public int getTotalCharges() { - return Rs2FuzzyItem.getTotalCharges(getId()); - } - - /** - * Gets the count of charged items within a specific charge range in inventory. - * - * @param minCharges minimum charge level (inclusive) - * @param maxCharges maximum charge level (inclusive) - * @return count of charged items in the specified range - */ - public int getChargedInventoryCount(int minCharges, int maxCharges) { - return Rs2FuzzyItem.getChargedInventoryCount(getId(), true, minCharges, maxCharges); - } - public int getChargedInventoryQuantity(int minCharges, int maxCharges) { - return Rs2FuzzyItem.getChargedInventoryQuantity(getId(), true, minCharges, maxCharges); - } - - /** - * Gets the count of charged items within a specific charge range in bank. - * - * @param minCharges minimum charge level (inclusive) - * @param maxCharges maximum charge level (inclusive) - * @return count of charged items in the specified range - */ - public int getChargedBankCount(int minCharges, int maxCharges) { - return Rs2FuzzyItem.getChargedBankCount(getId(), true, minCharges, maxCharges); - } - - /** - * Gets all fuzzy item variations available in inventory with detailed information. - * - * @return list of FuzzyItemInfo objects sorted by charges (highest first) - */ - public List getAllFuzzyItemsInInventory() { - return Rs2FuzzyItem.getAllFuzzyItemsInInventory(getId(), true); - } - - /** - * Gets all fuzzy item variations available in bank with detailed information. - * - * @return list of FuzzyItemInfo objects sorted by charges (highest first) - */ - public List getAllFuzzyItemsInBank() { - return Rs2FuzzyItem.getAllFuzzyItemsInBank(getId(), true); - } - - /** - * Gets the best (highest charged) fuzzy item match in inventory. - * - * @return the best FuzzyItemInfo match, or null if none found - */ - public Rs2FuzzyItem.FuzzyItemInfo getBestFuzzyItemInInventory() { - return Rs2FuzzyItem.getBestFuzzyItemInInventory(getId(), true); - } - - /** - * Gets the best (highest charged) fuzzy item match in bank. - * - * @return the best FuzzyItemInfo match, or null if none found - */ - public Rs2FuzzyItem.FuzzyItemInfo getBestFuzzyItemInBank() { - return Rs2FuzzyItem.getBestFuzzyItemInBank(getId(), true); - } - - /** - * Gets information about the fuzzy item currently equipped. - * - * @return the equipped FuzzyItemInfo, or null if none found - */ - public Rs2FuzzyItem.FuzzyItemInfo getFuzzyItemEquipped() { - return Rs2FuzzyItem.getFuzzyItemEquipped(getId(), true); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/RunePouchRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/RunePouchRequirement.java deleted file mode 100644 index b762a6d3b06..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/RunePouchRequirement.java +++ /dev/null @@ -1,328 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item; - -import lombok.Getter; -import lombok.EqualsAndHashCode; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.inventory.Rs2RunePouch; -import net.runelite.client.plugins.microbot.util.inventory.RunePouchType; -import net.runelite.client.plugins.microbot.util.magic.Runes; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; - -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ScheduledExecutorService; -import java.util.stream.Collectors; - -/** - * Represents a rune pouch requirement that extends ItemRequirement to handle both - * the pouch item itself and its required rune contents. - * - * This requirement ensures: - * 1. The player has a rune pouch in their inventory - * 2. The rune pouch contains the specified runes with minimum quantities - * 3. The required runes are available (inventory + bank + already in pouch) - */ -@Getter -@EqualsAndHashCode(callSuper = true) -public class RunePouchRequirement extends ItemRequirement { - - /** - * Map of required runes and their minimum quantities. - */ - private final Map requiredRunes; - - /** - * Whether to allow combination runes to satisfy basic rune requirements. - * For example, dust runes can satisfy both air and earth rune requirements. - */ - private final boolean allowCombinationRunes; - - /** - * Helper method to check if player has any valid rune pouch in inventory. - * @return true if any rune pouch variant is found - */ - private boolean hasAnyRunePouch() { - for (RunePouchType pouchType : RunePouchType.values()) { - if (Rs2Inventory.hasItem(pouchType.getItemId())) { - return true; - } - } - return false; - } - - /** - * Full constructor for rune pouch requirement. - * - * @param requiredRunes Map of runes to their minimum required quantities - * @param allowCombinationRunes Whether combination runes can satisfy basic rune requirements - * @param priority Priority level for this requirement - * @param rating Rating/importance (1-10) - * @param description Human-readable description - * @param TaskContext When this requirement applies (PRE_SCHEDULE, POST_SCHEDULE, BOTH) - */ - public RunePouchRequirement( - Map requiredRunes, - boolean allowCombinationRunes, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext) { - - // Call parent constructor with first rune pouch ID - we'll handle multiple IDs in our override methods - super(RunePouchType.values()[0].getItemId(), // Use first pouch ID as primary - 1, // amount - we need exactly 1 rune pouch - -1, // inventorySlot - any available slot - priority, - rating, - description, - taskContext); - - this.requiredRunes = requiredRunes; - this.allowCombinationRunes = allowCombinationRunes; - } - - /** - * Simplified constructor with default settings. - * - * @param requiredRunes Map of runes to their minimum required quantities - * @param priority Priority level for this requirement - * @param rating Rating/importance (1-10) - * @param description Human-readable description - * @param TaskContext When this requirement applies - */ - public RunePouchRequirement( - Map requiredRunes, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext) { - this(requiredRunes, false, priority, rating, description, taskContext); - } - - @Override - public String getName() { - String runesDescription = requiredRunes.entrySet().stream() - .map(entry -> entry.getValue() + "x " + entry.getKey().name()) - .collect(Collectors.joining(", ")); - return "Rune Pouch (" + runesDescription + ")"; - } - - /** - * Checks if this rune pouch requirement is currently fulfilled. - * A requirement is fulfilled if: - * 1. Player has a rune pouch in inventory - * 2. The rune pouch contains all required runes with sufficient quantities - * - * @return true if requirement is fulfilled, false otherwise - */ - @Override - public boolean isFulfilled() { - // First check if we have any rune pouch - if (!hasAnyRunePouch()) { - return false; - } - - // Then check if the rune pouch has the required runes - return Rs2RunePouch.contains(requiredRunes, allowCombinationRunes); - } - - /** - * Attempts to fulfill this rune pouch requirement. - * - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if successfully fulfilled, false otherwise - */ - @Override - public boolean fulfillRequirement(CompletableFuture scheduledFuture) { - try { - // Check if we already have any rune pouch - if (!hasAnyRunePouch()) { - // Try to get a rune pouch using parent logic (will try to get the first type) - if (!super.fulfillRequirement(scheduledFuture)) { - Microbot.log("Failed to obtain rune pouch"); - return false; - } - } - - // Check if rune pouch already has required runes - if (Rs2RunePouch.contains(requiredRunes, allowCombinationRunes)) { - Microbot.log("Rune pouch already contains required runes"); - return true; - } - - // Check if we have all required runes available (inventory + bank + current pouch) - if (!areRequiredRunesAvailable()) { - Microbot.log("Required runes are not available"); - return false; - } - - // Load the required runes into the pouch - return loadRunesIntoPouch(); - - } catch (Exception e) { - Microbot.log("Error fulfilling rune pouch requirement: " + e.getMessage()); - return false; - } - } - - /** - * Checks if this rune pouch requirement meets all conditions. - * This includes having the pouch available AND having all required runes available. - * - * @return true if all requirements are met, false otherwise - */ - @Override - public boolean meetsAllRequirements() { - // First check if the pouch itself is available - if (!super.meetsAllRequirements()) { - return false; - } - - // Then check if all required runes are available - return areRequiredRunesAvailable(); - } - - /** - * Checks if all required runes are available across inventory, bank, and current pouch contents. - * - * @return true if all runes are available in sufficient quantities - */ - private boolean areRequiredRunesAvailable() { - for (Map.Entry entry : requiredRunes.entrySet()) { - Runes rune = entry.getKey(); - int required = entry.getValue(); - - // Count runes from all sources - int inInventory = Rs2Inventory.count(rune.getItemId()); - int inBank = Rs2Bank.count(rune.getItemId()); - int inPouch = Rs2RunePouch.getQuantity(rune); - - int totalAvailable = inInventory + inBank + inPouch; - - if (allowCombinationRunes) { - // Add combination runes that can provide this base rune - for (Runes combinationRune : Runes.values()) { - - // Convert base runes array to a list for easier contains check - List baseRunesList =Arrays.asList(combinationRune.getBaseRunes()); - if (baseRunesList.contains(rune)) { - totalAvailable += Rs2Inventory.count(combinationRune.getItemId()); - totalAvailable += Rs2Bank.count(combinationRune.getItemId()); - totalAvailable += Rs2RunePouch.getQuantity(combinationRune); - } - } - } - - if (totalAvailable < required) { - Microbot.log("Insufficient " + rune.name() + " runes: need " + required + ", have " + totalAvailable); - return false; - } - } - - return true; - } - - /** - * Loads the required runes into the rune pouch using Rs2RunePouch utility. - * - * @return true if runes were successfully loaded - */ - private boolean loadRunesIntoPouch() { - try { - // Ensure bank is open for rune pouch configuration - if (!Rs2Bank.isOpen()) { - if (!Rs2Bank.openBank()) { - Microbot.log("Failed to open bank for rune pouch configuration"); - return false; - } - } - - // Use Rs2RunePouch.load() method to configure the pouch - boolean success = Rs2RunePouch.load(requiredRunes); - - if (success) { - Microbot.log("Successfully loaded runes into pouch: " + formatRuneMap(requiredRunes)); - } else { - Microbot.log("Failed to load runes into pouch"); - } - - return success; - - } catch (Exception e) { - Microbot.log("Error loading runes into pouch: " + e.getMessage()); - return false; - } - } - - /** - * Formats a rune map for display purposes. - * - * @param runes Map of runes to quantities - * @return Formatted string representation - */ - private String formatRuneMap(Map runes) { - return runes.entrySet().stream() - .map(entry -> entry.getValue() + "x " + entry.getKey().name()) - .collect(Collectors.joining(", ")); - } - - /** - * Returns a detailed display string with rune pouch requirement information. - * - * @return Formatted string containing requirement details - */ - @Override - public String displayString() { - StringBuilder sb = new StringBuilder(); - sb.append("=== Rune Pouch Requirement Details ===\n"); - sb.append("Name:\t\t\t").append(getName()).append("\n"); - sb.append("Type:\t\t\t").append(getRequirementType().name()).append("\n"); - sb.append("Priority:\t\t").append(getPriority().name()).append("\n"); - sb.append("Rating:\t\t\t").append(getRating()).append("/10\n"); - sb.append("Schedule Context:\t").append(getTaskContext().name()).append("\n"); - sb.append("Allow Combination Runes:\t").append(allowCombinationRunes).append("\n"); - sb.append("Description:\t\t").append(getDescription() != null ? getDescription() : "No description").append("\n"); - - sb.append("\n--- Required Runes ---\n"); - for (Map.Entry entry : requiredRunes.entrySet()) { - Runes rune = entry.getKey(); - int quantity = entry.getValue(); - int available = Rs2RunePouch.getQuantity(rune) + - Rs2Inventory.count(rune.getItemId()) + - Rs2Bank.count(rune.getItemId()); - - sb.append(rune.name()).append(":\t\t") - .append("Required: ").append(quantity) - .append(", Available: ").append(available) - .append(available >= quantity ? " ✓" : " ✗") - .append("\n"); - } - - sb.append("\n--- Current Status ---\n"); - sb.append("Has Rune Pouch:\t\t").append(Rs2Inventory.hasRunePouch() ? "Yes" : "No").append("\n"); - sb.append("Runes Available:\t\t").append(areRequiredRunesAvailable() ? "Yes" : "No").append("\n"); - sb.append("Requirement Met:\t\t").append(isFulfilled() ? "Yes" : "No").append("\n"); - - return sb.toString(); - } - - /** - * Creates a unique identity string for this requirement. - * Used for debugging and logging purposes. - * - * @return Unique identity string - */ - public String getUniqueIdentity() { - String runesSignature = requiredRunes.entrySet().stream() - .map(entry -> entry.getKey().name() + ":" + entry.getValue()) - .sorted() - .collect(Collectors.joining("|")); - - return "RunePouch[" + runesSignature + "|combo:" + allowCombinationRunes + "]"; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/LocationOption.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/LocationOption.java deleted file mode 100644 index 5b351e73209..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/LocationOption.java +++ /dev/null @@ -1,148 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location; - -import java.util.HashMap; -import java.util.Map; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Quest; -import net.runelite.api.QuestState; -import net.runelite.api.Skill; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.inventory.Rs2RunePouch; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; - -/** - * Container class for location data with requirements. - */ -@Getter -@Slf4j -public class LocationOption { - private final WorldPoint worldPoint; - private final String name; - private final boolean membersOnly; // Indicates if this location is members-only - private final Map requiredQuests; - private final Map requiredSkills; - private final Map requiredVarbits; - private final Map requiredVarplayer; - private final Map requiredItems; //id key ,and amount value - - - - public LocationOption(WorldPoint worldPoint, String name, boolean membersOnly) { - this(worldPoint, name,membersOnly, new HashMap<>(), new HashMap<>(),new HashMap<>(),new HashMap<>(),new HashMap<>()); - } - - public LocationOption(WorldPoint worldPoint, String name, - boolean membersOnly, - Map requiredQuests, - Map requiredSkills, - Map requiredVarbits, - Map requiredVarplayer, - Map requiredItems - ) { - this.worldPoint = worldPoint; - this.name = name; - this.membersOnly = membersOnly; - this.requiredQuests = requiredQuests != null ? new HashMap<>(requiredQuests) : new HashMap<>(); - this.requiredSkills = requiredSkills != null ? new HashMap<>(requiredSkills) : new HashMap<>(); - this.requiredVarbits = requiredVarbits != null ? new HashMap<>(requiredVarbits) : new HashMap<>(); - this.requiredVarplayer = requiredVarplayer != null ? new HashMap<>(requiredVarplayer) : new HashMap<>(); - this.requiredItems = requiredItems != null ? new HashMap<>(requiredItems) : new HashMap<>(); - - } - public boolean canReach() { - return Rs2Walker.canReach(worldPoint); - } - /** - * Checks if the player meets all requirements for this location. - * Improved implementation using streams for better performance and readability. - */ - public boolean hasRequirements() { - if (Microbot.getClient() == null) { - log.debug("LocationRequirement hasRequirements called outside client thread"); - return false; - } - if(!Microbot.isLoggedIn()){ - log.debug("Player is not logged in, cannot check location requirements"); - return false; - } - // Check quest requirements using streams - boolean questRequirementsMet = requiredQuests.entrySet().stream() - .allMatch(questReq -> { - QuestState currentState = Rs2Player.getQuestState(questReq.getKey()); - QuestState requiredState = questReq.getValue(); - - // If required state is FINISHED, player must have finished - if (requiredState == QuestState.FINISHED) { - return currentState == QuestState.FINISHED; - } - // If required state is IN_PROGRESS, player must have started (IN_PROGRESS or FINISHED) - if (requiredState == QuestState.IN_PROGRESS) { - return currentState == QuestState.IN_PROGRESS || currentState == QuestState.FINISHED; - } - return true; - }); - - if (!questRequirementsMet) { - return false; - } - - // Check skill requirements using streams - boolean skillRequirementsMet = requiredSkills.entrySet().stream() - .allMatch(skillReq -> Rs2Player.getSkillRequirement(skillReq.getKey(), skillReq.getValue())); - - if (!skillRequirementsMet) { - return false; - } - - // Check varbit requirements using streams - boolean varbitRequirementsMet = requiredVarbits.entrySet().stream() - .allMatch(varbitReq -> Microbot.getVarbitValue(varbitReq.getKey()) == varbitReq.getValue()); - - if (!varbitRequirementsMet) { - return false; - } - - // Check varplayer requirements using streams - boolean varplayerRequirementsMet = requiredVarplayer.entrySet().stream() - .allMatch(varplayerReq -> Microbot.getVarbitPlayerValue(varplayerReq.getKey()) == varplayerReq.getValue()); - - if (!varplayerRequirementsMet) { - return false; - } - - // Check item requirements using streams - boolean itemRequirementsMet = requiredItems.entrySet().stream() - .allMatch(itemReq -> { - int itemId = itemReq.getKey(); - int requiredAmount = itemReq.getValue(); - - int numberOfItems = Rs2Inventory.count(itemId) + - (Rs2Equipment.isWearing(itemId) ? 1 : 0); //TODO we must check if we are checking for stackable items.. - int numberOfItemsInPouch = Rs2RunePouch.getQuantity(itemId); - int numberOfItemsInBank = Rs2Bank.count(itemId); - // todo check rune pouches ? when the ids runes.., - // bolt ammo slot ? when the ids is any ammo - - if (numberOfItems+numberOfItemsInPouch +numberOfItemsInBank< requiredAmount) { - log.warn("Missing required item: {} x{} (have {})", itemId, requiredAmount, numberOfItems); - Microbot.log("Missing required item: " + itemId + " x" + requiredAmount + " (have " + numberOfItems + ")"); - return false; - } - return true; - }); - - return itemRequirementsMet; - } - - @Override - public String toString() { - return name + " (" + worldPoint.getX() + ", " + worldPoint.getY() + ", " + worldPoint.getPlane() + ")"; - } - } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/LocationRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/LocationRequirement.java deleted file mode 100644 index 8254eb7e579..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/LocationRequirement.java +++ /dev/null @@ -1,759 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location; - -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import lombok.EqualsAndHashCode; -import net.runelite.api.Constants; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -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.shortestpath.ShortestPathPlugin; -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.util.player.Rs2Player; -import net.runelite.client.plugins.microbot.util.shop.Rs2Shop; -import net.runelite.client.plugins.microbot.util.walker.TransportRouteAnalysis; -import net.runelite.client.plugins.microbot.util.world.Rs2WorldUtil; -import net.runelite.client.plugins.microbot.util.world.WorldHoppingConfig; -import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; - -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.stream.Collectors; - -/** - * Represents a location requirement for pre and post schedule tasks. - * This requirement ensures the player is at a specific location before or after script execution. - * - * LocationRequirement integrates with Rs2Walker for intelligent pathfinding and travel, - * supporting both simple location checks and complex travel operations. - * - * Enhanced to support multiple target locations with quest and skill requirements. - */ -@Slf4j -@Getter -@EqualsAndHashCode(callSuper = true) -public class LocationRequirement extends Requirement { - - - - /** - * List of possible target locations for this requirement. - */ - private final List targetLocations; - - /** - * The acceptable distance from the target location to consider the requirement fulfilled. - * Default is 5 tiles for most operations. - */ - private final int acceptableDistance; - - /** - * Whether to use transport methods (teleports, boats, etc.) when traveling to this location. - * Default is true for efficient travel. - */ - private final boolean useTransports; - /** -1 indicate not a spefic world - * The world hop to the targeted game world if specified. - * - */ - private final int world; - /** - * Configuration for world hopping behavior with exponential backoff and retry limits. - */ - @Setter - private WorldHoppingConfig worldHoppingConfig = WorldHoppingConfig.createDefault(); - @Override - public String getName() { - LocationOption location = getBestAvailableLocation(); - String bestLocationString = location != null ? String.format("Location %s (%d, %d, %d)", location.getName(), - location.getWorldPoint().getX(), - location.getWorldPoint().getY(), - location.getWorldPoint().getPlane()) : "Unknown Location"; - - - if (targetLocations.size() == 1) { - - - - return String.format("\n\tSingle Location -> %s", bestLocationString); - } else { - - return String.format("Multi-Location (%d options), best location -> %s", targetLocations.size(), bestLocationString); - } - } - - /** - * Gets the best available location option based on current player requirements and position. - * Prioritizes accessible locations, then proximity to player. - */ - public LocationOption getBestAvailableLocation() { - WorldPoint playerLocation = Rs2Player.getWorldLocation(); - - // Filter to only accessible locations - List accessibleLocations = targetLocations.stream() - .filter(LocationOption::hasRequirements) - .collect(Collectors.toList()); - - if (accessibleLocations.isEmpty()) { - log.warn("No accessible locations found for requirement: {}", getName()); - // Return the first location even if not accessible (for error reporting) - return targetLocations.isEmpty() ? null : targetLocations.get(0); - } - - if (accessibleLocations.size() == 1) { - return accessibleLocations.get(0); - } - - // If player location is available, find closest accessible location - if (playerLocation != null) { - return accessibleLocations.stream() - .min((loc1, loc2) -> Integer.compare( - playerLocation.distanceTo(loc1.getWorldPoint()), - playerLocation.distanceTo(loc2.getWorldPoint()) - )) - .orElse(accessibleLocations.get(0)); - } - - // Fall back to first accessible location - return accessibleLocations.get(0); - } - - /** - * Gets the best available location option based on a reference point. - * Prioritizes accessible locations, then proximity to reference point. - */ - public LocationOption getBestAvailableLocation(WorldPoint referencePoint) { - // Filter to only accessible locations - List accessibleLocations = targetLocations.stream() - .filter(LocationOption::hasRequirements) - .collect(Collectors.toList()); - - if (accessibleLocations.isEmpty()) { - log.warn("No accessible locations found for requirement: {}", getName()); - return targetLocations.isEmpty() ? null : targetLocations.get(0); - } - - if (accessibleLocations.size() == 1) { - return accessibleLocations.get(0); - } - - // Find closest accessible location to reference point - return accessibleLocations.stream() - .min((loc1, loc2) -> Integer.compare( - referencePoint.distanceTo(loc1.getWorldPoint()), - referencePoint.distanceTo(loc2.getWorldPoint()) - )) - .orElse(accessibleLocations.get(0)); - } - - /** - * Full constructor for LocationRequirement with multiple locations. - * - * @param targetLocations List of possible target locations with requirements - * @param acceptableDistance Distance tolerance for considering requirement fulfilled - * @param useTransports Whether to use teleports and other transport methods - * @param locationName Custom name for this location requirement - * @param TaskContext When this requirement should be fulfilled - * @param priority Priority level of this requirement - * @param rating Effectiveness rating (1-10) - * @param description Human-readable description - */ - public LocationRequirement( - List targetLocations, - int acceptableDistance, - boolean useTransports, - int world, - TaskContext taskContext, - RequirementPriority priority, - int rating, - String description) { - - super(RequirementType.PLAYER_STATE, - priority, - rating, - description != null ? description : generateDefaultDescription(targetLocations, taskContext), - Collections.emptyList(), // Location requirements don't use item IDs - taskContext); - - this.targetLocations = new ArrayList<>(targetLocations); - this.acceptableDistance = acceptableDistance; - this.useTransports = useTransports; - this.world = world; // Default to no specific world - } - - /** - * Constructor for single location requirement (backwards compatibility). - * - * @param targetLocation The world point to travel to - * @param acceptableDistance Distance tolerance for considering requirement fulfilled - * @param useTransports Whether to use teleports and other transport methods - * @param locationName Custom name for this location - * @param TaskContext When this requirement should be fulfilled - * @param priority Priority level of this requirement - * @param rating Effectiveness rating (1-10) - * @param description Human-readable description - */ - public LocationRequirement( - WorldPoint targetLocation, - String locationName, - boolean membersOnly, - int acceptableDistance, - boolean useTransports, - int world, - TaskContext taskContext, - RequirementPriority priority, - int rating, - String description) { - this(Arrays.asList(new LocationOption(targetLocation,locationName, membersOnly)), // Default to non-members for single location - acceptableDistance, useTransports, world,taskContext, priority, rating, description); - } - - /** - * Simplified constructor with common defaults. - * - * @param targetLocation The world point to travel to - * @param useTransports Whether to use teleports and other transport methods - * @param locationName Custom name for this location - * @param TaskContext When this requirement should be fulfilled - * @param priority Priority level of this requirement - */ - public LocationRequirement( - WorldPoint targetLocation, - String locationName, - boolean membersOnly, - int acceptableDistance, - boolean useTransports, - int world, - TaskContext taskContext, - RequirementPriority priority) { - this(targetLocation,locationName, membersOnly,acceptableDistance, useTransports ,world, taskContext, priority, 8, null); - } - - /** - * Basic constructor for mandatory location requirements. - * - * @param targetLocation The world point to travel to - * @param useTransports Whether to use teleports and other transport methods - * @param locationName Custom name for this location - * @param TaskContext When this requirement should be fulfilled - */ - public LocationRequirement( - WorldPoint targetLocation, - String locationName, - boolean membersOnly, - int acceptableDistance, - boolean useTransports, - int world, - TaskContext taskContext) { - this(targetLocation, locationName,membersOnly,acceptableDistance,useTransports, world,taskContext, RequirementPriority.MANDATORY); - } - - /** - * Constructor for bank locations using existing bank infrastructure. - * - * @param bankLocation The bank location to use as target - * @param acceptableDistance Distance tolerance for considering requirement fulfilled - * @param useTransports Whether to use teleports and other transport methods - * @param TaskContext When this requirement should be fulfilled - * @param priority Priority level of this requirement - */ - public LocationRequirement( - BankLocation bankLocation, - int acceptableDistance, - boolean useTransports, - int world, - TaskContext taskContext, - RequirementPriority priority) { - this(Arrays.asList(new LocationOption(bankLocation.getWorldPoint(), - bankLocation.toString(), - bankLocation.isMembers())), - acceptableDistance, useTransports, world,taskContext, priority, 5, bankLocation.getClass().getSimpleName() + " Bank Location Requirement"); - } - public LocationRequirement( - BankLocation bankLocation, - boolean useTransports, - int world, - TaskContext taskContext, - RequirementPriority priority) { - this(bankLocation, 15, useTransports, world,taskContext, priority); - } - /** - * Checks if the player is currently at any of the required locations. - * - * @return true if the player is within acceptable distance of any valid target location - */ - public boolean isAtRequiredLocation() { - WorldPoint currentLocation = Rs2Player.getWorldLocation(); - if (currentLocation == null) { - return false; - } - - if (world != -1 && isWorldHopRequired()){ - log.error("Player is not in the required world: {} (current: {})", world, Rs2Player.getWorld()); - return false; // Player is not in the required world - } - // Check if player is at any accessible location - for (LocationOption location : targetLocations) { - if (location.hasRequirements() && - currentLocation.distanceTo(location.getWorldPoint()) <= acceptableDistance) { - log.debug("Player is at required location: {} (distance: {})", location.getName(), currentLocation.distanceTo(location.getWorldPoint())); - return true; - } - } - List distanceIntegers = targetLocations.stream() - .map(loc -> loc.getWorldPoint().distanceTo(currentLocation)) - .collect(Collectors.toList()); - log.debug("Player is not at any required location.\n\tCurrent location: {}\n\tRequired locations: {}\n\tdistances to each location: {}", currentLocation, targetLocations, distanceIntegers); - - return false; - } - - /** - * Checks if at least one required location is reachable using available methods. - * This method uses Rs2Walker to determine reachability without actually traveling. - * - * @return true if at least one location can be reached, false otherwise - */ - public boolean isLocationReachable() { - // Check if any accessible location is reachable - for (LocationOption location : targetLocations) { - if (location.hasRequirements()) { - try { - if (Rs2Walker.canReach(location.getWorldPoint(), true)) { - return true; - } - } catch (Exception e) { - log.warn("Error checking reachability for location {}: {}", location, e.getMessage()); - } - } - } - - // If no accessible locations are reachable, check if any location is reachable (for non-mandatory requirements) - if (!isMandatory()) { - for (LocationOption location : targetLocations) { - try { - if (Rs2Walker.canReach(location.getWorldPoint(), true)) { - return true; - } - } catch (Exception e) { - log.warn("Error checking reachability for location {}: {}", location, e.getMessage()); - } - } - } - - return false; - } - @Override - public boolean isFulfilled() { - // Check if the player is at any of the required locations - return isAtRequiredLocation(); - } - - /** - * Implements the abstract fulfillRequirement method from the base Requirement class. - * Attempts to fulfill this location requirement by traveling to the target location. - * - * @param executorService The ScheduledExecutorService on which this requirement fulfillment is running - * @return true if the requirement was successfully fulfilled, false otherwise - */ - @Override - public boolean fulfillRequirement(CompletableFuture scheduledFuture) { - boolean currentUseWithBankedItems = Microbot.getConfigManager().getConfiguration(ShortestPathPlugin.CONFIG_GROUP, "walkWithBankedTransports", Boolean.class); - Microbot.getConfigManager().setConfiguration(ShortestPathPlugin.CONFIG_GROUP, "walkWithBankedTransports", useTransports); - try { - if (Microbot.getClient() == null || Microbot.getClient().isClientThread()) { - log.error("Cannot fulfill location requirement outside client thread"); - return false; // Cannot fulfill outside client thread - } - - - log.info("Attempting to fulfill location requirement: {}", getName()); - // Check if the requirement is already fulfilled - if (isAtRequiredLocation()) { - return true; - } - - - log.info("Checking if location is reachable for requirement: {}", getName()); - // Check if the location is reachable - if (!isLocationReachable()) { - if (isMandatory()) { - log.error("MANDATORY location requirement cannot be fulfilled: " + getName() + " - Location not reachable"); - return false; - } else { - log.error("OPTIONAL/RECOMMENDED location requirement skipped: " + getName() + " - Location not reachable"); - return true; // Non-mandatory requirements return true if location isn't reachable - } - } - log.info("Location is reachable, proceeding to travel for requirement: {}", getName()); - - - // Attempt to travel to the location - boolean success = travelToLocation(scheduledFuture); - - if (!success && isMandatory()) { - log.error("MANDATORY location requirement failed: " + getName()); - return false; - } - - return true; - - } catch (Exception e) { - log.error("Error fulfilling location requirement " + getName() + ": " + e.getMessage()); - return !isMandatory(); // Don't fail mandatory requirements due to exceptions - }finally { - // Restore the original setting for banked transports - Microbot.getConfigManager().setConfiguration(ShortestPathPlugin.CONFIG_GROUP, "walkWithBankedTransports", currentUseWithBankedItems); - } - } - - /** - * Attempts to travel to the best available target location using Rs2Walker with movement watchdog. - * Creates its own executor service for the watchdog that gets cleaned up when done. - * - * @param scheduledFuture The CompletableFuture to monitor for cancellation - * @return true if the travel was successful, false otherwise - */ - private boolean travelToLocation(CompletableFuture scheduledFuture) { - ScheduledExecutorService travelExecutorService = null; - ScheduledFuture watchdogFuture = null; - final int MAX_RETRIES = 3; // Maximum retries for walking to the location - AtomicBoolean watchdogTriggered = new AtomicBoolean(false); - if (world !=-1 && !Rs2WorldUtil.canAccessWorld(world)){ - log.warn("Cannot access world {} for requirement: {}", world, getName()); - return false; // Cannot proceed if world is not accessible - } - if( world != -1 && isWorldHopRequired()){ - boolean successWorldHop = Rs2WorldUtil.hopWorld(scheduledFuture, world, 1, worldHoppingConfig); - if (!successWorldHop) { - log.warn("World hop failed for requirement: {}", getName()); - return false; // World hop failed, cannot proceed - } - } - try { - LocationOption bestLocation = getBestAvailableLocation(); - if (bestLocation == null) { - log.warn("No available location found for requirement: {}", getName()); - return false; - } - - WorldPoint targetLocation = bestLocation.getWorldPoint(); - - // Create a dedicated executor service for this travel operation - travelExecutorService = Executors.newSingleThreadScheduledExecutor(r -> { - Thread thread = new Thread(r, "LocationRequirement-Travel-" + getName()); - thread.setDaemon(true); // Daemon thread so it doesn't prevent JVM shutdown - return thread; - }); - - // Start movement watchdog with our own executor service - watchdogFuture = startMovementWatchdog(travelExecutorService, scheduledFuture, watchdogTriggered, getName()); - if (watchdogFuture != null && !watchdogFuture.isDone()) { - log.debug("Movement watchdog started for location: {}", getName()); - - - } - - // Check if we need to get transport items from bank - boolean walkResult = false; - - for (int retries = 0; retries < MAX_RETRIES; retries++) { - if (walkResult==true || (scheduledFuture != null && scheduledFuture.isDone()) || watchdogTriggered.get()){ - break; // Exit loop if we successfully walked or if the future is cancelled - } - Rs2Walker.setTarget(null); // Reset target in Rs2Walker - if (useTransports) { - // This would be enhanced to check for specific transport items - // For now, just ensure we have access to basic travel - walkResult = Rs2Walker.walkWithBankedTransports(targetLocation,acceptableDistance,false); - } else { - // Use Rs2Walker to travel to the location - walkResult = Rs2Walker.walkTo(targetLocation,acceptableDistance); - } - } - - if(isAtRequiredLocation()) { - log.debug("\nSuccessfully reached required location: {}", getName()); - return true; // Already at the required location - } - - if (walkResult && !watchdogTriggered.get()) { - sleepUntil(()-> !Rs2Player.isMoving() , 5000); - return isAtRequiredLocation() && !watchdogTriggered.get(); - } - - return false; - - } catch (Exception e) { - log.error("Error traveling to location " + getName() + ": " + e.getMessage()); - return false; - } finally { - // Always clean up the watchdog first - if (watchdogFuture != null && !watchdogFuture.isDone()) { - watchdogFuture.cancel(true); - } - - // Then shutdown the executor service - if (travelExecutorService != null) { - travelExecutorService.shutdown(); - try { - // Wait a bit for graceful shutdown - if (!travelExecutorService.awaitTermination(2, TimeUnit.SECONDS)) { - // Force shutdown if tasks don't complete quickly - travelExecutorService.shutdownNow(); - if (!travelExecutorService.awaitTermination(1, TimeUnit.SECONDS)) { - log.warn("Travel executor service did not terminate cleanly for {}", getName()); - } - } - } catch (InterruptedException e) { - // Restore interrupted status and force shutdown - Thread.currentThread().interrupt(); - travelExecutorService.shutdownNow(); - } - } - } - } - - /** - * Gets the estimated travel time to the best available location in game ticks. - * This is a rough estimate based on distance and available transport methods. - * - * @return Estimated travel time in game ticks, or -1 if cannot estimate - */ - public int getEstimatedTravelTime() { - WorldPoint currentLocation = Rs2Player.getWorldLocation(); - if (currentLocation == null) { - return -1; - } - - LocationOption bestLocation = getBestAvailableLocation(); - if (bestLocation == null) { - return -1; - } - - WorldPoint targetLocation = bestLocation.getWorldPoint(); - TransportRouteAnalysis result = Rs2Walker.compareRoutes(targetLocation); // Ensure Rs2Walker has the latest pathfinding data - - int distance = Integer.MAX_VALUE; // Default to a large value if no path found - if (result != null && (result.isDirectIsFaster() || !useTransports)) { - distance = result.getDirectDistance(); // Use direct distance if it's faster - } else if (result != null && useTransports) { - distance = result.getBankingRouteDistance(); // Otherwise, use banking route distance - } - - // Walking only, slower travel - return (distance / 2); // when running we can move 2 tiles per game tick, so 2 tiles per Constants.GAME_TICK - } - private boolean isWorldHopRequired() { - // If world is -1, no specific world hop is required - if (world == -1) { - return false; - } - - // Check if the current world matches the target world - return Rs2Player.getWorld() != world; - } - - - /** - * Returns a detailed display string with location requirement information. - * - * @return A formatted string containing location requirement details - */ - @Override - public String displayString() { - StringBuilder sb = new StringBuilder(); - sb.append("=== Location Requirement Details ===\n"); - sb.append("Name:\t\t\t").append(getName()).append("\n"); - sb.append("Type:\t\t\t").append(getRequirementType().name()).append("\n"); - sb.append("Priority:\t\t").append(getPriority().name()).append("\n"); - sb.append("Rating:\t\t\t").append(getRating()).append("/10\n"); - sb.append("Schedule Context:\t").append(taskContext.name()).append("\n"); - sb.append("Acceptable Distance:\t").append(acceptableDistance).append(" tiles\n"); - sb.append("Use Transports:\t\t").append(useTransports ? "Yes" : "No").append("\n"); - sb.append("World:\t\t\t").append(world != -1 ? world : "Any world").append("\n"); - sb.append("canAccessWorld:\t").append(Rs2WorldUtil.canAccessWorld(world) ? "Yes" : "No").append("\n"); - sb.append("Description:\t\t").append(getDescription() != null ? getDescription() : "No description").append("\n"); - - // Add location details - sb.append("\n--- Available Locations (").append(targetLocations.size()).append(") ---\n"); - for (int i = 0; i < targetLocations.size(); i++) { - LocationOption location = targetLocations.get(i); - sb.append("Location ").append(i + 1).append(":\t\t").append(location.getName()).append("\n"); - sb.append(" Coordinates:\t\t").append(String.format("(%d, %d, %d)", - location.getWorldPoint().getX(), - location.getWorldPoint().getY(), - location.getWorldPoint().getPlane())).append("\n"); - sb.append(" Accessible:\t\t").append(location.hasRequirements() ? "Yes" : "No").append("\n"); - - if (!location.getRequiredQuests().isEmpty()) { - sb.append(" Required Quests:\t"); - location.getRequiredQuests().entrySet().stream() - .forEach(entry -> sb.append(entry.getKey().name()).append(" (").append(entry.getValue().name()).append(") ")); - sb.append("\n"); - } - - if (!location.getRequiredSkills().isEmpty()) { - sb.append(" Required Skills:\t"); - location.getRequiredSkills().entrySet().stream() - .forEach(entry -> sb.append(entry.getKey().name()).append(" ").append(entry.getValue()).append(" ")); - sb.append("\n"); - } - } - - // Add current status - sb.append("\n--- Current Status ---\n"); - sb.append("Currently at Location:\t").append(isAtRequiredLocation() ? "Yes" : "No").append("\n"); - sb.append("Location Reachable:\t").append(isLocationReachable() ? "Yes" : "No").append("\n"); - sb.append("Estimated Travel Time:\t").append(getEstimatedTravelTime()).append(" seconds\n"); - - LocationOption bestLocation = getBestAvailableLocation(); - if (bestLocation != null) { - sb.append("Best Available Location:\t").append(bestLocation.toString()).append("\n"); - } - - return sb.toString(); - } - /** - * Generates a default description based on the target locations and context. - * - * @param locations The target locations - * @param context When the requirement should be fulfilled - * @return A descriptive string explaining the requirement - */ - public static String generateDefaultDescription(List locations, TaskContext context) { - if (locations == null || locations.isEmpty()) { - return "Unknown Location Requirement"; - } - - String contextPrefix = (context == TaskContext.PRE_SCHEDULE) ? "Pre-task" : "Post-task"; - - if (locations.size() == 1) { - LocationOption location = locations.get(0); - WorldPoint wp = location.getWorldPoint(); - String bestLocationString = location.getName() != null ? - location.getName() : - String.format("(%d, %d, %d)", wp.getX(), wp.getY(), wp.getPlane()); - - return String.format("%s location requirement: %s", contextPrefix, bestLocationString); - } else { - // Multiple locations - show best available - LocationOption bestLocation = locations.get(0); // First available location - WorldPoint wp = bestLocation.getWorldPoint(); - String bestLocationString = bestLocation.getName() != null ? - bestLocation.getName() : - String.format("(%d, %d, %d)", wp.getX(), wp.getY(), wp.getPlane()); - - return String.format("%s multi-location requirement (%d options), primary: %s", - contextPrefix, locations.size(), bestLocationString); - } - } - - /** - * Checks if the player has moved out of a defined area around the last position. - * This is more robust than checking single coordinates as it accounts for small movements. - * - * @param lastPosition The last recorded position - * @param currentPosition The current position - * @param areaRadius The radius of the area to check - * @return true if the player has moved significantly outside the area - */ - public static boolean hasMovedOutOfArea(WorldPoint lastPosition, WorldPoint currentPosition, int areaRadius) { - if (lastPosition == null || currentPosition == null) { - return false; - } - - // Calculate distance between positions - int distance = lastPosition.distanceTo(currentPosition); - return distance > areaRadius; - } - - /** - * Starts a movement watchdog that monitors player position and stops walking if no movement is detected. - * - * @param executorService The executor service to run the watchdog on - * @param scheduledFuture The future to monitor for cancellation - * @param watchdogTriggered Atomic boolean to signal when watchdog triggers - * @param taskName Name of the task for logging purposes - * @return The scheduled future for the watchdog task - */ - public static ScheduledFuture startMovementWatchdog(ScheduledExecutorService executorService, - CompletableFuture scheduledFuture, - AtomicBoolean watchdogTriggered, - String taskName) { - AtomicReference lastPosition = new AtomicReference<>(Rs2Player.getWorldLocation()); - AtomicReference lastMovementTime = new AtomicReference<>(System.currentTimeMillis()); - long watchdogCheckInterval_ms = Constants.GAME_TICK_LENGTH; // Check every game tick - return executorService.scheduleAtFixedRate(() -> { - try { - // Check for cancellation first - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.info("Movement watchdog cancelled for: {}", taskName); - watchdogTriggered.set(true); - Rs2Walker.setTarget(null); - throw new RuntimeException("Watchdog cancelled - stopping task"); - } - - WorldPoint currentPosition = Rs2Player.getWorldLocation(); - if (currentPosition == null) { - watchdogTriggered.set(true); - // Stop walking by clearing the target - Rs2Walker.setTarget(null); - throw new RuntimeException("Watchdog cancelled - stopping task"); - } - - WorldPoint lastPos = lastPosition.get(); - if (lastPos == null) { - lastPosition.set(currentPosition); - lastMovementTime.set(System.currentTimeMillis()); - return; - } - log.debug("Current position: {}, Last position: {}", currentPosition, lastPos); - // Check if player has moved significantly (using area detection for robustness) - boolean hasMovedSignificantly = hasMovedOutOfArea(lastPos, currentPosition, 2); - - if (hasMovedSignificantly || Rs2Bank.isOpen() || Rs2Shop.isOpen()) { - // Player has moved, update last movement time and position - lastPosition.set(currentPosition); - lastMovementTime.set(System.currentTimeMillis()); - } else { - // Player hasn't moved significantly, check timeout - long timeSinceLastMovement = System.currentTimeMillis() - lastMovementTime.get(); - log.debug ("Time since last movement: {} ms", timeSinceLastMovement); - if (timeSinceLastMovement > watchdogCheckInterval_ms*10) { // more than 5 times the check interval - log.warn("Movement watchdog triggered - no significant movement detected for 1 minute"); - watchdogTriggered.set(true); - - // Stop walking by clearing the target - Rs2Walker.setTarget(null); - - // Cancel this watchdog - throw new RuntimeException("Watchdog triggered - stopping task"); - } - } - } catch (Exception e) { - log.warn("Watchdog error: {}", e.getMessage()); - watchdogTriggered.set(true); - Rs2Walker.setTarget(null); - throw e; // Re-throw to stop the scheduled task - } - }, 0, watchdogCheckInterval_ms, TimeUnit.MILLISECONDS); // Check every ms - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/ResourceLocationOption.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/ResourceLocationOption.java deleted file mode 100644 index 6b549ce92bd..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/ResourceLocationOption.java +++ /dev/null @@ -1,145 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Quest; -import net.runelite.api.QuestState; -import net.runelite.api.Skill; -import net.runelite.api.coords.WorldPoint; - -import java.util.Map; - -/** - * Extended LocationOption with resource tracking capabilities. - * This class is specifically designed for resource-based locations like mining rocks, - * fishing spots, or woodcutting trees where the number of available resources matters. - * - * The numberOfResources field allows for intelligent location selection based on - * resource availability, helping to choose locations with sufficient resources - * for efficient skilling activities. - */ -@Getter -@Slf4j -public class ResourceLocationOption extends LocationOption { - - /** - * The number of available resources at this location. - * For example: - * - Mining: Number of rock spawns - * - Fishing: Number of fishing spots - * - Woodcutting: Number of tree spawns - */ - private final int numberOfResources; - - /** - * Constructor with resource count and members-only flag. - * - * @param worldPoint The world coordinates of this location - * @param name The display name of this location - * @param membersOnly Whether this location requires membership - * @param numberOfResources The number of resources available at this location - */ - public ResourceLocationOption(WorldPoint worldPoint, String name, boolean membersOnly, int numberOfResources) { - super(worldPoint, name, membersOnly); - this.numberOfResources = numberOfResources; - } - - /** - * Full constructor with all requirements and resource count. - * - * @param worldPoint The world coordinates of this location - * @param name The display name of this location - * @param membersOnly Whether this location requires membership - * @param numberOfResources The number of resources available at this location - * @param requiredQuests Quest requirements for accessing this location - * @param requiredSkills Skill level requirements for accessing this location - * @param requiredVarbits Varbit requirements for accessing this location - * @param requiredVarplayer Varplayer requirements for accessing this location - * @param requiredItems Item requirements for accessing this location - */ - public ResourceLocationOption(WorldPoint worldPoint, String name, - boolean membersOnly, - int numberOfResources, - Map requiredQuests, - Map requiredSkills, - Map requiredVarbits, - Map requiredVarplayer, - Map requiredItems) { - super(worldPoint, name, membersOnly, requiredQuests, requiredSkills, - requiredVarbits, requiredVarplayer, requiredItems); - this.numberOfResources = numberOfResources; - } - - /** - * Checks if this location has the minimum required number of resources. - * - * @param minResources The minimum number of resources required - * @return true if this location has enough resources, false otherwise - */ - public boolean hasMinimumResources(int minResources) { - return numberOfResources >= minResources; - } - - /** - * Calculates a resource efficiency score based on the number of resources - * and distance from a reference point. - * Higher scores indicate better locations. - * - * @param referencePoint The point to calculate distance from - * @return Efficiency score (higher is better) - */ - public double calculateResourceEfficiencyScore(WorldPoint referencePoint) { - if (referencePoint == null) { - return numberOfResources; // Just return resource count if no reference point - } - - double distance = Math.sqrt( - Math.pow(getWorldPoint().getX() - referencePoint.getX(), 2) + - Math.pow(getWorldPoint().getY() - referencePoint.getY(), 2) - ); - - // Avoid division by zero and give closer locations higher scores - // Formula: resources * (100 / (distance + 1)) - // This gives more weight to resource count while factoring in distance - return numberOfResources * (100.0 / (distance + 1)); - } - - /** - * Determines if this location is better than another based on resource count and requirements. - * Prioritizes accessible locations first, then resource count, then proximity. - * - * @param other The other location to compare against - * @param referencePoint Optional reference point for distance comparison - * @return true if this location is better than the other - */ - public boolean isBetterThan(ResourceLocationOption other, WorldPoint referencePoint) { - if (other == null) return true; - - // First priority: accessibility (meeting requirements) - boolean thisAccessible = this.hasRequirements(); - boolean otherAccessible = other.hasRequirements(); - - if (thisAccessible && !otherAccessible) return true; - if (!thisAccessible && otherAccessible) return false; - - // Second priority: resource count (more resources = better) - if (this.numberOfResources != other.numberOfResources) { - return this.numberOfResources > other.numberOfResources; - } - - // Third priority: efficiency score (considers distance) - if (referencePoint != null) { - return this.calculateResourceEfficiencyScore(referencePoint) > - other.calculateResourceEfficiencyScore(referencePoint); - } - - // Fallback: prefer this location if all else is equal - return true; - } - - @Override - public String toString() { - return getName() + " (" + getWorldPoint().getX() + ", " + getWorldPoint().getY() + - ", " + getWorldPoint().getPlane() + ") - Resources: " + numberOfResources; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/logical/LogicalRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/logical/LogicalRequirement.java deleted file mode 100644 index 082ba45ff1c..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/logical/LogicalRequirement.java +++ /dev/null @@ -1,879 +0,0 @@ - -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.collection.LootRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.ShopRequirement; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; - -/** - * Abstract base class for logical combinations of requirements. - * Provides common functionality for AND and OR requirement combinations. - * - * Logical requirements enforce type homogeneity - all child requirements must be of the same type. - * This prevents mixing different requirement types (e.g., SpellbookRequirement with ItemRequirement) - * which would make caching and optimization difficult. Complex mixed requirements should use - * ConditionalRequirement instead. - * - * Similar to LogicalCondition but adapted for the requirement system. - * Logical requirements can contain other requirements (including other logical requirements) - * and evaluate them according to logical rules. - * - * @see net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LogicalCondition - */ -@Slf4j -@EqualsAndHashCode(callSuper = true) -public abstract class LogicalRequirement extends Requirement { - - @Getter - protected final List childRequirements = new ArrayList<>(); - - /** - * The allowed requirement type for child requirements in this logical group. - * All child requirements must be of this type or be LogicalRequirements that also - * enforce the same child type. This enables efficient caching and type-safe operations. - */ - @Getter - protected final Class allowedChildType; - - /** - * Protected constructor for logical requirements. - * Child classes must call this constructor and provide their own requirement type. - * - * @param requirementType The type of logical requirement (AND_LOGICAL or OR_LOGICAL) - * @param priority Priority level of this logical requirement - * @param rating Effectiveness rating (1-10) - * @param description Human-readable description - * @param TaskContext When this requirement should be fulfilled - * @param allowedChildType The class type that child requirements must be (or null to infer from first requirement) - * @param requirements Child requirements to combine logically - */ - protected LogicalRequirement(RequirementType requirementType, RequirementPriority priority, int rating, - String description, TaskContext taskContext, - Class allowedChildType, - Requirement... requirements) { - super(requirementType, priority, rating, description, List.of(), taskContext); - - // Determine allowed child type - if (allowedChildType != null) { - this.allowedChildType = allowedChildType; - } else if (requirements.length > 0) { - // Infer from first requirement - Requirement firstReq = requirements[0]; - if (firstReq instanceof LogicalRequirement) { - this.allowedChildType = ((LogicalRequirement) firstReq).getAllowedChildType(); - } else { - this.allowedChildType = firstReq.getClass(); - } - } else { - // Default to Requirement if no children and no explicit type - this.allowedChildType = Requirement.class; - } - - // Validate and add requirements - for (Requirement requirement : requirements) { - addRequirement(requirement); - } - } - - protected LogicalRequirement(RequirementType requirementType, RequirementPriority priority, int rating, - String description, TaskContext taskContext, - Class allowedChildType) { - super(requirementType, priority, rating, description, List.of(), taskContext); - - // Determine allowed child type - if (allowedChildType != null) { - this.allowedChildType = allowedChildType; - } else { - // Default to Requirement if no children and no explicit type - this.allowedChildType = Requirement.class; - } - - } - - /** - * Validates that a requirement is compatible with this logical requirement's type constraints. - * - * @param requirement The requirement to validate - * @throws IllegalArgumentException if the requirement is not compatible - */ - private void validateRequirement(Requirement requirement) { - if (requirement == null) { - throw new IllegalArgumentException("Child requirement cannot be null"); - } - - // Check if it's a LogicalRequirement with compatible child type - if (requirement instanceof LogicalRequirement) { - LogicalRequirement logicalReq = (LogicalRequirement) requirement; - if (!allowedChildType.isAssignableFrom(logicalReq.getAllowedChildType()) && - !logicalReq.getAllowedChildType().isAssignableFrom(allowedChildType)) { - throw new IllegalArgumentException(String.format( - "Logical requirement child type %s is not compatible with required type %s", - logicalReq.getAllowedChildType().getSimpleName(), - allowedChildType.getSimpleName())); - } - } else { - // Check if it's assignable to our allowed type - if (!allowedChildType.isAssignableFrom(requirement.getClass())) { - throw new IllegalArgumentException(String.format( - "Requirement type %s is not compatible with required type %s", - requirement.getClass().getSimpleName(), - allowedChildType.getSimpleName())); - } - } - - // Validate schedule context compatibility - if (this.getTaskContext() != null && requirement.getTaskContext() != null) { - if (this.getTaskContext() != requirement.getTaskContext() && - this.getTaskContext() != TaskContext.BOTH && - requirement.getTaskContext() != TaskContext.BOTH) { - throw new IllegalArgumentException(String.format( - "Schedule context mismatch: logical requirement has %s but child has %s", - this.getTaskContext(), requirement.getTaskContext())); - } - } - } - - - - /** - * Adds a child requirement to this logical requirement with type validation. - * - * @param requirement The requirement to add - * @return This logical requirement for method chaining - * @throws IllegalArgumentException if the requirement type is not compatible - */ - public LogicalRequirement addRequirement(Requirement requirement) { - validateRequirement(requirement); - - // Add the requirement to the child requirements list - this was missing! - childRequirements.add(requirement); - - // Merge all child ids into this.ids - always create new mutable list - if (this.ids == null) { - this.ids = new java.util.ArrayList<>(); - } else { - // Always create a new mutable ArrayList to avoid immutable collection issues - this.ids = new java.util.ArrayList<>(this.ids); - } - if (requirement.getIds() != null) { - for (Integer id : requirement.getIds()) { - if (!this.ids.contains(id)) { - this.ids.add(id); - } - } - } - // Update rating to highest among all children - int maxRating = this.rating; - for (Requirement child : childRequirements) { - if (child.getRating() > maxRating) { - maxRating = child.getRating(); - } - } - this.rating = maxRating; - return this; - } - - /** - * Removes a child requirement from this logical requirement. - * - * @param requirement The requirement to remove - * @return true if the requirement was removed, false if it wasn't found - */ - public boolean removeRequirement(Requirement requirement) { - return childRequirements.remove(requirement); - } - - /** - * Checks if this logical requirement contains the specified requirement, - * either directly or within any nested logical requirements. - * - * @param targetRequirement The requirement to search for - * @return true if the requirement exists within this logical structure, false otherwise - */ - public boolean contains(Requirement targetRequirement) { - if (childRequirements.contains(targetRequirement)) { - return true; - } - - // Check nested logical requirements - for (Requirement child : childRequirements) { - if (child instanceof LogicalRequirement) { - if (((LogicalRequirement) child).contains(targetRequirement)) { - return true; - } - } - } - - return false; - } - - /** - * Gets the total number of requirements in this logical structure (including nested). - * - * @return Total count of all requirements - */ - public int getTotalRequirementCount() { - int count = 0; - for (Requirement child : childRequirements) { - if (child instanceof LogicalRequirement) { - count += ((LogicalRequirement) child).getTotalRequirementCount(); - } else { - count++; - } - } - return count; - } - - /** - * Gets the count of fulfilled requirements in this logical structure. - * - * @return Count of fulfilled requirements - */ - public int getFulfilledRequirementCount() { - int count = 0; - for (Requirement child : childRequirements) { - if (child instanceof LogicalRequirement) { - // For logical requirements, check if they are fulfilled as a whole - if (((LogicalRequirement) child).isLogicallyFulfilled()) { - count += ((LogicalRequirement) child).getTotalRequirementCount(); - } - } else { - if (child.isFulfilled()) { - count++; - } - } - } - return count; - } - - /** - * Abstract method to check if this logical requirement is fulfilled. - * AND requirements need all children fulfilled, OR requirements need at least one. - * - * @return true if the logical requirement is fulfilled, false otherwise - */ - public abstract boolean isLogicallyFulfilled(); - - /** - * Abstract method to get requirements that are blocking fulfillment. - * For AND: all unfulfilled requirements block - * For OR: all requirements block if none are fulfilled - * - * @return List of requirements that are preventing fulfillment - */ - public abstract List getBlockingRequirements(); - - /** - * Gets the progress percentage for this logical requirement. - * - * @return Progress percentage (0.0 to 100.0) - */ - public double getProgressPercentage() { - if (childRequirements.isEmpty()) { - return 100.0; - } - - int total = getTotalRequirementCount(); - int fulfilled = getFulfilledRequirementCount(); - - return total > 0 ? (fulfilled * 100.0) / total : 0.0; - } - - /** - * Checks if this logical requirement is fulfilled. - * This method is required by the Requirement base class. - * - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if the logical requirement is fulfilled, false otherwise - */ - @Override - public boolean fulfillRequirement(CompletableFuture scheduledFuture) { - log.debug("Attempting to fulfill logical requirement: {}", getName()); - - // For logical requirements, we don't directly fulfill them - // Instead, we check if they are already fulfilled by their children - - boolean fulfilled = fulfillLogicalRequirement(scheduledFuture); - - if (fulfilled) { - log.debug("Logical requirement {} is already fulfilled", getName()); - } else { - log.debug("Logical requirement {} is not fulfilled. Blocking requirements: {}", - getName(), getBlockingRequirements().size()); - } - - return fulfilled; - } - - /** - * Gets a unique identifier for this logical requirement. - * Includes the logical operator and child requirement identifiers. - * - * @return A unique identifier string - */ - @Override - public String getUniqueIdentifier() { - String childIds = childRequirements.stream() - .map(Requirement::getUniqueIdentifier) - .collect(Collectors.joining(",")); - - return String.format("%s:LOGICAL:%s:[%s]", - requirementType.name(), - getClass().getSimpleName(), - childIds); - } - - /** - * Gets the best available requirement from this logical structure. - * For OR requirements, this returns the highest-rated fulfilled requirement. - * For AND requirements, this returns null if not all are fulfilled. - * - * @return The best available requirement, or null if none available - */ - public Optional getBestAvailableRequirement() { - if (!isLogicallyFulfilled()) { - return Optional.empty(); - } - - // Find the highest-rated fulfilled requirement - return childRequirements.stream() - .filter(req -> { - if (req instanceof LogicalRequirement) { - return ((LogicalRequirement) req).isLogicallyFulfilled(); - } else { - return req.isFulfilled(); - } - }) - .max(Requirement::compareTo); - } - - /** - * Gets all requirements that can fulfill this logical requirement. - * For OR requirements, returns all fulfilled requirements. - * For AND requirements, returns all requirements if all are fulfilled. - * - * @return List of requirements that can fulfill this logical requirement - */ - public List getAvailableRequirements() { - if (!isLogicallyFulfilled()) { - return new ArrayList<>(); - } - - List available = new ArrayList<>(); - - for (Requirement child : childRequirements) { - if (child instanceof LogicalRequirement) { - LogicalRequirement logical = (LogicalRequirement) child; - if (logical.isLogicallyFulfilled()) { - available.addAll(logical.getAvailableRequirements()); - } - } else { - if (child.isFulfilled()) { - available.add(child); - } - } - } - - return available; - } - - /** - * Gets a detailed status string for this logical requirement. - * - * @return Formatted status information - */ - public String getStatusInfo() { - StringBuilder sb = new StringBuilder(); - - sb.append(getName()).append(" (").append(getClass().getSimpleName()).append(")\n"); - sb.append(" Status: ").append(isLogicallyFulfilled() ? "Fulfilled" : "Not Fulfilled").append("\n"); - sb.append(" Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - sb.append(" Child Requirements: ").append(childRequirements.size()).append("\n"); - - for (int i = 0; i < childRequirements.size(); i++) { - Requirement child = childRequirements.get(i); - String prefix = (i == childRequirements.size() - 1) ? " └─ " : " ├─ "; - - if (child instanceof LogicalRequirement) { - sb.append(prefix).append(((LogicalRequirement) child).getStatusInfo().replace("\n", "\n ")); - } else { - sb.append(prefix).append(child.getName()) - .append(" [").append(child.isFulfilled() ? "FULFILLED" : "NOT FULFILLED").append("]\n"); - } - } - - return sb.toString(); - } - - /** - * Checks if this logical requirement contains only ItemRequirements (or LogicalRequirements that contain only ItemRequirements). - * This is useful for RequirementRegistry caching to identify logical requirements that can be processed - * alongside ConditionalRequirements with ItemRequirement steps. - * - * @return true if this logical requirement and all nested logical requirements contain only ItemRequirements - */ - public boolean containsOnlyItemRequirements() { - // Check if our allowed child type is ItemRequirement - if (!ItemRequirement.class.isAssignableFrom(allowedChildType) && allowedChildType != ItemRequirement.class) { - return false; - } - - // Recursively check all child requirements - for (Requirement child : childRequirements) { - if (child instanceof LogicalRequirement) { - if (!((LogicalRequirement) child).containsOnlyItemRequirements()) { - return false; - } - } else if (!(child instanceof ItemRequirement)) { - return false; - } - } - - return true; - } - - /** - * Gets all ItemRequirements from this logical structure, flattening any nested logical requirements. - * This is useful for RequirementRegistry to extract all item requirements for caching purposes. - * - * @return List of all ItemRequirements in this logical structure - */ - public List getAllItemRequirements() { - List itemRequirements = new ArrayList<>(); - - for (Requirement child : childRequirements) { - if (child instanceof LogicalRequirement) { - itemRequirements.addAll(((LogicalRequirement) child).getAllItemRequirements()); - } else if (child instanceof ItemRequirement) { - itemRequirements.add((ItemRequirement) child); - } - } - - return itemRequirements; - } - - @Override - public String toString() { - return String.format("%s[%s children, %s, type: %s]", - getClass().getSimpleName(), - childRequirements.size(), - isLogicallyFulfilled() ? "FULFILLED" : "NOT FULFILLED", - allowedChildType.getSimpleName()); - } - - - - /** - * Attempts to fulfill a single logical requirement. - * - * @param logicalReq The logical requirement to fulfill - * @param preferEquipment If true, prefer equipping items; if false, prefer inventory - * @return true if the logical requirement was fulfilled - */ - private boolean fulfillLogicalRequirement(CompletableFuture scheduledFuture) { - // If already fulfilled, nothing to do - - if (this instanceof OrRequirement) { - return fulfillOrRequirement((OrRequirement) this,scheduledFuture); - } else { - - log.warn("Unknown logical requirement type: {}", this.getClass().getSimpleName()); - return false; - } - } - - /** - * Fulfills an OR logical requirement (at least one child must be fulfilled). - * Sorts child requirements by rating (highest first) and attempts to fulfill - * only the highest-rated available requirement. - * - * @param orReq The OR requirement - * @param preferEquipment If true, prefer equipping items; if false, prefer inventory - * @return true if at least one child requirement was fulfilled - */ - private boolean fulfillOrRequirement(OrRequirement orReq, CompletableFuture scheduledFuture) { - // Sort child requirements by rating (highest first), then by priority - List sortedRequirements = orReq.getChildRequirements().stream() - .sorted((r1, r2) -> { - // First sort by rating (highest first) - int ratingCompare = Integer.compare(r2.getRating(), r1.getRating()); - if (ratingCompare != 0) { - return ratingCompare; - } - // Then by priority (mandatory first) - return r1.getPriority().compareTo(r2.getPriority()); - }) - .collect(Collectors.toList()); - boolean foundFulfilled = false; - // Try to fulfill all requirements in order of rating (highest first), but only need one to succeed - for (Requirement childReq : sortedRequirements) { - if(scheduledFuture.isCancelled() || scheduledFuture.isDone()) { - log.info("Scheduled future was cancelled or completed, stopping OR requirement fulfillment."); - return foundFulfilled; // Stop if future is cancelled or done - } - if (childReq instanceof LogicalRequirement) { - if (childReq.fulfillRequirement(scheduledFuture)) { - log.info("Fulfilled OR requirement using logical child: {} (rating: {})", - childReq.getDescription(), childReq.getRating()); - foundFulfilled = true; - } - } else if (childReq instanceof ItemRequirement) { - ItemRequirement itemReq = (ItemRequirement) childReq; - itemReq.fulfillRequirement(scheduledFuture); - - if (orReq.isFulfilled() || itemReq.isFulfilled()) { - log.info("Fulfilled OR requirement using item: {} (rating: {})", - itemReq.getName(), itemReq.getRating()); - foundFulfilled = true; - } - } else if (childReq instanceof ShopRequirement) { - ShopRequirement shopReq = (ShopRequirement) childReq; - try { - if (shopReq.fulfillRequirement(scheduledFuture)) { - log.info("Fulfilled shop OR requirement: {} (rating: {})", - shopReq.getName(), shopReq.getRating()); - foundFulfilled = true; - } - } catch (Exception e) { - log.info("Failed to fulfill shop requirement {}: {}", shopReq.getName(), e.getMessage()); - } - } else if (childReq instanceof LootRequirement) { - LootRequirement lootReq = (LootRequirement) childReq; - try { - if (lootReq.fulfillRequirement(scheduledFuture)) { - log.debug("Fulfilled loot OR requirement: {} (rating: {})", - lootReq.getName(), lootReq.getRating()); - foundFulfilled = true; - } - } catch (Exception e) { - log.debug("Failed to fulfill loot requirement {}: {}", lootReq.getName(), e.getMessage()); - } - } - } - - log.info("OR requirement {} fulfilled: {}", - orReq.getDescription(), foundFulfilled); - return foundFulfilled; // No child requirements were fulfilled - } - - // ========== STATIC UTILITY METHODS FOR LOGICAL REQUIREMENT PROCESSING ========== - - /** - * Filters logical requirements by schedule context. - * - * @param requirements The logical requirements to filter - * @param context The schedule context to match - * @return List of requirements matching the context - */ - public static List filterByContext(List requirements, TaskContext context) { - return requirements.stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(Collectors.toList()); - } - - /** - * Checks if any requirement within a logical requirement collection has mandatory items. - * - * @param logicalReqs The logical requirements to check - * @return true if any requirement contains mandatory items - */ - public static boolean hasMandatoryItems(List logicalReqs) { - return logicalReqs.stream() - .anyMatch(req -> extractItemRequirementsFromLogical(req).stream() - .anyMatch(ItemRequirement::isMandatory)); - } - - /** - * Checks if any requirement within a logical requirement collection has mandatory shop items. - * - * @param logicalReqs The logical requirements to check - * @return true if any requirement contains mandatory shop items - */ - public static boolean hasMandatoryShopItems(List logicalReqs) { - return logicalReqs.stream() - .anyMatch(req -> extractShopRequirementsFromLogical(req).stream() - .anyMatch(ShopRequirement::isMandatory)); - } - - /** - * Checks if any requirement within a logical requirement collection has mandatory loot items. - * - * @param logicalReqs The logical requirements to check - * @return true if any requirement contains mandatory loot items - */ - public static boolean hasMandatoryLootItems(List logicalReqs) { - return logicalReqs.stream() - .anyMatch(req -> extractLootRequirementsFromLogical(req).stream() - .anyMatch(LootRequirement::isMandatory)); - } - - /** - * Extracts all item requirements from a collection of logical requirements. - * - * @param logicalReqs The logical requirements to process - * @return List of all item requirements found within the logical structure - */ - public static List extractAllItemRequirements(List logicalReqs) { - return logicalReqs.stream() - .flatMap(req -> extractItemRequirementsFromLogical(req).stream()) - .collect(Collectors.toList()); - } - - /** - * Extracts all shop requirements from a collection of logical requirements. - * - * @param logicalReqs The logical requirements to process - * @return List of all shop requirements found within the logical structure - */ - public static List extractAllShopRequirements(List logicalReqs) { - return logicalReqs.stream() - .flatMap(req -> extractShopRequirementsFromLogical(req).stream()) - .collect(Collectors.toList()); - } - - /** - * Extracts all loot requirements from a collection of logical requirements. - * - * @param logicalReqs The logical requirements to process - * @return List of all loot requirements found within the logical structure - */ - public static List extractAllLootRequirements(List logicalReqs) { - return logicalReqs.stream() - .flatMap(req -> extractLootRequirementsFromLogical(req).stream()) - .collect(Collectors.toList()); - } - - /** - * Extracts only mandatory item requirements from a collection of logical requirements. - * - * @param logicalReqs The logical requirements to process - * @return List of mandatory item requirements found within the logical structure - */ - public static List extractMandatoryItemRequirements(List logicalReqs) { - return extractAllItemRequirements(logicalReqs).stream() - .filter(ItemRequirement::isMandatory) - .collect(Collectors.toList()); - } - - /** - * Processes a collection of logical requirements and fulfills them according to their logic. - * Provides common error handling and logging patterns. - * - * @param logicalReqs The logical requirements to fulfill - * @param requirementType Description of the requirement type for logging - * @return true if all mandatory requirements were fulfilled, false otherwise - */ - public static boolean fulfillLogicalRequirements(CompletableFuture scheduledFuture, List logicalReqs, String requirementType) { - if (logicalReqs.isEmpty()) { - log.debug("No {} requirements to fulfill", requirementType); - return true; - } - - boolean success = true; - int fulfilled = 0; - - for (int i = 0; i < logicalReqs.size(); i++) { - LogicalRequirement logicalReq = logicalReqs.get(i); - - try { - log.debug("Processing {} logical requirement {}/{}: {}", - requirementType, i + 1, logicalReqs.size(), logicalReq.getDescription()); - - if (logicalReq.isLogicallyFulfilled()) { - fulfilled++; - continue; - } - - boolean requirementFulfilled = logicalReq.fulfillRequirement(scheduledFuture); - - if (requirementFulfilled) { - fulfilled++; - } else { - // Check if any child requirement was mandatory - boolean hasMandatory = hasMandatoryItems(Arrays.asList(logicalReq)); - - if (hasMandatory) { - log.error("Failed to fulfill mandatory {} requirement: {}", - requirementType, logicalReq.getDescription()); - success = false; - break; // Stop on mandatory failure - } else { - log.debug("Failed to fulfill optional {} requirement: {}", - requirementType, logicalReq.getDescription()); - } - } - } catch (Exception e) { - log.error("Error fulfilling {} requirement {}: {}", - requirementType, logicalReq.getDescription(), e.getMessage()); - - boolean hasMandatory = hasMandatoryItems(Arrays.asList(logicalReq)); - - if (hasMandatory) { - success = false; - } - } - } - - log.debug("{} requirements fulfillment completed. Success: {}, Fulfilled: {}/{}", - requirementType, success, fulfilled, logicalReqs.size()); - return success; - } - - /** - * Recursively extracts all ItemRequirement instances from a logical requirement structure. - * This method handles nested logical requirements and returns a flat list of all item requirements. - * - * @param logicalReq The logical requirement to extract from - * @return List of all ItemRequirement instances found within the logical structure - */ - public static List extractItemRequirementsFromLogical(LogicalRequirement logicalReq) { - List items = new ArrayList<>(); - - for (Requirement child : logicalReq.getChildRequirements()) { - if (child instanceof ItemRequirement) { - items.add((ItemRequirement) child); - } else if (child instanceof LogicalRequirement) { - items.addAll(extractItemRequirementsFromLogical((LogicalRequirement) child)); - } - } - - return items; - } - - /** - * Recursively extracts all ShopRequirement instances from a logical requirement structure. - * This method handles nested logical requirements and returns a flat list of all shop requirements. - * - * @param logicalReq The logical requirement to extract from - * @return List of all ShopRequirement instances found within the logical structure - */ - public static List extractShopRequirementsFromLogical(LogicalRequirement logicalReq) { - List shops = new ArrayList<>(); - - for (Requirement child : logicalReq.getChildRequirements()) { - if (child instanceof ShopRequirement) { - shops.add((ShopRequirement) child); - } else if (child instanceof LogicalRequirement) { - shops.addAll(extractShopRequirementsFromLogical((LogicalRequirement) child)); - } - } - - return shops; - } - - /** - * Recursively extracts all LootRequirement instances from a logical requirement structure. - * This method handles nested logical requirements and returns a flat list of all loot requirements. - * - * @param logicalReq The logical requirement to extract from - * @return List of all LootRequirement instances found within the logical structure - */ - public static List extractLootRequirementsFromLogical(LogicalRequirement logicalReq) { - List loots = new ArrayList<>(); - - for (Requirement child : logicalReq.getChildRequirements()) { - if (child instanceof LootRequirement) { - loots.add((LootRequirement) child); - } else if (child instanceof LogicalRequirement) { - loots.addAll(extractLootRequirementsFromLogical((LogicalRequirement) child)); - } - } - - return loots; - } - - - - /** - * Breaks down all ItemRequirements in a logical requirement tree by the slot(s) they occupy. - * Equipment items are grouped by EquipmentInventorySlot name. - * Inventory items are grouped by inventory slot ("inventory:X") or "inventory:any" for -1. - * EITHER items are grouped in both equipment and inventory as appropriate. - * - * @param logicalReq The logical requirement to analyze - * @return Map of slot key to list of ItemRequirements occupying that slot - */ - public static java.util.Map> breakdownItemRequirementsBySlot(LogicalRequirement logicalReq) { - java.util.List allItems = extractItemRequirementsFromLogical(logicalReq); - java.util.Map> slotMap = new java.util.HashMap<>(); - for (ItemRequirement item : allItems) { - boolean slotted = false; - if (item.getEquipmentSlot() != null) { - String key = "equipment:" + item.getEquipmentSlot().name(); - slotMap.computeIfAbsent(key, k -> new java.util.ArrayList<>()).add(item); - slotted = true; - } - if (item.getInventorySlot() != null) { - if (item.getInventorySlot() >= 0) { - String key = "inventory:" + item.getInventorySlot(); - slotMap.computeIfAbsent(key, k -> new java.util.ArrayList<>()).add(item); - slotted = true; - } else if (item.getInventorySlot() == -1) { - String key = "inventory:any"; - slotMap.computeIfAbsent(key, k -> new java.util.ArrayList<>()).add(item); - slotted = true; - } - } - // If neither slot is set, group under "unslotted" - if (!slotted) { - slotMap.computeIfAbsent("unslotted", k -> new java.util.ArrayList<>()).add(item); - } - } - return slotMap; - } - - /** - * Pretty-prints the slot breakdown for all ItemRequirements in a logical requirement. - * - * @param logicalReq The logical requirement to analyze - * @return A formatted string showing the breakdown per slot - */ - public static String itemSlotBreakdown(LogicalRequirement logicalReq) { - java.util.Map> slotMap = breakdownItemRequirementsBySlot(logicalReq); - StringBuilder sb = new StringBuilder(); - sb.append("Slot Breakdown for LogicalRequirement: ").append(logicalReq.getName()).append("\n"); - for (String slot : slotMap.keySet()) { - sb.append(" [").append(slot).append("] ").append(slotMap.get(slot).size()).append(" item(s):\n"); - for (ItemRequirement item : slotMap.get(slot)) { - sb.append(" - ").append(item.getName()) - .append(" (id:").append(item.getId()).append(", amt:").append(item.getAmount()).append(")\n"); - } - } - return sb.toString(); - } - // private String formatLogicalRequirement(LogicalRequirement logicalReq) { -// if (logicalReq instanceof OrRequirement) { -// OrRequirement orReq = (OrRequirement) logicalReq; -// StringBuilder sb = new StringBuilder(); -// sb.append("OR(").append(orReq.getChildRequirements().size()).append(" options): "); -// boolean first = true; -// for (Requirement childReq : orReq.getChildRequirements()) { -// if (!first) sb.append(" | "); -// sb.append(childReq.getName()).append("[").append(childReq.getPriority().name()).append("]"); -// first = false; -// } -// return sb.toString(); -// } else { -// return String.format("%s [%s, Rating: %d, Context: %s]", -// logicalReq.getName(), -// logicalReq.getPriority().name(), -// logicalReq.getRating(), -// logicalReq.getTaskContext().name()); -// } -// } -// } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/logical/OrRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/logical/OrRequirement.java deleted file mode 100644 index 6809a27f3a0..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/logical/OrRequirement.java +++ /dev/null @@ -1,543 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical; - -import lombok.EqualsAndHashCode; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.api.Skill; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement; -import net.runelite.client.plugins.microbot.Microbot; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** - * OR logical requirement - at least one child requirement must be fulfilled. - * This is useful for situations where multiple alternatives exist but only one is needed. - * - * All child requirements must be of the same type to ensure type safety and enable - * efficient caching. Mixed requirement types should use ConditionalRequirement instead. - * - * Examples: - * - Food items: Any one type of food is sufficient - * - Equipment slots: Any one item for the slot is sufficient - * - Transportation methods: Any one method to reach a location - * - * Similar to OrCondition but adapted for the requirement system. - */ -@EqualsAndHashCode(callSuper = true) -@Slf4j -public class OrRequirement extends LogicalRequirement { - - /** - * Creates an OR requirement with explicit child type specification. - * - * @param priority Priority level of this logical requirement - * @param rating Effectiveness rating (1-10) - * @param description Human-readable description - * @param TaskContext When this requirement should be fulfilled - * @param allowedChildType The class type that child requirements must be - * @param requirements Child requirements - any one of these can fulfill the OR requirement - */ - public OrRequirement(RequirementPriority priority, int rating, String description, - TaskContext taskContext, Class allowedChildType, - Requirement... requirements) { - super(RequirementType.OR_LOGICAL, priority, rating, description, taskContext, allowedChildType, requirements); - } - public OrRequirement(RequirementPriority priority, int rating, String description, - TaskContext taskContext, Class allowedChildType) { - super(RequirementType.OR_LOGICAL, priority, rating, description, taskContext, allowedChildType); - } - - /** - * Creates an OR requirement with the specified parameters, inferring child type from first requirement. - * - * @param priority Priority level of this logical requirement - * @param rating Effectiveness rating (1-10) - * @param description Human-readable description - * @param TaskContext When this requirement should be fulfilled - * @param requirements Child requirements - any one of these can fulfill the OR requirement - */ - public OrRequirement(RequirementPriority priority, int rating, String description, - TaskContext taskContext, Requirement... requirements) { - super(RequirementType.OR_LOGICAL, priority, rating, description, taskContext, - inferChildTypeFromRequirements(requirements), requirements); - } - - /** - * Helper method to infer the child type from the provided requirements. - * If all requirements are ItemRequirements, returns ItemRequirement.class. - * Otherwise returns the most common compatible type. - */ - private static Class inferChildTypeFromRequirements(Requirement... requirements) { - if (requirements.length == 0) { - return Requirement.class; // Default fallback - } - - // Check if all requirements are ItemRequirements - boolean allItemRequirements = true; - for (Requirement req : requirements) { - if (!(req instanceof ItemRequirement)) { - allItemRequirements = false; - break; - } - } - - if (allItemRequirements) { - return ItemRequirement.class; - } - - // Fallback to first requirement's class - Requirement firstReq = requirements[0]; - if (firstReq instanceof LogicalRequirement) { - return ((LogicalRequirement) firstReq).getAllowedChildType(); - } else { - return firstReq.getClass(); - } - } - - /** - * Convenience constructor with default rating of 5, inferring child type from first requirement. - * - * @param priority Priority level of this logical requirement - * @param description Human-readable description - * @param TaskContext When this requirement should be fulfilled - * @param requirements Child requirements - any one of these can fulfill the OR requirement - */ - public OrRequirement(RequirementPriority priority, String description, TaskContext taskContext, - Requirement... requirements) { - this(priority, 5, description, taskContext, requirements); - } - - /** - * Checks if this OR requirement is logically fulfilled. - * Returns true if at least one child requirement is fulfilled. - * - * @return true if any child requirement is fulfilled, false if none are fulfilled - */ - @Override - public boolean isLogicallyFulfilled() { - if (childRequirements.isEmpty()) { - return true; // Empty OR is considered satisfied - } - return childRequirements.stream().anyMatch(req -> { - if (req instanceof LogicalRequirement) { - return ((LogicalRequirement) req).isLogicallyFulfilled(); - } else { - return req.isFulfilled(); - } - }); - } - public boolean isFulfilled() { - // Check if any child requirement is fulfilled - return isLogicallyFulfilled(); - } - - /** - * Gets requirements that are blocking fulfillment of this OR requirement. - * For an OR requirement, all child requirements are blocking if none are fulfilled. - * If at least one is fulfilled, nothing is blocking. - * - * @return List of all child requirements if none are fulfilled, otherwise empty list - */ - @Override - public List getBlockingRequirements() { - // For an OR requirement, if any requirement is fulfilled, nothing is blocking - if (isLogicallyFulfilled()) { - return new ArrayList<>(); - } - - // If we reach here, none are fulfilled, so all requirements are blocking - List blocking = new ArrayList<>(); - - for (Requirement child : childRequirements) { - if (child instanceof LogicalRequirement) { - // Add the logical requirement itself as blocking, not its children - blocking.add(child); - } else { - blocking.add(child); - } - } - - return blocking; - } - - /** - * Gets the name of this OR requirement. - * - * @return A descriptive name for this OR requirement - */ - @Override - public String getName() { - if (childRequirements.isEmpty()) { - return "Empty OR Requirement"; - } - - if (childRequirements.size() == 1) { - return childRequirements.get(0).getName(); - } - - return String.format("OR(%s alternatives)", childRequirements.size()); - } - - /** - * Gets the best fulfilled requirement from this OR requirement. - * Returns the highest-rated fulfilled requirement, or empty if none are fulfilled. - * - * @return The best fulfilled requirement, or empty optional - */ - public java.util.Optional getBestFulfilledRequirement() { - return childRequirements.stream() - .filter(req -> { - if (req instanceof LogicalRequirement) { - return ((LogicalRequirement) req).isLogicallyFulfilled(); - } else { - return req.isFulfilled(); - } - }) - .max(Requirement::compareTo); - } - - /** - * Gets all fulfilled requirements from this OR requirement. - * Useful when multiple alternatives are fulfilled and you want to see all options. - * - * @return List of all fulfilled requirements - */ - public List getAllFulfilledRequirements() { - return childRequirements.stream() - .filter(req -> { - if (req instanceof LogicalRequirement) { - return ((LogicalRequirement) req).isLogicallyFulfilled(); - } else { - return req.isFulfilled(); - } - }) - .collect(java.util.stream.Collectors.toList()); - } - - /** - * Returns a detailed description of the OR requirement with status information. - * - * @return Formatted description with child requirement status - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - // Basic description - sb.append("OR Requirement: Any requirement can be fulfilled\n"); - - // Status information - boolean fulfilled = isLogicallyFulfilled(); - sb.append("Status: ").append(fulfilled ? "Fulfilled" : "Not fulfilled").append("\n"); - sb.append("Child Requirements: ").append(childRequirements.size()).append("\n"); - - // Progress information - double progress = getProgressPercentage(); - sb.append(String.format("Overall Progress: %.1f%%\n", progress)); - - // Count fulfilled requirements - int fulfilledCount = getAllFulfilledRequirements().size(); - sb.append("Fulfilled Requirements: ").append(fulfilledCount).append("/").append(childRequirements.size()).append("\n\n"); - - // List all child requirements - sb.append("Child Requirements:\n"); - for (int i = 0; i < childRequirements.size(); i++) { - Requirement requirement = childRequirements.get(i); - boolean childFulfilled = requirement instanceof LogicalRequirement ? - ((LogicalRequirement) requirement).isLogicallyFulfilled() : - requirement.isFulfilled(); - - sb.append(String.format("%d. %s [%s]\n", - i + 1, - requirement.getName(), - childFulfilled ? "FULFILLED" : "NOT FULFILLED")); - } - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - // Basic information - sb.append("OrRequirement:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Type: OR (Any requirement can be fulfilled)\n"); - sb.append(" │ Child Requirements: ").append(childRequirements.size()).append("\n"); - sb.append(" │ Priority: ").append(priority.name()).append("\n"); - sb.append(" │ Rating: ").append(rating).append("/10\n"); - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - boolean anyFulfilled = isLogicallyFulfilled(); - sb.append(" │ Fulfilled: ").append(anyFulfilled).append("\n"); - - // Count fulfilled requirements - int fulfilledCount = getAllFulfilledRequirements().size(); - sb.append(" │ Fulfilled Requirements: ").append(fulfilledCount).append("/").append(childRequirements.size()).append("\n"); - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Child requirements - if (!childRequirements.isEmpty()) { - sb.append(" ├─ Child Requirements ────────────────────────\n"); - - for (int i = 0; i < childRequirements.size(); i++) { - Requirement requirement = childRequirements.get(i); - String prefix = (i == childRequirements.size() - 1) ? " └─ " : " ├─ "; - - boolean childFulfilled = requirement instanceof LogicalRequirement ? - ((LogicalRequirement) requirement).isLogicallyFulfilled() : - requirement.isFulfilled(); - - sb.append(prefix).append(String.format("Requirement %d: %s [%s]\n", - i + 1, - requirement.getClass().getSimpleName(), - childFulfilled ? "FULFILLED" : "NOT FULFILLED")); - } - } else { - sb.append(" └─ No Child Requirements ───────────────────────\n"); - } - - return sb.toString(); - } - - - /** - * Merges this OrRequirement with another OrRequirement. - * Combines all child requirements, merges ids, and sets rating to highest of both. - * - * @param other The OrRequirement to merge with - * @return A new merged OrRequirement - */ - public static OrRequirement merge(OrRequirement first, OrRequirement other) { - if (!(other instanceof OrRequirement)) { - throw new IllegalArgumentException("Can only merge with another OrRequirement"); - } - OrRequirement otherOr = (OrRequirement) other; - // Combine all children - List mergedChildren = new ArrayList<>(first.childRequirements); - for (Requirement req : otherOr.childRequirements) { - if (!mergedChildren.contains(req)) { - mergedChildren.add(req); - } - } - // Merge ids - List mergedIds = new ArrayList<>(); - if (first.getIds() != null) mergedIds.addAll(first.getIds()); - if (otherOr.ids != null) { - for (Integer id : otherOr.ids) { - if (!mergedIds.contains(id)) mergedIds.add(id); - } - } - // Find max rating - int maxRating = first.getRating(); - for (Requirement req : mergedChildren) { - if (req.getRating() > maxRating) { - maxRating = req.getRating(); - } - } - // Use the first non-empty description, or combine - String desc = (first.getDescription() != null && !first.getDescription().isEmpty()) ? first.getDescription() : otherOr.description; - if (desc == null || desc.isEmpty()) desc = "Merged OR Requirement"; - // Use the higher priority - RequirementPriority mergedPriority = first.getPriority().ordinal() < otherOr.priority.ordinal() ? first.getPriority() : otherOr.priority; - // Use the most general allowedChildType - Class mergedType = first.allowedChildType.isAssignableFrom(otherOr.allowedChildType) ? first.allowedChildType : otherOr.allowedChildType; - // Create merged OrRequirement - OrRequirement merged = new OrRequirement(mergedPriority, maxRating, desc, first.getTaskContext(), mergedType); - merged.ids = mergedIds; - for (Requirement req : mergedChildren) { - merged.addRequirement(req); - } - - return merged; - } - - - // ============================== - // Static Convenience Functions - // ============================== - - // Pattern to detect charged items with numbers in parentheses, e.g., "Amulet of glory(6)" - private static final Pattern CHARGED_ITEM_PATTERN = Pattern.compile(".*\\((\\d+)\\)$"); - - /** - * Creates an OR requirement from a list of item IDs with automatic rating assignment for charged items. - * This convenience function automatically detects charged items in the list and assigns ratings based on charge levels. - * - * @param itemIds List of item IDs to create OR group from - * @param amount Amount of each item required (default: 1) - * @param equipmentSlot Equipment slot for equipment requirements (null for inventory) - * @param inventorySlot Inventory slot (-1 for any slot) - * @param priority Priority level of the OR requirement - * @param baseRating Base rating for non-charged items (1-10) - * @param description Description of the OR requirement - * @param TaskContext When this requirement should be fulfilled - * @param skillToUse Skill required to use items (optional) - * @param minimumLevelToUse Minimum level required to use items (optional) - * @param skillToEquip Skill required to equip items (optional) - * @param minimumLevelToEquip Minimum level required to equip items (optional) - * @param preferLowerCharge If true, lower charge items get higher ratings; if false, higher charge items get higher ratings - * @param chargedItemsOnly If true, only include charged items from the list; if false, include all items - * @return OrRequirement with ItemRequirement children, rated appropriately - */ - public static OrRequirement fromItemIds(List itemIds, int amount, EquipmentInventorySlot equipmentSlot, - int inventorySlot, RequirementPriority priority, int baseRating, - String description, TaskContext taskContext, - Skill skillToUse, Integer minimumLevelToUse, - Skill skillToEquip, Integer minimumLevelToEquip, - boolean preferLowerCharge, boolean chargedItemsOnly) { - - List itemRequirements = new ArrayList<>(); - - for (Integer itemId : itemIds) { - String itemName = getItemNameById(itemId); - if (itemName == null) { - continue; // Skip items with unknown names - } - - boolean isCharged = isChargedItem(itemName); - - // Skip non-charged items if chargedItemsOnly is true - if (chargedItemsOnly && !isCharged) { - continue; - } - - int rating = baseRating; - - // Adjust rating for charged items - if (isCharged) { - int chargeLevel = getChargeLevel(itemName); - if (chargeLevel != Integer.MAX_VALUE) { - // Assign rating based on charge level - // For preferLowerCharge=true: lower charge = higher rating - // For preferLowerCharge=false: higher charge = higher rating - if (preferLowerCharge) { - // Lower charge gets higher rating (inverse relationship) - // Charge 1 = rating 10, Charge 10 = rating 1 - rating = Math.max(1, Math.min(10, 11 - chargeLevel)); - } else { - // Higher charge gets higher rating (direct relationship) - // Charge 1 = rating 1, Charge 10 = rating 10 - rating = Math.max(1, Math.min(10, chargeLevel)); - } - } - } - - // Create individual ItemRequirement for this item - ItemRequirement itemReq = new ItemRequirement( - itemId, amount, equipmentSlot, inventorySlot, - priority, rating, itemName, taskContext, - skillToUse, minimumLevelToUse, skillToEquip, minimumLevelToEquip, preferLowerCharge - ); - - itemRequirements.add(itemReq); - } - - // Convert to array for constructor - Requirement[] reqArray = itemRequirements.toArray(new Requirement[0]); - - return new OrRequirement(priority, baseRating, description, taskContext, reqArray); - } - - /** - * Simplified convenience method for creating OR requirement from item IDs with default parameters. - * Uses preferLowerCharge=false and chargedItemsOnly=false by default. - * - * @param itemIds List of item IDs to create OR group from - * @param amount Amount required of each item - * @param equipmentSlot Equipment slot for equipment requirements (null for inventory) - * @param inventorySlot Inventory slot (-1 for any slot) - * @param priority Priority level of the OR requirement - * @param baseRating Base rating for items (1-10) - * @param description Description of the OR requirement - * @param TaskContext When this requirement should be fulfilled - * @param preferLowerCharge Whether to prefer lower charge variants - * @return OrRequirement with ItemRequirement children - */ - public static OrRequirement fromItemIds(List itemIds, int amount, EquipmentInventorySlot equipmentSlot, - int inventorySlot, RequirementPriority priority, int baseRating, - String description, TaskContext taskContext, boolean preferLowerCharge) { - return fromItemIds(itemIds, amount, equipmentSlot, inventorySlot, priority, baseRating, - description, taskContext, null, null, null, null, preferLowerCharge, false); - } - - /** - * Convenience method for creating OR requirement from varargs item IDs. - * - * @param amount Amount of each item required - * @param equipmentSlot Equipment slot for equipment requirements (null for inventory) - * @param inventorySlot Inventory slot (-1 for any slot) - * @param priority Priority level of the OR requirement - * @param baseRating Base rating for items (1-10) - * @param description Description of the OR requirement - * @param TaskContext When this requirement should be fulfilled - * @param preferLowerCharge Whether to prefer lower charge variants - * @param itemIds Varargs list of item IDs - * @return OrRequirement with ItemRequirement children - */ - public static OrRequirement fromItemIds(int amount, EquipmentInventorySlot equipmentSlot, int inventorySlot, - RequirementPriority priority, int baseRating, String description, - TaskContext taskContext, boolean preferLowerCharge, Integer... itemIds) { - return fromItemIds(Arrays.asList(itemIds), amount, equipmentSlot, inventorySlot, priority, baseRating, - description, taskContext, preferLowerCharge); - } - - /** - * Utility method to get item name by ID. - * - * @param itemId The item ID - * @return Item name or null if not found - */ - private static String getItemNameById(int itemId) { - try { - return Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemComposition(itemId).getName() - ).orElse(""); - } catch (Exception e) { - return null; - } - } - - /** - * Checks if an item name represents a charged item. - * - * @param itemName The item name to check - * @return true if the item appears to be charged, false otherwise - */ - private static boolean isChargedItem(String itemName) { - return itemName != null && CHARGED_ITEM_PATTERN.matcher(itemName).matches(); - } - - /** - * Gets the charge level from an item name. - * - * @param itemName The item name to parse - * @return The charge level, or Integer.MAX_VALUE if not a charged item - */ - private static int getChargeLevel(String itemName) { - if (itemName == null) { - return Integer.MAX_VALUE; - } - - Matcher matcher = CHARGED_ITEM_PATTERN.matcher(itemName); - if (matcher.matches()) { - try { - return Integer.parseInt(matcher.group(1)); - } catch (NumberFormatException e) { - return Integer.MAX_VALUE; - } - } - - return Integer.MAX_VALUE; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/ShopItemRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/ShopItemRequirement.java deleted file mode 100644 index c882ce83411..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/ShopItemRequirement.java +++ /dev/null @@ -1,217 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.models.ShopOperation; -import net.runelite.client.plugins.microbot.util.grandexchange.models.TimeSeriesInterval; -import net.runelite.client.plugins.microbot.util.shop.models.Rs2ShopItem; -import net.runelite.client.plugins.microbot.util.shop.models.Rs2ShopType; - -/** - * Represents an individual item requirement within a shop operation. - * Uses unified stock management system based on operation type. - * Enhanced with Grand Exchange time-series pricing configuration. - * - * Key improvements: - * - Extracted from inner class for better reusability - * - Enhanced stock validation logic - * - Better quantity tracking and completion status - * - Time-series pricing configuration for Grand Exchange operations - */ -@Getter -@EqualsAndHashCode() -@Slf4j -public class ShopItemRequirement { - private final Rs2ShopItem shopItem; - private final int amount; - private final int stockTolerance; - private int completedAmount = 0; - - // Grand Exchange time-series configuration - private final TimeSeriesInterval timeSeriesInterval; - private final boolean useTimeSeriesAveraging; - - /** - * Creates a shop item requirement with unified stock management. - * Uses the Rs2ShopItem's baseStock for stock calculations. - * - * @param shopItem The shop item to buy/sell (contains baseStock information) - * @param amount Total amount needed - * @param stockTolerance Acceptable deviation from shopItem's baseStock - * @param timeSeriesInterval Time-series interval for Grand Exchange price averaging - * @param useTimeSeriesAveraging Whether to use time-series averaging for Grand Exchange - * - * For BUY operations: We buy when shop has (baseStock - stockTolerance) or more - * For SELL operations: We sell when shop has (baseStock + stockTolerance) or less - */ - public ShopItemRequirement(Rs2ShopItem shopItem, int amount, int stockTolerance, - TimeSeriesInterval timeSeriesInterval, boolean useTimeSeriesAveraging) { - if (shopItem == null) { - throw new IllegalArgumentException("ShopItem cannot be null"); - } - if (amount <= 0) { - throw new IllegalArgumentException("Amount must be positive, got: " + amount); - } - if (stockTolerance < 0) { - throw new IllegalArgumentException("Stock tolerance cannot be negative, got: " + stockTolerance); - } - - this.shopItem = shopItem; - this.amount = amount; - this.stockTolerance = stockTolerance; - this.timeSeriesInterval = timeSeriesInterval != null ? timeSeriesInterval : TimeSeriesInterval.ONE_HOUR; - this.useTimeSeriesAveraging = useTimeSeriesAveraging; - } - - /** - * Creates a shop item requirement with default time-series configuration. - * Uses 1-hour averaging for Grand Exchange operations by default. - */ - public ShopItemRequirement(Rs2ShopItem shopItem, int amount, int stockTolerance) { - this(shopItem, amount, stockTolerance, TimeSeriesInterval.ONE_HOUR, true); - } - - - - /** - * For BUY operations: Checks if shop has enough stock to allow buying - * For SELL operations: Checks if shop has low enough stock to allow selling - */ - public boolean canProcessInShop(int currentShopStock, ShopOperation operation) { - if (operation == ShopOperation.BUY) { - return currentShopStock >= getMinimumStockForBuying(); - } else { // SELL - return currentShopStock <= getMaximumStockForSelling(); - } - } - - /** - * For BUY operations: Minimum stock required in shop to allow buying - */ - public int getMinimumStockForBuying() { - return Math.max(0, shopItem.getBaseStock() - stockTolerance); - } - - /** - * For SELL operations: Maximum stock allowed in shop to allow selling - */ - public int getMaximumStockForSelling() { - return shopItem.getBaseStock() + stockTolerance; - } - public int allowedToBuy(int currentStock){ - return Math.max(0, currentStock - getMinimumStockForBuying() + 1); - } - public int allowedToSell(int currentStock){ - return Math.max(0, getMaximumStockForSelling() - currentStock ); - } - - - - /** - * Calculates how many items we can safely buy/sell in current shop stock situation - */ - public int getQuantityForCurrentVisit(int currentShopStock, ShopOperation operation) { - int remaining = getRemainingAmount(); - if (operation == ShopOperation.BUY) { - // Can't buy more than available stock - int availableStockForBuying =allowedToBuy(currentShopStock); - log.info(" Remaing {} to buy -- Available stock for buying {}: {}",remaining, shopItem.getItemName(), availableStockForBuying); - - return Math.min(remaining, availableStockForBuying); - } else { // SELL - // Can't sell more than shop can accept - int shopCapacityForSelling =allowedToSell(currentShopStock); - return Math.min( remaining, shopCapacityForSelling); - } - } - - /** - * Legacy compatibility - returns baseStock from shopItem - */ - @Deprecated - public int getMinimumStock() { - return shopItem.getBaseStock(); - } - - /** - * Gets the base stock from the shop item - */ - public int getBaseStock() { - return shopItem.getBaseStock(); - } - - public boolean isCompleted() { - return completedAmount >= amount; - } - - public int getRemainingAmount() { - return Math.max(0, amount - completedAmount); - } - - public void addCompletedAmount(int additionalAmount) { - if (additionalAmount < 0) { - throw new IllegalArgumentException("Additional amount cannot be negative: " + additionalAmount); - } - this.completedAmount = Math.min(this.amount, this.completedAmount + additionalAmount); - } - - public void setCompletedAmount(int newAmount) { - if (newAmount < 0) { - throw new IllegalArgumentException("Completed amount cannot be negative: " + newAmount); - } - this.completedAmount = Math.min(this.amount, newAmount); - } - - public String getItemName() { - return shopItem.getItemName(); - } - - public int getItemId() { - return shopItem.getItemId(); - } - - /** - * Progress percentage (0.0 to 1.0) - */ - public double getProgress() { - return amount > 0 ? (double) completedAmount / amount : 1.0; - } - - /** - * Human readable progress string - */ - public String getProgressString() { - return String.format("%d/%d (%.1f%%)", completedAmount, amount, getProgress() * 100); - } - - /** - * Determines if this item requirement should use time-series pricing. - * Only applies to Grand Exchange operations when enabled. - */ - public boolean shouldUseTimeSeriesPricing() { - return useTimeSeriesAveraging && isGrandExchangeItem(); - } - - /** - * Checks if this item is for Grand Exchange operations. - * This can be determined from the shop item's context or type. - */ - private boolean isGrandExchangeItem() { - return shopItem.getShopType()==Rs2ShopType.GRAND_EXCHANGE; // Default to true for now - can be refined based on shop context - } - - /** - * Gets the recommended time-series interval for pricing this item. - * Returns the configured interval, defaulting to 1-hour if not set. - */ - public TimeSeriesInterval getRecommendedTimeSeriesInterval() { - return timeSeriesInterval; - } - - @Override - public String toString() { - return String.format("ShopItemRequirement{item='%s', amount=%d, completed=%d, stockTolerance=%d, baseStock=%d}", - getItemName(), amount, completedAmount, stockTolerance, getBaseStock()); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/ShopRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/ShopRequirement.java deleted file mode 100644 index ec6964a598f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/ShopRequirement.java +++ /dev/null @@ -1,2572 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop; -import static net.runelite.client.plugins.microbot.util.Global.sleep; -import static net.runelite.client.plugins.microbot.util.Global.sleepGaussian; -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import org.slf4j.event.Level; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Constants; -import net.runelite.api.GameState; -import net.runelite.api.GrandExchangeOffer; -import net.runelite.api.GrandExchangeOfferState; -import net.runelite.api.NPC; -import net.runelite.api.coords.WorldArea; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.gameval.ItemID; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -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.util.world.Rs2WorldUtil; -import net.runelite.client.plugins.microbot.util.world.WorldHoppingConfig; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.models.CancelledOfferState; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.models.MultiItemConfig; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.models.ShopOperation; -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.util.grandexchange.GrandExchangeSlots; -import net.runelite.client.plugins.microbot.util.grandexchange.Rs2GrandExchange; -import net.runelite.client.plugins.microbot.util.grandexchange.models.GrandExchangeOfferDetails; -import net.runelite.client.plugins.microbot.util.grandexchange.models.TimeSeriesAnalysis; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; -import net.runelite.client.plugins.microbot.util.npc.Rs2Npc; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import net.runelite.client.plugins.microbot.util.security.LoginManager; -import net.runelite.client.plugins.microbot.util.shop.Rs2Shop; -import net.runelite.client.plugins.microbot.util.shop.models.Rs2ShopItem; -import net.runelite.client.plugins.microbot.util.shop.models.Rs2ShopType; -import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; - -/** - * Represents a requirement to buy or sell multiple items from/to the same shop. - * This extends Requirement directly with additional properties specific to shop operations. - * Enhanced with BanksShopper patterns for world hopping, stock tracking, and quantity management. - * - * Supports batch operations for multiple items at the same shop location to optimize efficiency. - */ -@Getter -@EqualsAndHashCode(callSuper = true) -@Slf4j -public class ShopRequirement extends Requirement { - - // Pattern to detect charged items with numbers in parentheses, e.g., "Amulet of glory(6)" - protected static final Pattern CHARGED_ITEM_PATTERN = Pattern.compile(".*\\((\\d+)\\)$"); - - // Track cancelled offers for recovery - private final List cancelledOffers = new ArrayList<>(); - - /** - * Map of shop items to their individual requirements and settings. - * All items must be from the same shop (same location and NPC). - */ - private final Map shopItemRequirements; - - /** - * The primary shop information (location, NPC, type) - derived from the first shop item. - * All shop items must match this shop's location and NPC. - */ - private final Rs2ShopItem primaryShopItem; - - /** - * The shop operation type - either BUY or SELL. - * All items in this requirement must use the same operation. - */ - private final ShopOperation operation; - - /** - * Whether to handle noted items when selling (unnote them if needed). - */ - @Setter - private boolean handleNotedItems = true; - - /** - * Whether to use next world progression or random world selection. - */ - @Setter - private boolean useNextWorld = false; - - /** - * Whether to enable automatic world hopping when stock is low. - */ - @Setter - private boolean enableWorldHopping = true; - - /** - * Whether to bank purchased items automatically. - */ - @Setter - private boolean enableBanking = true; - - @Setter - private int timeout = 120000; // Default timeout for shop operations - - /** - * Configuration for world hopping behavior with exponential backoff and retry limits. - */ - @Setter - private WorldHoppingConfig worldHoppingConfig = WorldHoppingConfig.createDefault(); - - /** - * Set of world IDs that have been tried and failed during this requirement session. - * Used to avoid repeatedly attempting the same problematic worlds. - */ - private final Set excludedWorlds = new HashSet<>(); - - - public String getName() { - if (shopItemRequirements.isEmpty()) { - return "No Shop Items"; - } - if (shopItemRequirements.size() == 1) { - return shopItemRequirements.values().iterator().next().getItemName(); - } - return shopItemRequirements.size() + " items from " + primaryShopItem.getShopNpcName(); - } - - /** - * Returns a multi-line display string with detailed shop requirement information. - * Uses StringBuilder with tabs for proper formatting. - * - * @return A formatted string containing shop requirement details - */ - public String displayString() { - StringBuilder sb = new StringBuilder(); - sb.append("=== Enhanced Multi-Item Shop Requirement Details ===\n"); - sb.append("Name:\t\t\t").append(getName()).append("\n"); - sb.append("Type:\t\t\t").append(getRequirementType().name()).append("\n"); - sb.append("Priority:\t\t").append(getPriority().name()).append("\n"); - sb.append("Rating:\t\t\t").append(getRating()).append("/10\n"); - sb.append("Operation:\t\t").append(operation.name()).append("\n"); - sb.append("Total Items:\t\t").append(shopItemRequirements.size()).append("\n"); - sb.append("Item IDs:\t\t").append(getIds().toString()).append("\n"); - sb.append("Description:\t\t").append(getDescription() != null ? getDescription() : "No description").append("\n"); - - // Enhanced shopping configuration - sb.append("\n--- Shopping Configuration ---\n"); - sb.append("World Hopping:\t\t").append(enableWorldHopping ? "Enabled" : "Disabled").append("\n"); - sb.append("Use Next World:\t\t").append(useNextWorld ? "Yes" : "Random").append("\n"); - sb.append("Auto Banking:\t\t").append(enableBanking ? "Enabled" : "Disabled").append("\n"); - sb.append("Handle Noted:\t\t").append(handleNotedItems ? "Enabled" : "Disabled").append("\n"); - - // Individual item details - sb.append("\n--- Individual Item Requirements ---\n"); - for (ShopItemRequirement itemReq : shopItemRequirements.values()) { - sb.append("Item: ").append(itemReq.getItemName()).append("\n"); - sb.append(" Amount: ").append(itemReq.getAmount()).append(" (completed: ").append(itemReq.getCompletedAmount()).append(")\n"); - sb.append(" Base Stock: ").append(itemReq.getBaseStock()).append(" items\n"); - sb.append(" Stock Tolerance: ").append(itemReq.getStockTolerance()).append(" items\n"); - sb.append(" max sell stock: ").append(itemReq.getMaximumStockForSelling()).append(" items\n"); - sb.append(" min buy stock: ").append(itemReq.getMinimumStockForBuying()).append(" items\n"); - sb.append(" Status: ").append(itemReq.isCompleted() ? "COMPLETED" : "PENDING").append("\n"); - } - - if (primaryShopItem != null) { - sb.append("\n--- Shop Source Information ---\n"); - sb.append(primaryShopItem.displayString()); - } else { - sb.append("Shop Source:\t\tNot specified\n"); - } - - return sb.toString(); - } - - /** - * Creates a new multi-item shop requirement with schedule context. - * - * @param shopItems Map of shop items to their individual requirements - * @param operation Shop operation type (BUY or SELL) - * @param requirementType Where this item should be located (equipment slot, inventory, or either) - * @param priority Priority level of this item for plugin functionality - * @param rating Effectiveness rating from 1-10 (10 being most effective) - * @param description Human-readable description of the item's purpose - * @param TaskContext When this requirement should be fulfilled - */ - public ShopRequirement( - Map shopItems, - ShopOperation operation, - RequirementType requirementType, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext - ) { - - super(requirementType, priority, rating, description, extractItemIds(shopItems), taskContext); - - if (shopItems.isEmpty()) { - throw new IllegalArgumentException("Shop items map cannot be empty"); - } - - this.shopItemRequirements = new HashMap<>(shopItems); - this.operation = operation; - this.primaryShopItem = shopItems.keySet().iterator().next(); - - // Validate all items are from the same shop - validateSameShop(); - } - - /** - * Convenience constructor for single item requirement (backward compatibility). - */ - public ShopRequirement( - Rs2ShopItem shopItem, - int amount, - ShopOperation operation, - RequirementType requirementType, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext - ) { - this(createSingleItemMap(shopItem, amount), operation, requirementType, priority, rating, description, taskContext); - } - - /** - * Helper method to extract item IDs from shop items map. - */ - private static List extractItemIds(Map shopItems) { - return shopItems.keySet().stream() - .map(Rs2ShopItem::getItemId) - .collect(Collectors.toList()); - } - - /** - * Helper method to create single item map for backward compatibility. - */ - private static Map createSingleItemMap(Rs2ShopItem shopItem, int amount) { - Map map = new HashMap<>(); - map.put(shopItem, new ShopItemRequirement(shopItem, amount, 10)); // Default stockTolerance=10 - return map; - } - - /** - * Validates that all shop items are from the same shop (same location and NPC). - */ - private void validateSameShop() { - String primaryShopNpc = primaryShopItem.getShopNpcName(); - Rs2ShopType primaryShopType = primaryShopItem.getShopType(); - - for (Rs2ShopItem item : shopItemRequirements.keySet()) { - if (!item.getShopNpcName().equals(primaryShopNpc) || - !item.getShopType().equals(primaryShopType) || - !item.getLocation().equals(primaryShopItem.getLocation())) { - throw new IllegalArgumentException( - "All shop items must be from the same shop. Found mismatch: " + - item.getShopNpcName() + " vs " + primaryShopNpc - ); - } - } - } - - /** - * Gets the total number of items needed across all shop items. - */ - public int getTotalAmount() { - return shopItemRequirements.values().stream() - .mapToInt(ShopItemRequirement::getAmount) - .sum(); - } - - /** - * Gets the total number of completed items across all shop items. - */ - public int getTotalCompletedAmount() { - return shopItemRequirements.values().stream() - .mapToInt(ShopItemRequirement::getCompletedAmount) - .sum(); - } - - /** - * Resets the excluded worlds set. This can be called when starting a new requirement - * session or when you want to give previously failed worlds another chance. - */ - public void resetExcludedWorlds() { - excludedWorlds.clear(); - log.debug("Reset excluded worlds list for shop requirement: {}", getName()); - } - - /** - * Checks if all items in this requirement are completed. - */ - public boolean isAllItemsCompleted() { - return shopItemRequirements.values().stream() - .allMatch(ShopItemRequirement::isCompleted); - } - /** - * Collects all completed Grand Exchange offers and updates item requirements accordingly. - * This method handles offers from previous sessions and cancelled offers with partial fills. - * - * @return true if any offers were collected, false otherwise - */ - private boolean collectExistingCompletedOffers() { - try { - Map completedOffers = Rs2GrandExchange.getCompletedOffers(); - - if (completedOffers.isEmpty()) { - return false; - } - - log.info("Found {} completed offers to collect", completedOffers.size()); - boolean collectedAny = false; - - for (Map.Entry entry : completedOffers.entrySet()) { - GrandExchangeSlots slot = entry.getKey(); - GrandExchangeOfferDetails details = entry.getValue(); - - // Find matching shop item requirement - ShopItemRequirement matchingReq = null; - for (ShopItemRequirement itemReq : shopItemRequirements.values()) { - log.info("Checking item requirement: {} offer item ID: {} req item ID: {}", - itemReq.getItemName(), details.getItemId(), itemReq.getShopItem().getItemId()); - if (itemReq.getShopItem().getItemId() == details.getItemId()) { - matchingReq = itemReq; - break; - } - } - - if (matchingReq != null) { - // Collect the offer and get exact quantity - int itemsTransacted = Rs2GrandExchange.collectOfferAndGetQuantity(slot, enableBanking, details.getItemId()); - - if (itemsTransacted > 0) { - matchingReq.addCompletedAmount(itemsTransacted); - collectedAny = true; - - String transactionType = details.isSelling() ? "sold" : "bought"; - log.info("Collected previous {} offer: {} {} of {}", - transactionType, itemsTransacted, matchingReq.getItemName(), matchingReq.getItemName()); - - Microbot.status = "Collected " + itemsTransacted + "x " + matchingReq.getItemName() + - " from previous " + transactionType + " offer"; - }else{ - log.warn("No items transacted for slot {} with details: {}", slot, details); - } - }else{ - //collect, to bank to free slot, but we dont need to update any requirements - Rs2GrandExchange.collectOfferAndGetQuantity(slot, enableBanking, details.getItemId()); - } - } - - return collectedAny; - - } catch (Exception e) { - log.error("Error collecting existing completed offers: {}", e.getMessage()); - return false; - } - } - - /** - * Handles purchasing multiple items from the Grand Exchange. - * Implements proper slot management, offer placement, waiting, and collection patterns. - * Enhanced with 8-slot limit tracking and batch processing. - * - * @return true if the purchase was successful, false otherwise - */ - private boolean buyFromGrandExchange(CompletableFuture scheduledFuture) { - try { - Microbot.status = "Buying " + getName() + " from Grand Exchange"; - WorldArea locationArea = primaryShopItem.getLocationArea(); - WorldPoint[] locationCorners = new WorldPoint[] { - locationArea.toWorldPoint(), // Southwest corner (x, y) - new WorldPoint(locationArea.getX() + locationArea.getWidth() - 1, locationArea.getY(), locationArea.getPlane()), // Southeast corner - new WorldPoint(locationArea.getX(), locationArea.getY() + locationArea.getHeight() - 1, locationArea.getPlane()), // Northwest corner - new WorldPoint(locationArea.getX() + locationArea.getWidth() - 1, locationArea.getY() + locationArea.getHeight() - 1, locationArea.getPlane()) // Northeast corner - }; - // Walk to Grand Exchange and open interface - if (!Rs2Walker.isInArea(locationCorners) && !Rs2Walker.walkTo(BankLocation.GRAND_EXCHANGE.getWorldPoint(),10)) { - Microbot.status = "Failed to walk to Grand Exchange"; - return false; - } - - if (!Rs2GrandExchange.openExchange()) { - Microbot.status = "Failed to open Grand Exchange interface"; - return false; - } - - // Wait for interface to stabilize with proper game state checking - if (!sleepUntil(() -> Rs2GrandExchange.isOpen() && Microbot.getClient().getGameState() == GameState.LOGGED_IN, 8000)) { - Microbot.status = "Grand Exchange interface failed to open properly"; - return false; - } - - // First, collect any existing completed offers from previous sessions - collectExistingCompletedOffers(); - - // Check if we have sufficient coins for all items - int totalCost = calculateTotalCost(); - - if (totalCost > 0 && Rs2Inventory.itemQuantity("Coins") + Rs2Bank.count("Coins") < totalCost) { - Microbot.status = "Insufficient coins for Grand Exchange purchase (need " + totalCost + " coins)"; - return false; - } - - // Process each item that still needs to be purchased - List pendingItems = shopItemRequirements.values().stream() - .filter(itemReq -> !itemReq.isCompleted()) - .collect(Collectors.toList()); - - if (pendingItems.isEmpty()) { - Microbot.status = "All items already obtained"; - return true; - } - - // Pre-check GE slot availability for all pending items - int requiredSlots = pendingItems.size(); - if (!ensureGrandExchangeSlots(requiredSlots)) { - Microbot.status = "Cannot allocate sufficient GE slots for " + requiredSlots + " items"; - return false; - } - - - // BATCH PROCESSING: First, place as many offers as possible in available slots - Microbot.status = "Placing batch of Grand Exchange buy offers..."; - - // Track what items we're buying and their slots - Map activeOffers = new HashMap<>(); - Map itemToSlotMap = new HashMap<>(); - Map initialItemCounts = new HashMap<>(); - - int placedOffers = 0; - - // First phase: Place as many offers as possible at once - for (ShopItemRequirement itemReq : pendingItems) { - // Stop if we've used all available GE slots - if (Rs2GrandExchange.getAvailableSlotsCount() == 0) { - Microbot.status = "Maximum GE slots used - proceeding with " + placedOffers + " offers"; - break; - } - - // Calculate offer price using enhanced pricing method - int itemId = itemReq.getShopItem().getItemId(); - int offerPrice = calculateOfferPrice(itemReq); - int remainingAmount = itemReq.getRemainingAmount(); - - // Check for duplicate offers before placing new ones - cancelDuplicateOffers(itemId, itemReq.getItemName()); - - // Find available slot - GrandExchangeSlots availableSlot = Rs2GrandExchange.getAvailableSlot(); - if (availableSlot == null) { - log.warn("No available GE slots for {}", itemReq.getItemName()); - continue; - } - - // Place the buy order - Microbot.status = "Placing offer #" + (placedOffers+1) + ": " + remainingAmount + "x " + - itemReq.getItemName() + " at " + offerPrice + " gp each"; - - boolean offerPlaced = Rs2GrandExchange.buyItem( - itemReq.getItemName(), - offerPrice, - remainingAmount - ); - - if (!offerPlaced) { - Microbot.status = "Failed to place offer for " + itemReq.getItemName(); - continue; - } - - // Track this offer - activeOffers.put(itemId, itemReq); - itemToSlotMap.put(itemId, availableSlot); - - // Record initial item count for comparison later - int initialCount = Rs2Inventory.itemQuantity(itemId) + Rs2Bank.count(itemId); - initialItemCounts.put(itemId, initialCount); - - // Increment counter and brief pause between placing offers - placedOffers++; - sleep(Constants.GAME_TICK_LENGTH / 2); - } - - if (placedOffers == 0) { - Microbot.status = "Failed to place any Grand Exchange offers"; - } else { - // Second phase: Wait for and collect offers - Microbot.status = "Waiting for " + placedOffers + " Grand Exchange offers to complete..."; - log.info( "Waiting for " + placedOffers + " Grand Exchange offers to complete..."); - // Create a set of items that need to be completed - Set pendingItemIds = new HashSet<>(activeOffers.keySet()); - - // Start time for timeout calculations - long startTime = System.currentTimeMillis(); - long maxWaitTime = 120000; // 2 minutes total wait time for all offers - - // Wait for offers to complete using proper game state checking - while ((!pendingItemIds.isEmpty() && System.currentTimeMillis() - startTime < maxWaitTime) - && scheduledFuture != null && !scheduledFuture.isDone() && !scheduledFuture.isCancelled() - ) { - // Use sleepUntil to wait for any offers to complete, with shorter intervals - boolean hasCompletedOffer = sleepUntil(() -> { - return pendingItemIds.stream().anyMatch(itemId -> { - GrandExchangeSlots slot = itemToSlotMap.get(itemId); - return slot != null && Rs2GrandExchange.hasBoughtOffer(slot) && !scheduledFuture.isDone() && !scheduledFuture.isCancelled(); - }); - }, 5000); // Check every 5 seconds - - if (!hasCompletedOffer && System.currentTimeMillis() - startTime >= maxWaitTime) { - log.info("Timeout reached while waiting for offers"); - break; - } - - // Check each pending item for completion - for (Iterator it = pendingItemIds.iterator(); it.hasNext();) { - int itemId = it.next(); - ShopItemRequirement itemReq = activeOffers.get(itemId); - GrandExchangeSlots slot = itemToSlotMap.get(itemId); - - // Check if this offer has completed - boolean slotHasCompletedOffer = (slot != null) && Rs2GrandExchange.hasBoughtOffer(slot); - - // If offer completed, collect it - if (slotHasCompletedOffer) { - Microbot.status = "Offer completed for: " + itemReq.getItemName(); - - // Use the enhanced collection method to get exact quantity - int itemsPurchased = 0; - if (slot != null && Rs2GrandExchange.hasBoughtOffer(slot)) { - // Use the new method to collect and get exact quantity - itemsPurchased = Rs2GrandExchange.collectOfferAndGetQuantity(slot, enableBanking, itemId); - log.debug("Collected {} items from offer for {}", itemsPurchased, itemReq.getItemName()); - } - - // Update completed amount with actual purchased quantity - if (itemsPurchased > 0) { - itemReq.addCompletedAmount(itemsPurchased); - log.info("Updated completed amount for {}: purchased {} items, new total: {}/{}", - itemReq.getItemName(), itemsPurchased, itemReq.getCompletedAmount(), itemReq.getAmount()); - } else { - log.warn("No items collected from offer for {}", itemReq.getItemName()); - return false; // If we couldn't collect, return false - } - - // Remove this item from pending list - it.remove(); - - // Brief pause after collecting - sleep(Constants.GAME_TICK_LENGTH); - } - } - } - - // Handle any remaining pendingItemIds as unsuccessful - if (!pendingItemIds.isEmpty()) { - Microbot.status = "Timed out waiting for " + pendingItemIds.size() + " offers"; - - // List the items that didn't complete in time - for (int itemId : pendingItemIds) { - ShopItemRequirement itemReq = activeOffers.get(itemId); - log.warn("Offer timeout for: {}", itemReq.getItemName()); - } - - // Enhanced timeout handling: If timeout > 0 and we placed offers successfully, - // consider this a partial success rather than complete failure - if (timeout > 0 && placedOffers > 0) { - log.info("Timeout reached but {} offers were placed successfully - treating as partial success", placedOffers); - Microbot.status = "Grand Exchange orders placed but timed out waiting - continuing"; - } - } - } - - - - // If we have any tracked slots that weren't properly cleared during processing, - // make one final check and collection attempt for them - for (Integer itemId : itemToSlotMap.keySet()) { - GrandExchangeSlots slot = itemToSlotMap.get(itemId); - ShopItemRequirement itemReq = activeOffers.get(itemId); - - if (slot != null && itemReq != null) { - // Check if there's an offer in this slot that needs collection - if (Rs2GrandExchange.hasBoughtOffer(slot)) { - Microbot.status = "Final collection for: " + itemReq.getItemName(); - - // Use enhanced collection to get exact quantity - int itemsPurchased = Rs2GrandExchange.collectOfferAndGetQuantity(slot, enableBanking, itemId); - - if (itemsPurchased > 0) { - // Update completed amount with actual purchased quantity - itemReq.addCompletedAmount(itemsPurchased); - log.info("Final collection - Updated completed amount for {}: purchased {} items, new total: {}/{}", - itemReq.getItemName(), itemsPurchased, itemReq.getCompletedAmount(), itemReq.getAmount()); - }else { - log.warn("No items collected from final slot for {}", itemReq.getItemName()); - return false; // If we couldn't collect, return false - } - - } - - // Also check for cancelled offers with partial fills - else if (Rs2GrandExchange.isCancelledOfferWithItems(slot)) { - Microbot.status = "Collecting partial fill from cancelled offer: " + itemReq.getItemName(); - - int itemsPurchased = Rs2GrandExchange.collectOfferAndGetQuantity(slot, enableBanking, itemId); - if (itemsPurchased > 0) { - itemReq.addCompletedAmount(itemsPurchased); - log.info("Collected {} items from cancelled offer for {}", itemsPurchased, itemReq.getItemName()); - } - else { - log.warn("No items collected from cancelled offer for {}", itemReq.getItemName()); - return false; // If we couldn't collect, return false - } - } - } - } - - // No need to clear allocations - using simplified approach - // clearGrandExchangeSlotAllocations(); - - // Bank the items if banking is enabled - if (enableBanking) { - boolean hasItemsToBank = shopItemRequirements.values().stream() - .anyMatch(itemReq -> Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()) > 0); - - if (hasItemsToBank && !bankItems()) { - Microbot.status = "Purchase successful but failed to bank items"; - } - } - - // Attempt to restore any cancelled offers before checking final completion - int restoredOffers = restoreCancelledOffers(); - if (restoredOffers > 0) { - log.info("Restored {} previously cancelled offers after Grand Exchange buy operation", restoredOffers); - } - - // Final completion check - items should already be updated during collection - boolean allCompleted = isAllItemsCompleted(); - boolean partialSuccess = timeout > 0 && placedOffers > 0; - - if (allCompleted) { - Microbot.status = "Successfully purchased all items from Grand Exchange"; - log.info("Successfully purchased all items from Grand Exchange"); - return true; - } else if (partialSuccess) { - Microbot.status = "Grand Exchange orders placed successfully (some may still be pending)"; - log.info("Partial success: {} offers placed, timeout reached but treating as success", placedOffers); - log.warn("Remaining items: {}", - shopItemRequirements.values().stream() - .filter(itemReq -> !itemReq.isCompleted()) - .map(ShopItemRequirement::getItemName) - .collect(Collectors.joining(", "))); - return true; - } else { - Microbot.status = "Some Grand Exchange purchases failed"; - log.warn("Some Grand Exchange purchases failed, remaining items: {}", - shopItemRequirements.values().stream() - .filter(itemReq -> !itemReq.isCompleted()) - .map(ShopItemRequirement::getItemName) - .collect(Collectors.joining(", "))); - return !isMandatory(); - } - - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.buyFromGrandExchange", e); - // Clear tracked offers on error to prevent stale state - clearCancelledOffers(); - return false; - } - } - - /** - * Calculates the optimal offer price for an item using time-series data or fallback methods. - * Uses intelligent pricing patterns from Rs2GrandExchange for better market interaction. - * - * @param itemReq The item requirement to calculate price for - * @return The calculated offer price - */ - private int calculateOfferPrice(ShopItemRequirement itemReq) { - try { - int itemId = itemReq.getShopItem().getItemId(); - int offerPrice; - - if (itemReq.shouldUseTimeSeriesPricing()) { - // Use time-series average price for more intelligent buying - TimeSeriesAnalysis analysis = Rs2GrandExchange.getTimeSeriesData( - itemId, itemReq.getRecommendedTimeSeriesInterval()); - - if (analysis.averagePrice > 0) { - // Use recommended buy price from time-series analysis - offerPrice = analysis.getRecommendedBuyPrice(); - log.info("Using time-series buy price for {}: {} (avg: {}, high: {})", - itemReq.getItemName(), offerPrice, analysis.averagePrice, analysis.averageHighPrice); - } else { - // Fallback to intelligent pricing with current market data - offerPrice = Rs2GrandExchange.getAdaptiveBuyPrice(itemId, itemReq.getShopItem().getPercentageBoughtAt(), 0); - log.info("Time-series unavailable for {}, using adaptive pricing: {}", - itemReq.getItemName(), offerPrice); - } - } else { - // Traditional pricing method - int gePrice = Microbot.getRs2ItemManager().getGEPrice(itemReq.getItemName()); - offerPrice = Math.max((int) (gePrice * 1.1), (int) itemReq.getShopItem().getInitialPriceBuyAt()); - log.debug("Using traditional buy price for {}: {} (GE: {})", - itemReq.getItemName(), offerPrice, gePrice); - } - - // Ensure price respects the maximum limit from shop item configuration - int maxPrice = (int) itemReq.getShopItem().getInitialPriceBuyAt(); - offerPrice = Math.min(offerPrice, maxPrice); - - log.info("Final offer price for {}: {} (max allowed: {})", - itemReq.getItemName(), offerPrice, maxPrice); - - return offerPrice; - - } catch (Exception e) { - log.warn("Error calculating offer price for {}, using fallback: {}", - itemReq.getItemName(), e.getMessage()); - - // Fallback to shop item's initial price - return (int) itemReq.getShopItem().getInitialPriceBuyAt(); - } - } - - /** - * Handles selling multiple items to the Grand Exchange. - * Implements proper slot management, offer placement, waiting, and collection patterns. - * - * @return true if the sale was successful, false otherwise - */ - private boolean sellToGrandExchange() { - try { - Microbot.status = "Selling items to Grand Exchange"; - - // Get items from bank if needed first - if (enableBanking && !getItemsFromBankForSelling()) { - Microbot.status = "Failed to get items from bank for selling"; - return !isMandatory(); - } - - // Check if we have items to sell in inventory - boolean hasItemsToSell = shopItemRequirements.values().stream() - .anyMatch(itemReq -> Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()) > 0); - - if (!hasItemsToSell) { - Microbot.status = "No items to sell"; - return !isMandatory(); - } - - // Walk to Grand Exchange and open interface - if (!Rs2GrandExchange.walkToGrandExchange()) { - Microbot.status = "Failed to walk to Grand Exchange for selling"; - return false; - } - - if (!Rs2GrandExchange.openExchange()) { - Microbot.status = "Failed to open Grand Exchange interface for selling"; - return false; - } - - // Wait for interface to stabilize - if (!sleepUntil(() -> Rs2GrandExchange.isOpen(), 3000)) { - Microbot.status = "Grand Exchange interface failed to stabilize for selling"; - return false; - } - - // First, collect any existing completed offers from previous sessions - collectExistingCompletedOffers(); - - // Process each item that has inventory stock to sell - List sellableItems = shopItemRequirements.values().stream() - .filter(itemReq -> Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()) > 0) - .collect(Collectors.toList()); - - if (sellableItems.isEmpty()) { - Microbot.status = "No items found in inventory to sell"; - return true; - } - - // First, try to free up GE slots if needed - if (Rs2GrandExchange.getAvailableSlotsCount() == 0) { - Microbot.status = "No free GE slots available for selling, attempting to free them"; - - // Try to collect completed offers first - if (Rs2GrandExchange.hasBoughtOffer() || Rs2GrandExchange.hasSoldOffer()) { - Rs2GrandExchange.collectAll(enableBanking); - sleepUntil(() -> Rs2GrandExchange.getAvailableSlotsCount() > 0, 3000); - } else { - Rs2GrandExchange.abortAllOffers(enableBanking); - sleepUntil(() -> Rs2GrandExchange.getAvailableSlotsCount() > 0, 5000); - } - - if (Rs2GrandExchange.getAvailableSlotsCount() == 0) { - Microbot.status = "Failed to free Grand Exchange slots for selling"; - return !isMandatory(); - } - } - - // BATCH PROCESSING: Place as many offers as possible first - Microbot.status = "Placing batch of Grand Exchange sell offers..."; - - // Track what items we're selling and their slots - Map activeOffers = new HashMap<>(); - Map itemToSlotMap = new HashMap<>(); - Map initialInventoryCounts = new HashMap<>(); - - int placedOffers = 0; - - // First phase: Place as many offers as possible at once - for (ShopItemRequirement itemReq : sellableItems) { - // Stop if we've used all available GE slots - if (Rs2GrandExchange.getAvailableSlotsCount() == 0) { - Microbot.status = "Maximum GE slots used - proceeding with " + placedOffers + " offers"; - break; - } - - // Check inventory count for this item - int inventoryCount = Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()); - if (inventoryCount == 0) { - continue; // No items to sell for this item type - } - - // Calculate sell price using time-series data if enabled, fallback to traditional GE price - int sellPrice; - int sellAmount = Math.min(inventoryCount, itemReq.getAmount()); - int itemId = itemReq.getShopItem().getItemId(); - - if (itemReq.shouldUseTimeSeriesPricing()) { - // Use time-series average price for more intelligent selling - TimeSeriesAnalysis analysis = Rs2GrandExchange.getTimeSeriesData( - itemId, itemReq.getRecommendedTimeSeriesInterval()); - - if (analysis.averagePrice > 0) { - // Use recommended sell price from time-series analysis - sellPrice = analysis.getRecommendedSellPrice(); - log.info("Using time-series sell price for {}: {} (avg: {}, low: {})", - itemReq.getItemName(), sellPrice, analysis.averagePrice, analysis.averageLowPrice); - } else { - // Fallback to intelligent pricing with current market data - sellPrice = Rs2GrandExchange.getAdaptiveSellPrice(itemId, itemReq.getShopItem().getInitialPriceSellAt(), 0); - log.info("Time-series unavailable for {}, using intelligent pricing: {}", - itemReq.getItemName(), sellPrice); - } - } else { - // Traditional pricing method - int gePrice = Microbot.getRs2ItemManager().getGEPrice(itemReq.getItemName()); - sellPrice = Math.max((int) (gePrice * 0.9), (int) itemReq.getShopItem().getInitialPriceSellAt()); - log.info("Using traditional sell price for {}: {} (GE: {})", - itemReq.getItemName(), sellPrice, gePrice); - } - - // Ensure minimum price from shop item configuration - sellPrice = Math.max(sellPrice, (int) itemReq.getShopItem().getInitialPriceSellAt()); - - // Check for duplicate offers before placing new ones - cancelDuplicateOffers(itemId, itemReq.getItemName()); - - // Find available slot - GrandExchangeSlots availableSlot = Rs2GrandExchange.getAvailableSlot(); - if (availableSlot == null) { - log.warn("No available GE slots for selling {}", itemReq.getItemName()); - continue; - } - - // Place the sell order - Microbot.status = "Placing offer #" + (placedOffers+1) + ": " + sellAmount + "x " + - itemReq.getItemName() + " at " + sellPrice + " gp each"; - - boolean offerPlaced = Rs2GrandExchange.sellItem( - itemReq.getItemName(), - sellAmount, - sellPrice - ); - - if (!offerPlaced) { - Microbot.status = "Failed to place sell offer for " + itemReq.getItemName(); - continue; - } - - // Track this offer - activeOffers.put(itemId, itemReq); - itemToSlotMap.put(itemId, availableSlot); - - // Record initial inventory count for comparison later - initialInventoryCounts.put(itemId, inventoryCount); - - // Increment counter and brief pause between placing offers - placedOffers++; - sleep(Constants.GAME_TICK_LENGTH / 2); - } - if (placedOffers == 0) { - Microbot.status = "Failed to place any Grand Exchange sell offers"; - } else { - // Second phase: Wait for and collect offers - Microbot.status = "Waiting for " + placedOffers + " Grand Exchange sell offers to complete..."; - - // Create a set of items that need to be completed - Set pendingItemIds = new HashSet<>(activeOffers.keySet()); - - // Start time for timeout calculations - long startTime = System.currentTimeMillis(); - long maxWaitTime = timeout; // 2 minutes total wait time for all offers - - // Wait for offers to complete and collect as they do - while (!pendingItemIds.isEmpty() && System.currentTimeMillis() - startTime < maxWaitTime) { - // Check each pending item - for (Iterator it = pendingItemIds.iterator(); it.hasNext();) { - int itemId = it.next(); - ShopItemRequirement itemReq = activeOffers.get(itemId); - GrandExchangeSlots slot = itemToSlotMap.get(itemId); - int initialInventoryCount = initialInventoryCounts.get(itemId); - - // Check if this offer has completed - boolean slotHasCompletedOffer = (slot != null) && Rs2GrandExchange.hasSoldOffer(slot); - - // Also check if the item count has decreased (items sold) - int currentInventoryCount = Rs2Inventory.itemQuantity(itemId); - boolean itemCountDecreased = currentInventoryCount < initialInventoryCount; - - // If either condition is met, consider the offer complete - if (slotHasCompletedOffer || itemCountDecreased) { - Microbot.status = "Sell offer completed for: " + itemReq.getItemName(); - - // Use enhanced collection to get exact quantity sold - int itemsSold = 0; - if (slot != null && Rs2GrandExchange.hasSoldOffer(slot)) { - // Get the exact number of items sold from the offer - itemsSold = Rs2GrandExchange.getItemsSoldFromOffer(slot); - - // Collect the coins from this slot - Rs2GrandExchange.collectOffer(slot, enableBanking); - sleepUntil(() -> !Rs2GrandExchange.hasSoldOffer(slot) || - Rs2Inventory.itemQuantity("Coins") > 0, 3000); - - log.debug("Collected coins from sale of {} items of {}", itemsSold, itemReq.getItemName()); - } else { - // Fallback: calculate based on inventory change - itemsSold = initialInventoryCount - currentInventoryCount; - } - - // Update completed amount based on how many items were actually sold - if (itemsSold > 0) { - itemReq.addCompletedAmount(itemsSold); - log.debug("Updated completed amount for {}: sold {} items, new total: {}/{}", - itemReq.getItemName(), itemsSold, itemReq.getCompletedAmount(), itemReq.getAmount()); - } - - // Remove this item from pending list - it.remove(); - - // Brief pause after collecting - sleep(Constants.GAME_TICK_LENGTH); - } - } - sleepUntil(() -> Rs2GrandExchange.hasSoldOffer(), (int)maxWaitTime); // Refresh state - - } - - // Handle any remaining pendingItemIds as unsuccessful - if (!pendingItemIds.isEmpty()) { - Microbot.status = "Timed out waiting for " + pendingItemIds.size() + " sell offers"; - log.warn("Timed out waiting for {} sell offers", pendingItemIds.size()); - - // List the items that didn't complete in time - for (int itemId : pendingItemIds) { - ShopItemRequirement itemReq = activeOffers.get(itemId); - log.warn("Sell offer timeout for: {}", itemReq.getItemName()); - } - - // Enhanced timeout handling: If timeout > 0 and we placed offers successfully, - // consider this a partial success rather than complete failure - if (timeout > 0 && placedOffers > 0) { - log.info("Timeout reached but {} sell offers were placed successfully - treating as partial success", placedOffers); - Microbot.status = "Grand Exchange sell orders placed but timed out waiting - continuing"; - } - } - } - - - - - // No need to clear allocations - using simplified approach - // clearGrandExchangeSlotAllocations(); - - - // Bank the coins if banking is enabled and we got them in inventory - if (enableBanking && Rs2Inventory.itemQuantity("Coins") > 0) { - if (!bankCoinsAfterSelling()) { - Microbot.status = "Sale successful but failed to bank coins"; - // Don't fail the requirement just because banking failed - } - } - - // Attempt to restore any cancelled offers before checking final completion - int restoredOffers = restoreCancelledOffers(); - if (restoredOffers > 0) { - log.info("Restored {} previously cancelled offers after Grand Exchange sell operation", restoredOffers); - } - - // Final completion check with enhanced timeout handling - boolean allCompleted = isAllItemsCompleted(); - boolean partialSuccess = timeout > 0 && placedOffers > 0; - - if (allCompleted) { - Microbot.status = "Successfully sold all items to Grand Exchange"; - log.info("Successfully sold all items to Grand Exchange"); - return true; - } else if (partialSuccess) { - Microbot.status = "Grand Exchange sell orders placed successfully (some may still be pending)"; - log.info("Partial success: {} sell offers placed, timeout reached but treating as success", placedOffers); - return true; - } else { - Microbot.status = "Some Grand Exchange sales failed"; - log.warn("Some Grand Exchange sales failed, remaining items: {}", - shopItemRequirements.values().stream() - .filter(itemReq -> !itemReq.isCompleted()) - .map(ShopItemRequirement::getItemName) - .collect(Collectors.joining(", "))); - return !isMandatory(); // Allow continuation if not mandatory - } - - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.sellToGrandExchange", e); - // Clear tracked offers on error to prevent stale state - clearCancelledOffers(); - return false; - } - } - /** - * Opens the shop interface by interacting with the specified NPC. - * - * @param npcName The name of the shop NPC to interact with - * @param exact Whether to match the name exactly or allow partial matches - * @return true if the shop is successfully opened, false otherwise. - */ - public static boolean openShop(String npcName, boolean exact) { - // Delegate to Rs2Shop utility which now handles finding nearest shop NPC with Trade action - return Rs2Shop.openShop(npcName, exact); - } - - /** - * Handles purchasing multiple items from regular shops with stock management and world hopping. - * Supports both single and multi-item operations with individual item requirements. - * - * @return true if the purchase was successful, false otherwise - */ - private boolean buyFromRegularShop(CompletableFuture scheduledFuture) { - try { - Microbot.status = "Buying items from " + primaryShopItem.getShopNpcName(); - - int maxAttempts = enableWorldHopping ? worldHoppingConfig.getMaxWorldHops (): 0; // More attempts if world hopping is enabled - int successiveWorldHopAttempts = 0; - log.info("walking to shop location: x: {}, y: {}", primaryShopItem.getLocation().getX(), primaryShopItem.getLocation().getY()); - - while (!isAllItemsCompleted() && successiveWorldHopAttempts < maxAttempts) { - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - Microbot.status = "Task cancelled, stopping shop purchases"; - log.info("Shop purchase task cancelled, exiting"); - return false; // Exit if task was cancelled - } - - // Walk to the shop - - if (!Rs2Walker.isInArea(primaryShopItem.getLocationArea().toWorldPointList().toArray(new WorldPoint[0])) && !Rs2Walker.walkTo(primaryShopItem.getLocation(),4)) { - Microbot.status = "Failed to walk to shop"; - log.error("Failed to walk to shop: " + primaryShopItem.getLocation()); - return false; // Exit if walking to shop failed - } - - - // Open shop interface - if (!Rs2Shop.isOpen()){ - if (!Rs2Shop.openShop(primaryShopItem.getShopNpcName())) { - Microbot.status = "Failed to open shop"; - log.error("\n\tFailed to open shop: \"{}\"", primaryShopItem.getShopNpcName()); - continue; - } - } - - //sleepUntil(() -> Rs2Shop.isOpen(), Constants.GAME_TICK_LENGTH * 3); // Ensure shop is open - if (!Rs2Shop.isOpen()) { - Microbot.status = "Shop interface not open"; - log.error("Shop interface failed to open for " + primaryShopItem.getShopNpcName()); - return false; // Exit if shop interface failed to open - } - - // Process each item that still needs to be purchased - List pendingItems = shopItemRequirements.values().stream() - .filter(itemReq -> !itemReq.isCompleted()) - .collect(Collectors.toList()); - - if (pendingItems.isEmpty()) { - Rs2Shop.closeShop(); - break; - } - - boolean needWorldHop = false; - boolean needBanking = false; - boolean purchasedAnything = false; - - int currentStock = 0; - for (ShopItemRequirement itemReq : pendingItems) { - if ( scheduledFuture != null && scheduledFuture.isCancelled()) { - Microbot.status = "Task cancelled, stopping shop purchases"; - log.info("Shop purchase task cancelled, exiting"); - return false; // Exit if task was cancelled - } - // Track initial inventory count before attempting purchase - int initialItemCount = Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()); - - if (itemReq.isCompleted()) { - log.info("Skipping completed item: " + itemReq.getItemName()); - continue; // Skip completed items - } - - - // Get current stock level from the shop interface (real-time check) - currentStock = getShopStock(itemReq.getItemName()); - - if (currentStock == -1) { - Microbot.status = itemReq.getItemName() + " not found in shop"; - log.error("Shop item not found: " + itemReq.getItemName()); - Rs2Shop.closeShop(); - return false; - } - if (itemReq.allowedToBuy(currentStock) ==0){ - log.error("We can't fulfill buy operation,when minimum stock requirement is zero, we cant buy items when the minium stock is zero for: " + itemReq.getItemName()); - return false; // Skip items with zero minimum stock requirement - } - // Check if stock is sufficient using new unified logic - if (!itemReq.canProcessInShop(currentStock, ShopOperation.BUY)) { - Microbot.status = "Insufficient stock for " + itemReq.getItemName() + - " (current: " + currentStock + ", minimum: " + itemReq.getMinimumStockForBuying() + ")"; - needWorldHop = true; - log.warn("Insufficient stock for " + itemReq.getItemName() + - " in shop " + primaryShopItem.getShopNpcName() + - " (current: " + currentStock + ", minimum: " + itemReq.getMinimumStockForBuying() + ")"); - continue; // Check other items - } - - // **STOCK MANAGEMENT**: Calculate quantity using unified logic - int quantityThisVisit = itemReq.getQuantityForCurrentVisit(currentStock, ShopOperation.BUY); - log.info(" Calculated quantity to buy for \n\t{}: {}", itemReq.getItemName(), quantityThisVisit); - quantityThisVisit = Math.min(quantityThisVisit, currentStock); - - // Check if item is stackable to determine inventory limit - boolean isStackable = itemReq.getShopItem().getItemComposition() != null && - itemReq.getShopItem().getItemComposition().isStackable(); - if (!isStackable) { - // For non-stackable items, limit by available inventory space - int freeSlots = 28 - Rs2Inventory.count(); - quantityThisVisit = Math.min(quantityThisVisit, freeSlots); - } - - if (quantityThisVisit <= 0) { - needBanking = true; - if (!isStackable && Rs2Inventory.count() >= 28) { - Microbot.status = "Inventory full - banking non-stackable items"; - Rs2Shop.closeShop(); - if (enableBanking) { - log.info( "Banking items to make space for more purchases"); - break; // Restart the shop visit after banking - } - log.error("would not be able to buy " + itemReq.getItemName() + - " due to insufficient inventory space, banking not enabled"); - return false; - } - continue; // Can't buy this item right now, check next - } - - // Purchase items using optimal buying method - boolean purchaseSuccessful = false; - try { - Microbot.status = "Purchasing " + quantityThisVisit + "x " + itemReq.getItemName() + - " (" + itemReq.getCompletedAmount() + "/" + itemReq.getAmount() + " total)"; - - purchaseSuccessful = Rs2Shop.buyItem(itemReq.getItemName(), String.valueOf(quantityThisVisit)); - if (purchaseSuccessful) { - // Wait for purchase to complete - sleepUntil(() -> Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()) > initialItemCount, 3000); - - // Calculate how many items were actually purchased - int finalItemCount = Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()); - int itemsPurchased = finalItemCount - initialItemCount; - - // Update completed amount with actual purchased quantity - itemReq.addCompletedAmount(itemsPurchased); - purchasedAnything = true; - - log.info("Purchased {} items of {}, new completion: {}/{}", - itemsPurchased, itemReq.getItemName(), itemReq.getCompletedAmount(), itemReq.getAmount()); - - Microbot.status = "Successfully purchased " + itemsPurchased + "x " + itemReq.getItemName(); - }else { - Microbot.status = "Failed to purchase " + itemReq.getItemName(); - log.error("Purchase failed for " + itemReq.getItemName()); - } - - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.buyFromRegularShop - Purchase failed for " + itemReq.getItemName(), e); - purchaseSuccessful = false; - } - - // Brief pause between item purchases - if (purchaseSuccessful) { - sleepGaussian(Constants.GAME_TICK_LENGTH, 300); - } - } - - Rs2Shop.closeShop(); - if (purchasedAnything) { - successiveWorldHopAttempts = successiveWorldHopAttempts > 0 ? successiveWorldHopAttempts-1 : 0; // Reset counter if we successfully purchased items - } else { - successiveWorldHopAttempts++; - } - - // Handle world hopping if needed - if (needWorldHop && enableWorldHopping && primaryShopItem.getShopType().supportsWorldHopping()) { - log.info("World hopping due to insufficient stock in shop"); - - // Use enhanced world hopping with retry mechanism - boolean hopSuccess = false; - - if (useNextWorld || worldHoppingConfig.isUseSequentialWorlds()) { - // Try specific world selection first - int world = LoginManager.getNextWorld(Rs2Player.isMember()); - if (world != -1 && !excludedWorlds.contains(world)) { - hopSuccess = Rs2WorldUtil.hopWorld(scheduledFuture, world, successiveWorldHopAttempts, worldHoppingConfig); - if (!hopSuccess) { - excludedWorlds.add(world); // Mark this world as problematic - } - } - } - - // If specific world hop failed or not using sequential, try enhanced world hopping - if (!hopSuccess) { - hopSuccess = Rs2WorldUtil.hopToNextBestWorld(scheduledFuture, - successiveWorldHopAttempts, - worldHoppingConfig, - excludedWorlds); - } - - if (hopSuccess) { - log.info("World hop successful after insufficient stock in shop"); - continue; - } else { - log.error("Failed to hop to a new world after insufficient stock in shop"); - return false; - } - } - - // Check if we should bank items - if (needBanking && enableBanking && Rs2Inventory.count() > 20) { - if (!bankItems()) { - Microbot.status = "Failed to bank items - continuing without banking"; - log.error("Failed to bank items, we can not make space for more items"); - return false; // Exit if banking failed - } - continue; // Restart shop visit after banking - } - if (!needBanking && !needWorldHop && !purchasedAnything) { - Microbot.status = "No items purchased this visit - checking next item"; - log.info("No items purchased this visit, we cant hop worlds or bank items, checking next item"); - break; // No items purchased, check next item - } - // Brief pause between shop visits - sleepGaussian(Constants.GAME_TICK_LENGTH*3, Constants.GAME_TICK_LENGTH); - } - - // Final status update - if (isAllItemsCompleted()) { - log.info("Successfully completed purchase of all items from regular shop"); - } else { - log.warn("Purchase incomplete after {} attempts, remaining items: {}", - successiveWorldHopAttempts, - shopItemRequirements.values().stream() - .filter(itemReq -> !itemReq.isCompleted()) - .map(ShopItemRequirement::getItemName) - .collect(Collectors.joining(", "))); - } - - return isAllItemsCompleted(); - - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.buyFromRegularShop", e); - return false; - } - } - - /** - * Enhanced selling method with BanksShopper patterns for multiple items. - * Implements proper while loop logic for selling multiple items across worlds. - * - * @return true if the sale was successful, false otherwise - */ - private boolean sellToRegularShop(CompletableFuture scheduledFuture) { - try { - Microbot.status = "Selling items to " + primaryShopItem.getShopNpcName(); - - int maxAttempts = enableWorldHopping ? worldHoppingConfig.getMaxWorldHops() : 0; // More attempts if world hopping is enabled - int successiveWorldHopAttempts = 0; - - - while (!isAllItemsCompleted() && successiveWorldHopAttempts < maxAttempts) { - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - Microbot.status = "Task cancelled, stopping shop sales"; - log.info("Shop sale task cancelled, exiting"); - return false; // Exit if task was cancelled - } - - // Walk to the shop - if (!Rs2Walker.isInArea(primaryShopItem.getLocationArea().toWorldPointList().toArray(new WorldPoint[0])) - && !Rs2Walker.walkTo(primaryShopItem.getLocation())) { - log.error("\n\tFailed to walk to shop: " + primaryShopItem.getLocation()); - return false; // Exit if walking to shop failed - } - - // Open shop interface - if (!Rs2Shop.openShop(primaryShopItem.getShopNpcName())) { - log.error("\n\tFailed to open shop for selling: " + primaryShopItem.getShopNpcName()); - return false; // Exit if shop interface failed to open - } - - // Wait for shop data to update - check if shop is properly loaded - if (!sleepUntil(() -> Rs2Shop.isOpen(), 3000)) { - log.error("\n\tShop interface failed to stabilize for selling: " + primaryShopItem.getShopNpcName()); - Rs2Shop.closeShop(); - return false; // Exit if shop interface failed to open - } - - // Process each item that still needs to be sold - List pendingItems = shopItemRequirements.values().stream() - .filter(itemReq -> { - // Check if we have items to sell in inventory - int currentInventoryCount = Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()); - return currentInventoryCount > 0 && !itemReq.isCompleted(); - }) - .collect(Collectors.toList()); - - if (pendingItems.isEmpty()) { - Rs2Shop.closeShop(); - break; - } - - boolean needWorldHop = false; - boolean soldAnything = false; - - for (ShopItemRequirement itemReq : pendingItems) { - if(itemReq.isCompleted()){ - continue; // Skip completed items - } - // Check if we still have items to sell - int currentInventoryCount = Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()); - if (currentInventoryCount == 0) { - continue; // No more of this item to sell - } - - // Get current shop stock and check if we can sell safely using unified API - int currentStock = getShopStock(itemReq.getItemName()); - if (currentStock == -1) { - log.error("\n\tShop item not found: " + itemReq.getItemName()); - Rs2Shop.closeShop(); - return false; - } - - // **STOCK MANAGEMENT**: Use unified stock validation - if (!itemReq.canProcessInShop(currentStock, operation)) { - Microbot.status = "Shop stock too high for " + itemReq.getItemName() + - " (current: " + currentStock + ") - cannot sell"; - needWorldHop = true; - continue; // Check other items, which may have lower stock, and we can sell - } - - // **STOCK MANAGEMENT**: Use unified quantity calculation - int quantityThisVisit = itemReq.getQuantityForCurrentVisit(currentStock, ShopOperation.SELL); - quantityThisVisit = Math.min(quantityThisVisit, currentInventoryCount); - - if (quantityThisVisit <= 0) { - continue; // Cannot sell any items this visit, check next item - } - - Microbot.status = "Selling " + quantityThisVisit + "x " + itemReq.getItemName() + - " (shop stock: " + currentStock + ")"; - - // Sell items using inventory method (standard approach for selling) - boolean sellSuccessful = false; - try { - sellSuccessful = Rs2Inventory.sellItem(itemReq.getItemName(), String.valueOf(quantityThisVisit)); - if (sellSuccessful) { - // Wait for sale to complete - sleepUntil(() -> Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()) < currentInventoryCount, 3000); - - soldAnything = true; - Microbot.status = "Successfully sold " + quantityThisVisit + "x " + itemReq.getItemName(); - int slotItems = currentInventoryCount - Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()); - itemReq.addCompletedAmount(slotItems); - } - - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.sellToRegularShop - Sale failed for " + itemReq.getItemName(), e); - sellSuccessful = false; - } - - // Brief pause between item sales - if (sellSuccessful) { - sleepGaussian(900, 300); - } - } - - Rs2Shop.closeShop(); - if (soldAnything){ - successiveWorldHopAttempts = successiveWorldHopAttempts >0 ? 0 : successiveWorldHopAttempts-1; // Reset if we sold items - }else{ - successiveWorldHopAttempts++; // Increment if no items sold - } - // Handle world hopping if needed - if (needWorldHop && enableWorldHopping && primaryShopItem.getShopType().supportsWorldHopping()) { - log.info("World hopping due to high stock levels in shop for selling"); - - // Use enhanced world hopping with retry mechanism - boolean hopSuccess = false; - - if (useNextWorld || worldHoppingConfig.isUseSequentialWorlds()) { - // Try specific world selection first - int world = LoginManager.getNextWorld(Rs2Player.isMember()); - if (world != -1 && !excludedWorlds.contains(world)) { - hopSuccess = Rs2WorldUtil.hopWorld(scheduledFuture, world, successiveWorldHopAttempts, worldHoppingConfig); - if (!hopSuccess) { - excludedWorlds.add(world); // Mark this world as problematic - } - } - } - - // If specific world hop failed or not using sequential, try enhanced world hopping - if (!hopSuccess) { - hopSuccess = Rs2WorldUtil.hopToNextBestWorld(scheduledFuture, successiveWorldHopAttempts, worldHoppingConfig, excludedWorlds); - } - - if (hopSuccess) { - log.info("World hop successful after high stock levels in shop"); - continue; - } else { - Microbot.status = "Failed to hop worlds - shop stock too high"; - log.error("Failed to hop to a new world due to high stock levels in shop"); - return false; - } - } - // Brief pause between shop visits - sleep(1000, 2000); - } - - // Final status update - if (isAllItemsCompleted()) { - log.info("Successfully completed sale of all items to regular shop"); - } else { - log.warn("Sale incomplete after {} attempts, remaining items: {}", - successiveWorldHopAttempts, - shopItemRequirements.values().stream() - .filter(itemReq -> !itemReq.isCompleted()) - .map(ShopItemRequirement::getItemName) - .collect(Collectors.joining(", "))); - - } - - return isAllItemsCompleted(); - - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.sellToRegularShop", e); - return false; - } - } - - /** - * Banks purchased items for multiple shop items if banking is enabled. - * - * @return true if banking was successful, false otherwise - */ - private boolean bankItems() { - try { - Microbot.status = "Banking purchased items"; - - // Get all item names from our shop requirements - List itemNames = shopItemRequirements.keySet().stream() - .map(Rs2ShopItem::getItemName) - .collect(Collectors.toList()); - - // Use Rs2Bank utility for banking - boolean success = Rs2Bank.bankItemsAndWalkBackToOriginalPosition( - itemNames, - primaryShopItem.getLocation() - ); - - if (success) { - Microbot.status = "Successfully banked items"; - } else { - Microbot.status = "Failed to bank items"; - } - - return success; - - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.bankItems", e); - return false; - } - } - - - - /** - * Calculates the total cost for purchasing all required items with dynamic pricing. - * Uses Rs2ShopItem dynamic pricing calculations based on stock levels. - * - * @return The total cost in coins, or -1 if calculation failed - */ - private int calculateTotalCost() { - if (shopItemRequirements.isEmpty()) { - return -1; - } - - try { - int totalCost = 0; - - for (ShopItemRequirement itemReq : shopItemRequirements.values()) { - if (itemReq.isCompleted()) { - continue; // Skip completed items - } - - int currentCount = Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()) + - Rs2Bank.count(itemReq.getShopItem().getItemId()); - int amountToBuy = Math.max(0, itemReq.getAmount() - currentCount); - - if (amountToBuy > 0) { - int itemCost = itemReq.getShopItem().getCostForBuyingX(itemReq.getAmount(), amountToBuy); - if (itemCost > 0) { - totalCost += itemCost; - } - } - } - - return totalCost; - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.calculateTotalCost", e); - return -1; - } - } - - /** - * Calculates the total value for selling all items with dynamic pricing. - * Uses Rs2ShopItem dynamic pricing calculations based on stock levels. - * - * @return The total sell value in coins, or -1 if calculation failed - */ - @SuppressWarnings("unused") - private int calculateTotalSellValue() { - try { - int totalValue = 0; - - for (ShopItemRequirement itemReq : shopItemRequirements.values()) { - int currentCount = Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()); - - if (currentCount > 0) { - int itemValue = itemReq.getShopItem().getReturnForSellingX(itemReq.getAmount(), currentCount); - if (itemValue > 0) { - totalValue += itemValue; - } - } - } - - return totalValue; - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.calculateTotalSellValue", e); - return -1; - } - } - - /** - * Estimates the number of world hops needed based on stock levels and requirements for multiple items. - * - * @return Estimated world hops needed, or -1 if cannot estimate - */ - public boolean isFulfilled() { - // Check if all items are completed - return shopItemRequirements.values().stream().allMatch(ShopItemRequirement::isCompleted); - } - /** - * Implements the abstract fulfillRequirement method from the base Requirement class. - * Handles both buying and selling operations based on the operation type. - * - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if the requirement was successfully fulfilled, false otherwise - */ - @Override - public boolean fulfillRequirement(CompletableFuture scheduledFuture) { - try { - if (Microbot.getClient().isClientThread()) { - Microbot.log("Please run fulfillRequirement() on a non-client thread.", Level.ERROR); - return false; - } - - // Reset excluded worlds at the start of each fulfillment attempt - resetExcludedWorlds(); - - Microbot.status = "Fulfilling shop requirement: " + operation.name() + " " + getName(); - boolean success = false; - if (isFulfilled()) { - Microbot.status = "Shop requirement not fulfilled, proceeding with " + operation.name(); - log.info("Shop requirement already fulfilled: " + getName() + " (" + operation + ")"); - return true; // Already fulfilled, no need to proceed - } - - // Validate items can be sold (are tradeable) - for (ShopItemRequirement itemReq : shopItemRequirements.values()) { - boolean isAccessible = itemReq.getShopItem().canAccess(); - if (!itemReq.getShopItem().getItemComposition().isTradeable() || !isAccessible) { - Microbot.log("Item " + itemReq.getItemName() + " is not tradeable - cannot sell to shop or shop could not be accessed"); - return !isMandatory(); - } - //updateCompletedAmount(itemReq); - } - switch (operation) { - case BUY: - success = handleBuyOperation(scheduledFuture); - break; - case SELL: - success = handleSellOperation(scheduledFuture); - break; - default: - Microbot.log("Unknown shop operation: " + operation); - return false; - } - - if (!success && isMandatory()) { - Microbot.log("MANDATORY shop requirement failed: " + getName() + " (" + operation + ")"); - return false; - } - - return true; - - } catch (Exception e) { - Microbot.log("Error fulfilling shop requirement " + getName() + ": " + e.getMessage()); - return !isMandatory(); // Don't fail mandatory requirements due to exceptions - } - } - - /** - * Handles buy operations with proper separation between Grand Exchange and regular shops. - * - * @return true if the buy operation was successful, false otherwise - */ - private boolean handleBuyOperation(CompletableFuture scheduledFuture) { - try { - // Check if we already have enough items - if (isAllItemsCompleted()) { - Microbot.status = "Already have required amount of all items"; - log.info("All items already completed for shop requirement: " + getName()); - return true; - } - // Handle Grand Exchange vs Regular Shop differently - if (primaryShopItem.getShopType() == Rs2ShopType.GRAND_EXCHANGE) { - log.info("Handling Grand Exchange buy operation for shop requirement: " + getName()); - return handleGrandExchangeBuyOperation(scheduledFuture); - } else { - log.info("Handling regular shop buy operation for shop requirement: " + getName()); - return handleRegularShopBuyOperation(scheduledFuture); - } - - } catch (Exception e) { - Microbot.log("Error in buy operation: " + e.getMessage()); - return false; - } - } - - /** - * Handles buying multiple items from Grand Exchange - must wait for offers to complete. - */ - private boolean handleGrandExchangeBuyOperation(CompletableFuture scheduledFuture) { - try { - Microbot.status = "Buying items from Grand Exchange"; - - // Get coins from bank if needed - if (enableBanking && !ensureSufficientCoins()) { - Microbot.status = "Failed to get sufficient coins for Grand Exchange"; - log.info("Insufficient coins for Grand Exchange purchase: " + getName()); - return isMandatory() ? false : true; - } - - // Walk to Grand Exchange - if (!Rs2Walker.walkTo(primaryShopItem.getLocation(), primaryShopItem.getLocationArea().getHeight())) { - Microbot.status = "Failed to walk to Grand Exchange"; - log.info("Failed to walk to Grand Exchange for shop requirement: " + getName()); - return false; - } - - // Calculate total cost for all items - int totalCost = calculateTotalCost(); - - if (Rs2Inventory.itemQuantity("Coins") < totalCost) { - Microbot.status = "Insufficient coins for Grand Exchange purchase"; - return !isMandatory(); - } - - // Use the enhanced buyFromGrandExchange method that handles multiple items - return buyFromGrandExchange(scheduledFuture); - - } catch (Exception e) { - Microbot.log("Error in Grand Exchange buy operation: " + e.getMessage()); - return false; - } - } - - /** - * Handles buying multiple items from regular shops with stock management and world hopping. - */ - private boolean handleRegularShopBuyOperation(CompletableFuture scheduledFuture) { - try { - // Get coins from bank if needed - if (enableBanking && !ensureSufficientCoins()) { - Microbot.status = "Failed to get sufficient coins for shop purchase"; - return isMandatory() ? false : true; - } - - // For multi-item regular shop purchases, use the enhanced buyFromRegularShop method - return buyFromRegularShop(scheduledFuture); - - } catch (Exception e) { - Microbot.log("Error in regular shop buy operation: " + e.getMessage()); - return false; - } - } - - /** - * Handles sell operations with proper separation between Grand Exchange and regular shops. - * - * @return true if the sell operation was successful, false otherwise - */ - private boolean handleSellOperation( CompletableFuture scheduledFuture) { - try { - - - // Handle Grand Exchange vs Regular Shop differently - if (primaryShopItem.getShopType() == Rs2ShopType.GRAND_EXCHANGE) { - return handleGrandExchangeSellOperation(); - } else { - return handleRegularShopSellOperation(scheduledFuture); - } - - } catch (Exception e) { - Microbot.log("Error in sell operation for " + getName() + ": " + e.getMessage()); - return false; - } - } - - /** - * Handles selling to Grand Exchange - must wait for offers to complete. - */ - private boolean handleGrandExchangeSellOperation() { - return sellToGrandExchange(); - } - - /** - * Handles selling to regular shops with stock management and world hopping. - */ - private boolean handleRegularShopSellOperation(CompletableFuture scheduledFuture) { - try { - // Get items from bank at the beginning if banking is enabled - if (enableBanking) { - if (!getItemsFromBankForSelling()) { - Microbot.status = "Failed to get items from bank for selling"; - return !isMandatory(); // Not an error for optional requirements - } - } - - // Check if we have any items to sell in inventory - boolean hasItemsToSell = shopItemRequirements.values().stream() - .anyMatch(itemReq -> Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()) > 0); - - if (!hasItemsToSell) { - Microbot.status = "No items to sell"; - return !isMandatory(); // Not an error for optional requirements - } - - // Check if any items are noted and handle them appropriately - for (ShopItemRequirement itemReq : shopItemRequirements.values()) { - boolean hasNotedItems = Rs2Inventory.hasNotedItem(itemReq.getItemName()); - boolean isNoteable = itemReq.getShopItem().isNoteable(); - - if (hasNotedItems) { - if (handleNotedItems && !isNoteable) { - // Items are noted but shouldn't be - this is an error state - Microbot.log("Item " + itemReq.getItemName() + " is noted but is not noteable - inconsistent state"); - return false; - } - - if (!handleNotedItems && !unnoteItemsForSelling()) { - Microbot.status = "Failed to unnote " + itemReq.getItemName() + " for selling"; - return false; - } - } - } - - // Perform the sale to regular shop - boolean sellSuccess = sellToRegularShop(scheduledFuture); - - // After selling, bank coins if banking is enabled - if (sellSuccess && enableBanking) { - if (!bankCoinsAfterSelling()) { - Microbot.status = "Sale successful but failed to bank coins"; - // Don't fail the requirement just because banking failed - } - } - - return sellSuccess; - - } catch (Exception e) { - Microbot.log("Error in regular shop sell operation: " + e.getMessage()); - return false; - } - } - - /** - * Ensures the player has sufficient coins for buying operations. - * - * @return true if sufficient coins are available, false otherwise - */ - private boolean ensureSufficientCoins() { - try { - int totalCost = calculateTotalCost(); - if (totalCost <= 0) { - return true; // No cost or calculation failed - } - - int currentCoins = Rs2Inventory.itemQuantity("Coins") + Rs2Bank.count("Coins"); - if (currentCoins >= totalCost) { - return true; // Already have enough coins - } - - Microbot.status = "Getting coins from bank for " + getName(); - Rs2Bank.walkToBankAndUseBank(); - if (!Rs2Bank.isOpen()) { - Microbot.status = "Failed to open bank for coins"; - return false; - } - - int neededCoins = totalCost - currentCoins; - int bankCoins = Rs2Bank.count("Coins"); - - if (bankCoins < neededCoins) { - Microbot.status = "Insufficient coins in bank. Need " + neededCoins + ", have " + bankCoins; - Rs2Bank.closeBank(); - return false; - } - - Rs2Bank.withdrawX("Coins", neededCoins); - sleepUntil(() -> Rs2Inventory.itemQuantity("Coins") >= totalCost, 5000); - - Rs2Bank.closeBank(); - return Rs2Inventory.itemQuantity("Coins") >= totalCost; - - } catch (Exception e) { - Microbot.log("Error ensuring sufficient coins: " + e.getMessage()); - Rs2Bank.closeBank(); - return false; - } - } - - /** - * Enhanced banking method for selling operations with comprehensive item handling. - * Properly handles stackable/non-stackable and noted/unnoted items. - * - * @return true if items were successfully retrieved, false otherwise - */ - private boolean getItemsFromBankForSelling() { - try { - Microbot.status = "Getting items from bank for selling with enhanced logic"; - - if (!Rs2Bank.openBank()) { - Microbot.status = "Failed to open bank for items"; - return false; - } - - // **BANKING IMPROVEMENT**: Calculate inventory space needed for non-stackable items - int inventorySpaceNeeded = 0; - List itemsNeedingWithdrawal = new ArrayList<>(); - - for (ShopItemRequirement itemReq : shopItemRequirements.values()) { - int currentCount = Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()); - int neededCount = itemReq.getAmount() - currentCount; - - if (neededCount <= 0) { - continue; // Already have enough of this item - } - - // Check if item is available in bank - if (!Rs2Bank.hasItem(itemReq.getItemName())) { - Microbot.status = "Bank does not contain " + itemReq.getItemName() + ", cannot sell"; - continue; - } - - itemsNeedingWithdrawal.add(itemReq); - - // **BANKING IMPROVEMENT**: Count space needed for non-stackable items - boolean isStackable = itemReq.getShopItem().getItemComposition() != null && - itemReq.getShopItem().getItemComposition().isStackable(); - if (!isStackable && !itemReq.getShopItem().isNoteable()) { - inventorySpaceNeeded += Math.min(neededCount, 28); // Cap at inventory limit - } else { - inventorySpaceNeeded += 1; // Stackable or noted items take 1 slot - } - } - - // **BANKING IMPROVEMENT**: Check if we have enough inventory space - int currentInventoryCount = Rs2Inventory.count(); - int availableSpace = 28 - currentInventoryCount; - - if (inventorySpaceNeeded > availableSpace) { - Microbot.status = "Insufficient inventory space: need " + inventorySpaceNeeded + - ", have " + availableSpace + " - depositing items first"; - - // Deposit non-essential items to make space - Rs2Bank.depositAllExcept("Coins"); // Keep coins for potential fees - sleepUntil(() -> Rs2Inventory.count() <= 1, 3000); // Should only have coins - } - - boolean allItemsRetrieved = true; - - for (ShopItemRequirement itemReq : itemsNeedingWithdrawal) { - int currentCount = Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()); - int neededCount = itemReq.getAmount() - currentCount; - - // **BANKING IMPROVEMENT**: Smart withdrawal based on item properties - boolean isStackable = itemReq.getShopItem().getItemComposition() != null && - itemReq.getShopItem().getItemComposition().isStackable(); - boolean isNoteable = itemReq.getShopItem().isNoteable(); - - if (isNoteable && handleNotedItems) { - // **BANKING IMPROVEMENT**: Use noted items for efficient selling - Microbot.status = "Withdrawing " + neededCount + "x " + itemReq.getItemName() + " as noted"; - Rs2Bank.setWithdrawAs(true); // Withdraw as noted - Rs2Bank.withdrawX(itemReq.getShopItem().getItemName(), neededCount); - sleepUntil(() -> Rs2Inventory.itemQuantity(itemReq.getShopItem().getNoteId()) >= neededCount, 5000); - } else if (isStackable) { - // **BANKING IMPROVEMENT**: Stackable items can be withdrawn in full - Microbot.status = "Withdrawing " + neededCount + "x " + itemReq.getItemName() + " (stackable)"; - Rs2Bank.setWithdrawAs(false); // Withdraw as unnoted - Rs2Bank.withdrawX(itemReq.getShopItem().getItemName(), neededCount); - sleepUntil(() -> Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()) >= neededCount, 5000); - } else { - // **BANKING IMPROVEMENT**: Non-stackable items need careful space management - int availableSlots = 28 - Rs2Inventory.count(); - int withdrawAmount = Math.min(neededCount, availableSlots); - - if (withdrawAmount <= 0) { - Microbot.status = "No inventory space for non-stackable " + itemReq.getItemName(); - allItemsRetrieved = false; - continue; - } - - Microbot.status = "Withdrawing " + withdrawAmount + "x " + itemReq.getItemName() + - " (non-stackable, space limited)"; - Rs2Bank.setWithdrawAs(false); // Withdraw as unnoted - Rs2Bank.withdrawX(itemReq.getShopItem().getItemName(), withdrawAmount); - sleepUntil(() -> Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()) >= - (currentCount + withdrawAmount), 5000); - } - - // Brief pause between withdrawals - sleep(500, 200); - } - - Rs2Bank.setWithdrawAs(false); // Reset to default - Rs2Bank.closeBank(); - - return allItemsRetrieved; - - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.getItemsFromBankForSelling", e); - Rs2Bank.closeBank(); - return false; - } - } - - - /** - * Unnotes items for selling if they are noted and shop doesn't accept noted items. - * - * @return true if items were successfully unnoted, false otherwise - */ - private boolean unnoteItemsForSelling() { - try { - Microbot.status = "Unnoting items for selling"; - - boolean allItemsUnnoted = true; - - for (ShopItemRequirement itemReq : shopItemRequirements.values()) { - // Check if we have noted items for this shop item - int notedId = itemReq.getShopItem().getNoteId(); - if (notedId == -1 || !Rs2Inventory.hasItem(notedId)) { - continue; // No noted items or item is not noteable - } - - // For unnoting, we typically need to use the items on a banker or shop keeper - // This is a simplified implementation - in practice you would: - // 1. Find the nearest banker or the shop keeper - // 2. Use the noted items on them to unnote - // 3. Wait for the unnoting to complete - - // For now, we'll try to use the shop keeper from our primary shop source - if (primaryShopItem != null && primaryShopItem.getShopNPC() != null) { - // Walk to shop keeper - if (!Rs2Walker.walkTo(primaryShopItem.getLocation())) { - Microbot.status = "Failed to walk to shop keeper for unnoting " + itemReq.getItemName(); - allItemsUnnoted = false; - continue; - } - - // Find the shop keeper NPC - NPC shopKeeperNpc = Rs2Npc.getNpc(primaryShopItem.getShopNpcName()); - if (shopKeeperNpc == null) { - Microbot.status = "Shop keeper not found for unnoting " + itemReq.getItemName(); - allItemsUnnoted = false; - continue; - } - - Rs2NpcModel shopKeeper = new Rs2NpcModel(shopKeeperNpc); - - // Use noted items on shop keeper (this may need adjustment based on actual game mechanics) - if (Rs2Inventory.use(notedId)) { - sleep(600); // Game tick - if (Rs2Npc.interact(shopKeeper, "Use")) { - sleepUntil(() -> !Rs2Inventory.hasItem(notedId) || Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()) > 0, 5000); - - if (Rs2Inventory.hasItem(notedId)) { - Microbot.log("Failed to unnote " + itemReq.getItemName()); - allItemsUnnoted = false; - } - } else { - allItemsUnnoted = false; - } - } else { - allItemsUnnoted = false; - } - } - } - - if (!allItemsUnnoted) { - Microbot.log("Unnoting feature needs specific implementation for some items"); - } - - return allItemsUnnoted; // Return success status based on all items - - } catch (Exception e) { - Microbot.log("Error unnoting items for selling: " + e.getMessage()); - return false; - } - } - - /** - * Banks coins after selling items. - * - * @return true if coins were successfully banked, false otherwise - */ - private boolean bankCoinsAfterSelling() { - try { - Microbot.status = "Banking coins after selling items"; - - if (!Rs2Bank.isNearBank(10)) { - Rs2Bank.walkToBank(); - } - if (!Rs2Bank.openBank()) { - Microbot.status = "Failed to open bank for banking coins"; - return false; - } - - Rs2Bank.depositAll("Coins"); - sleepUntil(() -> Rs2Inventory.itemQuantity("Coins") == 0, 5000); - - Rs2Bank.closeBank(); - return true; - - } catch (Exception e) { - Microbot.log("Error banking coins after selling: " + e.getMessage()); - Rs2Bank.closeBank(); - return false; - } - } - - /** - * Factory method to create a multi-item shop requirement for the same shop. - * All items must be from the same shop (same location and NPC). - * - * @param primaryShopItem The primary shop item (used for shop location/NPC validation) - * @param itemRequirements Map of item IDs to their amounts and individual settings - * @param operation Shop operation type (BUY or SELL) - * @param requirementType Where this item should be located - * @param priority Priority level of this item for plugin functionality - * @param rating Effectiveness rating from 1-10 - * @param description Human-readable description - * @param TaskContext When this requirement should be fulfilled - */ - public static ShopRequirement createMultiItemRequirement( - Rs2ShopItem primaryShopItem, - Map itemRequirements, - ShopOperation operation, - RequirementType requirementType, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext) { - - Map shopItems = new HashMap<>(); - - for (Map.Entry entry : itemRequirements.entrySet()) { - Rs2ShopItem shopItem = entry.getKey(); - MultiItemConfig config = entry.getValue(); - - shopItems.put(shopItem, new ShopItemRequirement( - shopItem, - config.amount, - config.stockTolerance // **UNIFIED SYSTEM**: Use stockTolerance directly - )); - } - - return new ShopRequirement(shopItems, operation, requirementType, priority, rating, description, taskContext); - } - - - - /** - * Checks if the player has enough coins to buy all required shop items. - * - * @return true if the player has sufficient funds, false otherwise - */ - public boolean hasSufficientCoinsForRequirements() { - int totalCost = calculateTotalCost(); - if (totalCost == -1) { - Microbot.log("Failed to calculate total cost for shop requirements"); - return false; - } - - if (totalCost <= 0) { - return true; - } - final List coinIDs = Arrays.asList(ItemID.COINS, ItemID.COINS_2, ItemID.COINS_3, ItemID.COINS_4,ItemID.COINS_5, ItemID.COINS_25, ItemID.COINS_100, ItemID.COINS_250,ItemID.COINS_1000, ItemID.COINS_10000); - - - - // Check if player has enough coins in inventory - int inventoryCoins =Rs2Inventory.itemQuantity(item -> coinIDs.contains(item.getId())); - boolean hasCoinsInInventory = inventoryCoins >= totalCost; - - if (hasCoinsInInventory) { - return true; - } - - // If not in inventory, check bank - if (Rs2Bank.isOpen() || Rs2Bank.openBank()) { - int bankCoins = Rs2Bank.count("Coins"); - boolean hasCoinsInBank = bankCoins >= (totalCost - inventoryCoins); - - if (hasCoinsInBank) { - // We need to use custom withdraw logic since Rs2Bank doesn't have a method - // to withdraw a specific amount directly - try { - Rs2Bank.withdrawX("Coins", totalCost - inventoryCoins); - - // Check if we now have enough coins - sleepUntil(() -> Rs2Inventory.itemQuantity(item -> coinIDs.contains(item.getId())) >= totalCost, 5000); - return Rs2Inventory.itemQuantity(item -> coinIDs.contains(item.getId())) >= totalCost; - } catch (Exception e) { - Microbot.log("Failed to withdraw coins: " + e.getMessage()); - return false; - } - } - } - - Microbot.log("Insufficient coins for shop requirements. Need " + totalCost + " coins."); - return false; - } - - /** - * Gets the current stock quantity for a given item name in the shop - * @param itemName The name of the item to check stock for - * @return The current stock quantity, or -1 if not found - */ - private int getShopStock(String itemName) { - if (Rs2Shop.shopItems == null || Rs2Shop.shopItems.isEmpty()) { - return -1; - } - - for (Rs2ItemModel item : Rs2Shop.shopItems) { - if (item.getName().equalsIgnoreCase(itemName)) { - return item.getQuantity(); - } - } - return -1; - } - - - - // ===== GRAND EXCHANGE SLOT MANAGEMENT ===== - - /** - * Enhanced Grand Exchange slot management with state tracking and recovery. - * Only clears the minimum number of slots needed and tracks cancelled offers for restoration. - * Uses proper game state checking and sleepUntil patterns for reliability. - * - * @param requiredSlots Number of slots needed for pending operations - * @return true if sufficient slots can be made available, false otherwise - */ - private boolean ensureGrandExchangeSlots(int requiredSlots) { - try { - if (!Rs2GrandExchange.isOpen()) { - Microbot.status = "Grand Exchange not open, cannot manage slots"; - return false; - } - - // Check current slot availability - int availableSlots = Rs2GrandExchange.getAvailableSlotsCount(); - - if (availableSlots >= requiredSlots) { - Microbot.status = "Sufficient GE slots available: " + availableSlots + "/" + requiredSlots; - return true; - } - - Microbot.status = "Need " + requiredSlots + " slots, have " + availableSlots + " - optimizing slot usage"; - - // Step 1: Try to collect completed offers first (most efficient approach) - if (Rs2GrandExchange.hasBoughtOffer() || Rs2GrandExchange.hasSoldOffer()) { - Microbot.status = "Collecting completed offers to free slots"; - Rs2GrandExchange.collectAll(enableBanking); - - // Wait for collection to complete and slots to update - boolean collectionSuccess = sleepUntil(() -> { - return Rs2GrandExchange.getAvailableSlotsCount() >= requiredSlots; - }, 8000); - - if (collectionSuccess) { - Microbot.status = "Freed sufficient slots by collecting completed offers"; - return true; - } - } - - // Step 2: If still insufficient, selectively cancel offers with least progress - int currentAvailable = Rs2GrandExchange.getAvailableSlotsCount(); - int slotsStillNeeded = requiredSlots - currentAvailable; - - if (slotsStillNeeded > 0) { - Microbot.status = "Selectively cancelling " + slotsStillNeeded + " offers with least progress"; - - // Get active offers sorted by progress (least progress first) - List activeSlots = Rs2GrandExchange.getActiveOfferSlotsByProgress(); - - if (activeSlots.size() < slotsStillNeeded) { - log.warn("Not enough active offers to cancel ({} available, need {})", - activeSlots.size(), slotsStillNeeded); - return false; - } - - // Cancel only the required number of slots with least progress - List slotsToCancel = activeSlots.subList(0, slotsStillNeeded); - List> cancelledDetails = Rs2GrandExchange.cancelSpecificOffers(slotsToCancel, enableBanking); - - // Track cancelled offers for potential recovery - trackCancelledOffers(cancelledDetails); - - // Wait for cancellations to complete and slots to become available - boolean cancellationSuccess = sleepUntil(() -> { - return Rs2GrandExchange.getAvailableSlotsCount() >= requiredSlots; - }, 12000); - - if (!cancellationSuccess) { - log.warn("Timeout waiting for slot cancellations to complete"); - return false; - } - } - - // Final verification - int finalAvailable = Rs2GrandExchange.getAvailableSlotsCount(); - boolean success = finalAvailable >= requiredSlots; - - if (success) { - Microbot.status = "Successfully allocated " + finalAvailable + " GE slots"; - if (!cancelledOffers.isEmpty()) { - log.info("Cancelled {} offers for slot allocation", cancelledOffers.size()); - } - } else { - log.error("Failed to allocate sufficient GE slots: need {}, have {}", - requiredSlots, finalAvailable); - } - - return success; - - } catch (Exception e) { - log.error("Error in ensureGrandExchangeSlots: {}", e.getMessage()); - Microbot.logStackTrace("ShopRequirement.ensureGrandExchangeSlots", e); - return false; - } - } - - /** - * Tracks cancelled offers for potential restoration. - * Only tracks offers that were actively BUYING or SELLING (not empty or completed). - * Converts from Rs2GrandExchange format to our CancelledOfferState objects. - * - * @param cancelledDetails List of cancelled offer details from Rs2GrandExchange - */ - private void trackCancelledOffers(List> cancelledDetails) { - for (Map details : cancelledDetails) { - try { - int itemId = (Integer) details.get("itemId"); - String itemName = details.containsKey("itemName") - ? (String) details.get("itemName") - : Microbot.getClient().getItemDefinition(itemId).getName(); - - int totalQuantity = (Integer) details.get("totalQuantity"); - int remainingQuantity = (Integer) details.get("remainingQuantity"); - int price = (Integer) details.get("price"); - boolean isBuyOffer = (Boolean) details.get("isBuyOffer"); - GrandExchangeSlots originalSlot = (GrandExchangeSlots) details.get("slot"); - - // Verify this was an active offer worth tracking - if (remainingQuantity > 0 && price > 0 && itemId > 0) { - CancelledOfferState cancelledOffer = new CancelledOfferState( - itemId, itemName, totalQuantity, remainingQuantity, - price, isBuyOffer, originalSlot - ); - - cancelledOffers.add(cancelledOffer); - log.info("Tracked cancelled {} offer for recovery: {} ({} items remaining at {} gp)", - isBuyOffer ? "BUY" : "SELL", itemName, remainingQuantity, price); - } else { - log.debug("Skipping tracking of empty/invalid offer: itemId={}, remaining={}, price={}", - itemId, remainingQuantity, price); - } - - } catch (Exception e) { - log.warn("Failed to track cancelled offer details: {}", e.getMessage()); - } - } - } - - /** - * Attempts to restore previously cancelled Grand Exchange offers. - * This should be called after completing buy/sell operations. - * Uses proper game state checking and sleepUntil patterns. - * - * @return Number of offers successfully restored - */ - private int restoreCancelledOffers() { - if (cancelledOffers.isEmpty()) { - return 0; - } - - try { - if (!Rs2GrandExchange.isOpen()) { - log.warn("Grand Exchange not open, cannot restore cancelled offers"); - return 0; - } - - Microbot.status = "Attempting to restore " + cancelledOffers.size() + " cancelled GE offers"; - log.info("Attempting to restore {} cancelled offers", cancelledOffers.size()); - - int restoredCount = 0; - Iterator iterator = cancelledOffers.iterator(); - - while (iterator.hasNext()) { - CancelledOfferState cancelledOffer = iterator.next(); - - // Skip offers that are no longer worth restoring - if (!cancelledOffer.isWorthRestoring()) { - log.debug("Skipping offer not worth restoring: {}", cancelledOffer); - iterator.remove(); - continue; - } - - // Check if we have available slots - if (Rs2GrandExchange.getAvailableSlotsCount() == 0) { - log.info("No available slots for restoration, stopping here"); - break; - } - - // Convert to the map format expected by restoreOffer - Map offerDetails = new HashMap<>(); - offerDetails.put("itemId", cancelledOffer.getItemId()); - offerDetails.put("itemName", cancelledOffer.getItemName()); - offerDetails.put("remainingQuantity", cancelledOffer.getRemainingQuantity()); - offerDetails.put("price", cancelledOffer.getPrice()); - offerDetails.put("isBuyOffer", cancelledOffer.isBuyOffer()); - - // Check if original slot is available, otherwise use any available slot - GrandExchangeSlots targetSlot = Rs2GrandExchange.isSlotAvailable(cancelledOffer.getOriginalSlot()) - ? cancelledOffer.getOriginalSlot() : null; - - Microbot.status = "Restoring " + cancelledOffer.getOperationType() + " offer for " + cancelledOffer.getItemName(); - - // Attempt to restore the offer - boolean restored = Rs2GrandExchange.restoreOffer(offerDetails, targetSlot); - - if (restored) { - // Wait for offer to be placed successfully - boolean offerPlaced = sleepUntil(() -> { - return Rs2GrandExchange.getAvailableSlotsCount() < Rs2GrandExchange.getAvailableSlotsCount() + 1; - }, 5000); - - if (offerPlaced) { - restoredCount++; - iterator.remove(); // Remove from our tracking list - log.info("Successfully restored offer: {}", cancelledOffer); - } else { - log.warn("Offer restoration timed out: {}", cancelledOffer); - } - - // Brief pause between restorations to avoid interface conflicts - sleep(Constants.GAME_TICK_LENGTH); - } else { - log.warn("Failed to restore offer: {}", cancelledOffer); - // Don't remove from list immediately, might retry later - } - } - - if (restoredCount > 0) { - Microbot.status = "Restored " + restoredCount + " cancelled GE offers"; - log.info("Successfully restored {} out of {} cancelled offers", - restoredCount, restoredCount + cancelledOffers.size()); - } else if (!cancelledOffers.isEmpty()) { - log.info("No offers could be restored, {} offers remain tracked", cancelledOffers.size()); - } - - return restoredCount; - - } catch (Exception e) { - log.error("Error during offer restoration: {}", e.getMessage()); - Microbot.logStackTrace("ShopRequirement.restoreCancelledOffers", e); - return 0; - } - } - - /** - * Clears all tracked cancelled offers (called when giving up on restoration). - */ - private void clearCancelledOffers() { - if (!cancelledOffers.isEmpty()) { - log.info("Clearing {} tracked cancelled offers", cancelledOffers.size()); - cancelledOffers.clear(); - } - } - - /** - * Allocates a Grand Exchange slot for tracking purposes. - * - * @param itemName Name of the item using the slot - * @param slots Number of slots to allocate - */ - /** - * Cancels any existing Grand Exchange offers for the specified item ID to prevent duplicates. - * Only cancels offers that are actively BUYING or SELLING (not completed or empty). - * Uses proper game state checking and sleepUntil patterns. - * - * @param itemId The item ID to check for existing offers - * @param itemName The item name for logging purposes - * @return true if any offers were cancelled, false otherwise - */ - private boolean cancelDuplicateOffers(int itemId, String itemName) { - if (!Rs2GrandExchange.isOpen()) { - log.warn("Grand Exchange not open, cannot cancel duplicate offers"); - return false; - } - - boolean cancelledAny = false; - - try { - GrandExchangeOffer[] offers = Microbot.getClient().getGrandExchangeOffers(); - - for (int slotIndex = 0; slotIndex < offers.length; slotIndex++) { - final int finalSlotIndex = slotIndex; // Make effectively final for lambda - GrandExchangeOffer offer = offers[slotIndex]; - - // Skip empty slots - if (offer == null || offer.getItemId() == 0) { - continue; - } - - // Check if this offer is for our item AND is actively buying/selling - if (offer.getItemId() == itemId) { - // Only cancel offers that are actively BUYING or SELLING - // Following the pattern from Rs2GrandExchange.java - boolean isActiveBuyOffer = offer.getState() == GrandExchangeOfferState.BUYING; - boolean isActiveSellOffer = offer.getState() == GrandExchangeOfferState.SELLING; - - if (isActiveBuyOffer || isActiveSellOffer) { - GrandExchangeSlots slot = GrandExchangeSlots.values()[slotIndex]; - String offerType = isActiveBuyOffer ? "BUY" : "SELL"; - - Microbot.status = "Cancelling duplicate " + offerType + " offer for " + itemName + " in slot " + (slot.ordinal() + 1); - log.info("Cancelling duplicate {} offer for {} in slot {} (state: {})", - offerType, itemName, slot.ordinal() + 1, offer.getState()); - - // Cancel the offer using Rs2GrandExchange utility - Rs2GrandExchange.abortOffer(itemName, enableBanking); - - // Wait for cancellation to complete with proper game state checking - boolean cancellationSuccess = sleepUntil(() -> { - GrandExchangeOffer[] updatedOffers = Microbot.getClient().getGrandExchangeOffers(); - if (finalSlotIndex >= updatedOffers.length) return false; - - GrandExchangeOffer updatedOffer = updatedOffers[finalSlotIndex]; - return updatedOffer == null || - updatedOffer.getItemId() == 0 || - updatedOffer.getItemId() != itemId || - (updatedOffer.getState() != GrandExchangeOfferState.BUYING && - updatedOffer.getState() != GrandExchangeOfferState.SELLING); - }, 5000); - - if (cancellationSuccess) { - cancelledAny = true; - log.info("Successfully cancelled duplicate {} offer for {}", offerType, itemName); - } else { - log.warn("Timeout waiting for duplicate {} offer cancellation for {}", offerType, itemName); - } - - // Brief pause between cancellations to avoid interface conflicts - sleep(Constants.GAME_TICK_LENGTH); - } else { - log.debug("Found offer for {} in slot {} but it's not active (state: {})", - itemName, slotIndex + 1, offer.getState()); - } - } - } - - // If any offers were cancelled, collect them and wait for interface to update - if (cancelledAny) { - // Allow time for cancellations to be processed - sleep(Constants.GAME_TICK_LENGTH * 2); - - // Collect cancelled offers to clear the interface - if (Rs2GrandExchange.hasBoughtOffer() || Rs2GrandExchange.hasSoldOffer()) { - Rs2GrandExchange.collectAll(enableBanking); - - // Wait for collection to complete - sleepUntil(() -> { - return !Rs2GrandExchange.hasBoughtOffer() && !Rs2GrandExchange.hasSoldOffer(); - }, 5000); - } - } - - } catch (Exception e) { - log.error("Error checking for duplicate offers for {}: {}", itemName, e.getMessage()); - Microbot.logStackTrace("ShopRequirement.cancelDuplicateOffers", e); - } - - return cancelledAny; - } - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/CancelledOfferState.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/CancelledOfferState.java deleted file mode 100644 index ba0106af490..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/CancelledOfferState.java +++ /dev/null @@ -1,171 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.models; - -import lombok.Getter; -import net.runelite.client.plugins.microbot.util.grandexchange.GrandExchangeSlots; - -/** - * Tracks the state of a cancelled Grand Exchange offer for potential recovery. - * This class stores all the necessary information to restore a cancelled offer - * at a later time, including item details, quantities, pricing, and timing. - * - *

Only tracks offers that were actively BUYING or SELLING when cancelled, - * ensuring we only restore meaningful offer states.

- * - *

The class is designed to be immutable to ensure data integrity and - * thread safety when tracking offer states across operations.

- * - * @author Enhanced GE Slot Management System - * @version 1.1 - */ -@Getter -public class CancelledOfferState { - - /** Maximum age for an offer to be considered still relevant (5 minutes) */ - public static final long DEFAULT_MAX_AGE_MS = 300_000L; - - private final int itemId; - private final String itemName; - private final int totalQuantity; - private final int remainingQuantity; - private final int price; - private final boolean isBuyOffer; - private final GrandExchangeSlots originalSlot; - private final long cancelledTime; - - /** - * Creates a new cancelled offer state with all required details. - * - * @param itemId The RuneScape item ID - * @param itemName The display name of the item - * @param totalQuantity The original total quantity in the offer - * @param remainingQuantity The quantity that was not yet bought/sold when cancelled - * @param price The price per item in the offer - * @param isBuyOffer True if this was a buy offer, false for sell offer - * @param originalSlot The Grand Exchange slot this offer was in - */ - public CancelledOfferState(int itemId, String itemName, int totalQuantity, - int remainingQuantity, int price, boolean isBuyOffer, - GrandExchangeSlots originalSlot) { - this.itemId = itemId; - this.itemName = itemName; - this.totalQuantity = totalQuantity; - this.remainingQuantity = remainingQuantity; - this.price = price; - this.isBuyOffer = isBuyOffer; - this.originalSlot = originalSlot; - this.cancelledTime = System.currentTimeMillis(); - } - - /** - * Checks if this cancelled offer is still relevant for restoration. - * Uses the default maximum age of 5 minutes. - * - * @return true if the offer is still recent enough to be relevant - */ - public boolean isStillRelevant() { - return isStillRelevant(DEFAULT_MAX_AGE_MS); - } - - /** - * Checks if this cancelled offer is still relevant for restoration. - * - * @param maxAgeMs Maximum age in milliseconds for the offer to be considered relevant - * @return true if the offer is still within the acceptable age limit - */ - public boolean isStillRelevant(long maxAgeMs) { - return System.currentTimeMillis() - cancelledTime < maxAgeMs; - } - - /** - * Validates if this offer is worth restoring based on game logic. - * Checks for valid item ID, remaining quantity, and reasonable price. - * - * @return true if this offer should be restored - */ - public boolean isWorthRestoring() { - return itemId > 0 && - remainingQuantity > 0 && - price > 0 && - isStillRelevant() && - originalSlot != null; - } - - /** - * Gets the age of this cancelled offer in milliseconds. - * - * @return The time elapsed since the offer was cancelled - */ - public long getAge() { - return System.currentTimeMillis() - cancelledTime; - } - - /** - * Checks if this offer has any remaining quantity worth restoring. - * - * @return true if there are items remaining to be bought/sold - */ - public boolean hasRemainingQuantity() { - return remainingQuantity > 0; - } - - /** - * Calculates the progress percentage of the original offer. - * - * @return A value between 0.0 and 1.0 representing completion percentage - */ - public double getProgressPercentage() { - if (totalQuantity <= 0) { - return 0.0; - } - int completedQuantity = totalQuantity - remainingQuantity; - return (double) completedQuantity / totalQuantity; - } - - /** - * Gets the operation type as a readable string. - * - * @return "BUY" or "SELL" depending on the offer type - */ - public String getOperationType() { - return isBuyOffer ? "BUY" : "SELL"; - } - - /** - * Creates a summary string suitable for logging and debugging. - * - * @return A formatted string containing key offer details - */ - public String getSummary() { - return String.format("%s %d/%d %s at %d gp (slot %s, age: %.1fs)", - getOperationType(), remainingQuantity, totalQuantity, - itemName, price, originalSlot, getAge() / 1000.0); - } - - @Override - public String toString() { - return getSummary(); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || getClass() != obj.getClass()) return false; - - CancelledOfferState that = (CancelledOfferState) obj; - return itemId == that.itemId && - price == that.price && - isBuyOffer == that.isBuyOffer && - originalSlot == that.originalSlot && - cancelledTime == that.cancelledTime; - } - - @Override - public int hashCode() { - int result = itemId; - result = 31 * result + price; - result = 31 * result + (isBuyOffer ? 1 : 0); - result = 31 * result + (originalSlot != null ? originalSlot.hashCode() : 0); - result = 31 * result + (int) (cancelledTime ^ (cancelledTime >>> 32)); - return result; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/MultiItemConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/MultiItemConfig.java deleted file mode 100644 index db43e873240..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/MultiItemConfig.java +++ /dev/null @@ -1,57 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.models; - -import lombok.Getter; -import net.runelite.client.plugins.microbot.Microbot; -/** - * Enhanced configuration class for individual items in a multi-item requirement. - * Updated to use the unified stock management system. - */ -public class MultiItemConfig { - public final int amount; - public final int stockTolerance; // **UNIFIED SYSTEM**: Replaces minimumStock + maxQuantityPerVisit - - /** - * Creates a new MultiItemConfig with unified stock management. - * - * @param amount Total amount needed for this item - * @param stockTolerance Stock tolerance around baseStock (affects both min stock and max per visit) - */ - public MultiItemConfig(int amount, int stockTolerance) { - this.amount = amount; - this.stockTolerance = stockTolerance; - } - - /** - * Creates a new MultiItemConfig with default stock tolerance. - * - * @param amount Total amount needed for this item - */ - public MultiItemConfig(int amount) { - this(amount, 10); // Default tolerance of 10 - } - - /** - * Legacy constructor for backward compatibility. - * Converts old minimumStock/maxQuantityPerVisit to unified stockTolerance. - * - * @param amount Total amount needed - * @param minimumStock Legacy minimum stock (ignored in new system) - * @param maxQuantityPerVisit Legacy max per visit (used as stockTolerance) - */ - @Deprecated - public MultiItemConfig(int amount, int minimumStock, int maxQuantityPerVisit) { - this.amount = amount; - this.stockTolerance = maxQuantityPerVisit; // Use maxQuantityPerVisit as tolerance - - // Log the conversion for debugging - if (minimumStock != 5) { // 5 was the old default - Microbot.log("MultiItemConfig: Converting legacy minimumStock=" + minimumStock + - " to unified stockTolerance=" + this.stockTolerance); - } - } - - @Override - public String toString() { - return String.format("MultiItemConfig{amount=%d, stockTolerance=%d}", amount, stockTolerance); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/ShopOperation.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/ShopOperation.java deleted file mode 100644 index da3bb58ebc3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/ShopOperation.java +++ /dev/null @@ -1,9 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.models; - -/** - * Enum defining the type of shop operation. - */ -public enum ShopOperation { - BUY, // Purchase items from the shop - SELL // Sell items to the shop -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/ConditionalRequirementBuilder.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/ConditionalRequirementBuilder.java deleted file mode 100644 index 6dc1750b214..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/ConditionalRequirementBuilder.java +++ /dev/null @@ -1,232 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.util; - -import net.runelite.api.Skill; -import net.runelite.api.ItemID; -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location.LocationRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.SpellbookRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.ConditionalRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.OrderedRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.OrRequirement; -import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.magic.Rs2Magic; -import net.runelite.client.plugins.microbot.util.magic.Rs2Spellbook; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -import java.util.Arrays; -import java.util.function.BooleanSupplier; -import java.util.stream.Collectors; - -/** - * Utility class for creating common conditional and ordered requirements. - * Provides pre-built conditions and requirement patterns for typical OSRS workflows. - */ -public class ConditionalRequirementBuilder { - - /** - * Creates a spellbook switching conditional requirement. - * Only switches if the player has the required magic level and doesn't already have the spellbook. - * - * @param targetSpellbook The spellbook to switch to - * @param requiredLevel Minimum magic level required - * @param priority Priority level for this requirement - * @param TaskContext When to fulfill this requirement - * @return ConditionalRequirement for spellbook switching - */ - public static ConditionalRequirement createSpellbookSwitcher(Rs2Spellbook targetSpellbook, int requiredLevel, - RequirementPriority priority, TaskContext taskContext) { - ConditionalRequirement spellbookSwitcher = new ConditionalRequirement( - priority, 8, "Smart Spellbook Switching", taskContext, false - ); - - // Only switch if we have the level and don't already have the spellbook - BooleanSupplier needsSpellbookSwitch = () -> - Rs2Player.getRealSkillLevel(Skill.MAGIC) >= requiredLevel && ! Rs2Magic.isSpellbook(targetSpellbook); - - SpellbookRequirement spellbookReq = new SpellbookRequirement( - targetSpellbook, taskContext, priority, 8, - "Switch to " + targetSpellbook + " spellbook" - ); - - spellbookSwitcher.addStep(needsSpellbookSwitch, spellbookReq, - "Check magic level and switch to " + targetSpellbook + " if needed", true); - - return spellbookSwitcher; - } - - /** - * Creates an equipment upgrade conditional requirement. - * Upgrades to better equipment if the player can afford it and doesn't already have it. - * - * @param basicItemIds Basic equipment item IDs (always required) - * @param upgradeItemIds Upgrade equipment item IDs (conditional) - * @param minGpRequired Minimum GP required for upgrade - * @param equipmentSlot Equipment slot type - * @param description Description for the requirement - * @param priority Priority level - * @param TaskContext When to fulfill this requirement - * @return ConditionalRequirement for equipment upgrading - */ - public static ConditionalRequirement createEquipmentUpgrader(int[] basicItemIds, int[] upgradeItemIds, - int minGpRequired, EquipmentInventorySlot equipmentSlot, - String description, RequirementPriority priority, - TaskContext taskContext) { - ConditionalRequirement equipmentUpgrader = new ConditionalRequirement( - priority, 7, "Smart Equipment Upgrading: " + description, taskContext, false - ); - - // Step 1: Ensure we have basic equipment - OrRequirement basicEquipment = ItemRequirement.createOrRequirement( - Arrays.stream(basicItemIds).boxed().collect(Collectors.toList()),1, equipmentSlot,-2, - priority, 6, "Basic " + description, taskContext - ); - - equipmentUpgrader.addStep( - () -> !hasAnyItem(basicItemIds) && !hasAnyItem(upgradeItemIds), - basicEquipment, - "Get basic " + description + " if none available" - ); - - // Step 2: Upgrade if affordable and beneficial - if (upgradeItemIds.length > 0 && minGpRequired > 0) { - OrRequirement upgradeEquipment = ItemRequirement.createOrRequirement( - Arrays.stream(upgradeItemIds).boxed().collect(Collectors.toList()), equipmentSlot, - RequirementPriority.RECOMMENDED, 9, "Upgraded " + description, taskContext - ); - - equipmentUpgrader.addStep( - () -> !hasAnyItem(upgradeItemIds) && hasGP(minGpRequired), - upgradeEquipment, - "Upgrade to better " + description + " if affordable", - true // Optional upgrade - ); - } - - return equipmentUpgrader; - } - - /** - * Creates a shop-then-equip ordered requirement. - * First shops for items, then ensures they are equipped. - * - * @param shopLocation Where to shop - * @param itemIds Items to shop for - * @param itemName Display name for items - * @param quantity How many to buy - * @param priority Priority level - * @param TaskContext When to fulfill this requirement - * @return OrderedRequirement for shop-then-equip workflow - */ - public static OrderedRequirement createShopThenEquip(BankLocation shopLocation, int[] itemIds, - String itemName, int quantity, RequirementPriority priority, - TaskContext taskContext) { - OrderedRequirement shopThenEquip = new OrderedRequirement( - priority, 8, "Shop and Equip: " + itemName, taskContext - ); - - // Step 1: Go to shop location - LocationRequirement location = new LocationRequirement( - shopLocation, true,-1, taskContext, priority - ); - shopThenEquip.addStep(location, "Travel to " + shopLocation.name() + " for shopping"); - - // Step 2: Shop for items (assuming ShopRequirement exists) - // This is a placeholder - you'll need to implement ShopRequirement.createBuyRequirement - // ShopRequirement shopReq = ShopRequirement.createBuyRequirement(itemIds[0], quantity, priority); - // shopThenEquip.addStep(shopReq, "Buy " + quantity + "x " + itemName); - - // Step 3: Equip the items (Note: This is a simplified example - you may need to specify equipment slot) -// ItemRequirement equipmentReq = ItemRequirement.createOrRequirement( - // Arrays.stream(itemIds).boxed().collect(Collectors.toList()), null,null,-1, - // priority, 7, "Equipped " + itemName, TaskContext - // ); - // shopThenEquip.addStep(equipmentReq, "Equip " + itemName); - - return shopThenEquip; - } - - /** - * Creates a bank-preparation ordered requirement. - * Ensures player is at bank, withdraws needed items, and organizes inventory. - * - * @param bankLocation Preferred bank location - * @param withdrawItems Items to withdraw from bank - * @param priority Priority level - * @param TaskContext When to fulfill this requirement - * @return OrderedRequirement for bank preparation - */ - public static OrderedRequirement createBankPreparation(BankLocation bankLocation, ItemRequirement[] withdrawItems, - RequirementPriority priority, TaskContext taskContext) { - OrderedRequirement bankPrep = new OrderedRequirement( - priority, 9, "Bank Preparation", taskContext - ); - - // Step 1: Go to bank - LocationRequirement bankLocationReq = new LocationRequirement( - bankLocation, true, -1,taskContext, priority - ); - bankPrep.addStep(bankLocationReq, "Travel to " + bankLocation.name() + " bank"); - - // Step 2: Open bank - // This would need a custom requirement for opening bank - // bankPrep.addStep(new CustomRequirement(() -> Rs2Bank.openBank()), "Open bank"); - - // Step 3: Withdraw each required item - for (int i = 0; i < withdrawItems.length; i++) { - ItemRequirement item = withdrawItems[i]; - bankPrep.addStep(item, "Withdraw " + item.getName(), !item.isMandatory()); - } - - return bankPrep; - } - - /** - * Creates a level-based conditional requirement. - * Only fulfills the requirement if the player has sufficient level. - * - * @param skill Required skill - * @param requiredLevel Minimum level required - * @param requirement Requirement to fulfill if level is met - * @param description Description for this conditional - * @param priority Priority level - * @param TaskContext When to fulfill this requirement - * @return ConditionalRequirement based on skill level - */ - public static ConditionalRequirement createLevelBasedRequirement(Skill skill, int requiredLevel, - Requirement requirement, String description, - RequirementPriority priority, TaskContext taskContext) { - ConditionalRequirement levelBased = new ConditionalRequirement( - priority, 8, "Level-based: " + description, taskContext, false - ); - - BooleanSupplier hasLevel = () ->Rs2Player.getRealSkillLevel(skill) >= requiredLevel; - - levelBased.addStep(hasLevel, requirement, - description + " (requires " + skill.getName() + " level " + requiredLevel + ")", true); - - return levelBased; - } - - // Helper methods for common conditions - private static boolean isCurrentSpellbook(Rs2Spellbook spellbook) { - return Rs2Magic.isSpellbook(spellbook); - } - - private static boolean hasAnyItem(int[] itemIds) { - for (int itemId : itemIds) { - if (Rs2Inventory.hasItem(itemId)) { - return true; - } - } - return false; - } - - private static boolean hasGP(int amount) { return Rs2Inventory.hasItem(ItemID.COINS) && - Rs2Inventory.itemQuantity(ItemID.COINS) >= amount; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSelector.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSelector.java deleted file mode 100644 index 4ad1036dfb2..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSelector.java +++ /dev/null @@ -1,314 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.util; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.InventorySetupPlanner; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.LogicalRequirement; - -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Utility class for selecting the best available items to fulfill requirements. - * Contains logic for finding optimal items based on availability, priority, and constraints. - */ -@Slf4j -public class RequirementSelector { - - /** - * Finds the best available item from a list of logical requirements. - * - * @param logicalReqs List of logical requirements to evaluate - * @return The best available ItemRequirement, or null if none found - */ - public static ItemRequirement findBestAvailableItemForInventory(List logicalReqs, int inventorySlot) { - log.debug("Finding best available item from {} logical requirements", logicalReqs.size()); - - for (LogicalRequirement logicalReq : logicalReqs) { - List items = LogicalRequirement.extractItemRequirementsFromLogical(logicalReq); - log.debug("Checking {} items in logical requirement: {}", items.size(), logicalReq.displayString()); - - for (ItemRequirement item : items) { - if (isItemAvailable(item) && canPlayerUse(item)) { - log.debug("Found available item: {}", item.displayString()); - return item; - } - } - } - - log.debug("No available items found in any logical requirement"); - return null; - } - - /** - * Finds the best available item that hasn't already been planned for use. - * - * @param logicalReqs List of logical requirements to evaluate - * @param plan Current inventory setup plan - * @return The best available ItemRequirement not already planned, or null if none found - */ - public static ItemRequirement findBestAvailableItemNotAlreadyPlannedForInventory(LinkedHashSet items, InventorySetupPlanner plan) { - - for (ItemRequirement item : items) { - if (isItemAvailable(item) && - canPlayerUse(item) && - !isItemAlreadyPlanned(item, plan)) { - log.debug("Found available item not already planned: {}", item.displayString()); - return item; - } - } - - log.debug("No available items found that aren't already planned"); - return null; - } - - - - /** - * Enhanced method for finding the best available item for a specific equipment slot. - * This method considers already planned items to avoid conflicts and validates equipment slot compatibility. - * - * @param logicalReqs The logical requirements to search through - * @param targetEquipmentSlot The specific equipment slot to find an item for - * @param alreadyPlanned Items already planned (for conflict avoidance) - * @return The best available item for the slot, or null if none found - */ - public static ItemRequirement findBestAvailableItemForEquipmentSlot(List equipmentSlotReqs, - EquipmentInventorySlot targetEquipmentSlot, - Set alreadyPlanned) { - - List items = equipmentSlotReqs; - - // Sort by priority, then by type priority: EQUIPMENT > EITHER > INVENTORY, then by rating - items.sort((a, b) -> { - // First sort by priority (MANDATORY > RECOMMENDED > OPTIONAL) - int priorityCompare = a.getPriority().compareTo(b.getPriority()); - if (priorityCompare != 0) { - return priorityCompare; - } - - // Then sort by type priority: EQUIPMENT > EITHER > INVENTORY - int typePriorityA = getTypePriority(a.getRequirementType()); - int typePriorityB = getTypePriority(b.getRequirementType()); - - if (typePriorityA != typePriorityB) { - return Integer.compare(typePriorityB, typePriorityA); // Higher priority first - } - - // Finally by rating (higher is better) - return Integer.compare(b.getRating(), a.getRating()); - }); - - for (ItemRequirement item : items) { - // Skip if already planned - if (alreadyPlanned.contains(item)) { - continue; - } - - // Validate that this item can be assigned to the target equipment slot and meets skill requirements - if (!canAssignToEquipmentSlot(item, targetEquipmentSlot)) { - log.debug("Item {} cannot be assigned to equipment slot {}, skipping", - item.getName(), targetEquipmentSlot.name()); - continue; - } - - // Check if item is available - if (isItemAvailable(item)) { - log.debug("Found compatible item {} for equipment slot {}", - item.getName(), targetEquipmentSlot.name()); - return item; - } - } - - return null; - } - - /** - * Gets the type priority for sorting equipment requirements. - * Higher numbers = higher priority. - */ - private static int getTypePriority(RequirementType type) { - switch (type) { - case EQUIPMENT: - return 3; - case EITHER: - return 2; - case INVENTORY: - return 1; - default: - return 0; - } - } - - /** - * Validates if two ItemRequirements represent the same logical requirement. - * This accounts for the fact that requirements may be copied or modified during planning. - * - * @param original The original requirement - * @param planned The planned requirement - * @return true if they match, false otherwise - */ - public static boolean itemRequirementMatches(ItemRequirement original, ItemRequirement planned) { - // Check if they have overlapping item IDs - return original.getIds().stream().anyMatch(planned.getIds()::contains) && - original.getAmount() <= planned.getAmount(); - } - - /** - * Finds items that can fulfill multiple requirements simultaneously. - * Useful for optimizing inventory space. - * - * @param logicalReqs The logical requirements to analyze - * @return Map of items to the number of requirements they can fulfill - */ - public static Map findMultiPurposeItems(List logicalReqs) { - Map multiPurposeMap = new HashMap<>(); - List allItems = LogicalRequirement.extractAllItemRequirements(logicalReqs); - - for (ItemRequirement item : allItems) { - int count = 0; - for (ItemRequirement other : allItems) { - if (itemRequirementMatches(item, other)) { - count++; - } - } - if (count > 1) { - multiPurposeMap.put(item, count); - } - } - - return multiPurposeMap; - } - - - - /** - * Checks if an item is currently available (in inventory, equipment, or bank). - * Dummy items are always considered "available" since they're just slot placeholders. - * Uses the ItemRequirement's own availability checking methods which properly handle fuzzy matching and amounts. - * - * @param item ItemRequirement to check - * @return true if the item is available - */ - public static boolean isItemAvailable(ItemRequirement item) { - // Dummy items are always considered available since they're just slot placeholders - if (item.isDummyItemRequirement()) { - return true; - } - - // Use ItemRequirement's own availability checking which handles fuzzy matching and amounts properly - return item.isAvailableInInventoryOrBank(); - } - - /** - * Validates if the player can use or equip an item based on skill requirements. - * Checks both equipment and usage skill requirements. - * - * @param item ItemRequirement to validate - * @return true if player meets all skill requirements for the item - */ - public static boolean canPlayerUse(ItemRequirement item) { - // Dummy items can always be used - if (item.isDummyItemRequirement()) { - return true; - } - // For all items, check usage requirements if specified - if (!ItemRequirement.canPlayerUseItem(item)) { - log.debug("Player cannot use item {} due to skill requirements", item.getName()); - return false; - } - - return true; - } - - /** - * Validates if an item can be assigned to a specific equipment slot and meets skill requirements. - * - * @param item ItemRequirement to validate - * @param targetEquipmentSlot Target equipment slot - * @return true if item can be assigned to the slot and player meets requirements - */ - public static boolean canAssignToEquipmentSlot(ItemRequirement item, EquipmentInventorySlot targetEquipmentSlot) { - // Dummy items can always be assigned - if (item.isDummyItemRequirement()) { - return true; - } - - // Item must be able to be equipped - if (!item.canBeEquipped()) { - log.debug("Item {} cannot be equipped", item.getName()); - return false; - } - - // Check if item matches the equipment slot - if (item.getEquipmentSlot() != null && item.getEquipmentSlot() != targetEquipmentSlot) { - log.debug("Item {} equipment slot mismatch: expected {}, actual {}", - item.getName(), targetEquipmentSlot, item.getEquipmentSlot()); - return false; - } - - // Check skill requirements for equipping - if (!ItemRequirement.canPlayerEquipItem(item)) { - log.debug("Player cannot equip item {} due to skill requirements", item.getName()); - return false; - } - // Check skill requirements for equipping - if (!ItemRequirement.canPlayerUseItem(item)) { - log.debug("Player cannot use item {} due to skill requirements", item.getName()); - return false; - } - - return true; - } - - - - /** - * Checks if an item is already planned for use in the current plan. - * - * @param item ItemRequirement to check - * @param plan Current inventory setup plan - * @return true if the item is already planned - */ - private static boolean isItemAlreadyPlanned(ItemRequirement item, InventorySetupPlanner plan) { - // Check equipment assignments - for (ItemRequirement plannedItem : plan.getEquipmentAssignments().values()) { - if (plannedItem != null && hasMatchingItemId(plannedItem, item)) { - return true; - } - } - - // Check inventory slot assignments - for (ItemRequirement plannedItem : plan.getInventorySlotAssignments().values()) { - if (plannedItem != null && hasMatchingItemId(plannedItem, item)) { - return true; - } - } - - // Check flexible inventory items - for (ItemRequirement plannedItem : plan.getFlexibleInventoryItems()) { - if (hasMatchingItemId(plannedItem, item)) { - return true; - } - } - - return false; - } - - /** - * Checks if two ItemRequirements have matching item IDs. - * - * @param item1 First item requirement - * @param item2 Second item requirement - * @return true if they have at least one matching item ID - */ - private static boolean hasMatchingItemId(ItemRequirement item1, ItemRequirement item2) { - // Since ItemRequirement now only supports single IDs, simply compare them - return item1.getId() == item2.getId(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSolver.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSolver.java deleted file mode 100644 index 66bf02c5e62..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSolver.java +++ /dev/null @@ -1,347 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.util; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementMode; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.registry.RequirementRegistry; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location.LocationRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.ShopRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.state.TaskExecutionState; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.SpellbookRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.ConditionalRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.OrderedRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.LogicalRequirement; -import org.slf4j.event.Level; - -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -/** - * Utility class for solving different types of requirements with common patterns. - * Provides reusable fulfillment logic and error handling patterns. - * - * This is the unified fulfillment system that handles both standard and external requirements - * using the same logic patterns and error handling. - */ -@Slf4j -public class RequirementSolver { - - /** - * Fulfills shop requirements for the specified schedule context. - * Uses the unified filtering system to automatically handle pre/post schedule requirements. - * - * @param shopRequirements The shop requirements to fulfill - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @return true if all shop requirements were fulfilled successfully, false otherwise - */ - public static boolean fulfillShopRequirements(CompletableFuture scheduledFuture,List shopRequirements, TaskContext context) { - List contextReqs = shopRequirements.stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(java.util.stream.Collectors.toList()); - - if (contextReqs.isEmpty()) { - log.debug("No shop requirements for context: {}", context); - return true; - } - - boolean success = true; - int fulfilled = 0; - - for (int i = 0; i < contextReqs.size(); i++) { - ShopRequirement requirement = contextReqs.get(i); - - try { - log.info("Processing shop requirement {}/{}: {}", i + 1, contextReqs.size(), requirement.getName()); - boolean requirementFulfilled = requirement.fulfillRequirement(scheduledFuture); - - if (requirementFulfilled) { - fulfilled++; - } else { - if (requirement.isMandatory()) { - log.error("Failed to fulfill mandatory shop requirement: {}", requirement.getName()); - success = false; - break; // Stop on mandatory failure - } else { - log.debug("Failed to fulfill optional shop requirement: {}", requirement.getName()); - } - } - } catch (Exception e) { - log.error("Error fulfilling shop requirement {}: {}", requirement.getName(), e.getMessage()); - if (requirement.isMandatory()) { - success = false; - } - } - } - - log.info("Shop requirements fulfillment completed. Success: {}, Fulfilled: {}/{}", success, fulfilled, contextReqs.size()); - return success; - } - - /** - * Fulfills loot requirements for the specified schedule context. - * Uses the unified filtering system to automatically handle pre/post schedule requirements. - * - * @param lootLogical The logical loot requirements to fulfill - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @return true if all loot requirements were fulfilled successfully, false otherwise - */ - public static boolean fulfillLootRequirements(CompletableFuture scheduledFuture, List lootLogical, TaskContext context) { - List contextReqs = LogicalRequirement.filterByContext(lootLogical, context); - - if (contextReqs.isEmpty()) { - log.debug("No loot requirements for context: {}", context); - return true; - } - - return LogicalRequirement.fulfillLogicalRequirements(scheduledFuture,contextReqs, "loot"); - } - - /** - * Fulfills location requirements for the specified schedule context. - * Uses the unified filtering system to automatically handle pre/post schedule requirements. - * - * @param locationReqs The location requirements to fulfill - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @return true if all location requirements were fulfilled successfully, false otherwise - */ - public static boolean fulfillLocationRequirements(CompletableFuture scheduledFuture, List locationReqs, TaskContext context) { - List contextReqs = locationReqs.stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(java.util.stream.Collectors.toList()); - - if (contextReqs.isEmpty()) { - log.debug("No location requirements for context: {}", context); - return true; - } - - boolean success = true; - int fulfilled = 0; - - for (int i = 0; i < contextReqs.size(); i++) { - LocationRequirement requirement = contextReqs.get(i); - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Scheduled future is cancelled, skipping location requirement fulfillment: {}", requirement.getName()); - return false; // Skip if scheduled future is cancelled - } - try { - log.debug("Processing location requirement {}/{}: {}", i + 1, contextReqs.size(), requirement.getName()); - boolean requirementFulfilled = requirement.fulfillRequirement(scheduledFuture); - - if (requirementFulfilled) { - fulfilled++; - } else { - if (requirement.isMandatory()) { - log.error("Failed to fulfill mandatory location requirement: {}", requirement.getName()); - success = false; - break; // Stop on mandatory failure - } else { - log.debug("Failed to fulfill optional location requirement: {}", requirement.getName()); - } - } - } catch (Exception e) { - log.error("Error fulfilling location requirement {}: {}", requirement.getName(), e.getMessage()); - if (requirement.isMandatory()) { - success = false; - } - } - } - - log.debug("Location requirements fulfillment completed. Success: {}, Fulfilled: {}/{}", success, fulfilled, contextReqs.size()); - return success; - } - - /** - * Fulfills spellbook requirements for the specified schedule context. - * - * @param spellbookReqs The spellbook requirements to fulfill - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param saveCurrentSpellbook Whether to save the current spellbook before switching - * @return true if all spellbook requirements were fulfilled successfully, false otherwise - */ - public static boolean fulfillSpellbookRequirements(CompletableFuture scheduledFuture, List spellbookReqs, - TaskContext context, - boolean saveCurrentSpellbook) { - List contextReqs = spellbookReqs.stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(java.util.stream.Collectors.toList()); - - if (contextReqs.isEmpty()) { - log.debug("No spellbook requirements for context: {}", context); - return true; - } - - boolean success = true; - int fulfilled = 0; - - for (int i = 0; i < contextReqs.size(); i++) { - SpellbookRequirement requirement = contextReqs.get(i); - - try { - log.debug("Processing spellbook requirement {}/{}: {}", i + 1, contextReqs.size(), requirement.getName()); - - // Save current spellbook if requested (typically for PRE_SCHEDULE context) - if (saveCurrentSpellbook) { - // This would need to be handled by the calling class as it maintains state - log.debug("Spellbook saving requested (handled by caller)"); - } - - boolean requirementFulfilled = requirement.fulfillRequirement(scheduledFuture); - - if (requirementFulfilled) { - fulfilled++; - } else { - if (requirement.isMandatory()) { - log.error("Failed to fulfill mandatory spellbook requirement: {}", requirement.getName()); - success = false; - break; // Stop on mandatory failure - } else { - log.debug("Failed to fulfill optional spellbook requirement: {}", requirement.getName()); - } - } - } catch (Exception e) { - log.error("Error fulfilling spellbook requirement {}: {}", requirement.getName(), e.getMessage()); - if (requirement.isMandatory()) { - success = false; - } - } - } - - log.debug("Spellbook requirements fulfillment completed. Success: {}, Fulfilled: {}/{}", success, fulfilled, contextReqs.size()); - return success; - } - - /** - * Fulfills conditional requirements for the specified schedule context. - * - * @param conditionalReqs The conditional requirements to fulfill - * @param orderedReqs The ordered requirements to fulfill - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @return true if all conditional requirements were fulfilled successfully, false otherwise - */ - public static boolean fulfillConditionalRequirements(CompletableFuture scheduledFuture, - TaskExecutionState executionState, - List conditionalReqs, - List orderedReqs, - TaskContext context) { - List contextConditionalReqs = conditionalReqs.stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(java.util.stream.Collectors.toList()); - - List contextOrderedReqs = orderedReqs.stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(java.util.stream.Collectors.toList()); - - if (contextConditionalReqs.isEmpty() && contextOrderedReqs.isEmpty()) { - log.debug("No conditional or ordered requirements to fulfill for context: {}", context); - return true; // No requirements to fulfill - } - - boolean success = true; - int currentIndex = 0; - int totalReqs = contextConditionalReqs.size() + contextOrderedReqs.size(); - - // Process ConditionalRequirements first - for (ConditionalRequirement requirement : contextConditionalReqs) { - try { - log.debug("Processing conditional requirement {}/{}: {}", ++currentIndex, totalReqs, requirement.getName()); - boolean fulfilled = requirement.fulfillRequirement(scheduledFuture); - if (!fulfilled && requirement.getPriority() == RequirementPriority.MANDATORY) { - Microbot.log("Failed to fulfill mandatory conditional requirement: " + requirement.getName(), Level.ERROR); - success = false; - } else if (!fulfilled) { - Microbot.log("Failed to fulfill optional conditional requirement: " + requirement.getName(), Level.WARN); - } - } catch (Exception e) { - log.error("Error fulfilling conditional requirement '{}': {}", requirement.getName(), e.getMessage(), e); - Microbot.log("Error fulfilling conditional requirement " + requirement.getName() + ": " + e.getMessage(), Level.ERROR); - if (requirement.getPriority() == RequirementPriority.MANDATORY) { - success = false; - } - } - } - - // Process OrderedRequirements second - for (OrderedRequirement requirement : contextOrderedReqs) { - try { - log.debug("Processing ordered requirement {}/{}: {}", ++currentIndex, totalReqs, requirement.getName()); - boolean fulfilled = requirement.fulfillRequirement(scheduledFuture); - if (!fulfilled && requirement.getPriority() == RequirementPriority.MANDATORY) { - Microbot.log("Failed to fulfill mandatory ordered requirement: " + requirement.getName(), Level.ERROR); - success = false; - } else if (!fulfilled) { - Microbot.log("Failed to fulfill optional ordered requirement: " + requirement.getName(), Level.WARN); - } - } catch (Exception e) { - log.error("Error fulfilling ordered requirement '{}': {}", requirement.getName(), e.getMessage(), e); - Microbot.log("Error fulfilling ordered requirement " + requirement.getName() + ": " + e.getMessage(), Level.ERROR); - if (requirement.getPriority() == RequirementPriority.MANDATORY) { - success = false; - } - } - } - - log.debug("Conditional requirements fulfillment completed. Success: {}, Total processed: {}", success, totalReqs); - return success; - } - - /** - * Generic requirement fulfillment method with common error handling patterns. - * - * @param requirements List of requirements to fulfill - * @param requirementTypeName Name of the requirement type for logging - * @param context The schedule context - * @param The requirement type - * @return true if all mandatory requirements were fulfilled successfully - */ - public static boolean fulfillRequirements( CompletableFuture scheduledFuture, - List requirements, - String requirementTypeName, - TaskContext context) { - List contextReqs = requirements.stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(java.util.stream.Collectors.toList()); - - if (contextReqs.isEmpty()) { - log.debug("No {} requirements for context: {}", requirementTypeName, context); - return true; - } - - boolean success = true; - int fulfilled = 0; - - for (int i = 0; i < contextReqs.size(); i++) { - T requirement = contextReqs.get(i); - - try { - log.debug("Processing {} requirement {}/{}: {}", requirementTypeName, i + 1, contextReqs.size(), requirement.getName()); - boolean requirementFulfilled = requirement.fulfillRequirement(scheduledFuture); - - if (requirementFulfilled) { - fulfilled++; - } else { - if (requirement.isMandatory()) { - log.error("Failed to fulfill mandatory {} requirement: {}", requirementTypeName, requirement.getName()); - success = false; - break; // Stop on mandatory failure - } else { - log.debug("Failed to fulfill optional {} requirement: {}", requirementTypeName, requirement.getName()); - } - } - } catch (Exception e) { - log.error("Error fulfilling {} requirement {}: {}", requirementTypeName, requirement.getName(), e.getMessage()); - if (requirement.isMandatory()) { - success = false; - } - } - } - - log.debug("{} requirements fulfillment completed. Success: {}, Fulfilled: {}/{}", - requirementTypeName, success, fulfilled, contextReqs.size()); - return success; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/state/FulfillmentStep.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/state/FulfillmentStep.java deleted file mode 100644 index 366f61ec984..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/state/FulfillmentStep.java +++ /dev/null @@ -1,72 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.state; - -import lombok.Getter; - -/** - * Represents the different steps in the requirement fulfillment process. - * These steps are executed in order for both pre-schedule and post-schedule contexts. - */ -@Getter -public enum FulfillmentStep { - CONDITIONAL(0, "Conditional", "Executing conditional and ordered requirements"), - LOOT(1, "Loot", "Collecting required loot items"), - SHOP(2, "Shop", "Purchasing required shop items"), - ITEMS(3, "Items", "Preparing inventory and equipment"), - SPELLBOOK(4, "Spellbook", "Switching to required spellbook"), - LOCATION(5, "Location", "Moving to required location"), - EXTERNAL_REQUIREMENTS(6, "External", "Fulfilling externally added requirements"); - - private final int order; - private final String displayName; - private final String description; - - FulfillmentStep(int order, String displayName, String description) { - this.order = order; - this.displayName = displayName; - this.description = description; - } - - /** - * Gets the total number of fulfillment steps. - */ - public static int getTotalSteps() { - return values().length; - } - - /** - * Gets the next step in the fulfillment process. - * @return The next step, or null if this is the last step - */ - public FulfillmentStep getNext() { - FulfillmentStep[] values = values(); - if (ordinal() < values.length - 1) { - return values[ordinal() + 1]; - } - return null; - } - - /** - * Gets the previous step in the fulfillment process. - * @return The previous step, or null if this is the first step - */ - public FulfillmentStep getPrevious() { - if (ordinal() > 0) { - return values()[ordinal() - 1]; - } - return null; - } - - /** - * Checks if this is the first step. - */ - public boolean isFirst() { - return ordinal() == 0; - } - - /** - * Checks if this is the last step. - */ - public boolean isLast() { - return ordinal() == values().length - 1; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/state/TaskExecutionState.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/state/TaskExecutionState.java deleted file mode 100644 index b14a64733fa..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/state/TaskExecutionState.java +++ /dev/null @@ -1,441 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.state; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.state.FulfillmentStep; - -/** - * Centralized state tracking system for pre/post schedule task execution and requirement fulfillment. - * This class provides a single source of truth for the current execution state, eliminating - * the redundant state tracking that existed across AbstractPrePostScheduleTasks and PrePostScheduleRequirements. - */ -@Slf4j -public class TaskExecutionState { - - /** - * Represents the overall execution phase - */ - public enum ExecutionPhase { - IDLE("Idle"), - PRE_SCHEDULE("Pre-Schedule"), - MAIN_EXECUTION("Main Execution"), - POST_SCHEDULE("Post-Schedule"); - - @Getter - private final String displayName; - - ExecutionPhase(String displayName) { - this.displayName = displayName; - } - } - - /** - * Represents the current state of task execution - */ - public enum ExecutionState { - STARTING("Starting"), - FULFILLING_REQUIREMENTS("Fulfilling Requirements"), - CUSTOM_TASKS("Custom Tasks"), - COMPLETED("Completed"), - FAILED("Failed"), - ERROR("Error"); - - @Getter - private final String displayName; - - ExecutionState(String displayName) { - this.displayName = displayName; - } - } - - // Current execution state - @Getter - private volatile ExecutionPhase currentPhase = ExecutionPhase.IDLE; - @Getter - private volatile ExecutionState currentState = ExecutionState.STARTING; - - @Getter - private volatile String currentDetails = null; - @Getter - private volatile boolean hasError = false; - @Getter - private volatile String errorMessage = null; - - // Progress tracking - @Getter - private volatile int currentStepNumber = 0; - @Getter - private volatile int totalSteps = 0; - - // Individual requirement tracking within steps - @Getter - private volatile FulfillmentStep currentStep = null; - @Getter - private volatile Object currentRequirement = null; // The specific requirement being processed - @Getter - private volatile String currentRequirementName = null; // readable name of current requirement - @Getter - private volatile int currentRequirementIndex = 0; // Current requirement index within step - @Getter - private volatile int totalRequirementsInStep = 0; // Total requirements in current step - - // Execution phase completion tracking - prevents multiple executions - @Getter - private volatile boolean hasPreTaskStarted = false; - @Getter - private volatile boolean hasPreTaskCompleted = false; - @Getter - private volatile boolean hasMainTaskStarted = false; - @Getter - private volatile boolean hasMainTaskCompleted = false; - @Getter - private volatile boolean hasPostTaskStarted = false; - @Getter - private volatile boolean hasPostTaskCompleted = false; - - /** - * Updates the current execution phase and resets step tracking - */ - public synchronized void update(ExecutionPhase phase, ExecutionState state) { - this.currentPhase = phase; - this.currentState = state; - // Mark phase as started - switch (phase) { - case PRE_SCHEDULE: - this.hasPreTaskStarted = true; - if (state == ExecutionState.COMPLETED|| state == ExecutionState.FAILED || state == ExecutionState.ERROR) { - this.hasPreTaskCompleted = true; - } else { - this.hasPreTaskCompleted = false; - } - break; - case MAIN_EXECUTION: - if (!hasPreTaskCompleted) { - log.warn("Main execution started without pre-schedule tasks. Ensure pre-tasks are executed first."); - }else{ - hasPreTaskCompleted = true; - } - this.hasMainTaskStarted = true; - if (state == ExecutionState.COMPLETED|| state == ExecutionState.FAILED || state == ExecutionState.ERROR) { - this.hasMainTaskCompleted = true; - } else { - this.hasMainTaskCompleted = false; - } - break; - case POST_SCHEDULE: - this.hasPostTaskStarted = true; - if (state == ExecutionState.COMPLETED|| state == ExecutionState.FAILED || state == ExecutionState.ERROR) { - this.hasPostTaskCompleted = true; - } else { - this.hasPostTaskCompleted = false; - } - break; - default: - break; - } - - log.debug("Execution phase updated to: {}", phase.getDisplayName()); - } - - - - /** - * Updates the current fulfillment step and progress - */ - public synchronized void updateFulfillmentStep(FulfillmentStep step, String details) { - this.currentStep = step; - this.currentDetails = details; - this.currentState = ExecutionState.FULFILLING_REQUIREMENTS; - this.currentStepNumber = step != null ? step.getOrder() : 0; - this.totalSteps = FulfillmentStep.getTotalSteps(); - - // Reset requirement tracking when starting new step - this.currentRequirement = null; - this.currentRequirementName = null; - this.currentRequirementIndex = 0; - this.totalRequirementsInStep = 0; - - log.debug("Fulfillment step updated to: {} ({}/{}) - {}", - step != null ? step.getDisplayName() : "None", - currentStepNumber, totalSteps, details); - } - - /** - * Updates the current fulfillment step with requirement counts - */ - public synchronized void updateFulfillmentStep(FulfillmentStep step, String details, int totalRequirementsInStep) { - updateFulfillmentStep(step, details); - this.totalRequirementsInStep = totalRequirementsInStep; - - log.debug("Fulfillment step updated with {} total requirements", totalRequirementsInStep); - } - - /** - * Updates the current requirement being processed within a fulfillment step - */ - public synchronized void updateCurrentRequirement(Object requirement, String requirementName, int requirementIndex) { - this.currentRequirement = requirement; - this.currentRequirementName = requirementName; - this.currentRequirementIndex = requirementIndex; - - // Update details to show current requirement - if (requirementName != null && totalRequirementsInStep > 0) { - this.currentDetails = String.format("Processing: %s (%d/%d)", - requirementName, requirementIndex, totalRequirementsInStep); - } else if (requirementName != null) { - this.currentDetails = "Processing: " + requirementName; - } - - log.debug("Current requirement updated to: {} ({}/{})", - requirementName, requirementIndex, totalRequirementsInStep); - } - - - - /** - * Marks the current execution as failed with an error message - */ - public synchronized void markFailed(String errorMessage) { - this.currentState = ExecutionState.FAILED; - this.hasError = true; - this.errorMessage = errorMessage; - this.currentDetails = errorMessage; - - log.warn("Execution marked as failed: {}", errorMessage); - } - - /** - * Marks the current execution as having an error - */ - public synchronized void markError(String errorMessage) { - this.currentState = ExecutionState.ERROR; - this.hasError = true; - this.errorMessage = errorMessage; - this.currentDetails = errorMessage; - - log.error("Execution marked as error: {}", errorMessage); - } - public synchronized void markCompleted() { - this.currentState = ExecutionState.COMPLETED; - this.hasError = false; - this.errorMessage = null; - this.currentDetails = "Execution completed successfully"; - - log.info("Execution marked as completed"); - } - public synchronized void markIdle() { - this.currentPhase = ExecutionPhase.IDLE; - this.currentState = ExecutionState.STARTING; - this.currentStep = null; - this.currentDetails = null; - this.hasError = false; - this.errorMessage = null; - this.currentStepNumber = 0; - this.totalSteps = 0; - - log.info("Execution state marked as idle"); - } - public synchronized void clearRequirementState() { - // Clear individual requirement tracking - this.currentStep = null; - this.currentRequirement = null; - this.currentRequirementName = null; - this.currentRequirementIndex = 0; - this.totalRequirementsInStep = 0; - - log.debug("Current requirement state cleared"); - } - /** - * Clears all state and returns to idle - */ - public synchronized void clear() { - this.currentPhase = ExecutionPhase.IDLE; - this.currentState = ExecutionState.STARTING; - this.currentStep = null; - this.currentDetails = null; - this.hasError = false; - this.errorMessage = null; - this.currentStepNumber = 0; - this.totalSteps = 0; - this.currentRequirement = null; - this.currentRequirementName = null; - this.currentRequirementIndex = 0; - this.totalRequirementsInStep = 0; - - log.debug("Execution state cleared"); - } - - /** - * Resets all execution tracking to allow tasks to be run again. - * This clears the completion flags but keeps current state if still executing. - */ - public synchronized void reset() { - this.hasPreTaskStarted = false; - this.hasPreTaskCompleted = false; - this.hasMainTaskStarted = false; - this.hasMainTaskCompleted = false; - this.hasPostTaskStarted = false; - this.hasPostTaskCompleted = false; - - // Only clear current state if we're not actively executing - if (currentPhase == ExecutionPhase.IDLE || hasError) { - clear(); - } - - log.debug("\n\t##Task execution state reset - tasks can now be executed again##"); - } - - /** - * Gets a concise status string for overlay display - * @return A formatted status string, or null if idle - */ - public String getDisplayStatus() { - if (currentPhase == ExecutionPhase.IDLE) { - return null; - } - - StringBuilder status = new StringBuilder(); - status.append(currentPhase.getDisplayName()); - - if (currentState == ExecutionState.FULFILLING_REQUIREMENTS && currentStep != null) { - // Show step progress: "Pre-Schedule: Items (3/5)" - status.append(": ").append(currentStep.getDisplayName()) - .append(" (").append(currentStepNumber).append("/").append(totalSteps).append(")"); - - // Add requirement progress if available: "Pre-Schedule: Items (3/5) [2/4]" - if (totalRequirementsInStep > 0 && currentRequirementIndex > 0) { - status.append(" [").append(currentRequirementIndex).append("/").append(totalRequirementsInStep).append("]"); - } - } else if (currentState != ExecutionState.STARTING) { - // Show state: "Pre-Schedule: Custom Tasks" - status.append(": ").append(currentState.getDisplayName()); - } - - return status.toString(); - } - - /** - * Gets a detailed status string including current details and requirement name - * @return A formatted detailed status string, or null if idle - */ - public String getDetailedStatus() { - String displayStatus = getDisplayStatus(); - if (displayStatus == null) { - return null; - } - - StringBuilder detailed = new StringBuilder(displayStatus); - - // Add current requirement name if available - if (currentRequirementName != null && !currentRequirementName.isEmpty()) { - detailed.append(" - ").append(currentRequirementName); - } else if (currentDetails != null && !currentDetails.isEmpty()) { - detailed.append(" - ").append(currentDetails); - } - - return detailed.toString(); - } - - /** - * Checks if any execution is currently in progress - */ - public boolean isExecuting() { - return currentPhase != ExecutionPhase.IDLE; - } - - /** - * Checks if requirements are currently being fulfilled - */ - public boolean isFulfillingRequirements() { - return currentState == ExecutionState.FULFILLING_REQUIREMENTS; - } - - /** - * Checks if the current execution is in an error state - */ - public boolean isInErrorState() { - return hasError; - } - - /** - * Gets the current progress as a percentage (0-100) - */ - public int getProgressPercentage() { - if (totalSteps == 0) { - return 0; - } - return (int) ((double) currentStepNumber / totalSteps * 100); - } - - // Convenience methods for checking task completion and execution states - - /** - * Checks if pre-schedule tasks can be executed (not started or already completed) - */ - public boolean canExecutePreTasks() { - return !hasPreTaskStarted || hasPreTaskCompleted; - } - - /** - * Checks if main task can be executed (pre-tasks completed, main not started or already completed) - */ - public boolean canExecuteMainTask() { - return hasPreTaskCompleted && (!hasMainTaskStarted || hasMainTaskCompleted); - } - - /** - * Checks if post-schedule tasks can be executed (main task completed, post not started or already completed) - */ - public boolean canExecutePostTasks() { - return hasMainTaskStarted && (!hasPostTaskStarted || hasPostTaskCompleted); - } - - /** - * Checks if pre-schedule tasks are currently running - */ - public boolean isPreTaskRunning() { - return hasPreTaskStarted && !hasPreTaskCompleted && currentPhase == ExecutionPhase.PRE_SCHEDULE; - } - - /** - * Checks if main task is currently running - */ - public boolean isMainTaskRunning() { - return hasMainTaskStarted && !hasMainTaskCompleted && currentPhase == ExecutionPhase.MAIN_EXECUTION; - } - - /** - * Checks if post-schedule tasks are currently running - */ - public boolean isPostTaskRunning() { - return hasPostTaskStarted && !hasPostTaskCompleted && currentPhase == ExecutionPhase.POST_SCHEDULE; - } - - /** - * Checks if pre-schedule tasks are completed - */ - public boolean isPreTaskComplete() { - return hasPreTaskCompleted; - } - - /** - * Checks if main task is completed - */ - public boolean isMainTaskComplete() { - return hasMainTaskCompleted; - } - - /** - * Checks if post-schedule tasks are completed - */ - public boolean isPostTaskComplete() { - return hasPostTaskCompleted; - } - - /** - * Checks if all tasks (pre, main, post) are completed - */ - public boolean areAllTasksComplete() { - return hasPreTaskCompleted && hasMainTaskCompleted && hasPostTaskCompleted; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/PrePostScheduleTasksInfoPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/PrePostScheduleTasksInfoPanel.java deleted file mode 100644 index fb55735cb68..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/PrePostScheduleTasksInfoPanel.java +++ /dev/null @@ -1,258 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.ui; - -import net.runelite.client.plugins.microbot.pluginscheduler.api.SchedulablePlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.AbstractPrePostScheduleTasks; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.PrePostScheduleRequirements; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.state.TaskExecutionState; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import java.awt.*; - -/** - * A comprehensive UI component for displaying pre/post schedule task information for a plugin. - * This panel shows both the execution state and requirements information. - */ -public class PrePostScheduleTasksInfoPanel extends JPanel { - - private TaskExecutionStatePanel preTaskStatePanel; - private TaskExecutionStatePanel postTaskStatePanel; - private RequirementsStatusPanel requirementsPanel; - private JLabel pluginNameLabel; - private JLabel tasksEnabledLabel; - - // State tracking - private SchedulablePlugin lastTrackedPlugin; - private AbstractPrePostScheduleTasks lastTrackedTasks; - - public PrePostScheduleTasksInfoPanel() { - setLayout(new BorderLayout(5, 5)); - setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.BRAND_ORANGE, 2), - "Pre/Post Schedule Tasks", - TitledBorder.CENTER, - TitledBorder.TOP, - FontManager.getRunescapeBoldFont(), - Color.WHITE - ), - new EmptyBorder(8, 8, 8, 8) - )); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setOpaque(true); - - // Header panel with plugin info - JPanel headerPanel = createHeaderPanel(); - add(headerPanel, BorderLayout.NORTH); - - // Main content with task states - JPanel contentPanel = createContentPanel(); - add(contentPanel, BorderLayout.CENTER); - - // Initially hidden until a plugin with tasks is set - setVisible(false); - } - - private JPanel createHeaderPanel() { - JPanel panel = new JPanel(new GridBagLayout()); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - panel.setOpaque(true); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(2, 5, 2, 5); - gbc.anchor = GridBagConstraints.WEST; - - // Plugin name - gbc.gridx = 0; gbc.gridy = 0; - gbc.weightx = 0.0; - JLabel pluginTitle = new JLabel("Plugin:"); - pluginTitle.setFont(FontManager.getRunescapeSmallFont()); - pluginTitle.setForeground(Color.LIGHT_GRAY); - panel.add(pluginTitle, gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - pluginNameLabel = new JLabel("None"); - pluginNameLabel.setFont(FontManager.getRunescapeBoldFont()); - pluginNameLabel.setForeground(Color.WHITE); - panel.add(pluginNameLabel, gbc); - - // Tasks enabled status - gbc.gridx = 0; gbc.gridy = 1; - gbc.weightx = 0.0; - gbc.fill = GridBagConstraints.NONE; - JLabel enabledTitle = new JLabel("Tasks:"); - enabledTitle.setFont(FontManager.getRunescapeSmallFont()); - enabledTitle.setForeground(Color.LIGHT_GRAY); - panel.add(enabledTitle, gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - tasksEnabledLabel = new JLabel("Disabled"); - tasksEnabledLabel.setFont(FontManager.getRunescapeSmallFont()); - tasksEnabledLabel.setForeground(Color.RED); - panel.add(tasksEnabledLabel, gbc); - - return panel; - } - - private JPanel createContentPanel() { - JPanel panel = new JPanel(); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - panel.setOpaque(true); - - // Create task state panels - preTaskStatePanel = new TaskExecutionStatePanel("Pre-Schedule Tasks"); - postTaskStatePanel = new TaskExecutionStatePanel("Post-Schedule Tasks"); - - // Create requirements status panel - requirementsPanel = new RequirementsStatusPanel(); - - // Add components with spacing - panel.add(preTaskStatePanel); - panel.add(Box.createVerticalStrut(5)); - panel.add(postTaskStatePanel); - panel.add(Box.createVerticalStrut(5)); - panel.add(requirementsPanel); - - return panel; - } - - /** - * Updates the panel with information from the specified plugin - */ - public void updatePlugin(SchedulablePlugin plugin) { - if (plugin == null) { - setVisible(false); - return; - } - - // Check if this is a different plugin or if tasks changed - AbstractPrePostScheduleTasks tasks = plugin.getPrePostScheduleTasks(); - boolean pluginChanged = plugin != lastTrackedPlugin; - boolean tasksChanged = tasks != lastTrackedTasks; - - if (pluginChanged || tasksChanged) { - updatePluginHeader(plugin, tasks); - lastTrackedPlugin = plugin; - lastTrackedTasks = tasks; - } - - // Update task execution states - if (tasks != null) { - TaskExecutionState executionState = tasks.getExecutionState(); - TaskExecutionState.ExecutionPhase currentPhase = executionState.getCurrentPhase(); - - // Update appropriate panel based on current phase - if (currentPhase == TaskExecutionState.ExecutionPhase.PRE_SCHEDULE) { - preTaskStatePanel.updateState(executionState); - preTaskStatePanel.setVisible(true); - postTaskStatePanel.setVisible(false); - setVisible(true); - } else if (currentPhase == TaskExecutionState.ExecutionPhase.POST_SCHEDULE) { - postTaskStatePanel.updateState(executionState); - postTaskStatePanel.setVisible(true); - preTaskStatePanel.setVisible(false); - setVisible(true); - } else { - // Idle or main execution - hide both task panels and the whole panel if no active tasks - preTaskStatePanel.setVisible(false); - postTaskStatePanel.setVisible(false); - - // Show the panel if tasks are available (even if not executing) to show requirements - // Hide completely only if in IDLE phase and no interesting state to show - if (currentPhase == TaskExecutionState.ExecutionPhase.IDLE) { - setVisible(false); - } else { - setVisible(true); - } - } - - // Update requirements panel with execution state for enhanced progress tracking - PrePostScheduleRequirements requirements = tasks.getRequirements(); - requirementsPanel.updateRequirements(requirements, executionState); - - } else { - // No tasks available at all - hide everything - preTaskStatePanel.setVisible(false); - postTaskStatePanel.setVisible(false); - setVisible(false); - } - } - - private void updatePluginHeader(SchedulablePlugin plugin, AbstractPrePostScheduleTasks tasks) { - // Update plugin name - String pluginName = "Unknown"; - if (plugin instanceof net.runelite.client.plugins.Plugin) { - net.runelite.client.plugins.Plugin p = (net.runelite.client.plugins.Plugin) plugin; - net.runelite.client.plugins.PluginDescriptor descriptor = p.getClass().getAnnotation(net.runelite.client.plugins.PluginDescriptor.class); - if (descriptor != null) { - pluginName = descriptor.name(); - } else { - pluginName = p.getClass().getSimpleName(); - } - } - pluginNameLabel.setText(pluginName); - - // Update tasks enabled status - if (tasks != null) { - tasksEnabledLabel.setText("Enabled"); - tasksEnabledLabel.setForeground(Color.GREEN); - } else { - tasksEnabledLabel.setText("Disabled"); - tasksEnabledLabel.setForeground(Color.RED); - } - } - - /** - * Clears the panel and hides it - */ - public void clear() { - pluginNameLabel.setText("None"); - tasksEnabledLabel.setText("Disabled"); - tasksEnabledLabel.setForeground(Color.RED); - - preTaskStatePanel.reset(); - postTaskStatePanel.reset(); - requirementsPanel.clear(); - - lastTrackedPlugin = null; - lastTrackedTasks = null; - - setVisible(false); - } - - /** - * Forces a refresh of the task states - useful when task execution begins or ends - */ - public void refresh() { - if (lastTrackedTasks != null) { - TaskExecutionState executionState = lastTrackedTasks.getExecutionState(); - - // Reset visibility for both panels - preTaskStatePanel.setVisible(false); - postTaskStatePanel.setVisible(false); - - // Show appropriate panel based on current phase - TaskExecutionState.ExecutionPhase phase = executionState.getCurrentPhase(); - if (phase == TaskExecutionState.ExecutionPhase.PRE_SCHEDULE) { - preTaskStatePanel.updateState(executionState); - preTaskStatePanel.setVisible(true); - } else if (phase == TaskExecutionState.ExecutionPhase.POST_SCHEDULE) { - postTaskStatePanel.updateState(executionState); - postTaskStatePanel.setVisible(true); - } - - // Update requirements panel with execution state - requirementsPanel.updateRequirements(lastTrackedTasks.getRequirements(), executionState); - - repaint(); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/RequirementsStatusPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/RequirementsStatusPanel.java deleted file mode 100644 index d4f2e53e5e9..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/RequirementsStatusPanel.java +++ /dev/null @@ -1,434 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.ui; - -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.PrePostScheduleRequirements; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.registry.RequirementRegistry; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.state.TaskExecutionState; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import java.awt.*; -import java.util.List; -import java.util.ArrayList; - -/** - * A UI component for displaying the status of pre/post schedule requirements. - * Shows requirement counts, fulfillment status, and progress information. - * Enhanced with context awareness to display requirements specific to current execution phase. - */ -public class RequirementsStatusPanel extends JPanel { - - private final JLabel totalRequirementsLabel; - private final JLabel fulfilledRequirementsLabel; - private final JLabel mandatoryRequirementsLabel; - private final JLabel optionalRequirementsLabel; - private final JProgressBar fulfillmentProgressBar; - private final JLabel currentRequirementLabel; - private final JLabel phaseLabel; - - // State tracking - private PrePostScheduleRequirements lastRequirements; - private int lastTotalRequirements = 0; - private int lastFulfilledRequirements = 0; - private TaskExecutionState lastExecutionState; - private TaskExecutionState.ExecutionPhase lastPhase; - - public RequirementsStatusPanel() { - setLayout(new BorderLayout(5, 5)); - setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.BRAND_ORANGE, 1), - "Requirements Status", - TitledBorder.CENTER, - TitledBorder.TOP, - FontManager.getRunescapeSmallFont(), - Color.WHITE - ), - new EmptyBorder(5, 5, 5, 5) - )); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setOpaque(true); - - // Create content panel - JPanel contentPanel = new JPanel(new GridBagLayout()); - contentPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - contentPanel.setOpaque(true); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(2, 5, 2, 5); - gbc.anchor = GridBagConstraints.WEST; - - // Phase row - shows current execution phase - gbc.gridx = 0; gbc.gridy = 0; - gbc.weightx = 0.0; - JLabel phaseTitle = new JLabel("Phase:"); - phaseTitle.setFont(FontManager.getRunescapeSmallFont()); - phaseTitle.setForeground(Color.LIGHT_GRAY); - contentPanel.add(phaseTitle, gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - phaseLabel = new JLabel("Idle"); - phaseLabel.setFont(FontManager.getRunescapeSmallFont()); - phaseLabel.setForeground(Color.CYAN); - contentPanel.add(phaseLabel, gbc); - - // Total requirements row - gbc.gridx = 0; gbc.gridy = 1; - gbc.weightx = 0.0; - gbc.fill = GridBagConstraints.NONE; - JLabel totalTitle = new JLabel("Total:"); - totalTitle.setFont(FontManager.getRunescapeSmallFont()); - totalTitle.setForeground(Color.LIGHT_GRAY); - contentPanel.add(totalTitle, gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - totalRequirementsLabel = new JLabel("0"); - totalRequirementsLabel.setFont(FontManager.getRunescapeSmallFont()); - totalRequirementsLabel.setForeground(Color.WHITE); - contentPanel.add(totalRequirementsLabel, gbc); - - // Fulfilled requirements row - gbc.gridx = 0; gbc.gridy = 2; - gbc.weightx = 0.0; - gbc.fill = GridBagConstraints.NONE; - JLabel fulfilledTitle = new JLabel("Fulfilled:"); - fulfilledTitle.setFont(FontManager.getRunescapeSmallFont()); - fulfilledTitle.setForeground(Color.LIGHT_GRAY); - contentPanel.add(fulfilledTitle, gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - fulfilledRequirementsLabel = new JLabel("0"); - fulfilledRequirementsLabel.setFont(FontManager.getRunescapeSmallFont()); - fulfilledRequirementsLabel.setForeground(Color.GREEN); - contentPanel.add(fulfilledRequirementsLabel, gbc); - - // Mandatory requirements row - gbc.gridx = 0; gbc.gridy = 3; - gbc.weightx = 0.0; - gbc.fill = GridBagConstraints.NONE; - JLabel mandatoryTitle = new JLabel("Mandatory:"); - mandatoryTitle.setFont(FontManager.getRunescapeSmallFont()); - mandatoryTitle.setForeground(Color.LIGHT_GRAY); - contentPanel.add(mandatoryTitle, gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - mandatoryRequirementsLabel = new JLabel("0"); - mandatoryRequirementsLabel.setFont(FontManager.getRunescapeSmallFont()); - mandatoryRequirementsLabel.setForeground(Color.RED); - contentPanel.add(mandatoryRequirementsLabel, gbc); - - // Optional requirements row - gbc.gridx = 0; gbc.gridy = 4; - gbc.weightx = 0.0; - gbc.fill = GridBagConstraints.NONE; - JLabel optionalTitle = new JLabel("Optional:"); - optionalTitle.setFont(FontManager.getRunescapeSmallFont()); - optionalTitle.setForeground(Color.LIGHT_GRAY); - contentPanel.add(optionalTitle, gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - optionalRequirementsLabel = new JLabel("0"); - optionalRequirementsLabel.setFont(FontManager.getRunescapeSmallFont()); - optionalRequirementsLabel.setForeground(Color.YELLOW); - contentPanel.add(optionalRequirementsLabel, gbc); - - // Current requirement row - shows what's being processed - gbc.gridx = 0; gbc.gridy = 5; - gbc.weightx = 0.0; - gbc.fill = GridBagConstraints.NONE; - JLabel currentTitle = new JLabel("Current:"); - currentTitle.setFont(FontManager.getRunescapeSmallFont()); - currentTitle.setForeground(Color.LIGHT_GRAY); - contentPanel.add(currentTitle, gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - currentRequirementLabel = new JLabel("None"); - currentRequirementLabel.setFont(FontManager.getRunescapeSmallFont()); - currentRequirementLabel.setForeground(Color.ORANGE); - contentPanel.add(currentRequirementLabel, gbc); - - // Progress bar row - gbc.gridx = 0; gbc.gridy = 6; - gbc.gridwidth = 2; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - fulfillmentProgressBar = new JProgressBar(0, 100); - fulfillmentProgressBar.setStringPainted(true); - fulfillmentProgressBar.setString("0%"); - fulfillmentProgressBar.setFont(FontManager.getRunescapeSmallFont()); - fulfillmentProgressBar.setForeground(ColorScheme.BRAND_ORANGE); - fulfillmentProgressBar.setBackground(ColorScheme.DARKER_GRAY_COLOR); - contentPanel.add(fulfillmentProgressBar, gbc); - - add(contentPanel, BorderLayout.CENTER); - - // Initially hidden - setVisible(false); - } - - /** - * Updates the panel with the current requirements status and execution state - */ - public void updateRequirements(PrePostScheduleRequirements requirements) { - updateRequirements(requirements, null); - } - - /** - * Enhanced update method that includes TaskExecutionState for progress tracking - */ - public void updateRequirements(PrePostScheduleRequirements requirements, TaskExecutionState executionState) { - if (requirements == null) { - setVisible(false); - return; - } - - boolean hasChanges = false; - - // Update execution phase display - if (executionState != null) { - TaskExecutionState.ExecutionPhase currentPhase = executionState.getCurrentPhase(); - if (currentPhase != lastPhase) { - phaseLabel.setText(currentPhase.getDisplayName()); - - // Color-code the phase - switch (currentPhase) { - case PRE_SCHEDULE: - phaseLabel.setForeground(Color.CYAN); - break; - case MAIN_EXECUTION: - phaseLabel.setForeground(Color.GREEN); - break; - case POST_SCHEDULE: - phaseLabel.setForeground(Color.ORANGE); - break; - default: - phaseLabel.setForeground(Color.LIGHT_GRAY); - break; - } - - lastPhase = currentPhase; - hasChanges = true; - } - - // Update current requirement display - String currentRequirementName = executionState.getCurrentRequirementName(); - int currentIndex = executionState.getCurrentRequirementIndex(); - int totalInStep = executionState.getTotalRequirementsInStep(); - - if (currentRequirementName != null && totalInStep > 0) { - currentRequirementLabel.setText(String.format("%s (%d/%d)", - currentRequirementName, currentIndex, totalInStep)); - currentRequirementLabel.setForeground(Color.ORANGE); - } else if (currentRequirementName != null) { - currentRequirementLabel.setText(currentRequirementName); - currentRequirementLabel.setForeground(Color.ORANGE); - } else { - currentRequirementLabel.setText("None"); - currentRequirementLabel.setForeground(Color.LIGHT_GRAY); - } - - lastExecutionState = executionState; - } else { - // No execution state - show idle - phaseLabel.setText("Idle"); - phaseLabel.setForeground(Color.LIGHT_GRAY); - currentRequirementLabel.setText("None"); - currentRequirementLabel.setForeground(Color.LIGHT_GRAY); - } - - // Get requirement registry - RequirementRegistry registry = requirements.getRegistry(); - if (registry == null) { - setVisible(false); - return; - } - - // Determine which requirements to show based on current execution phase - List relevantRequirements; - if (executionState != null) { - TaskExecutionState.ExecutionPhase currentPhase = executionState.getCurrentPhase(); - - // Filter requirements based on current execution phase - if (currentPhase == TaskExecutionState.ExecutionPhase.PRE_SCHEDULE) { - // Show PRE_SCHEDULE and BOTH requirements - relevantRequirements = registry.getRequirements(TaskContext.PRE_SCHEDULE).stream() - .collect(ArrayList::new, (list, req) -> { - if (!list.contains(req)) list.add(req); - }, ArrayList::addAll); - - registry.getExternalRequirements(TaskContext.PRE_SCHEDULE).stream() - .filter(req -> !relevantRequirements.contains(req)) - .forEach(relevantRequirements::add); - - // Add BOTH requirements, excluding duplicates - registry.getRequirements(TaskContext.BOTH).stream() - .filter(req -> !relevantRequirements.contains(req)) - .forEach(relevantRequirements::add); - - registry.getExternalRequirements(TaskContext.BOTH).stream() - .filter(req -> !relevantRequirements.contains(req)) - .forEach(relevantRequirements::add); - - } else if (currentPhase == TaskExecutionState.ExecutionPhase.POST_SCHEDULE) { - // Show POST_SCHEDULE and BOTH requirements - relevantRequirements = registry.getRequirements(TaskContext.POST_SCHEDULE).stream() - .collect(ArrayList::new, (list, req) -> { - if (!list.contains(req)) list.add(req); - }, ArrayList::addAll); - - registry.getExternalRequirements(TaskContext.POST_SCHEDULE).stream() - .filter(req -> !relevantRequirements.contains(req)) - .forEach(relevantRequirements::add); - - // Add BOTH requirements, excluding duplicates - registry.getRequirements(TaskContext.BOTH).stream() - .filter(req -> !relevantRequirements.contains(req)) - .forEach(relevantRequirements::add); - - registry.getExternalRequirements(TaskContext.BOTH).stream() - .filter(req -> !relevantRequirements.contains(req)) - .forEach(relevantRequirements::add); - - } else { - // For IDLE or MAIN_EXECUTION, show all requirements - relevantRequirements = registry.getAllRequirements().stream() - .collect(ArrayList::new, (list, req) -> { - if (!list.contains(req)) list.add(req); - }, ArrayList::addAll); - - // Add external requirements for all contexts, excluding duplicates - java.util.Arrays.stream(TaskContext.values()) - .flatMap(context -> registry.getExternalRequirements(context).stream()) - .filter(req -> !relevantRequirements.contains(req)) - .forEach(relevantRequirements::add); - } - } else { - // No execution state - show all requirements - relevantRequirements = registry.getAllRequirements().stream() - .collect(ArrayList::new, (list, req) -> { - if (!list.contains(req)) list.add(req); - }, ArrayList::addAll); - - // Add external requirements for all contexts, excluding duplicates - java.util.Arrays.stream(TaskContext.values()) - .flatMap(context -> registry.getExternalRequirements(context).stream()) - .filter(req -> !relevantRequirements.contains(req)) - .forEach(relevantRequirements::add); - } - - // Calculate requirement counts based on filtered requirements - int totalRequirements = relevantRequirements.size(); - int mandatoryCount = (int) relevantRequirements.stream() - .filter(req -> req.getPriority() == RequirementPriority.MANDATORY) - .count(); - int optionalCount = totalRequirements - mandatoryCount; - - // Calculate fulfilled count from execution state progress - int fulfilledCount = 0; - if (executionState != null) { - // Use execution state step progress to calculate fulfillment - int currentStepNumber = executionState.getCurrentStepNumber(); - int totalSteps = executionState.getTotalSteps(); - int currentRequirementIndex = executionState.getCurrentRequirementIndex(); - int totalRequirementsInStep = executionState.getTotalRequirementsInStep(); - - if (totalSteps > 0 && currentStepNumber > 0) { - // Calculate approximate fulfillment based on step progress - double stepProgress = (double) (currentStepNumber - 1) / totalSteps; - - // Add progress within current step if available - if (totalRequirementsInStep > 0 && currentRequirementIndex > 0) { - double stepCompletionRatio = (double) currentRequirementIndex / totalRequirementsInStep; - stepProgress += stepCompletionRatio / totalSteps; - } - - fulfilledCount = (int) Math.round(stepProgress * totalRequirements); - fulfilledCount = Math.min(fulfilledCount, totalRequirements); // Cap at total - } - } - - // Update total requirements - if (totalRequirements != lastTotalRequirements) { - totalRequirementsLabel.setText(String.valueOf(totalRequirements)); - lastTotalRequirements = totalRequirements; - hasChanges = true; - } - - // Update fulfilled requirements - if (fulfilledCount != lastFulfilledRequirements) { - fulfilledRequirementsLabel.setText(String.valueOf(fulfilledCount)); - lastFulfilledRequirements = fulfilledCount; - hasChanges = true; - } - - // Update mandatory count - mandatoryRequirementsLabel.setText(String.valueOf(mandatoryCount)); - - // Update optional count - optionalRequirementsLabel.setText(String.valueOf(optionalCount)); - - // Update progress bar - int percentage = totalRequirements > 0 ? (int) ((fulfilledCount / (double) totalRequirements) * 100) : 0; - fulfillmentProgressBar.setValue(percentage); - fulfillmentProgressBar.setString(percentage + "%"); - - // Update progress bar color based on fulfillment state - if (percentage == 100) { - fulfillmentProgressBar.setForeground(Color.GREEN); - } else if (percentage > 0) { - fulfillmentProgressBar.setForeground(ColorScheme.BRAND_ORANGE); - } else { - fulfillmentProgressBar.setForeground(Color.RED); - } - - lastRequirements = requirements; - setVisible(true); - - if (hasChanges) { - repaint(); - } - } - - /** - * Clears the panel and hides it - */ - public void clear() { - phaseLabel.setText("Idle"); - phaseLabel.setForeground(Color.LIGHT_GRAY); - totalRequirementsLabel.setText("0"); - fulfilledRequirementsLabel.setText("0"); - mandatoryRequirementsLabel.setText("0"); - optionalRequirementsLabel.setText("0"); - currentRequirementLabel.setText("None"); - currentRequirementLabel.setForeground(Color.LIGHT_GRAY); - fulfillmentProgressBar.setValue(0); - fulfillmentProgressBar.setString("0%"); - fulfillmentProgressBar.setForeground(Color.RED); - - lastRequirements = null; - lastTotalRequirements = 0; - lastFulfilledRequirements = 0; - lastExecutionState = null; - lastPhase = null; - - setVisible(false); - repaint(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/TaskExecutionStatePanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/TaskExecutionStatePanel.java deleted file mode 100644 index 80d6e9e0079..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/TaskExecutionStatePanel.java +++ /dev/null @@ -1,289 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.ui; - -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.state.TaskExecutionState; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import java.awt.*; - -/** - * A reusable UI component for displaying the current state of pre/post schedule task execution. - * This panel shows the current phase, execution state, progress, and any error information. - */ -public class TaskExecutionStatePanel extends JPanel { - - private final JLabel phaseLabel; - private final JLabel stateLabel; - private final JLabel detailsLabel; - private final JProgressBar progressBar; - private final JLabel progressLabel; - private final JLabel currentRequirementLabel; - private final JLabel errorLabel; - - // State tracking for optimized updates - private TaskExecutionState.ExecutionPhase lastPhase; - private TaskExecutionState.ExecutionState lastState; - private String lastDetails; - private int lastCurrentStep; - private int lastTotalSteps; - private boolean lastHasError; - private String lastErrorMessage; - private String lastCurrentRequirementName; - - public TaskExecutionStatePanel(String title) { - setLayout(new BorderLayout(5, 5)); - setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.BRAND_ORANGE, 1), - title, - TitledBorder.CENTER, - TitledBorder.TOP, - FontManager.getRunescapeSmallFont(), - Color.WHITE - ), - new EmptyBorder(5, 5, 5, 5) - )); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setOpaque(true); - - // Create main content panel - JPanel contentPanel = new JPanel(new GridBagLayout()); - contentPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - contentPanel.setOpaque(true); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(2, 5, 2, 5); - gbc.anchor = GridBagConstraints.WEST; - - // Phase row - gbc.gridx = 0; gbc.gridy = 0; - gbc.weightx = 0.0; - JLabel phaseTitle = new JLabel("Phase:"); - phaseTitle.setFont(FontManager.getRunescapeSmallFont()); - phaseTitle.setForeground(Color.LIGHT_GRAY); - contentPanel.add(phaseTitle, gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - phaseLabel = new JLabel("Idle"); - phaseLabel.setFont(FontManager.getRunescapeSmallFont()); - phaseLabel.setForeground(Color.WHITE); - contentPanel.add(phaseLabel, gbc); - - // State row - gbc.gridx = 0; gbc.gridy = 1; - gbc.weightx = 0.0; - gbc.fill = GridBagConstraints.NONE; - JLabel stateTitle = new JLabel("State:"); - stateTitle.setFont(FontManager.getRunescapeSmallFont()); - stateTitle.setForeground(Color.LIGHT_GRAY); - contentPanel.add(stateTitle, gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - stateLabel = new JLabel("Starting"); - stateLabel.setFont(FontManager.getRunescapeSmallFont()); - stateLabel.setForeground(Color.WHITE); - contentPanel.add(stateLabel, gbc); - - // Progress bar row - gbc.gridx = 0; gbc.gridy = 2; - gbc.gridwidth = 2; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - progressBar = new JProgressBar(0, 100); - progressBar.setStringPainted(true); - progressBar.setString("0 / 0"); - progressBar.setFont(FontManager.getRunescapeSmallFont()); - progressBar.setForeground(ColorScheme.BRAND_ORANGE); - progressBar.setBackground(ColorScheme.DARKER_GRAY_COLOR); - contentPanel.add(progressBar, gbc); - - // Progress label row - gbc.gridy = 3; - progressLabel = new JLabel("No progress"); - progressLabel.setFont(FontManager.getRunescapeSmallFont()); - progressLabel.setForeground(Color.LIGHT_GRAY); - contentPanel.add(progressLabel, gbc); - - // Current requirement row - gbc.gridy = 4; - currentRequirementLabel = new JLabel(""); - currentRequirementLabel.setFont(FontManager.getRunescapeSmallFont()); - currentRequirementLabel.setForeground(Color.CYAN); - contentPanel.add(currentRequirementLabel, gbc); - - // Details row - gbc.gridy = 5; - detailsLabel = new JLabel(""); - detailsLabel.setFont(FontManager.getRunescapeSmallFont()); - detailsLabel.setForeground(Color.WHITE); - contentPanel.add(detailsLabel, gbc); - - // Error row - gbc.gridy = 6; - errorLabel = new JLabel(""); - errorLabel.setFont(FontManager.getRunescapeSmallFont()); - errorLabel.setForeground(Color.RED); - contentPanel.add(errorLabel, gbc); - - add(contentPanel, BorderLayout.CENTER); - - // Set initial visibility - setVisible(false); - } - - /** - * Updates the panel with the current task execution state. - * Only redraws components that have actually changed for performance. - */ - public void updateState(TaskExecutionState state) { - if (state == null) { - setVisible(false); - return; - } - - boolean hasChanges = false; - - // Check phase changes - TaskExecutionState.ExecutionPhase currentPhase = state.getCurrentPhase(); - if (currentPhase != lastPhase) { - phaseLabel.setText(currentPhase.getDisplayName()); - lastPhase = currentPhase; - hasChanges = true; - } - - // Check state changes - TaskExecutionState.ExecutionState currentState = state.getCurrentState(); - if (currentState != lastState) { - stateLabel.setText(currentState.getDisplayName()); - lastState = currentState; - hasChanges = true; - } - - // Check details changes - String currentDetails = state.getCurrentDetails(); - if (!java.util.Objects.equals(currentDetails, lastDetails)) { - detailsLabel.setText(currentDetails != null ? currentDetails : ""); - lastDetails = currentDetails; - hasChanges = true; - } - - // Check progress changes - int currentStep = state.getCurrentStepNumber(); - int totalSteps = state.getTotalSteps(); - if (currentStep != lastCurrentStep || totalSteps != lastTotalSteps) { - updateProgressBar(currentStep, totalSteps); - lastCurrentStep = currentStep; - lastTotalSteps = totalSteps; - hasChanges = true; - } - - // Check current requirement changes - String currentRequirementName = state.getCurrentRequirementName(); - if (!java.util.Objects.equals(currentRequirementName, lastCurrentRequirementName)) { - updateCurrentRequirement(currentRequirementName, state.getCurrentRequirementIndex(), state.getTotalRequirementsInStep()); - lastCurrentRequirementName = currentRequirementName; - hasChanges = true; - } - - // Check error state changes - boolean hasError = state.isHasError(); - String errorMessage = state.getErrorMessage(); - if (hasError != lastHasError || !java.util.Objects.equals(errorMessage, lastErrorMessage)) { - updateErrorDisplay(hasError, errorMessage); - lastHasError = hasError; - lastErrorMessage = errorMessage; - hasChanges = true; - } - - // Show panel if it was hidden and we have actual execution happening - if (!isVisible() && currentPhase != TaskExecutionState.ExecutionPhase.IDLE) { - setVisible(true); - hasChanges = true; - } - - // Hide panel if back to idle - if (isVisible() && currentPhase == TaskExecutionState.ExecutionPhase.IDLE) { - setVisible(false); - hasChanges = true; - } - - // Repaint if there were changes - if (hasChanges) { - repaint(); - } - } - - private void updateProgressBar(int current, int total) { - if (total > 0) { - int percentage = (int) ((current / (double) total) * 100); - progressBar.setValue(percentage); - progressBar.setString(current + " / " + total); - progressLabel.setText(String.format("Step %d of %d", current, total)); - } else { - progressBar.setValue(0); - progressBar.setString("0 / 0"); - progressLabel.setText("No steps defined"); - } - } - - private void updateCurrentRequirement(String requirementName, int requirementIndex, int totalRequirements) { - if (requirementName != null && !requirementName.isEmpty()) { - if (totalRequirements > 0) { - currentRequirementLabel.setText(String.format("Requirement: %s (%d/%d)", - requirementName, requirementIndex + 1, totalRequirements)); - } else { - currentRequirementLabel.setText("Requirement: " + requirementName); - } - currentRequirementLabel.setVisible(true); - } else { - currentRequirementLabel.setText(""); - currentRequirementLabel.setVisible(false); - } - } - - private void updateErrorDisplay(boolean hasError, String errorMessage) { - if (hasError && errorMessage != null && !errorMessage.isEmpty()) { - errorLabel.setText("Error: " + errorMessage); - errorLabel.setVisible(true); - } else { - errorLabel.setText(""); - errorLabel.setVisible(false); - } - } - - /** - * Resets the panel to its initial state - */ - public void reset() { - phaseLabel.setText("Idle"); - stateLabel.setText("Starting"); - detailsLabel.setText(""); - progressBar.setValue(0); - progressBar.setString("0 / 0"); - progressLabel.setText("No progress"); - currentRequirementLabel.setText(""); - currentRequirementLabel.setVisible(false); - errorLabel.setText(""); - errorLabel.setVisible(false); - - // Reset state tracking - lastPhase = null; - lastState = null; - lastDetails = null; - lastCurrentStep = 0; - lastTotalSteps = 0; - lastHasError = false; - lastErrorMessage = null; - lastCurrentRequirementName = null; - - setVisible(false); - repaint(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/Antiban/AntibanDialogWindow.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/Antiban/AntibanDialogWindow.java deleted file mode 100644 index 2d638580310..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/Antiban/AntibanDialogWindow.java +++ /dev/null @@ -1,53 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.Antiban; - -import javax.swing.*; - -import net.runelite.client.plugins.microbot.util.antiban.ui.MasterPanel; - -import java.awt.*; - -/** - * A dialog window for displaying the Antiban Master Panel in a separate window. - * This allows users to configure antiban settings without having to switch to the Antiban plugin tab. - */ -public class AntibanDialogWindow extends JDialog { - - /** - * Creates a new dialog window containing the Antiban MasterPanel - * - * @param owner The parent frame for the dialog - */ - public AntibanDialogWindow(Frame owner) { - super(owner, "Antiban Settings", false); - - // Create a new master panel - MasterPanel masterPanel = new MasterPanel(); - - // Add the panel to the dialog - add(masterPanel); - - // Set dialog properties - setSize(new Dimension(320, 600)); - setLocationRelativeTo(owner); - setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); - - // Load initial settings - masterPanel.loadSettings(); - } - - /** - * Static utility method to show the Antiban settings in a new window - * - * @param parent The parent component (used to find the owner frame) - * @return The created dialog window - */ - public static AntibanDialogWindow showAntibanSettings(Component parent) { - // Find the parent frame - Frame parentFrame = JOptionPane.getFrameForComponent(parent); - - // Create and show the dialog - AntibanDialogWindow dialog = new AntibanDialogWindow(parentFrame); - dialog.setVisible(true); - return dialog; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/Antiban/AntibanWindowManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/Antiban/AntibanWindowManager.java deleted file mode 100644 index b3bdaea7b48..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/Antiban/AntibanWindowManager.java +++ /dev/null @@ -1,76 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.Antiban; - - -import javax.swing.*; - -import net.runelite.client.plugins.microbot.util.antiban.ui.MasterPanel; - -/** - * A utility class that manages opening the Antiban MasterPanel in a separate window. - * This allows the MasterPanel to be displayed outside of the normal RuneLite sidebar. - */ -public class AntibanWindowManager { - - private static JFrame antibanWindow; - private static MasterPanel masterPanel; - - /** - * Opens the Antiban MasterPanel in a new window - * - * @param injector The injector to use for creating the MasterPanel - * @return The created window - */ - public static JFrame openAntibanWindow(Object injector) { - // If the window is already open, just bring it to front - if (antibanWindow != null && antibanWindow.isDisplayable()) { - antibanWindow.toFront(); - antibanWindow.requestFocus(); - return antibanWindow; - } - - // Create a new MasterPanel - if (masterPanel == null) { - try { - masterPanel = new MasterPanel(); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - - // Create and set up the window - antibanWindow = new JFrame("Antiban Configuration"); - antibanWindow.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); - antibanWindow.setContentPane(masterPanel); - antibanWindow.pack(); - antibanWindow.setSize(400, 600); // Set an appropriate size - antibanWindow.setLocationRelativeTo(null); // Center on screen - - // Show the window - antibanWindow.setVisible(true); - - // Load the latest settings - masterPanel.loadSettings(); - - return antibanWindow; - } - - /** - * Checks if the Antiban window is currently open - * - * @return true if the window is open, false otherwise - */ - public static boolean isWindowOpen() { - return antibanWindow != null && antibanWindow.isDisplayable(); - } - - /** - * Closes the Antiban window if it's open - */ - public static void closeWindow() { - if (antibanWindow != null) { - antibanWindow.dispose(); - antibanWindow = null; - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/PrioritySpinnerEditor.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/PrioritySpinnerEditor.java deleted file mode 100644 index 27f6a3b5d8d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/PrioritySpinnerEditor.java +++ /dev/null @@ -1,113 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.PluginScheduleEntry; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.table.TableCellEditor; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.util.EventObject; - -/** - * Custom cell editor that provides a spinner for editing plugin priority values - */ -@Slf4j -public class PrioritySpinnerEditor extends AbstractCellEditor implements TableCellEditor, ActionListener { - private JSpinner spinner; - private SpinnerNumberModel spinnerModel; - private PluginScheduleEntry currentEntry; - - public PrioritySpinnerEditor() { - // Create spinner model with min 0, max 100, step 1, initial value 0 - spinnerModel = new SpinnerNumberModel(0, 0, 100, 1); - - // Setup spinner with custom model and editor - spinner = new JSpinner(spinnerModel); - spinner.setBackground(ColorScheme.DARK_GRAY_COLOR); - spinner.setBorder(new EmptyBorder(2, 5, 2, 5)); - - // Style the spinner editor - JSpinner.DefaultEditor editor = (JSpinner.DefaultEditor) spinner.getEditor(); - editor.getTextField().setForeground(Color.WHITE); - editor.getTextField().setBackground(ColorScheme.DARK_GRAY_COLOR); - editor.getTextField().setBorder(BorderFactory.createEmptyBorder()); - - // Add listener for value changes - spinner.addChangeListener(e -> { - // Update the underlying model when the spinner value changes - if (currentEntry != null) { - int newValue = (Integer) spinner.getValue(); - - // If this is a default plugin, don't allow changing priority from 0 - if (currentEntry.isDefault() && newValue != 0) { - spinner.setValue(0); // Reset to 0 - log.debug("Cannot change priority of default plugin {}. Resetting to 0.", currentEntry.getCleanName()); - return; - } - - currentEntry.setPriority(newValue); - log.debug("Updated priority for plugin {} to {}", currentEntry.getCleanName(), newValue); - } - }); - } - - @Override - public Component getTableCellEditorComponent(JTable table, Object value, - boolean isSelected, int row, int column) { - // Get the PluginScheduleEntry for this row - if (table.getModel().getValueAt(row, 0) instanceof PluginScheduleEntry) { - currentEntry = (PluginScheduleEntry) table.getModel().getValueAt(row, 0); - } else { - // If the first column doesn't contain the PluginScheduleEntry, try to get it from the table model - if (table.getModel() instanceof ScheduleTableModel) { - ScheduleTableModel model = (ScheduleTableModel) table.getModel(); - currentEntry = model.getPluginAtRow(row); - } - } - - // Set current value - int priority = value instanceof Integer ? (Integer) value : 0; - spinner.setValue(priority); - - // If this is a default plugin, disable editing - boolean isDefault = false; - if (table.getModel().getValueAt(row, 6) instanceof Boolean) { - isDefault = (Boolean) table.getModel().getValueAt(row, 6); - } - - spinner.setEnabled(!isDefault); - - // Set background color for selection - if (isSelected) { - spinner.setBackground(ColorScheme.MEDIUM_GRAY_COLOR); - JSpinner.DefaultEditor editor = (JSpinner.DefaultEditor) spinner.getEditor(); - editor.getTextField().setBackground(ColorScheme.MEDIUM_GRAY_COLOR); - } else { - spinner.setBackground(ColorScheme.DARK_GRAY_COLOR); - JSpinner.DefaultEditor editor = (JSpinner.DefaultEditor) spinner.getEditor(); - editor.getTextField().setBackground(ColorScheme.DARK_GRAY_COLOR); - } - - return spinner; - } - - @Override - public Object getCellEditorValue() { - return spinner.getValue(); - } - - @Override - public boolean isCellEditable(EventObject anEvent) { - // Allow editing if the current plugin is not a default plugin - return currentEntry != null && !currentEntry.isDefault(); - } - - @Override - public void actionPerformed(ActionEvent e) { - fireEditingStopped(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleFormPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleFormPanel.java deleted file mode 100644 index afea83d2634..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleFormPanel.java +++ /dev/null @@ -1,1339 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.PluginScheduleEntry; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerPlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.api.SchedulablePlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.DayOfWeekCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.ui.TimeConditionPanelUtil; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.pluginscheduler.util.PluginFilterUtil; -import net.runelite.client.plugins.Plugin; -import net.runelite.client.plugins.PluginDescriptor; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.CompoundBorder; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.ItemEvent; -import java.time.Duration; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -@Slf4j -public class ScheduleFormPanel extends JPanel { - private final SchedulerPlugin plugin; - - // Add a flag to track if the combo box change is from user or programmatic - private boolean isUserAction = true; - - @Getter - private JComboBox pluginComboBox; - private JComboBox primeFilterComboBox; - private JComboBox primaryFilterComboBox; - private JComboBox secondaryFilterComboBox; - private JComboBox timeConditionTypeComboBox; - private JCheckBox randomSchedulingCheckbox; - private JCheckBox timeBasedStopConditionCheckbox; - private JCheckBox allowContinueCheckbox; // Add new checkbox field - @Getter - private JSpinner prioritySpinner; - private JCheckBox defaultPluginCheckbox; - - // New panel for editing plugin properties when one is selected - private JPanel pluginPropertiesPanel; - private JSpinner selectedPluginPrioritySpinner; - private JCheckBox selectedPluginDefaultCheckbox; - private JCheckBox selectedPluginEnabledCheckbox; - private JCheckBox selectedPluginRandomCheckbox; - private JCheckBox selectedPluginTimeStopCheckbox; - private JCheckBox selectedPluginAllowContinueCheckbox; // Add new checkbox field for properties panel - - // Statistics labels - private JLabel selectedPluginNameLabel; - private JLabel runsLabel; - private JLabel lastRunLabel; - private JLabel lastDurationLabel; - private JLabel lastStopReasonLabel; - private JButton saveChangesButton; - - // Condition config panels - private JPanel conditionConfigPanel; - private JPanel currentConditionPanel; - - private JButton addButton; - private JButton updateButton; - private JButton removeButton; - private JButton controlButton; - private JTabbedPane tabbedPane; - private PluginScheduleEntry selectedPlugin; - - // Flag to prevent update loops - private boolean updatingValues = false; - - // Plugin change tracking - private Set lastKnownPlugins = new HashSet<>(); - - // Constants for time condition types - private static final String CONDITION_DEFAULT = "Run Default"; - private static final String CONDITION_SPECIFIC_TIME = "Run at Specific Time"; - private static final String CONDITION_INTERVAL = "Run at Interval"; - private static final String CONDITION_TIME_WINDOW = "Run in Time Window"; - private static final String CONDITION_DAY_OF_WEEK = "Run on Day of Week"; - private static final String[] TIME_CONDITION_TYPES = { - CONDITION_DEFAULT, - CONDITION_SPECIFIC_TIME, - CONDITION_INTERVAL, - CONDITION_TIME_WINDOW, - CONDITION_DAY_OF_WEEK - }; - - // Add fields and methods for the selection change listener - private Runnable selectionChangeListener; - - public void setSelectionChangeListener(Runnable listener) { - this.selectionChangeListener = listener; - } - - public ScheduleFormPanel(SchedulerPlugin plugin) { - this.plugin = plugin; - - setLayout(new BorderLayout()); - setBorder(BorderFactory.createTitledBorder( - BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(1, 1, 1, 1, ColorScheme.DARK_GRAY_COLOR), - BorderFactory.createEmptyBorder(5, 5, 5, 5)), - "Schedule Configuration", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE)); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Set minimum size - setMinimumSize(new Dimension(350, 300)); - setPreferredSize(new Dimension(400, 500)); - - // Create a tabbed pane to separate plugin selection and properties - tabbedPane = new JTabbedPane(); - tabbedPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - tabbedPane.setForeground(Color.WHITE); - tabbedPane.setFont(FontManager.getRunescapeFont()); - - // Add tabs - tabbedPane.addTab("New Schedule", createScheduleFormPanel()); - tabbedPane.addTab("Properties", createPropertiesPanel()); - - add(tabbedPane, BorderLayout.CENTER); - - // Create button panel - JPanel buttonPanel = new JPanel(new GridLayout(2, 2, 5, 5)); - buttonPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - buttonPanel.setBorder(new EmptyBorder(5, 0, 0, 0)); - - addButton = createButton("Add Schedule", ColorScheme.BRAND_ORANGE_TRANSPARENT); - updateButton = createButton("Update Schedule", ColorScheme.BRAND_ORANGE); - removeButton = createButton("Remove Schedule", ColorScheme.PROGRESS_ERROR_COLOR); - - // Control button (Run Now/Stop) - controlButton = createButton("Run Now", ColorScheme.PROGRESS_COMPLETE_COLOR); - controlButton.addActionListener(this::onControlButtonClicked); - - buttonPanel.add(addButton); - buttonPanel.add(updateButton); - buttonPanel.add(removeButton); - buttonPanel.add(controlButton); - - // Add button panel to the bottom - add(buttonPanel, BorderLayout.SOUTH); - - // Initialize the condition panel - updateConditionPanel(); - } - - /** - * Creates the main schedule form panel for adding new schedules - */ - private JScrollPane createScheduleFormPanel() { - // Create the form panel with GridBagLayout for flexibility - JPanel formPanel = new JPanel(new GridBagLayout()); - formPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(5, 5, 5, 5); - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.weightx = 1.0; // Make components expand horizontally - gbc.anchor = GridBagConstraints.WEST; - - // Filter section - gbc.gridx = 0; - gbc.gridy = 0; - gbc.gridwidth = 4; - JPanel filterPanel = createFilterPanel(); - formPanel.add(filterPanel, gbc); - - // Plugin selection - gbc.gridx = 0; - gbc.gridy = 1; - gbc.gridwidth = 1; - JLabel pluginLabel = new JLabel("Plugin:"); - pluginLabel.setForeground(Color.WHITE); - pluginLabel.setFont(FontManager.getRunescapeFont()); - formPanel.add(pluginLabel, gbc); - - gbc.gridx = 1; - gbc.gridy = 1; - gbc.gridwidth = 3; - pluginComboBox = new JComboBox<>(); - formPanel.add(pluginComboBox, gbc); - - // Initialize filters now that pluginComboBox exists - updateSecondaryFilter(); - - // Initialize known plugins for change detection - lastKnownPlugins = Microbot.getPluginManager().getPlugins().stream() - .filter(plugin -> plugin instanceof SchedulablePlugin) - .collect(Collectors.toSet()); - - updateFilteredPluginList(); - - // Add listener to clear table selection when ComboBox changes - pluginComboBox.addActionListener(e -> { - if (pluginComboBox.getSelectedItem() != null && selectionChangeListener != null && isUserAction) { - //selectionChangeListener.run(); - } - }); - - - - // Plugin settings section with improved UI - gbc.gridx = 0; - gbc.gridy = 2; - gbc.gridwidth = 4; - JPanel pluginSettingsPanel = new JPanel(new BorderLayout()); - pluginSettingsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - pluginSettingsPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Plugin Settings", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - - // Create a panel for the checkboxes with horizontal layout - JPanel checkboxesPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 5)); - checkboxesPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Random scheduling checkbox - randomSchedulingCheckbox = new JCheckBox("Allow random scheduling"); - randomSchedulingCheckbox.setSelected(true); - randomSchedulingCheckbox.setToolTipText( - "When enabled, this plugin can be randomly selected when multiple plugins are due to run.
" + - "If disabled, this plugin will have higher priority than randomizable plugins."); - randomSchedulingCheckbox.setForeground(Color.WHITE); - randomSchedulingCheckbox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - checkboxesPanel.add(randomSchedulingCheckbox); - - // Time-based stop condition checkbox - timeBasedStopConditionCheckbox = new JCheckBox("Requires time-based stop condition"); - timeBasedStopConditionCheckbox.setSelected(false); - timeBasedStopConditionCheckbox.setToolTipText( - "When enabled, the scheduler will prompt you to add a time-based stop condition for this plugin.
" + - "This helps prevent plugins from running indefinitely if other stop conditions don't trigger."); - timeBasedStopConditionCheckbox.setForeground(Color.WHITE); - timeBasedStopConditionCheckbox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - checkboxesPanel.add(timeBasedStopConditionCheckbox); - - // Allow continue checkbox - allowContinueCheckbox = new JCheckBox("Allow continue"); - allowContinueCheckbox.setSelected(false); - allowContinueCheckbox.setToolTipText( - "When enabled, the plugin will automatically resume after being interrupted by a higher-priority plugin.
" + - "This preserves the plugin's state and progress toward stop conditions without needing to re-evaluate start conditions.
" + - "Especially important for default plugins (priority 0) that should continue after higher-priority tasks finish."); - allowContinueCheckbox.setForeground(Color.WHITE); - allowContinueCheckbox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - checkboxesPanel.add(allowContinueCheckbox); - - // Add checkboxes panel to the top - pluginSettingsPanel.add(checkboxesPanel, BorderLayout.NORTH); - - // Priority and Default panel - improved layout with better spacing - JPanel priorityPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 5)); - priorityPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Priority label and spinner in one group - JLabel priorityLabel = new JLabel("Priority:"); - priorityLabel.setForeground(Color.WHITE); - priorityLabel.setFont(FontManager.getRunescapeFont()); - priorityPanel.add(priorityLabel); - - prioritySpinner = new JSpinner(new SpinnerNumberModel(0, 0, 100, 1)); - prioritySpinner.setToolTipText("Higher priority plugins will be scheduled before lower priority ones"); - prioritySpinner.setPreferredSize(new Dimension(60, 28)); - priorityPanel.add(prioritySpinner); - - // Add some spacing - priorityPanel.add(Box.createHorizontalStrut(20)); - - // Default plugin checkbox - defaultPluginCheckbox = new JCheckBox("Set as default plugin"); - defaultPluginCheckbox.setSelected(false); - defaultPluginCheckbox.setToolTipText( - "When enabled, this plugin is marked as a default option.
" + - "Default plugins always have priority 0.
" + - "Non-default plugins will always be scheduled first."); - defaultPluginCheckbox.setForeground(Color.WHITE); - defaultPluginCheckbox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - priorityPanel.add(defaultPluginCheckbox); - - // Synchronize priority and default checkbox - prioritySpinner.addChangeListener(e -> { - if (updatingValues) return; - updatingValues = true; - int value = (Integer) prioritySpinner.getValue(); - defaultPluginCheckbox.setSelected(value == 0); - updatingValues = false; - }); - - defaultPluginCheckbox.addItemListener(e -> { - if (updatingValues) return; - updatingValues = true; - if (e.getStateChange() == ItemEvent.SELECTED) { - prioritySpinner.setValue(0); // Default plugins always have priority 0 - } else if ((Integer) prioritySpinner.getValue() == 0) { - prioritySpinner.setValue(1); // If unchecking default and priority is 0, set to 1 - } - updatingValues = false; - }); - - // Add priority panel to bottom - pluginSettingsPanel.add(priorityPanel, BorderLayout.CENTER); - - formPanel.add(pluginSettingsPanel, gbc); - // Time condition type selection - gbc.gridx = 0; - gbc.gridy = 3; - gbc.gridwidth = 1; - JLabel conditionTypeLabel = new JLabel("Schedule Type:"); - conditionTypeLabel.setForeground(Color.WHITE); - conditionTypeLabel.setFont(FontManager.getRunescapeFont()); - formPanel.add(conditionTypeLabel, gbc); - - gbc.gridx = 1; - gbc.gridy = 3; - gbc.gridwidth = 3; - timeConditionTypeComboBox = new JComboBox<>(TIME_CONDITION_TYPES); - timeConditionTypeComboBox.addActionListener(e -> updateConditionPanel()); - formPanel.add(timeConditionTypeComboBox, gbc); - - // Dynamic condition config panel - gbc.gridx = 0; - gbc.gridy = 4; - gbc.gridwidth = 4; - conditionConfigPanel = new JPanel(new BorderLayout()); - conditionConfigPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - conditionConfigPanel.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(1, 1, 1, 1, ColorScheme.DARK_GRAY_COLOR), - BorderFactory.createEmptyBorder(5, 5, 5, 5))); - conditionConfigPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - conditionConfigPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Main Start Condition", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - formPanel.add(conditionConfigPanel, gbc); - - - - // Wrap the formPanel in a scroll pane - JScrollPane scrollPane = new JScrollPane(formPanel); - scrollPane.setBorder(BorderFactory.createEmptyBorder()); - scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - scrollPane.getVerticalScrollBar().setUnitIncrement(16); - - return scrollPane; - } - - /** - * Creates the filter panel with primary and secondary filter comboboxes - */ - private JPanel createFilterPanel() { - JPanel filterPanel = new JPanel(new GridBagLayout()); - filterPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - filterPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Plugin Filters", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(5, 5, 5, 5); - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.anchor = GridBagConstraints.WEST; - - // Primary filter - gbc.gridx = 0; - gbc.gridy = 0; - gbc.weightx = 0.0; - JLabel primaryFilterLabel = new JLabel("Filter by:"); - primaryFilterLabel.setForeground(Color.WHITE); - primaryFilterLabel.setFont(FontManager.getRunescapeFont()); - filterPanel.add(primaryFilterLabel, gbc); - - gbc.gridx = 1; - gbc.weightx = 0.5; - primaryFilterComboBox = new JComboBox<>(); - for (String category : PluginFilterUtil.getPrimaryFilterCategories()) { - primaryFilterComboBox.addItem(category); - } - primaryFilterComboBox.addActionListener(e -> updateSecondaryFilter()); - filterPanel.add(primaryFilterComboBox, gbc); - - // Secondary filter - gbc.gridx = 2; - gbc.weightx = 0.0; - JLabel secondaryFilterLabel = new JLabel("Sub-filter:"); - secondaryFilterLabel.setForeground(Color.WHITE); - secondaryFilterLabel.setFont(FontManager.getRunescapeFont()); - filterPanel.add(secondaryFilterLabel, gbc); - - gbc.gridx = 3; - gbc.weightx = 0.5; - secondaryFilterComboBox = new JComboBox<>(); - secondaryFilterComboBox.addActionListener(e -> updateFilteredPluginList()); - filterPanel.add(secondaryFilterComboBox, gbc); - - // Don't initialize filters here - wait until pluginComboBox is created - // updateSecondaryFilter() will be called later in initialization - - return filterPanel; - } - - /** - * Updates the secondary filter combobox based on primary filter selection - */ - private void updateSecondaryFilter() { - String selectedPrimary = (String) primaryFilterComboBox.getSelectedItem(); - if (selectedPrimary == null) return; - - secondaryFilterComboBox.removeAllItems(); - - List allPlugins = lastKnownPlugins.stream() - .filter(plugin -> plugin instanceof SchedulablePlugin) - .filter(plugin -> { - net.runelite.client.plugins.PluginDescriptor descriptor = - plugin.getClass().getAnnotation(net.runelite.client.plugins.PluginDescriptor.class); - return descriptor != null && !descriptor.hidden(); - }) - .collect(Collectors.toList()); - List secondaryOptions = PluginFilterUtil.getSecondaryFilterOptions(selectedPrimary, allPlugins); - - for (String option : secondaryOptions) { - secondaryFilterComboBox.addItem(option); - } - - updateFilteredPluginList(); - } - - /** - * Updates the plugin combobox based on current filter selections - */ - private void updateFilteredPluginList() { - String primaryFilter = (String) primaryFilterComboBox.getSelectedItem(); - String secondaryFilter = (String) secondaryFilterComboBox.getSelectedItem(); - - if (primaryFilter == null || pluginComboBox == null) return; - List allPlugins = lastKnownPlugins.stream() - .filter(plugin -> plugin instanceof SchedulablePlugin) - .filter(plugin -> { - PluginDescriptor descriptor = - plugin.getClass().getAnnotation(PluginDescriptor.class); - return descriptor != null && !descriptor.hidden(); - }) - .collect(Collectors.toList()); - - List filteredPlugins = PluginFilterUtil.filterPlugins(allPlugins, primaryFilter, secondaryFilter); - - // Convert to plugin names and update the main plugin combobox - List pluginNames = filteredPlugins.stream() - .map(Plugin::getName) - .sorted() - .collect(Collectors.toList()); - - // Temporarily disable action listeners to prevent feedback loops - ActionListener[] listeners = pluginComboBox.getActionListeners(); - for (ActionListener listener : listeners) { - pluginComboBox.removeActionListener(listener); - } - - pluginComboBox.removeAllItems(); - for (String name : pluginNames) { - pluginComboBox.addItem(name); - } - - // Re-add listeners - for (ActionListener listener : listeners) { - pluginComboBox.addActionListener(listener); - } - } - - /** - * Creates a panel for editing selected plugin properties - */ - private JScrollPane createPropertiesPanel() { - JPanel formPanel = new JPanel(new GridBagLayout()); - formPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 0; - gbc.gridwidth = 2; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.insets = new Insets(5, 5, 5, 5); - gbc.weightx = 1.0; - - // Create a message for when no plugin is selected - JPanel noSelectionPanel = new JPanel(new BorderLayout()); - noSelectionPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - JLabel noSelectionLabel = new JLabel("Select a plugin from the table to edit its properties"); - noSelectionLabel.setForeground(Color.WHITE); - noSelectionLabel.setFont(FontManager.getRunescapeFont()); - noSelectionLabel.setHorizontalAlignment(SwingConstants.CENTER); - noSelectionPanel.add(noSelectionLabel, BorderLayout.CENTER); - - // Create editor panel - JPanel editorPanel = new JPanel(new GridBagLayout()); - editorPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Plugin name header - selectedPluginNameLabel = new JLabel("Plugin Properties"); - selectedPluginNameLabel.setForeground(Color.WHITE); - selectedPluginNameLabel.setFont(FontManager.getRunescapeBoldFont()); - - gbc.gridx = 0; - gbc.gridy = 0; - gbc.gridwidth = 2; - editorPanel.add(selectedPluginNameLabel, gbc); - - // Enabled checkbox - gbc.gridx = 0; - gbc.gridy = 1; - gbc.gridwidth = 2; - selectedPluginEnabledCheckbox = createPropertyCheckbox("Enabled", - "When checked, the plugin is eligible to be scheduled based on its conditions"); - editorPanel.add(selectedPluginEnabledCheckbox, gbc); - - // Default plugin checkbox - gbc.gridx = 0; - gbc.gridy = 2; - gbc.gridwidth = 2; - selectedPluginDefaultCheckbox = createPropertyCheckbox("Default Plugin", - "When checked, this plugin will be treated as a default plugin (priority 0) and scheduled after all others"); - editorPanel.add(selectedPluginDefaultCheckbox, gbc); - - // Priority spinner with improved alignment - JPanel priorityGroupPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - priorityGroupPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel priorityLabel = new JLabel("Priority:"); - priorityLabel.setForeground(Color.WHITE); - priorityGroupPanel.add(priorityLabel); - - SpinnerModel priorityModel = new SpinnerNumberModel(1, 0, 100, 1); - selectedPluginPrioritySpinner = new JSpinner(priorityModel); - selectedPluginPrioritySpinner.setPreferredSize(new Dimension(60, 28)); - selectedPluginPrioritySpinner.setToolTipText("Sets the priority for this plugin.
Higher priority = run first.
0 = default plugin (scheduled after all others)"); - priorityGroupPanel.add(selectedPluginPrioritySpinner); - - gbc.gridx = 0; - gbc.gridy = 3; - gbc.gridwidth = 2; - editorPanel.add(priorityGroupPanel, gbc); - - // Random scheduling checkbox - gbc.gridx = 0; - gbc.gridy = 4; - gbc.gridwidth = 2; - selectedPluginRandomCheckbox = createPropertyCheckbox("Allow Random Scheduling", - "When enabled, the scheduler will apply some randomization to when this plugin runs"); - editorPanel.add(selectedPluginRandomCheckbox, gbc); - - gbc.gridx = 0; - gbc.gridy = 5; - gbc.gridwidth = 2; - selectedPluginTimeStopCheckbox = createPropertyCheckbox("Requires Time-based Stop Condition", - "When enabled, the scheduler will prompt you to add a time-based stop condition for this plugin."); - editorPanel.add(selectedPluginTimeStopCheckbox, gbc); - - // Add Allow Continue checkbox - gbc.gridx = 0; - gbc.gridy = 6; - gbc.gridwidth = 2; - selectedPluginAllowContinueCheckbox = createPropertyCheckbox("Allow Continue After Interruption", - "When enabled, the plugin will automatically resume after being interrupted by a higher-priority plugin.
" + - "This preserves all progress made toward stop conditions without resetting start conditions.
" + - "For default plugins (priority 0) in a cycle, this determines whether the plugin keeps its place
" + - "or must compete with other default plugins based on run counts when it's time to select the next plugin."); - editorPanel.add(selectedPluginAllowContinueCheckbox, gbc); - - // Plugin run statistics - gbc.gridx = 0; - gbc.gridy = 7; - gbc.gridwidth = 2; - JPanel statsPanel = new JPanel(new GridLayout(0, 1, 5, 5)); - statsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - statsPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Statistics", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeFont(), - Color.WHITE)); - - // Statistics info labels - runsLabel = new JLabel("Total Runs: 0"); - runsLabel.setForeground(Color.WHITE); - lastRunLabel = new JLabel("Last Run: Never"); - lastRunLabel.setForeground(Color.WHITE); - lastDurationLabel = new JLabel("Last Duration: N/A"); - lastDurationLabel.setForeground(Color.WHITE); - lastStopReasonLabel = new JLabel("Last Stop Reason: N/A"); - lastStopReasonLabel.setForeground(Color.WHITE); - - statsPanel.add(runsLabel); - statsPanel.add(lastRunLabel); - statsPanel.add(lastDurationLabel); - statsPanel.add(lastStopReasonLabel); - - gbc.gridx = 0; - gbc.gridy = 8; - editorPanel.add(statsPanel, gbc); - - // Save changes button with prominent styling - gbc.gridx = 0; - gbc.gridy = 9; - gbc.gridwidth = 2; - saveChangesButton = new JButton("Save Changes"); - saveChangesButton.setBackground(new Color(76, 175, 80)); // Green - saveChangesButton.setForeground(Color.WHITE); - saveChangesButton.setFocusPainted(false); - saveChangesButton.addActionListener(e -> updateSelectedPlugin()); - editorPanel.add(saveChangesButton, gbc); - - // Add property change listeners - selectedPluginEnabledCheckbox.addItemListener(e -> { - if (selectedPlugin != null) { - selectedPlugin.setEnabled(selectedPluginEnabledCheckbox.isSelected()); - if (tabbedPane.getSelectedIndex() == 1) { - updateSelectedPlugin(); - } - updateControlButton(); - updateStatistics(); - } - }); - - selectedPluginRandomCheckbox.addItemListener(e -> { - if (selectedPlugin != null) { - selectedPlugin.setAllowRandomScheduling(selectedPluginRandomCheckbox.isSelected()); - if (tabbedPane.getSelectedIndex() == 1) { - updateSelectedPlugin(); - } - } - }); - - selectedPluginTimeStopCheckbox.addItemListener(e -> { - if (selectedPlugin != null) { - selectedPlugin.setNeedsStopCondition(selectedPluginTimeStopCheckbox.isSelected()); - if (tabbedPane.getSelectedIndex() == 1) { - updateSelectedPlugin(); - } - } - }); - - // Add listener for the new Allow Continue checkbox - selectedPluginAllowContinueCheckbox.addItemListener(e -> { - if (selectedPlugin != null) { - selectedPlugin.setAllowContinue(selectedPluginAllowContinueCheckbox.isSelected()); - if (tabbedPane.getSelectedIndex() == 1) { - updateSelectedPlugin(); - } - } - }); - - selectedPluginDefaultCheckbox.addItemListener(e -> { - if (selectedPlugin != null) { - selectedPlugin.setDefault(selectedPluginDefaultCheckbox.isSelected()); - if (tabbedPane.getSelectedIndex() == 1) { - updateSelectedPlugin(); - } - } - }); - - // Initialize with no selection panel - pluginPropertiesPanel = new JPanel(new BorderLayout()); - pluginPropertiesPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - pluginPropertiesPanel.add(noSelectionPanel, BorderLayout.CENTER); - - // Store panels as client properties for later retrieval - pluginPropertiesPanel.putClientProperty("noSelectionPanel", noSelectionPanel); - pluginPropertiesPanel.putClientProperty("editorPanel", editorPanel); - - // Link default checkbox and priority spinner - selectedPluginDefaultCheckbox.addItemListener(e -> { - if (updatingValues) return; // Skip if we're programmatically updating values - - updatingValues = true; // Set flag to prevent recursive updates - try { - if (selectedPlugin != null) { - if (e.getStateChange() == ItemEvent.SELECTED) { - selectedPluginPrioritySpinner.setValue(0); // Default plugins have priority 0 - } else if ((Integer) selectedPluginPrioritySpinner.getValue() == 0) { - selectedPluginPrioritySpinner.setValue(1); // Non-default get priority 1 - } - - if (tabbedPane.getSelectedIndex() == 1) { - - updateSelectedPlugin(); - } - } - } finally { - updatingValues = false; // Always reset flag - } - }); - - selectedPluginPrioritySpinner.addChangeListener(e -> { - if (updatingValues) return; // Skip if we're programmatically updating values - - updatingValues = true; // Set flag to prevent recursive updates - try { - if (selectedPlugin != null) { - int priority = (Integer) selectedPluginPrioritySpinner.getValue(); - // Update default checkbox based on priority value - boolean shouldBeDefault = priority == 0; - if (selectedPluginDefaultCheckbox.isSelected() != shouldBeDefault) { - selectedPluginDefaultCheckbox.setSelected(shouldBeDefault); - } - if (tabbedPane.getSelectedIndex() == 1) { - updateSelectedPlugin(); - } - - } - } finally { - updatingValues = false; // Always reset flag - } - }); - - // Wrap the formPanel in a scroll pane - JScrollPane scrollPane = new JScrollPane(pluginPropertiesPanel); - scrollPane.setBorder(BorderFactory.createEmptyBorder()); - scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - scrollPane.getVerticalScrollBar().setUnitIncrement(16); - return scrollPane; - } - - /** - * Helper method to create a styled checkbox for properties panel - */ - private JCheckBox createPropertyCheckbox(String text, String tooltip) { - JCheckBox checkbox = new JCheckBox(text); - checkbox.setForeground(Color.WHITE); - checkbox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - checkbox.setToolTipText(tooltip); - checkbox.setFocusPainted(false); - return checkbox; - } - - /** - * Updates the statistics display in the properties panel - */ - private void updateStatistics() { - if (selectedPlugin == null) { - runsLabel.setText("Total Runs: 0"); - lastRunLabel.setText("Last Run: Never"); - lastDurationLabel.setText("Last Duration: N/A"); - lastStopReasonLabel.setText("Last Stop Reason: N/A"); - return; - } - - // Update run count - runsLabel.setText("Total Runs: " + selectedPlugin.getRunCount()); - - // Update last run time - ZonedDateTime lastEndRunTime = selectedPlugin.getLastRunEndTime(); - Duration lastRunTime = selectedPlugin.getLastRunDuration(); - if (lastEndRunTime != null) { - lastRunLabel.setText("Last End: " + lastEndRunTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))+ " (" + lastRunTime.toHoursPart() + ":" + lastRunTime.toMinutesPart() + ":" + lastRunTime.toSecondsPart() + ")"); - } else { - lastRunLabel.setText("Last End: Never"); - } - - // Update duration if available - if (selectedPlugin.getLastRunDuration() != null && !selectedPlugin.getLastRunDuration().isZero()) { - Duration duration = selectedPlugin.getLastRunDuration(); - long hours = duration.toHours(); - long minutes = duration.toMinutesPart(); - long seconds = duration.toSecondsPart(); - lastDurationLabel.setText(String.format("Last Duration: %d:%02d:%02d", hours, minutes, seconds)); - } else { - lastDurationLabel.setText("Last Duration: N/A"); - } - - // Update stop reason - String stopReason = selectedPlugin.getLastStopReason(); - if (stopReason != null && !stopReason.isEmpty()) { - if (stopReason.length() > 40) { - stopReason = stopReason.substring(0, 37) + "..."; - } - lastStopReasonLabel.setText("Last Stop: " + stopReason); - } else { - lastStopReasonLabel.setText("Last Stop: N/A"); - } - } - - /** - * Updates the condition configuration panel based on the selected time condition type - */ - private void updateConditionPanel() { - // Clear existing panel - if (currentConditionPanel != null) { - conditionConfigPanel.remove(currentConditionPanel); - } - - // Create a new panel based on selection - String selectedType = (String) timeConditionTypeComboBox.getSelectedItem(); - currentConditionPanel = new JPanel(); - currentConditionPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(5, 5, 5, 5); - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.gridx = 0; - gbc.gridy = 0; - gbc.gridwidth = 4; - - // Create the appropriate condition panel - if (CONDITION_DEFAULT.equals(selectedType)) { - JLabel defaultLabel = new JLabel("Default plugin with 1-second interval (always runs last)"); - defaultLabel.setForeground(Color.WHITE); - currentConditionPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); - currentConditionPanel.add(defaultLabel); - } - else if (CONDITION_SPECIFIC_TIME.equals(selectedType)) { - currentConditionPanel.setLayout(new GridBagLayout()); - TimeConditionPanelUtil.createSingleTriggerConfigPanel(currentConditionPanel, gbc); - } - else if (CONDITION_INTERVAL.equals(selectedType)) { - currentConditionPanel.setLayout(new GridBagLayout()); - TimeConditionPanelUtil.createIntervalConfigPanel(currentConditionPanel, gbc); - } - else if (CONDITION_TIME_WINDOW.equals(selectedType)) { - currentConditionPanel.setLayout(new GridBagLayout()); - TimeConditionPanelUtil.createTimeWindowConfigPanel(currentConditionPanel, gbc); - } - else if (CONDITION_DAY_OF_WEEK.equals(selectedType)) { - currentConditionPanel.setLayout(new GridBagLayout()); - TimeConditionPanelUtil.createDayOfWeekConfigPanel(currentConditionPanel, gbc); - } - - // Add the panel - conditionConfigPanel.add(currentConditionPanel, BorderLayout.CENTER); - conditionConfigPanel.revalidate(); - conditionConfigPanel.repaint(); - } - - private JButton createButton(String text, Color color) { - JButton button = new JButton(text); - button.setFont(FontManager.getRunescapeSmallFont()); - button.setFocusPainted(false); - button.setForeground(Color.WHITE); - button.setBackground(ColorScheme.DARKER_GRAY_COLOR); - button.setBorder(new CompoundBorder( - BorderFactory.createLineBorder(color.darker(), 1), - BorderFactory.createEmptyBorder(5, 5, 5, 5))); - - button.addMouseListener(new java.awt.event.MouseAdapter() { - @Override - public void mouseEntered(java.awt.event.MouseEvent e) { - button.setBackground(color); - button.setForeground(ColorScheme.DARK_GRAY_COLOR); - } - - @Override - public void mouseExited(java.awt.event.MouseEvent e) { - button.setBackground(ColorScheme.DARKER_GRAY_COLOR); - button.setForeground(Color.WHITE); - } - }); - - return button; - } - - public void updatePluginList(List plugins) { - if (plugins == null || plugins.isEmpty()) { - log.debug("No plugins available to populate combo box"); - return; - } - - // If filters are not yet initialized, use the old method - if (primaryFilterComboBox == null || secondaryFilterComboBox == null) { - pluginComboBox.removeAllItems(); - for (String plugin : plugins) { - pluginComboBox.addItem(plugin); - } - } else { - // Use the filter system to populate the combo box - updateFilteredPluginList(); - } - } - - public void loadPlugin(PluginScheduleEntry entry) { - - if (entry == null ) { - log.warn("Attempted to load null plugin entry"); - //switch to "new schedule" tab - tabbedPane.setSelectedIndex(0); - // set tab 1 not showing - // Disable the properties tab when no plugin is selected - tabbedPane.setEnabledAt(1, false); - setEditMode(false); - return; - } - tabbedPane.setEnabledAt(1, true); - if (entry.equals(selectedPlugin)){ - log.debug("Attempted to load already selected plugin entry"); - return; - } - - this.selectedPlugin = entry; - - // Block combobox events temporarily to avoid feedback loops - ActionListener[] listeners = pluginComboBox.getActionListeners(); - for (ActionListener listener : listeners) { - pluginComboBox.removeActionListener(listener); - } - - // Update plugin selection - pluginComboBox.setSelectedItem(entry.getName()); - - // Re-add listeners - for (ActionListener listener : listeners) { - pluginComboBox.addActionListener(listener); - } - - // Set random scheduling checkbox - randomSchedulingCheckbox.setSelected(entry.isAllowRandomScheduling()); - - // Set time-based stop condition checkbox - timeBasedStopConditionCheckbox.setSelected(entry.isNeedsStopCondition()); - - // Set allow continue checkbox - allowContinueCheckbox.setSelected(entry.isAllowContinue()); - - // Set priority spinner - prioritySpinner.setValue(entry.getPriority()); - - // Set default checkbox - defaultPluginCheckbox.setSelected(entry.isDefault()); - // Update the properties panel - updatePropertiesPanel(entry); - - // Determine the time condition type and set appropriate panel - TimeCondition startCondition = null; - if (entry.getStartConditionManager() != null) { - List timeConditions = entry.getStartConditionManager().getTimeConditions(); - if (!timeConditions.isEmpty()) { - startCondition = timeConditions.get(0); - } - } - - if (startCondition == null) { - // Default to showing the default panel, as we can't determine the condition type - timeConditionTypeComboBox.setSelectedItem(CONDITION_INTERVAL); - updateConditionPanel(); - return; - } - - TimeCondition mainStartCondition = entry.getMainTimeStartCondition(); - - // Block combobox events again for condition type changes - ActionListener[] conditionListeners = timeConditionTypeComboBox.getActionListeners(); - for (ActionListener listener : conditionListeners) { - timeConditionTypeComboBox.removeActionListener(listener); - } - - // If it's a default plugin (by flag or by interval), show "Run Default" - if (startCondition instanceof SingleTriggerTimeCondition) { - timeConditionTypeComboBox.setSelectedItem(CONDITION_SPECIFIC_TIME); - updateConditionPanel(); - setupTimeConditionPanel(startCondition); - } else if (startCondition instanceof IntervalCondition) { - Optional nextTriger = startCondition.getDurationUntilNextTrigger(); - IntervalCondition interval = (IntervalCondition) startCondition; - - if (interval.getInterval().getSeconds() <= 1 && entry.isDefault()) { - timeConditionTypeComboBox.setSelectedItem(CONDITION_DEFAULT); - updateConditionPanel(); - }else{ - // Configure the panel with existing values - timeConditionTypeComboBox.setSelectedItem(CONDITION_INTERVAL); - updateConditionPanel(); - setupTimeConditionPanel(startCondition); - } - //updateConditionPanel(); - - } else if (startCondition instanceof TimeWindowCondition) { - timeConditionTypeComboBox.setSelectedItem(CONDITION_TIME_WINDOW); - updateConditionPanel(); - setupTimeConditionPanel(startCondition); - } else if (startCondition instanceof DayOfWeekCondition) { - timeConditionTypeComboBox.setSelectedItem(CONDITION_DAY_OF_WEEK); - updateConditionPanel(); - setupTimeConditionPanel(startCondition); - } - // Re-add condition type listeners - for (ActionListener listener : conditionListeners) { - timeConditionTypeComboBox.addActionListener(listener); - } - - // Update the control button to reflect the current plugin - updateControlButton(); - - - } - - /** - * Updates the properties panel to show the selected plugin's properties - */ - private void updatePropertiesPanel(PluginScheduleEntry entry) { - - if (entry == null) { - // Show the no selection panel - JPanel noSelectionPanel = (JPanel) pluginPropertiesPanel.getClientProperty("noSelectionPanel"); - pluginPropertiesPanel.removeAll(); - pluginPropertiesPanel.add(noSelectionPanel, BorderLayout.CENTER); - pluginPropertiesPanel.revalidate(); - pluginPropertiesPanel.repaint(); - return; - } - - // Get the editor panel - JPanel editorPanel = (JPanel) pluginPropertiesPanel.getClientProperty("editorPanel"); - - // Update the header with the plugin name - directly use the selectedPluginNameLabel reference - if (selectedPluginNameLabel != null) { - selectedPluginNameLabel.setText("Plugin: " + entry.getCleanName()); - } - - // Set the flag to prevent update loops - updatingValues = true; - - try { - - selectedPluginEnabledCheckbox.setSelected(entry.isEnabled()); - - // Update default checkbox first as it may impact the priority spinner - selectedPluginDefaultCheckbox.setSelected(entry.isDefault()); - - // Then update priority spinner - selectedPluginPrioritySpinner.setValue(entry.getPriority()); - - // Update other checkboxes - selectedPluginRandomCheckbox.setSelected(entry.isAllowRandomScheduling()); - selectedPluginTimeStopCheckbox.setSelected(entry.isNeedsStopCondition()); - selectedPluginAllowContinueCheckbox.setSelected(entry.isAllowContinue()); - - // Update statistics - updateStatistics(); - } finally { - // Reset the flag after all updates - updatingValues = false; - } - - // Show the editor panel - pluginPropertiesPanel.removeAll(); - pluginPropertiesPanel.add(editorPanel, BorderLayout.CENTER); - pluginPropertiesPanel.revalidate(); - pluginPropertiesPanel.repaint(); - } - - public void clearForm() { - - this.selectedPlugin = null; - if (pluginComboBox.getItemCount() > 0) { - pluginComboBox.setSelectedIndex(0); - } - - // Reset condition type to default - timeConditionTypeComboBox.setSelectedItem(CONDITION_INTERVAL); - updateConditionPanel(); - - // Reset random scheduling - randomSchedulingCheckbox.setSelected(true); - - // Update the control button - updateControlButton(); - - // Reset properties panel - updatePropertiesPanel(null); - tabbedPane.setEnabledAt(1, false); - tabbedPane.setSelectedIndex(0); - } - - public PluginScheduleEntry getPluginFromForm(PluginScheduleEntry existingPlugin) { - String pluginName = (String) pluginComboBox.getSelectedItem(); - if (pluginName == null || pluginName.isEmpty()) { - return null; - } - - // Get the selected time condition type - String selectedType = (String) timeConditionTypeComboBox.getSelectedItem(); - TimeCondition timeCondition = null; - - // Create the appropriate time condition - if (CONDITION_DEFAULT.equals(selectedType)) { - // Default plugin with 1-second interval - timeCondition = new IntervalCondition(Duration.ofSeconds(1)); - } else if (CONDITION_SPECIFIC_TIME.equals(selectedType)) { - timeCondition = TimeConditionPanelUtil.createSingleTriggerCondition(currentConditionPanel); - } else if (CONDITION_INTERVAL.equals(selectedType)) { - timeCondition = TimeConditionPanelUtil.createIntervalCondition(currentConditionPanel); - } else if (CONDITION_TIME_WINDOW.equals(selectedType)) { - timeCondition = TimeConditionPanelUtil.createTimeWindowCondition(currentConditionPanel); - } else if (CONDITION_DAY_OF_WEEK.equals(selectedType)) { - timeCondition = TimeConditionPanelUtil.createDayOfWeekCondition(currentConditionPanel); - } - - // If we couldn't create a time condition, return null - if (timeCondition == null) { - log.warn("Could not create time condition from form"); - return null; - } - - // Get other settings - boolean randomScheduling = randomSchedulingCheckbox.isSelected(); - boolean needsStopCondition = timeBasedStopConditionCheckbox.isSelected(); - boolean allowContinue = allowContinueCheckbox.isSelected(); - int priority = (Integer) prioritySpinner.getValue(); - boolean isDefault = defaultPluginCheckbox.isSelected(); - - // Create the plugin schedule entry - PluginScheduleEntry entry; - log.debug("values for PluginScheduleEntry entry {}\n priority {}\n isDefault {} \n needsStopCondition {} \n randomScheduling {}",pluginName,priority, isDefault, needsStopCondition, randomScheduling); - if (existingPlugin != null) { - log.debug("Updating existing plugin entry"); - - // Update the existing plugin with new values - existingPlugin.updatePrimaryTimeCondition(timeCondition); - existingPlugin.setAllowRandomScheduling(randomScheduling); - existingPlugin.setNeedsStopCondition(needsStopCondition); - existingPlugin.setAllowContinue(allowContinue); - existingPlugin.setPriority(priority); - existingPlugin.setDefault(isDefault); - entry = existingPlugin; - } else { - - log.debug("Creating new plugin entry"); - // Create a new plugin schedule entry - entry = new PluginScheduleEntry( - pluginName, - timeCondition, - true, // Enabled by default - randomScheduling - ); - entry.setNeedsStopCondition(needsStopCondition); - entry.setAllowContinue(allowContinue); - entry.setPriority(priority); - entry.setDefault(isDefault); - } - if (entry != null) { - randomSchedulingCheckbox.setSelected(entry.isAllowRandomScheduling()); - timeBasedStopConditionCheckbox.setSelected(entry.isNeedsStopCondition()); - allowContinueCheckbox.setSelected(entry.isAllowContinue()); - prioritySpinner.setValue(entry.getPriority()); - defaultPluginCheckbox.setSelected(entry.isDefault()); - updatePropertiesPanel(entry); - } - return entry; - } - - /** - * Updates the selected plugin with values from the properties panel - */ - private void updateSelectedPlugin() { - if (selectedPlugin == null) return; - - boolean enabled = selectedPluginEnabledCheckbox.isSelected(); - boolean randomScheduling = selectedPluginRandomCheckbox.isSelected(); - boolean needsStopCondition = selectedPluginTimeStopCheckbox.isSelected(); - boolean allowContinue = selectedPluginAllowContinueCheckbox.isSelected(); - int priority = (Integer) selectedPluginPrioritySpinner.getValue(); - boolean isDefault = selectedPluginDefaultCheckbox.isSelected(); - - // Update the plugin - selectedPlugin.setEnabled(enabled); - selectedPlugin.setAllowRandomScheduling(randomScheduling); - selectedPlugin.setNeedsStopCondition(needsStopCondition); - selectedPlugin.setAllowContinue(allowContinue); - selectedPlugin.setPriority(priority); - selectedPlugin.setDefault(isDefault); - - // Save the changes - plugin.saveScheduledPlugins(); - - // Update the control button - updateControlButton(); - - // Notify the main window to refresh the table - //plugin.refreshScheduleTable(); - } - public void refresh(){ - // Check if plugins have changed - Set currentPlugins = Microbot.getPluginManager().getPlugins().stream() - .filter(plugin -> plugin instanceof SchedulablePlugin) - .collect(Collectors.toSet()); - - if (!currentPlugins.equals(lastKnownPlugins)) { - log.info("Plugin changes detected, updating plugin list"); - lastKnownPlugins = new HashSet<>(currentPlugins); - updateFilteredPluginList(); - } - - updateControlButton(); - } - public void updateControlButton() { - boolean isRunning = selectedPlugin != null && selectedPlugin.isRunning(); - boolean isEnabled = selectedPlugin != null && selectedPlugin.isEnabled(); - boolean anyPluginRunning = plugin.isScheduledPluginRunning(); - - if (isRunning) { - // If this plugin is running, show Stop button - controlButton.setText("Stop Plugin"); - controlButton.setBackground(ColorScheme.PROGRESS_ERROR_COLOR); - } else { - // Otherwise show Run Now button - controlButton.setText("Run Now"); - controlButton.setBackground(ColorScheme.PROGRESS_COMPLETE_COLOR); - } - - // Disable the button if: - // 1. No plugin is selected, OR - // 2. Selected plugin is disabled, OR - // 3. Any plugin is running (not just the selected one) and we're showing "Run Now" - controlButton.setEnabled( - selectedPlugin != null && - isEnabled && - (!anyPluginRunning || isRunning) - ); - - // Update tooltip to explain why button might be disabled - if (selectedPlugin == null) { - controlButton.setToolTipText("No plugin selected"); - } else if (!isEnabled) { - controlButton.setToolTipText("Plugin is disabled"); - } else if (anyPluginRunning && !isRunning) { - controlButton.setToolTipText("Cannot start: Another plugin is already running"); - } else { - controlButton.setToolTipText(isRunning ? "Stop the running plugin" : "Run this plugin now"); - } - } - - private void onControlButtonClicked(ActionEvent e) { - if (selectedPlugin == null) { - return; - } - - if (selectedPlugin.isRunning()) { - // Stop the plugin - if (plugin.getCurrentPlugin()!= null && plugin.getCurrentPlugin().equals(selectedPlugin)) { - plugin.forceStopCurrentPluginScheduleEntry(true); - }else{ - plugin.forceStopCurrentPluginScheduleEntry(false); - } - } else { - // Start the plugin using the new manualStartPlugin method - String result = plugin.manualStartPlugin(selectedPlugin); - if (!result.isEmpty()) { - // Show error message if starting failed - JOptionPane.showMessageDialog( - SwingUtilities.getWindowAncestor(this), - result, - "Cannot Start Plugin immediately, update only main time start condition", - JOptionPane.WARNING_MESSAGE - ); - } - } - - // Update control button and statistics - updateControlButton(); - updateStatistics(); - } - - public void setEditMode(boolean editMode) { - updateButton.setVisible(editMode); - addButton.setVisible(!editMode); - } - - public void setAddButtonAction(ActionListener listener) { - for (ActionListener l : addButton.getActionListeners()) { - addButton.removeActionListener(l); - } - addButton.addActionListener(listener); - } - - public void setUpdateButtonAction(ActionListener listener) { - for (ActionListener l : updateButton.getActionListeners()) { - updateButton.removeActionListener(l); - } - updateButton.addActionListener(listener); - } - - public void setRemoveButtonAction(ActionListener listener) { - for (ActionListener l : removeButton.getActionListeners()) { - removeButton.removeActionListener(l); - } - removeButton.addActionListener(listener); - } - - /** - * Sets up the time condition panel with values from an existing condition - */ - private void setupTimeConditionPanel(TimeCondition condition) { - if (condition == null || currentConditionPanel == null) { - return; - } - - if (condition instanceof SingleTriggerTimeCondition) { - TimeConditionPanelUtil.setupTimeCondition(currentConditionPanel, (SingleTriggerTimeCondition) condition); - } else if (condition instanceof IntervalCondition) { - TimeConditionPanelUtil.setupTimeCondition(currentConditionPanel, (IntervalCondition) condition); - } else if (condition instanceof TimeWindowCondition) { - TimeConditionPanelUtil.setupTimeCondition(currentConditionPanel, (TimeWindowCondition) condition); - } else if (condition instanceof DayOfWeekCondition) { - TimeConditionPanelUtil.setupTimeCondition(currentConditionPanel, (DayOfWeekCondition) condition); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleTableModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleTableModel.java deleted file mode 100644 index a874aabd0ac..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleTableModel.java +++ /dev/null @@ -1,17 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.PluginScheduleEntry; - -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; - -/** - * Interface for table models that store PluginScheduleEntry instances - * and can retrieve them by row index. - */ -public interface ScheduleTableModel { - /** - * Gets the PluginScheduleEntry at the specified row index - * - * @param row The row index - * @return The PluginScheduleEntry at that row, or null if not found - */ - PluginScheduleEntry getPluginAtRow(int row); -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleTablePanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleTablePanel.java deleted file mode 100644 index a4cd3ae6c49..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleTablePanel.java +++ /dev/null @@ -1,2038 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.PluginScheduleEntry; - -import net.runelite.client.plugins.microbot.Microbot; -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.ConditionType; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LogicalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.pluginscheduler.util.SchedulerPluginUtil; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import javax.swing.table.DefaultTableCellRenderer; -import javax.swing.table.DefaultTableModel; -import javax.swing.table.JTableHeader; - -import lombok.extern.slf4j.Slf4j; - -import java.awt.*; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.event.MouseWheelEvent; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.Consumer; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.awt.font.TextAttribute; - -@Slf4j -public class ScheduleTablePanel extends JPanel implements ScheduleTableModel { - private final SchedulerPlugin schedulerPlugin; - private final JTable scheduleTable; - private final DefaultTableModel tableModel; - private Consumer selectionListener; - private boolean updatingTable = false; - - // Tooltip enhancement fields - private Timer tooltipRefreshTimer; - private Point hoverLocation; - private int hoverRow = -1; - private int hoverColumn = -1; - private int lastSelectedRow = -1; - private static final int TOOLTIP_REFRESH_INTERVAL = 1000; // 1 second refresh - - // Colors for different row states with improved visibility - private static final Color CURRENT_PLUGIN_COLOR = new Color(138, 43, 226, 80); // Purple with transparency - private static final Color NEXT_PLUGIN_COLOR = new Color(255, 140, 0, 90); // Dark orange with more opacity - private static final Color SELECTION_COLOR = new Color(0, 120, 215, 150); // Blue with transparency - private static final Color CONDITION_MET_COLOR = new Color(76, 175, 80, 45); // Darker green with lower transparency - private static final Color CONDITION_NOT_MET_COLOR = new Color(244, 67, 54, 45); // Darker red with lower transparency - private static final Color DEFAULT_PLUGIN_COLOR = new Color(0, 150, 136, 40); // Teal with transparency - - // Blend method is already defined elsewhere in the class - - // Column indices for easy reference - kept for future development but currently unused - @SuppressWarnings("unused") private static final int COL_NAME = 0; - @SuppressWarnings("unused") private static final int COL_SCHEDULE = 1; - @SuppressWarnings("unused") private static final int COL_NEXT_RUN = 2; - @SuppressWarnings("unused") private static final int COL_START_COND = 3; - @SuppressWarnings("unused") private static final int COL_STOP_COND = 4; - @SuppressWarnings("unused") private static final int COL_PRIORITY = 5; - @SuppressWarnings("unused") private static final int COL_ENABLED = 6; - @SuppressWarnings("unused") private static final int COL_RUNS = 7; - - private List rowToPluginMap = new ArrayList<>(); - - public int getRowCount() { - if (tableModel == null) { - return 0; - } - return tableModel.getRowCount(); - } - - public ScheduleTablePanel(SchedulerPlugin schedulerPlugin) { - - this.schedulerPlugin = schedulerPlugin; - - // Configure ToolTipManager for persistent tooltips - ToolTipManager.sharedInstance().setDismissDelay(Integer.MAX_VALUE); // Keep tooltips visible indefinitely - ToolTipManager.sharedInstance().setInitialDelay(300); // Show tooltips faster (300ms) - ToolTipManager.sharedInstance().setReshowDelay(0); // No delay when moving between tooltips - - setLayout(new BorderLayout()); - setBorder(BorderFactory.createTitledBorder( - BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(1, 1, 1, 1, ColorScheme.DARK_GRAY_COLOR), - BorderFactory.createEmptyBorder(5, 5, 5, 5) - ), - "Scheduled Plugins", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont() - )); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Focused table model with essential columns only - // The columns are: Plugin, Schedule, Next Run, Start Conditions, Stop Conditions, Priority, Enabled, Run Count - tableModel = new DefaultTableModel( - new Object[]{"Plugin", "Schedule", "Next Run", "Start Conditions", "Stop Conditions", "Priority", "Enabled", "Runs"}, 0) { - @Override - public Class getColumnClass(int column) { - if (column == 6) return Boolean.class; - if (column == 5) return Integer.class; - if (column == 7) return Integer.class; // Run count as Integer - return String.class; - } - @Override - public boolean isCellEditable(int row, int column) { - return column == 5 || column == 6; - } - }; - - // Update the table model listener to handle Priority column changes - tableModel.addTableModelListener(e -> { - if (updatingTable) { - return; // Skip processing if we're already updating or it's not our columns - } - if (e.getColumn() == 5 || e.getColumn() == 6) { - try { - updatingTable = true; - int firstRow = e.getFirstRow(); - int lastRow = e.getLastRow(); - - // Update all rows in the affected range - for (int row = firstRow; row <= lastRow; row++) { - if (row >= 0 && row < rowToPluginMap.size()) { - PluginScheduleEntry scheduled = rowToPluginMap.get(row); - - // Skip separator rows (null) and unavailable plugins - if (scheduled == null || !scheduled.isPluginAvailable()) { - continue; - } - - if (e.getColumn() == 5) { // Priority column - Integer priority = (Integer) tableModel.getValueAt(row, 5); - - // Check if this is a default plugin that should always have priority 0 - if (scheduled.isDefault() && priority != 0) { - tableModel.setValueAt(0, row, 5); // Reset to 0 - JOptionPane.showMessageDialog( - this, - "Default plugins must have priority 0.", - "Invalid Priority", - JOptionPane.INFORMATION_MESSAGE - ); - } else { - // For non-default plugins, update the value - //scheduled.setPriority(priority); - // Set default flag based on priority - //scheduled.setDefault(priority == 0); - // Save changes - //schedulerPlugin.saveScheduledPlugins(); - } - } - else if (e.getColumn() == 6) { // Enabled column - Boolean enabled = (Boolean) tableModel.getValueAt(row, 6); - tableModel.setValueAt(enabled, row, 6); - - Microbot.getClientThread().invokeLater(() -> { - // Update the enabled status of the plugin - scheduled.setEnabled(enabled); - }); - } - } - } - - // Save after all updates are done - schedulerPlugin.saveScheduledPlugins(); - - // Refresh the table to update visual indicators - refreshTable(); - } finally { - updatingTable = false; - } - } - }); - - // Create table with custom styling - scheduleTable = new JTable(tableModel) { - @Override - public boolean isCellEditable(int row, int column) { - // Only allow editing of available plugins (not separators or unavailable plugins) - if (row >= 0 && row < rowToPluginMap.size()) { - PluginScheduleEntry plugin = rowToPluginMap.get(row); - if (plugin == null) { - return false; // Separator rows are not editable - } - // Unavailable plugins can't be edited for priority/enabled, but can be selected for removal - if (!plugin.isPluginAvailable() && (column == 5 || column == 6)) { - return false; // Priority and Enabled columns not editable for unavailable plugins - } - } - return super.isCellEditable(row, column); - } - }; - scheduleTable.setFillsViewportHeight(true); - scheduleTable.setRowHeight(25); // Reduced row height for better density - scheduleTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - - // Custom selection model to prevent selection of separators and unavailable plugins - scheduleTable.setSelectionModel(new DefaultListSelectionModel() { - @Override - public void setSelectionInterval(int index0, int index1) { - if (isRowSelectable(index0)) { - super.setSelectionInterval(index0, index1); - } - } - - @Override - public void addSelectionInterval(int index0, int index1) { - if (isRowSelectable(index0) && isRowSelectable(index1)) { - super.addSelectionInterval(index0, index1); - } - } - - private boolean isRowSelectable(int row) { - if (row >= 0 && row < rowToPluginMap.size()) { - PluginScheduleEntry plugin = rowToPluginMap.get(row); - // Separator rows (null) are not selectable, but unavailable plugins ARE selectable for removal - return plugin != null; - } - return false; - } - }); - - scheduleTable.setShowGrid(false); - scheduleTable.setIntercellSpacing(new Dimension(0, 0)); - scheduleTable.setBackground(ColorScheme.DARKER_GRAY_COLOR); - scheduleTable.setForeground(Color.WHITE); - - // Set the custom editor for the Priority column - scheduleTable.getColumnModel().getColumn(5).setCellEditor(new PrioritySpinnerEditor()); - - // Add mouse listener to handle clicks outside the table data and tooltip refreshing - scheduleTable.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - int row = scheduleTable.rowAtPoint(e.getPoint()); - int col = scheduleTable.columnAtPoint(e.getPoint()); - // Explicitly select the row before custom logic - final int currentSelectedRow = scheduleTable.getSelectedRow(); - boolean isLastSelected = currentSelectedRow == lastSelectedRow; - lastSelectedRow = row; // Update last selected row - // To check if the mouse event is a "pressed" event, use e.getID() == MouseEvent.MOUSE_PRESSED - if (e.getID() == MouseEvent.MOUSE_PRESSED && e.getButton() == MouseEvent.BUTTON1) { - - if (!isLastSelected) { - // If the clicked row is different, select it - scheduleTable.setRowSelectionInterval(row, row); - return; - } - if (row == -1 || col == -1) { - clearSelection(); - } - - - - if (col != 6 && col != 5) { - // handle single-click toggle for selection/deselection - if (e.getClickCount() == 1) { - // if we clicked on the previously selected row and it's still selected, deselect it - if (currentSelectedRow == row) { - clearSelection(); - return; - } - } - - // keep the double-click functionality for backwards compatibility - if (e.getClickCount() == 2) { - int selectedRow = scheduleTable.getSelectedRow(); - if (selectedRow == row) { - clearSelection(); - return; - } - } - } - } - super.mousePressed(e); - } - - @Override - public void mouseExited(MouseEvent e) { - // Reset hover tracking when mouse leaves table - hoverRow = -1; - hoverColumn = -1; - if (tooltipRefreshTimer != null && tooltipRefreshTimer.isRunning()) { - tooltipRefreshTimer.stop(); - } - } - }); - - // Add mouse motion listener for hover detection and tooltip refresh - scheduleTable.addMouseMotionListener(new MouseAdapter() { - @Override - public void mouseMoved(MouseEvent e) { - int row = scheduleTable.rowAtPoint(e.getPoint()); - int col = scheduleTable.columnAtPoint(e.getPoint()); - - if (row >= 0 && col >= 0) { - // If hovering over a new cell, update tracking - if (row != hoverRow || col != hoverColumn) { - hoverRow = row; - hoverColumn = col; - hoverLocation = e.getPoint(); - - // Start/restart timer for tooltip refresh - startTooltipRefreshTimer(); - } - } else { - // Not over any cell - hoverRow = -1; - hoverColumn = -1; - if (tooltipRefreshTimer != null && tooltipRefreshTimer.isRunning()) { - tooltipRefreshTimer.stop(); - } - } - } - }); - - // Style the table header - JTableHeader header = scheduleTable.getTableHeader(); - header.setBackground(ColorScheme.DARKER_GRAY_COLOR); - header.setForeground(Color.WHITE); - header.setFont(FontManager.getRunescapeBoldFont()); - header.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, ColorScheme.LIGHT_GRAY_COLOR)); - - // Add mouse listener to header to clear selection when clicked - header.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - clearSelection(); - } - }); - - // Set column widths - scheduleTable.getColumnModel().getColumn(0).setPreferredWidth(180); // Plugin - scheduleTable.getColumnModel().getColumn(1).setPreferredWidth(140); // Schedule - scheduleTable.getColumnModel().getColumn(2).setPreferredWidth(140); // Next Run - scheduleTable.getColumnModel().getColumn(3).setPreferredWidth(130); // Start Conditions - scheduleTable.getColumnModel().getColumn(4).setPreferredWidth(130); // Stop Conditions - scheduleTable.getColumnModel().getColumn(5).setPreferredWidth(70); // Priority - scheduleTable.getColumnModel().getColumn(6).setPreferredWidth(70); // Enabled - scheduleTable.getColumnModel().getColumn(7).setPreferredWidth(60); // Run Count - - // Custom cell renderer for alternating row colors and special highlights - DefaultTableCellRenderer renderer = new DefaultTableCellRenderer() { - @Override - public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { - Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); - if (row >= 0 && row < rowToPluginMap.size()) { - PluginScheduleEntry rowPlugin = rowToPluginMap.get(row); - - // Handle separator row (null plugin) - if (rowPlugin == null) { - c.setBackground(new Color(70, 70, 70)); // Dark gray background - c.setForeground(new Color(169, 169, 169)); // Light gray text - Font originalFont = c.getFont(); - c.setFont(originalFont.deriveFont(Font.BOLD)); - setHorizontalAlignment(SwingConstants.CENTER); - return c; - } - - if (isSelected) { - // Selected row styling takes precedence - use a distinct blue color - c.setBackground(SELECTION_COLOR); - c.setForeground(Color.WHITE); - } - else if (!rowPlugin.isPluginAvailable()) { - // Unavailable plugin styling (not installed) - improved styling - c.setBackground(new Color(69, 39, 19, 80)); // Darker brown with more transparency - c.setForeground(new Color(255, 160, 122)); // Sandy brown text for better readability - // Add italic styling and slightly dimmed appearance - Font originalFont = c.getFont(); - c.setFont(originalFont.deriveFont(Font.ITALIC)); - } - else if (!rowPlugin.isEnabled()) { - // Disabled plugin styling - c.setBackground(row % 2 == 0 ? ColorScheme.DARKER_GRAY_COLOR : ColorScheme.DARK_GRAY_COLOR); - c.setForeground(Color.GRAY); // Gray text to indicate disabled - // Add strikethrough for disabled plugins - if (value instanceof String) { - Font originalFont = c.getFont(); - Map attributes = new HashMap<>(); - attributes.put(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON); - attributes.put(TextAttribute.FONT, originalFont); - c.setFont(Font.getFont(attributes)); - } - } - else if ( schedulerPlugin.getCurrentPlugin() != null && schedulerPlugin.getCurrentPlugin().equals(rowPlugin) && - rowPlugin.getName().equals(schedulerPlugin.getCurrentPlugin().getName())) { - // Currently running plugin - c.setBackground(CURRENT_PLUGIN_COLOR); - c.setForeground(Color.WHITE); - } - else if (isNextToRun(rowPlugin)) { - // Next plugin to run - use better contrast colors - c.setBackground(NEXT_PLUGIN_COLOR); - c.setForeground(Color.BLACK); - // Make text bold to stand out more - Font originalFont = c.getFont(); - c.setFont(originalFont.deriveFont(Font.BOLD)); - } - else if (rowPlugin.isDefault()) { - // Default plugin styling - c.setBackground(DEFAULT_PLUGIN_COLOR); - c.setForeground(Color.WHITE); - } - else { - // Normal alternating row colors - c.setBackground(row % 2 == 0 ? ColorScheme.DARKER_GRAY_COLOR : ColorScheme.DARK_GRAY_COLOR); - c.setForeground(Color.WHITE); - } - - // Apply the condition renderer to the condition columns - // Custom cell renderer for the condition columns - if (row >= 0 && row < rowToPluginMap.size() && !isSelected) { - PluginScheduleEntry entry = rowToPluginMap.get(row); - - // Apply background color based on condition status - if (column == 3) { // Start conditions - boolean hasStartConditions = entry.hasAnyStartConditions(); - boolean startConditionsMet = hasStartConditions && entry.getStartConditionManager().areAllConditionsMet(); - boolean isRelevant = entry.isEnabled() && !entry.isRunning(); - - if (hasStartConditions) { - // When conditions are relevant (enabled but not running), show clearer status - if (isRelevant) { - // Use darker base colors with the game's theme - Color baseColor = ColorScheme.DARKER_GRAY_COLOR; - - if (startConditionsMet) { - // For met conditions, use a subtle green tint - c.setBackground(new Color( - blend(baseColor.getRed(), 70, 0.85f), - blend(baseColor.getGreen(), 130, 0.85f), - blend(baseColor.getBlue(), 70, 0.85f) - )); - } else { - // For unmet conditions, use a subtle red tint - c.setBackground(new Color( - blend(baseColor.getRed(), 140, 0.85f), - blend(baseColor.getGreen(), 60, 0.85f), - blend(baseColor.getBlue(), 60, 0.85f) - )); - - // For blocking conditions, use an indicator rather than border - if (c instanceof JComponent) { - ((JComponent)c).setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(0, 3, 0, 0, new Color(180, 50, 50, 120)), - BorderFactory.createEmptyBorder(1, 4, 1, 4) - )); - } - } - } else { - // Almost no color when not relevant - just slightly tinted - Color baseColor = row % 2 == 0 ? ColorScheme.DARKER_GRAY_COLOR : ColorScheme.DARK_GRAY_COLOR; - - if (startConditionsMet) { - c.setBackground(new Color( - blend(baseColor.getRed(), 76, 0.95f), - blend(baseColor.getGreen(), 120, 0.95f), - blend(baseColor.getBlue(), 76, 0.95f) - )); - } else { - c.setBackground(new Color( - blend(baseColor.getRed(), 120, 0.95f), - blend(baseColor.getGreen(), 76, 0.95f), - blend(baseColor.getBlue(), 76, 0.95f) - )); - } - } - c.setForeground(Color.BLACK); - } - } else if (column == 4) { // Stop conditions - boolean hasStopConditions = entry.hasAnyStopConditions(); - boolean stopConditionsMet = hasStopConditions && entry.getStopConditionManager().areAllConditionsMet(); - boolean isRelevant = entry.isRunning(); - - if (hasStopConditions) { - // More prominent when currently running - if (isRelevant) { - // Use darker base colors with the game's theme - Color baseColor = ColorScheme.DARKER_GRAY_COLOR; - - if (stopConditionsMet) { - // For met conditions, use a noticeable but not harsh green tint - c.setBackground(new Color( - blend(baseColor.getRed(), 60, 0.85f), - blend(baseColor.getGreen(), 140, 0.85f), - blend(baseColor.getBlue(), 70, 0.85f) - )); - - // For satisfied stop conditions, use an indicator rather than full border - if (c instanceof JComponent) { - ((JComponent)c).setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(0, 0, 0, 3, new Color(50, 180, 50, 140)), - BorderFactory.createEmptyBorder(1, 4, 1, 4) - )); - } - } else { - // For unmet conditions, use a blueish tint (less alarming than red for stop) - c.setBackground(new Color( - blend(baseColor.getRed(), 70, 0.85f), - blend(baseColor.getGreen(), 70, 0.85f), - blend(baseColor.getBlue(), 140, 0.85f) - )); - } - } else { - // Almost no color when not relevant - just slightly tinted - Color baseColor = row % 2 == 0 ? ColorScheme.DARKER_GRAY_COLOR : ColorScheme.DARK_GRAY_COLOR; - - if (stopConditionsMet) { - c.setBackground(new Color( - blend(baseColor.getRed(), 76, 0.95f), - blend(baseColor.getGreen(), 120, 0.95f), - blend(baseColor.getBlue(), 76, 0.95f) - )); - } else { - c.setBackground(new Color( - blend(baseColor.getRed(), 76, 0.95f), - blend(baseColor.getGreen(), 76, 0.95f), - blend(baseColor.getBlue(), 120, 0.95f) - )); - } - } - c.setForeground(Color.BLACK); - } - } - } - - // Set tooltip based on column for better information - always fresh with dynamic data - if (column == 0) { // Plugin name column - setToolTipText(getPluginDetailsTooltip(rowPlugin)); - } else if (column == 1) { // Schedule - setToolTipText(getScheduleTooltip(rowPlugin)); - } else if (column == 2) { // Next Run - setToolTipText(getNextRunTooltip(rowPlugin)); - } else if (column == 3) { // Start Conditions - setToolTipText(getStartConditionsTooltip(rowPlugin)); - } else if (column == 4) { // Stop Conditions - setToolTipText(getStopConditionsTooltip(rowPlugin)); - } else if (column == 5) { // Priority - setToolTipText(getPriorityTooltip(rowPlugin)); - } - } - setBorder(new EmptyBorder(2, 5, 2, 5)); - return c; - } - }; - - renderer.setHorizontalAlignment(SwingConstants.LEFT); - - // Apply renderer to all columns except the boolean column - for (int i = 0; i < scheduleTable.getColumnCount(); i++) { - if (i != 6) { // Skip Enabled column which is a checkbox - scheduleTable.getColumnModel().getColumn(i).setCellRenderer(renderer); - } - } - - // Add table to scroll pane with custom styling - JScrollPane scrollPane = new JScrollPane(scheduleTable); - scrollPane.setBorder(BorderFactory.createEmptyBorder()); - scrollPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Style the scrollbar - scrollPane.getVerticalScrollBar().setPreferredSize(new Dimension(10, 0)); - scrollPane.getVerticalScrollBar().setUnitIncrement(16); - scrollPane.getVerticalScrollBar().setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Add mouse listener to the scroll pane to clear selection when clicking empty space - scrollPane.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - clearSelection(); - } - - }); - scrollPane.addMouseWheelListener(new MouseAdapter() { - @Override - public void mouseWheelMoved(MouseWheelEvent e) { - if (e.getWheelRotation() == 0 || e.getUnitsToScroll() == 0) { - return; // Ignore zero rotation events - } - // Check if the mouse location is within the table's visible rectangle - Rectangle tableRect = scheduleTable.getVisibleRect(); - Point mousePoint = SwingUtilities.convertPoint(scrollPane, e.getPoint(), scheduleTable); - if (!tableRect.contains(mousePoint)) { - // Mouse is over the table, do not clear selection - return; - } - clearSelection(); - // Handle the scroll event here - // e.getWheelRotation() gives the number of clicks (positive or negative) - // e.getUnitsToScroll() gives the number of units to scroll - } - }); - - add(scrollPane, BorderLayout.CENTER); - - // Add an improved legend panel with more information - JPanel legendPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 2)); - legendPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - legendPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); - - // Current plugin indicator - addLegendItem(legendPanel, CURRENT_PLUGIN_COLOR, "Running", "Currently running plugin"); - - // Next plugin indicator - addLegendItem(legendPanel, NEXT_PLUGIN_COLOR, "Next", "Plugin scheduled to run next"); - - // Default plugin indicator - addLegendItem(legendPanel, DEFAULT_PLUGIN_COLOR, "Default", "Default plugin (Priority 0)"); - - // Condition indicators - addLegendItem(legendPanel, CONDITION_MET_COLOR, "Condition Met", "Condition has been satisfied"); - addLegendItem(legendPanel, CONDITION_NOT_MET_COLOR, "Not Met", "Condition not yet satisfied"); - - // Information about tooltips - JLabel tooltipInfo = new JLabel("Hover over cells for detailed tooltips"); - tooltipInfo.setForeground(Color.LIGHT_GRAY); - tooltipInfo.setFont(FontManager.getRunescapeSmallFont()); - tooltipInfo.setToolTipText("Tooltips show detailed information
They stay visible and refresh automatically"); - legendPanel.add(Box.createHorizontalStrut(10)); - legendPanel.add(tooltipInfo); - - // Add the legend panel to the bottom of the main panel - add(legendPanel, BorderLayout.SOUTH); - } - - /** - * Helper method to add a legend item with consistent styling - */ - private void addLegendItem(JPanel legendPanel, Color color, String text, String tooltip) { - JPanel indicator = new JPanel(); - indicator.setBackground(color); - indicator.setPreferredSize(new Dimension(15, 15)); - indicator.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY)); - indicator.setToolTipText(tooltip); - - JLabel label = new JLabel(text); - label.setForeground(Color.WHITE); - label.setFont(FontManager.getRunescapeSmallFont()); - label.setToolTipText(tooltip); - - legendPanel.add(indicator); - legendPanel.add(label); - } - - /** - * Creates a tooltip for the priority column - */ - private String getPriorityTooltip(PluginScheduleEntry entry) { - StringBuilder tooltip = new StringBuilder("Priority Information
"); - - if (entry.isDefault()) { - tooltip.append("
This is a default plugin with priority 0."); - tooltip.append("
Default plugins are always scheduled last."); - } else { - tooltip.append("
Priority: ").append(entry.getPriority()).append(""); - tooltip.append("
Higher priority plugins run before lower priority plugins."); - } - - tooltip.append("

To change a plugin to default status, set its priority to 0."); - - return tooltip.toString() + ""; - } - - /** - * Creates a detailed tooltip for schedule information - */ - private String getScheduleTooltip(PluginScheduleEntry entry) { - StringBuilder tooltip = new StringBuilder("Schedule Details:
"); - - // Add schedule type and details - tooltip.append(entry.getIntervalDisplay()); - - if (entry.hasAnyOneTimeStartConditions()) { - tooltip.append("
One-time schedule"); - if (entry.hasTriggeredOneTimeStartConditions() && !entry.canStartTriggerAgain()) { - tooltip.append(" (completed)"); - } - } - - // Add priority information - tooltip.append("

Priority: ").append(entry.getPriority()); - if (entry.isDefault()) { - tooltip.append(" (Default plugin)"); - } - - // Add random scheduling info - tooltip.append("
Random Scheduling: ").append(entry.isAllowRandomScheduling() ? "Enabled" : "Disabled"); - - return tooltip.toString() + ""; - } - /** - * Creates a detailed tooltip for start conditions with improved condition type display - */ - private String getStartConditionsTooltip(PluginScheduleEntry entry) { - if (!entry.hasStartConditions()) { - return "No start conditions defined"; - } - - List conditions = entry.getStartConditions(); - - // Determine if start conditions are relevant - when plugin is enabled but not started - boolean conditionsAreRelevant = entry.isEnabled() && !entry.isRunning(); - - StringBuilder tooltip = new StringBuilder("Start Conditions:
"); - - // Group conditions by type - Map> grouped = groupConditionsByType(conditions); - - // Show root causes prominently if conditions are not met but are relevant - if (conditionsAreRelevant && !entry.canBeStarted()) { - tooltip.append("
"); - tooltip.append("Blocking Conditions:
"); - - // First show user-defined root causes - String userRootCauses = entry.getStartConditionManager().getUserRootCausesSummary(); - if (!userRootCauses.isEmpty()) { - tooltip.append("User-defined:
"); - tooltip.append("").append(userRootCauses).append("
"); - } - - // Then show plugin-defined root causes - String pluginRootCauses = entry.getStartConditionManager().getPluginRootCausesSummary(); - if (!pluginRootCauses.isEmpty()) { - tooltip.append("Plugin-defined:
"); - tooltip.append("").append(pluginRootCauses).append("
"); - } - tooltip.append("

"); - } - - // If there are many conditions, add a summary first - if (conditions.size() > 3) { - tooltip.append("").append(entry.getDetailedStartConditionsStatus()).append(""); - tooltip.append("

"); - } - - if (grouped.isEmpty()) { - tooltip.append("Will start as soon as enabled"); - } else { - tooltip.append(""); - for (Map.Entry> group : grouped.entrySet()) { - ConditionType type = group.getKey(); - List typeConditions = group.getValue(); - - // Add a header for this type - tooltip.append(""); - - // Add each condition - for (Condition condition : typeConditions) { - boolean isSatisfied = condition.isSatisfied(); - tooltip.append(""); - - // Status icon column - tooltip.append(""); - - // Description column - tooltip.append(""); - tooltip.append(""); - } - } - tooltip.append("
") - .append(getConditionTypeIcon(type)) - .append(" ") - .append(formatConditionTypeName(type)) - .append("
"); - // Show either relevance icon or satisfaction status - if (conditionsAreRelevant) { - tooltip.append("⚡ ") - .append(getStatusSymbol(isSatisfied)); - } else { - tooltip.append("○"); - } - tooltip.append(""); - String description = formatConditionDescription(condition); - if (isSatisfied) { - if (conditionsAreRelevant) { - tooltip.append("").append(description).append(""); - } else { - tooltip.append("").append(description).append(""); - } - } else { - if (conditionsAreRelevant) { - tooltip.append("").append(description).append(""); - } else { - tooltip.append("").append(description).append(""); - } - } - tooltip.append("
"); - } - - // Add overall status - if (conditionsAreRelevant) { - tooltip.append("
Status: ⚡ Currently relevant"); - if (entry.canBeStarted()) { - tooltip.append("
All conditions met - plugin ready to start"); - } else { - tooltip.append("
Some conditions not met - waiting to start"); - - // Add detailed explanation section if there are blocking conditions - String userExplanation = entry.getStartConditionManager().getUserBlockingExplanation(); - String pluginExplanation = entry.getStartConditionManager().getPluginBlockingExplanation(); - - if (!userExplanation.isEmpty() || !pluginExplanation.isEmpty()) { - tooltip.append("

"); - tooltip.append("Show detailed diagnostics"); - - if (!userExplanation.isEmpty()) { - tooltip.append("
"); - tooltip.append("User condition details:
"); - tooltip.append(userExplanation.replace("\n", "
")); - tooltip.append("
"); - } - - if (!pluginExplanation.isEmpty()) { - tooltip.append("
"); - tooltip.append("Plugin condition details:
"); - tooltip.append(pluginExplanation.replace("\n", "
")); - tooltip.append("
"); - } - - tooltip.append("
"); - } - } - } else { - tooltip.append("
Status: Not currently relevant"); - } - - tooltip.append(""); - return tooltip.toString(); - } - - /** - * Creates a detailed tooltip for stop conditions with improved condition type display - * Updated to be more compact with less line breaks to reduce row height - */ - private String getStopConditionsTooltip(PluginScheduleEntry entry) { - if (!entry.hasStopConditions()) { - return "No stop conditions defined"; - } - - List conditions = entry.getStopConditions(); - - // Determine if stop conditions are relevant - when plugin is currently running - boolean conditionsAreRelevant = entry.isRunning(); - - StringBuilder tooltip = new StringBuilder("Stop Conditions:
"); - - // Group conditions by type - Map> grouped = groupConditionsByType(conditions); - - // Show blocking conditions prominently if plugin is running but stop conditions not met - if (conditionsAreRelevant && !entry.allowedToBeStop()) { - // Show overall progress first - double progress = entry.getStopConditionProgress(); - if (progress > 0) { - String progressColor = progress > 80 ? "#81C784" : progress > 50 ? "#FFB74D" : "#64B5F6"; - - tooltip.append("
"); - tooltip.append("Current Progress: "); - tooltip.append("") - .append(String.format("%.1f%%", progress)) - .append("

"); - } - - // Then show what's preventing the plugin from stopping - tooltip.append("
"); - tooltip.append("Waiting For:
"); - - // First show user-defined waiting conditions - String userRootCauses = entry.getStopConditionManager().getUserRootCausesSummary(); - if (!userRootCauses.isEmpty()) { - tooltip.append("User-defined:
"); - tooltip.append("").append(userRootCauses).append("
"); - } - - // Then show plugin-defined waiting conditions - String pluginRootCauses = entry.getStopConditionManager().getPluginRootCausesSummary(); - if (!pluginRootCauses.isEmpty()) { - tooltip.append("Plugin-defined:
"); - tooltip.append("").append(pluginRootCauses).append("
"); - } - tooltip.append("

"); - } - // If there are many conditions, add a summary first - else if (conditions.size() > 3) { - tooltip.append("").append(entry.getDetailedStopConditionsStatus()).append(""); - tooltip.append("

"); - } - - // Show overall progress if relevant and available - if (conditionsAreRelevant && !entry.allowedToBeStop()) { - // Progress already shown above - } else if (conditionsAreRelevant) { - double progress = entry.getStopConditionProgress(); - if (progress > 0) { - tooltip.append("Overall Progress: "); - - String progressColor = progress > 80 ? "green" : - progress > 50 ? "orange" : "blue"; - - tooltip.append("") - .append(String.format("%.1f%%", progress)) - .append("
"); - } - tooltip.append("
"); - } - - if (grouped.isEmpty()) { - tooltip.append("Will run until manually stopped"); - } else { - tooltip.append(""); - for (Map.Entry> group : grouped.entrySet()) { - ConditionType type = group.getKey(); - List typeConditions = group.getValue(); - - // Add a header for this type - tooltip.append(""); - - // Add each condition - for (Condition condition : typeConditions) { - boolean isSatisfied = condition.isSatisfied(); - tooltip.append(""); - - // Status icon column - tooltip.append(""); - - // Description column - tooltip.append(""); - tooltip.append(""); - } - } - tooltip.append("
") - .append(getConditionTypeIcon(type)) - .append(" ") - .append(formatConditionTypeName(type)) - .append("
"); - // Show either relevance icon or satisfaction status - if (conditionsAreRelevant) { - tooltip.append("⚡ ") - .append(getStatusSymbol(isSatisfied)); - } else { - tooltip.append("○"); - } - tooltip.append(""); - String description = formatConditionDescription(condition); - if (isSatisfied) { - if (conditionsAreRelevant) { - tooltip.append("").append(description).append(""); - } else { - tooltip.append("").append(description).append(""); - } - } else { - if (conditionsAreRelevant) { - tooltip.append("").append(description).append(""); - } else { - tooltip.append("").append(description).append(""); - } - } - tooltip.append("
"); - } - - // Add overall status - if (conditionsAreRelevant) { - tooltip.append("
Status: ⚡ Currently relevant"); - if (entry.allowedToBeStop()) { - tooltip.append("
Plugin Stop conditions met - plugin can be stopped"); - } else { - tooltip.append("
Waiting for plugin stop conditions"); - - // If there's timing info available, show it - String nextTrigger = entry.getNextStopTriggerTimeString(); - if (!nextTrigger.contains("None") && !nextTrigger.isEmpty()) { - tooltip.append("
Next potential trigger: ") - .append(nextTrigger) - .append(""); - } - - // Add detailed explanation section if there are blocking conditions - String userExplanation = entry.getStopConditionManager().getUserBlockingExplanation(); - String pluginExplanation = entry.getStopConditionManager().getPluginBlockingExplanation(); - - if (!userExplanation.isEmpty() || !pluginExplanation.isEmpty()) { - tooltip.append("

"); - tooltip.append("Show detailed diagnostics"); - - if (!userExplanation.isEmpty()) { - tooltip.append("
"); - tooltip.append("User condition details:
"); - tooltip.append(userExplanation.replace("\n", "
")); - tooltip.append("
"); - } - - if (!pluginExplanation.isEmpty()) { - tooltip.append("
"); - tooltip.append("Plugin condition details:
"); - tooltip.append(pluginExplanation.replace("\n", "
")); - tooltip.append("
"); - } - - tooltip.append("
"); - } - } - } else { - tooltip.append("
Status: Not currently relevant"); - } - - tooltip.append(""); - return tooltip.toString(); - } - - /** - * Groups conditions by their type for organized display - */ - private Map> groupConditionsByType(List conditions) { - Map> groupedConditions = new HashMap<>(); - - for (Condition condition : conditions) { - ConditionType type = condition.getType(); - groupedConditions.computeIfAbsent(type, k -> new ArrayList<>()).add(condition); - } - - return groupedConditions; - } - - /** - * Returns an appropriate icon for a condition type that will reliably display in tooltips - */ - private String getConditionTypeIcon(ConditionType type) { - // Using HTML entities and standard characters instead of Unicode emojis - // as these render more reliably in Swing tooltips - switch (type) { - case TIME: - return "⏰"; // Clock icon using HTML entity - //case SKILL: - // return "📊"; // Chart icon using HTML entity - case SKILL: - return "📈"; // Chart with upward trend using HTML entity - case RESOURCE: - return "▣"; // Square icon - case LOCATION: - return "☉"; // Compass icon - case LOGICAL: - return "↻"; // Recycling/Loop icon - case NPC: - return "☺"; // Face/Person icon - default: - return "•"; // Bullet point - } - } - - /** - * Returns a user-friendly name for a condition type - */ - private String formatConditionTypeName(ConditionType type) { - switch (type) { - case TIME: return "Time Conditions"; - case SKILL: return "Skill Conditions"; - case RESOURCE: return "Resource Conditions"; - case LOCATION: return "Location Conditions"; - case LOGICAL: return "Logical Conditions"; - case NPC: return "NPC Conditions"; - default: return type.toString() + " Conditions"; - } - } - - /** - * Returns a colored status symbol for condition status that will reliably display in tooltips - */ - private String getStatusSymbol(boolean satisfied) { - return satisfied ? - "" : // HTML entity for checkmark - ""; // HTML entity for X mark - } - - /** - * Formats a condition description for better readability - * Handles special cases like nested logical conditions - */ - private String formatConditionDescription(Condition condition) { - String description = condition.getDescription(); - - // For logical conditions, use HTML formatting when available - if (condition instanceof LogicalCondition) { - LogicalCondition logicalCondition = (LogicalCondition) condition; - // Get a truncated HTML description with reasonable length limit - return logicalCondition.getHtmlDescription(80) - .replace("", "") - .replace("", ""); - } - - // For logical conditions, include more detailed info about progress - if (condition.getType() == ConditionType.LOGICAL) { - // Try to get more detailed info about logical condition's progress - int metCount = condition.getMetConditionCount(); - int totalCount = condition.getTotalConditionCount(); - - if (totalCount > 1) { - description += String.format(" (%d/%d sub-conditions met)", metCount, totalCount); - } - } - - return description; - } - /** - * Creates a detailed tooltip for next run information - */ - private String getNextRunTooltip(PluginScheduleEntry entry) { - StringBuilder tooltip = new StringBuilder("Next Run Details:
"); - - // Add next run time - Optional nextTime = entry.getCurrentStartTriggerTime(); - if (nextTime.isPresent()) { - tooltip.append("Next scheduled time: ").append(nextTime.get().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); - } else { - tooltip.append("No specific next run time"); - } - - // Add completed runs - tooltip.append("
Times run: ").append(entry.getRunCount()); - - // Add enabled status - tooltip.append("
Status: ").append(entry.isEnabled() ? "Enabled" : "Disabled"); - if (entry.isRunning()) { - tooltip.append(" (Currently running)"); - } - - return tooltip.toString() + ""; - } - - /** - * Determines if the provided plugin is the next one scheduled to run - */ - private boolean isNextToRun(PluginScheduleEntry scheduledPlugin) { - if (!scheduledPlugin.isEnabled()) { - return false; - } - - PluginScheduleEntry nextPlugin = schedulerPlugin.getNextPluginToBeScheduled(); - PluginScheduleEntry nextUpCommigPlugin = schedulerPlugin.getNextPluginToBeScheduled(); - boolean isNextUpComingPlugin = nextUpCommigPlugin != null && nextUpCommigPlugin.equals(scheduledPlugin); - boolean isNextPlugin = nextPlugin != null && nextPlugin.equals(scheduledPlugin); - if (nextPlugin!= null){ - return isNextPlugin; - }else{ - return isNextUpComingPlugin; - } -// return nextPlugin != null && nextPlugin.equals(scheduledPlugin); - } - private void detectChangesInPluginlist(){ - // Get the full combined list (available + separator + unavailable) like in refreshTable - List availablePlugins = schedulerPlugin.getScheduledPlugins(); - List unavailablePlugins = schedulerPlugin.getUnavailableScheduledPlugins(); - List sortedAvailablePlugins = SchedulerPluginUtil.sortPluginScheduleEntries(availablePlugins); - - List expectedPlugins = new ArrayList<>(); - expectedPlugins.addAll(sortedAvailablePlugins); - - // Add separator if there are unavailable plugins - if (!unavailablePlugins.isEmpty() && !sortedAvailablePlugins.isEmpty()) { - expectedPlugins.add(null); // separator - } - - expectedPlugins.addAll(unavailablePlugins); - - // Only refresh if the structure has actually changed - if (expectedPlugins.size() != rowToPluginMap.size()) { - log.info("Plugin list size changed: {} -> {}, refreshing", rowToPluginMap.size(), expectedPlugins.size()); - this.rowToPluginMap = new ArrayList<>(expectedPlugins); - return; - } - - // Check if the content order has changed - boolean hasChanged = false; - for (int i = 0; i < expectedPlugins.size(); i++) { - PluginScheduleEntry expected = expectedPlugins.get(i); - PluginScheduleEntry current = i < rowToPluginMap.size() ? rowToPluginMap.get(i) : null; - - // Handle separator rows (both null) - if (expected == null && current == null) { - continue; - } - - // Handle mismatched null vs non-null - if (expected == null || current == null) { - hasChanged = true; - break; - } - - // Check if plugins are different - if (!expected.equals(current)) { - hasChanged = true; - break; - } - } - - if (hasChanged) { - log.info("Plugin list order changed, refreshing"); - this.rowToPluginMap = new ArrayList<>(expectedPlugins); - } - } - - - public void refreshTable() { - if (this.updatingTable) { - return; // Skip if already updating - } - - this.updatingTable = true; - - try { - detectChangesInPluginlist(); - // Save current selection - PluginScheduleEntry selectedPlugin = getSelectedPlugin(); - int selectedRow = scheduleTable.getSelectedRow(); - // Get available and unavailable plugins separately - List availablePlugins = schedulerPlugin.getScheduledPlugins(); - List unavailablePlugins = schedulerPlugin.getUnavailableScheduledPlugins(); - - // Sort available plugins by next run time - List sortedAvailablePlugins = SchedulerPluginUtil.sortPluginScheduleEntries(availablePlugins); - - // Combine lists: available plugins first, then separator, then unavailable plugins - List sortedPlugins = new ArrayList<>(); - sortedPlugins.addAll(sortedAvailablePlugins); - - // Add a placeholder separator entry if there are unavailable plugins - if (!unavailablePlugins.isEmpty() && !sortedAvailablePlugins.isEmpty()) { - sortedPlugins.add(null); // null entry will be rendered as separator - } - - sortedPlugins.addAll(unavailablePlugins); - //SchedulerPluginUtil.logSortedPluginScheduleEntryList(sortedPlugins); - - - - // Create a new row map with the correct size to match the sorted plugins - List newRowMap = new ArrayList<>(sortedPlugins.size()); - // Initialize with nulls first - for (int i = 0; i < sortedPlugins.size(); i++) { - newRowMap.add(null); - } - - - // We no longer need to save the previous row map for comparison - // Track if we need to force repaint (visual changes that might not trigger repaint) - boolean needsRepaint = false; - - // Set to track plugins we've processed to avoid duplicates - Set processedPlugins = new HashSet<>(); - - // First pass: update existing rows in place if possible - for (int newIndex = 0; newIndex < sortedPlugins.size(); newIndex++) { - PluginScheduleEntry plugin = sortedPlugins.get(newIndex); - - // Skip if this plugin has already been processed (but allow null separators) - if (plugin != null && processedPlugins.contains(plugin)) { - continue; - } - - - // Same position, just update data in place - if (newIndex < tableModel.getRowCount()) { - updateRowWithPlugin(newIndex, plugin); - //tableModel.insertRow(newIndex, createRowData(plugin)); - } else { - tableModel.addRow(createRowData(plugin)); - needsRepaint = true; - } - newRowMap.set(newIndex, plugin); - - // Mark plugin as processed (if it's not a separator) - if (plugin != null) { - processedPlugins.add(plugin); - } - } - - // Remove any excess rows - while (tableModel.getRowCount() > sortedPlugins.size()) { - tableModel.removeRow(tableModel.getRowCount() - 1); - needsRepaint = true; - } - - // Update our tracking map - this.rowToPluginMap = newRowMap; - - // Restore selection if possible - if (selectedPlugin != null) { - int newSlectedRow = -1; - for (int i = 0; i < newRowMap.size(); i++) { - if (newRowMap.get(i).equals(selectedPlugin)) { - newSlectedRow = i; - break; - } - } - if (newSlectedRow != -1) { - scheduleTable.setRowSelectionInterval(newSlectedRow, newSlectedRow); - scheduleTable.scrollRectToVisible(scheduleTable.getCellRect(newSlectedRow, 0, true)); - } else { - // If the selected plugin is no longer in the list, clear selection - scheduleTable.clearSelection(); - } - - } - - // Force repaint if needed - if (needsRepaint) { - scheduleTable.repaint(); - } - - // Update tooltips after refresh - updateTooltipsAfterRefresh(newRowMap); - } finally { - this.updatingTable = false; - } - } - - /** - * Updates row data array with current plugin values - * Currently unused but kept for future use - */ - @SuppressWarnings("unused") - private void updateRowData(Object[] rowData, PluginScheduleEntry plugin) { - // Handle separator row (null plugin) - if (plugin == null) { - Object[] separatorData = createRowData(null); // Use existing separator logic - System.arraycopy(separatorData, 0, rowData, 0, Math.min(separatorData.length, rowData.length)); - return; - } - - // Get basic information - String pluginName = plugin.getCleanName(); - - if (schedulerPlugin.isRunningEntry(plugin)) { - pluginName = "â–ķ " + pluginName; - } - - // For default plugins, add a visual indicator - if (plugin.isDefault()) { - pluginName = "* " + pluginName; // Use simple asterisk instead of emoji - } - - // For unavailable plugins, add visual indicator - if (!plugin.isPluginAvailable()) { - pluginName = "❌ " + pluginName + " (Not Installed)"; - } - - // Update row data - rowData[0] = pluginName; - rowData[1] = getEnhancedScheduleDisplay(plugin); - rowData[2] = getEnhancedNextRunDisplay(plugin); - rowData[3] = getStartConditionInfo(plugin); - rowData[4] = getStopConditionInfo(plugin); - rowData[5] = plugin.getPriority(); - rowData[6] = plugin.isEnabled(); - rowData[7] = plugin.getRunCount(); - } - - /** - * Updates existing row in the table with current plugin values - */ - private void updateRowWithPlugin(int rowIndex, PluginScheduleEntry plugin) { - // Handle separator row (null plugin) - if (plugin == null) { - Object[] separatorData = createRowData(null); // Use existing separator logic - for (int col = 0; col < separatorData.length && col < tableModel.getColumnCount(); col++) { - tableModel.setValueAt(separatorData[col], rowIndex, col); - } - return; - } - - // Get basic information - String pluginName = plugin.getCleanName(); - - if (schedulerPlugin.isRunningEntry(plugin)) { - pluginName = "â–ķ " + pluginName; - } - - // For default plugins, add a visual indicator - if (plugin.isDefault()) { - pluginName = "* " + pluginName; // Use simple asterisk instead of emoji - } - - // For unavailable plugins, add visual indicator - if (!plugin.isPluginAvailable()) { - pluginName = "❌ " + pluginName + " (Not Installed)"; - } - - // Update existing row with focused columns - tableModel.setValueAt(pluginName, rowIndex, 0); - tableModel.setValueAt(getEnhancedScheduleDisplay(plugin), rowIndex, 1); - tableModel.setValueAt(getEnhancedNextRunDisplay(plugin), rowIndex, 2); - tableModel.setValueAt(getStartConditionInfo(plugin), rowIndex, 3); - tableModel.setValueAt(getStopConditionInfo(plugin), rowIndex, 4); - tableModel.setValueAt(plugin.getPriority(), rowIndex, 5); - tableModel.setValueAt(plugin.isEnabled(), rowIndex, 6); - tableModel.setValueAt(plugin.getRunCount(), rowIndex, 7); - } - - /** - * Creates a new row data array for a plugin - */ - private Object[] createRowData(PluginScheduleEntry plugin) { - // Handle separator row (null plugin) - if (plugin == null) { - return new Object[]{ - "────── Not Installed / Would be scheduled if installed ──────", - "", - "", - "", - "", - 0, // Priority column - Integer - false, // Enabled column - Boolean - 0 // Run Count column - Integer - }; - } - - // Get basic information - String pluginName = plugin.getCleanName(); - - // Check if plugin is available - boolean isAvailable = plugin.isPluginAvailable(); - - if (schedulerPlugin.isRunningEntry(plugin)) { - pluginName = "â–ķ " + pluginName; - } - - // For default plugins, add a visual indicator - if (plugin.isDefault()) { - pluginName = "* " + pluginName; // Use simple asterisk instead of emoji - } - - // For unavailable plugins, add visual indicator - if (!isAvailable) { - pluginName = "❌ " + pluginName + " (Not Installed)"; - } - - return new Object[]{ - pluginName, - getEnhancedScheduleDisplay(plugin), - getEnhancedNextRunDisplay(plugin), - getStartConditionInfo(plugin), - getStopConditionInfo(plugin), - plugin.getPriority(), - plugin.isEnabled(), - plugin.getRunCount() - }; - } - /** - * Creates a display of start condition information - * Updated to be more compact with less line breaks to reduce row height - */ - private String getStartConditionInfo(PluginScheduleEntry entry) { - int startTotal = entry.getStartConditionManager().getConditions().size(); - if (startTotal == 0) { - return "None"; - } - - int startMet = (int) entry.getStartConditionManager().getConditions().stream() - .filter(Condition::isSatisfied).count(); - - StringBuilder info = new StringBuilder(); - info.append(""); - - // Check if conditions are relevant - boolean isRelevant = entry.isEnabled() && !entry.isRunning(); - - if (isRelevant && startMet < startTotal && entry.getStartConditionManager().requiresAll()) { - // When blocking conditions exist and ALL conditions required, show blocking info - int userBlockingCount = entry.getStartConditionManager().getUserLeafBlockingConditions().size(); - int pluginBlockingCount = entry.getStartConditionManager().getPluginLeafBlockingConditions().size(); - int totalBlocking = userBlockingCount + pluginBlockingCount; - - if (totalBlocking > 0) { - info.append("") - .append(startMet).append("/").append(startTotal) - .append(" met (").append(totalBlocking).append(" blocking)"); - } else { - info.append("").append(startMet).append("/").append(startTotal).append(" met"); - } - } else if (isRelevant && !entry.canBeStarted()) { - // When ANY condition is required but none are met - info.append("") - .append(startMet).append("/").append(startTotal).append(" (Blocked)"); - } else { - // Normal display - info.append("").append(startMet).append("/").append(startTotal).append(" met"); - } - - // Add type indicator and one-time indicator if applicable more concisely - boolean startRequiresAll = entry.getStartConditionManager().requiresAll(); - info.append(" ") - .append(startRequiresAll ? "ALL" : "ANY"); - - if (entry.hasAnyOneTimeStartConditions()) { - info.append(", One-time"); - } - info.append(""); - - return info.toString() + ""; - } - - /** - * Creates a display of stop condition information - * Updated to be more compact with less line breaks to reduce row height - */ - private String getStopConditionInfo(PluginScheduleEntry entry) { - int stopTotal = entry.getStopConditionManager().getConditions().size(); - if (stopTotal == 0) { - return "None"; - } - - int stopMet = (int) entry.getStopConditionManager().getConditions().stream() - .filter(Condition::isSatisfied).count(); - - StringBuilder info = new StringBuilder(); - info.append(""); - - // Check if conditions are relevant (plugin is running) - boolean isRelevant = entry.isRunning(); - - // Add progress for running plugins in a more compact format - if (isRelevant) { - double progress = entry.getStopConditionProgress(); - - // Show progress and satisfied conditions count on same line - if (entry.allowedToBeStop()) { - // Ready to stop - info.append("") - .append(stopMet).append("/").append(stopTotal) - .append(" met (Ready ✓)"); - } else { - // Still waiting for conditions - String progressStr = progress > 0 ? String.format(" %.0f%%", progress) : ""; - info.append("") - .append(stopMet).append("/").append(stopTotal) - .append(" met").append(progressStr).append(""); - - // If there are specific blocking conditions worth mentioning, add concisely - int waitingCount = entry.getStopConditionManager().getLeafBlockingConditions().size(); - if (waitingCount > 0) { - info.append(" (") - .append(waitingCount).append(" waiting)"); - } - } - } else { - // Standard display when not relevant - info.append("").append(stopMet).append("/").append(stopTotal).append(" met"); - } - - // Add type indicator and one-time indicator more concisely - boolean stopRequiresAll = entry.getStopConditionManager().requiresAll(); - info.append(" ") - .append(stopRequiresAll ? "ALL" : "ANY"); - - if (entry.hasAnyOneTimeStopConditions()) { - info.append(", One-time"); - } - info.append(""); - - return info.toString() + ""; - } - - - - /** - * Creates an enhanced display of schedule information - */ - private String getEnhancedScheduleDisplay(PluginScheduleEntry entry) { - // Check for one-time schedule first - boolean isOneTime = entry.hasAnyOneTimeStartConditions(); - - // If it's a one-time schedule that's already triggered, show completion status - if (isOneTime && entry.hasTriggeredOneTimeStartConditions() && !entry.canStartTriggerAgain()) { - return "One-time (Completed)"; - } - - // Get the base interval display - String baseDisplay = entry.getIntervalDisplay(); - - // For one-time schedules, add an indicator - if (isOneTime) { - return "One-time: " + baseDisplay; - } - - return baseDisplay; - } - - /** - * Creates an enhanced display of the next run time, including last stop icon - */ - private String getEnhancedNextRunDisplay(PluginScheduleEntry entry) { - StringBuilder display = new StringBuilder(); - display.append(""); - - // First add the plugin's optimized display method for scheduling - String baseDisplay = entry.getNextRunDisplay(); - - // Add extra information for one-time entries that can't run again - if (entry.hasAnyOneTimeStartConditions() && !entry.canStartTriggerAgain()) { - if (entry.getRunCount() > 0) { - display.append("Completed"); - } else { - display.append("Cannot run"); - } - } else { - display.append(baseDisplay); - } - - // Add just an icon for the last stop reason (if plugin has run before and isn't currently running) - if (entry.getRunCount() > 0 && !entry.isRunning() && entry.getLastStopReasonType() != PluginScheduleEntry.StopReason.NONE) { - // Add spacing - display.append(" "); - - // Show only an icon based on stop reason - switch(entry.getLastStopReasonType()) { - case SCHEDULED_STOP: - display.append("✓"); // Green checkmark for normal stop - break; - case PLUGIN_FINISHED: - display.append("✅"); // Green check box for self-completed - break; - case MANUAL_STOP: - display.append("âđ"); // Gray square for manual stop - break; - case HARD_STOP: - display.append("⚠"); // Orange warning for timeout - break; - case INTERRUPTED: - display.append("âļ"); // Blue pause for interrupted - break; - case ERROR: - display.append("❌"); // Red X for error - break; - default: - display.append("â€Ē"); // Gray dot for unknown - break; - } - } - - return display.toString() + ""; - } - - /** - * Creates an enhanced display of condition information - * Currently unused but kept for future use - */ - @SuppressWarnings("unused") - private String getEnhancedConditionInfo(PluginScheduleEntry entry) { - StringBuilder info = new StringBuilder(); - - // Add start condition info if available - int startTotal = entry.getStartConditionManager().getConditions().size(); - if (startTotal > 0) { - int startMet = (int) entry.getStartConditionManager().getConditions().stream() - .filter(Condition::isSatisfied).count(); - - info.append("Start: ").append(startMet).append("/").append(startTotal); - - // Add type indicator - boolean startRequiresAll = entry.getStartConditionManager().requiresAll(); - info.append(startRequiresAll ? " (ALL)" : " (ANY)"); - } - - // Add stop condition info if available - int stopTotal = entry.getStopConditionManager().getConditions().size(); - if (stopTotal > 0) { - if (info.length() > 0) { - info.append(" | "); - } - - int stopMet = (int) entry.getStopConditionManager().getConditions().stream() - .filter(Condition::isSatisfied).count(); - - info.append("Stop: ").append(stopMet).append("/").append(stopTotal); - - // Add type indicator - boolean stopRequiresAll = entry.getStopConditionManager().requiresAll(); - info.append(stopRequiresAll ? " (ALL)" : " (ANY)"); - - // Add progress for running plugins - if (entry.isRunning()) { - double progress = entry.getStopConditionProgress(); - if (progress > 0) { - info.append(String.format(" (%.0f%%)", progress)); - } - } - } - - // If no conditions, show "None" - if (info.length() == 0) { - return "None"; - } - - return info.toString(); - } - - public void addSelectionListener(Consumer listener) { - this.selectionListener = listener; - scheduleTable.getSelectionModel().addListSelectionListener(e -> { - if (!e.getValueIsAdjusting()) { - int selectedRow = scheduleTable.getSelectedRow(); - if (selectedRow >= 0 && selectedRow < schedulerPlugin.getScheduledPlugins().size()) { - listener.accept(getPluginAtRow(selectedRow)); - } else { - listener.accept(null); - } - } - }); - } - - public PluginScheduleEntry getSelectedPlugin() { - int selectedRow = scheduleTable.getSelectedRow(); - if (selectedRow >= 0 && selectedRow < schedulerPlugin.getScheduledPlugins().size()) { - if (rowToPluginMap.size() > selectedRow) { - // Use the rowToPluginMap to get the plugin - return rowToPluginMap.get(selectedRow); - }else { - // Fallback to the original list if rowToPluginMap is not available - // This should not happen in normal operation - rowToPluginMap = new ArrayList<>(schedulerPlugin.getScheduledPlugins()); - return schedulerPlugin.getScheduledPlugins().get(selectedRow); - } - - //return schedulerPlugin.getScheduledPlugins().get(selectedRow); - } - return null; - } - - /** - * Clears the current table selection and notifies the selection listener - */ - public void clearSelection() { - scheduleTable.clearSelection(); - lastSelectedRow = -1; // Reset last selected row - if (selectionListener != null) { - selectionListener.accept(null); - } - } - - /** - * Clears the table selection without triggering the selection callback. - * This is useful when we want to clear selection programmatically without - * triggering a cascade of UI updates. - */ - public void clearSelectionWithoutCallback() { - scheduleTable.clearSelection(); - // Unlike clearSelection(), this method does not call selectionListener - } - - public void addAndSelect(PluginScheduleEntry pluginEntry) { - if (pluginEntry == null) return; - - for (PluginScheduleEntry entry : schedulerPlugin.getScheduledPlugins()) { - if ( pluginEntry == entry) { - // Plugin already exists, no need to add -> no duplicate - return; - } - } - schedulerPlugin.addScheduledPlugin(pluginEntry); - rowToPluginMap.add(pluginEntry); - tableModel.addRow(new Object[]{ - pluginEntry.getName(), - pluginEntry.getIntervalDisplay(), - pluginEntry.getNextRunDisplay(), - getStartConditionInfo(pluginEntry), - getStopConditionInfo(pluginEntry), - pluginEntry.getPriority(), - pluginEntry.isDefault(), - pluginEntry.isEnabled(), - pluginEntry.isAllowRandomScheduling(), - pluginEntry.isNeedsStopCondition(), - pluginEntry.getRunCount() - }); - scheduleTable.setRowSelectionInterval(getRowCount(), getRowCount()); - } - /** - * Selects the given plugin in the table - * @param plugin The plugin to select - */ - public void selectPlugin(PluginScheduleEntry plugin) { - if (plugin == null) return; - List plugins = this.rowToPluginMap; - for (int i = 0; i < tableModel.getRowCount(); i++) { - String rowName = String.valueOf(tableModel.getValueAt(i, 0)) - .replaceAll("â–ķ ", ""); // Remove play indicator if present - // First try to find the exact same object reference - - if (plugins.get(i) == plugin) { // Use reference equality - scheduleTable.setRowSelectionInterval(i, i); - // Make sure the selected row is visible - Rectangle rect = scheduleTable.getCellRect(i, 0, true); - scheduleTable.scrollRectToVisible(rect); - - // Notify listeners - if (selectionListener != null) { - selectionListener.accept(plugin); - } - return; - } - } - } - /** - * Creates a comprehensive tooltip for plugin details including last stop information - */ - private String getPluginDetailsTooltip(PluginScheduleEntry entry) { - StringBuilder tooltip = new StringBuilder("Plugin Details: ").append(entry.getCleanName()); - - // Status section - tooltip.append("

Status: "); - if (entry.isRunning()) { - tooltip.append("Currently Running"); - } else if (!entry.isEnabled()) { - tooltip.append("Disabled"); - } else if (isNextToRun(entry)) { - tooltip.append("Next to Run"); - } else { - tooltip.append("Waiting for schedule"); - } - - // Run information - tooltip.append("
Run Count: ").append(entry.getRunCount()); - - // Last stop information - if (entry.getRunCount() > 0 && !entry.isRunning()) { - tooltip.append("

Last Stop Info:"); - - // Stop reason - String stopReason = entry.getLastStopReason(); - if (stopReason != null && !stopReason.isEmpty()) { - tooltip.append("
Reason: ").append(stopReason); - } - - // Stop reason type with matching icon - tooltip.append("
Type: "); - // Use the description from the enum if available - PluginScheduleEntry.StopReason reasonType = entry.getLastStopReasonType(); - if (reasonType != null) { - switch (reasonType) { - case SCHEDULED_STOP: - tooltip.append("✓ Scheduled Stop (conditions met)"); - break; - case MANUAL_STOP: - tooltip.append("âđ Manual Stop (user initiated)"); - break; - case HARD_STOP: - tooltip.append("⚠ Hard Stop (forced after timeout)"); - break; - case ERROR: - tooltip.append("❌ Error"); - break; - case PLUGIN_FINISHED: - tooltip.append("✅ Plugin Self-reported Completion"); - break; - case INTERRUPTED: - tooltip.append("âļ Plugin Interrupted"); - break; - case NONE: - tooltip.append("â€Ē " + reasonType.getDescription()); - break; - default: - tooltip.append("â€Ē " + reasonType.getDescription()); - break; - } - } else { - tooltip.append("â€Ē Unknown"); - } - - // Success status - tooltip.append("
Success: "); - tooltip.append(entry.isLastRunSuccessful() ? - "Yes" : - "No"); - } - - // Schedule information - tooltip.append("

Schedule: ").append(entry.getIntervalDisplay()); - - // Start/Stop condition information - use the detailed tooltip methods - tooltip.append("

"); - - // Extract the content from the start and stop condition tooltips, but exclude the html tags - String startConditions = getStartConditionsTooltip(entry) - .replace("", "") - .replace("", ""); - - String stopConditions = getStopConditionsTooltip(entry) - .replace("", "") - .replace("", ""); - - tooltip.append(startConditions); - tooltip.append("

"); - tooltip.append(stopConditions); - - // Configuration - tooltip.append("

Configuration:"); - tooltip.append("
Priority: ").append(entry.getPriority()); - tooltip.append("
Default: ").append(entry.isDefault() ? "Yes" : "No"); - tooltip.append("
Random Scheduling: ").append(entry.isAllowRandomScheduling() ? "Enabled" : "Disabled"); - - return tooltip.toString() + ""; - } - - /** - * Implementation of ScheduleTableModel interface - gets the plugin at a specific row - */ - @Override - public PluginScheduleEntry getPluginAtRow(int row) { - if (row >= 0 && row < rowToPluginMap.size()) { - return rowToPluginMap.get(row); - } - if (row >= 0 && row < tableModel.getRowCount()) { - return schedulerPlugin.getScheduledPlugins().get(row); - } - return null; - } - - /** - * Helper method to blend colors with a given ratio - * @param baseValue Base color component value (0-255) - * @param targetValue Target color component value (0-255) - * @param ratio Blend ratio (0.0-1.0) where 1.0 is completely base color - * @return Blended color component value - */ - private int blend(int baseValue, int targetValue, float ratio) { - return Math.max(0, Math.min(255, Math.round(baseValue * ratio + targetValue * (1 - ratio)))); - } - - /** - * Starts or restarts the tooltip refresh timer. - * This timer periodically triggers tooltip updates while hovering over a table cell. - * The implementation ensures tooltips remain visible and are refreshed with live data. - */ - private void startTooltipRefreshTimer() { - // Stop existing timer if running - if (tooltipRefreshTimer != null && tooltipRefreshTimer.isRunning()) { - tooltipRefreshTimer.stop(); - } - - // Create new timer if needed - if (tooltipRefreshTimer == null) { - tooltipRefreshTimer = new Timer(TOOLTIP_REFRESH_INTERVAL, e -> { - if (hoverRow >= 0 && hoverColumn >= 0 && hoverRow < rowToPluginMap.size()) { - try { - // Get current plugin entry for tooltip refresh - PluginScheduleEntry currentEntry = rowToPluginMap.get(hoverRow); - - // Force tooltip to hide and then show again with fresh content - // This two-step approach ensures the tooltip content is refreshed - //ToolTipManager.sharedInstance().mouseMoved( - // new MouseEvent(scheduleTable, MouseEvent.MOUSE_EXITED, - // System.currentTimeMillis(), 0, - // hoverLocation.x, hoverLocation.y, - // 0, false)); - - // Small delay to allow the tooltip to hide before showing again - SwingUtilities.invokeLater(() -> { - // Now show fresh tooltip - //ToolTipManager.sharedInstance().mouseMoved( - // new MouseEvent(scheduleTable, MouseEvent.MOUSE_MOVED, - // System.currentTimeMillis(), 0, - // hoverLocation.x, hoverLocation.y, - // 0, false)); - - // Also trigger a cell repaint to ensure tooltip data is current - scheduleTable.repaint(scheduleTable.getCellRect(hoverRow, hoverColumn, false)); - }); - } catch (IndexOutOfBoundsException | NullPointerException ex) { - // Safety check for race conditions when table data changes while hovering - // Just skip this refresh cycle - log.debug("Skipped tooltip refresh due to data change"); - } - } - }); - tooltipRefreshTimer.setRepeats(true); - } - - // Start the timer - tooltipRefreshTimer.start(); - } - - /** - * Handles tooltip refresh when the table data changes - * This is called from refreshTable to update tooltips with fresh data - */ - private void updateTooltipsAfterRefresh(List newRowMap) { - // Handle tooltip update if a tooltip is currently showing - if (hoverRow >= 0 && hoverColumn >= 0 && tooltipRefreshTimer != null && tooltipRefreshTimer.isRunning()) { - // If the row we were hovering over still exists - if (hoverRow < newRowMap.size()) { - // Force tooltip refresh with updated data - SwingUtilities.invokeLater(() -> { - // Re-trigger tooltip on the updated data - ToolTipManager.sharedInstance().mouseMoved( - new MouseEvent(scheduleTable, MouseEvent.MOUSE_MOVED, - System.currentTimeMillis(), 0, - hoverLocation.x, hoverLocation.y, - 0, false)); - }); - } else { - // Row is gone, reset tooltip tracking - hoverRow = -1; - hoverColumn = -1; - tooltipRefreshTimer.stop(); - } - } - } - - /** - * Cleans up timer resources when the panel is no longer used - */ - @Override - public void removeNotify() { - // Stop and clean up timer when component is removed from UI - if (tooltipRefreshTimer != null && tooltipRefreshTimer.isRunning()) { - tooltipRefreshTimer.stop(); - tooltipRefreshTimer = null; - } - super.removeNotify(); - } - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerInfoPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerInfoPanel.java deleted file mode 100644 index cfc59f61755..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerInfoPanel.java +++ /dev/null @@ -1,1494 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui; - -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerPlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerState; -import net.runelite.client.plugins.microbot.pluginscheduler.api.SchedulablePlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.ui.PrePostScheduleTasksInfoPanel; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.util.UIUtils; -import net.runelite.client.plugins.microbot.util.antiban.enums.Activity; -import net.runelite.client.plugins.microbot.util.antiban.enums.ActivityIntensity; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; -import net.runelite.client.plugins.microbot.util.events.PluginPauseEvent; -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import java.util.List; -import java.util.stream.Collectors; -import lombok.extern.slf4j.Slf4j; - -import java.awt.*; -import java.time.Duration; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; - - -/** - * Displays real-time information about the scheduler status and plugins - */ -@Slf4j -public class SchedulerInfoPanel extends JPanel { - private final SchedulerPlugin plugin; - // Scheduler status components - private final JLabel statusLabel; - private final JLabel runtimeLabel; - private final JLabel currentPluginInStatusLabel; // New field for current plugin in status section - private ZonedDateTime schedulerStartTime; - - // Control buttons - private final JButton runSchedulerButton; - private final JButton stopSchedulerButton; - private final JButton loginButton; - private final JButton pauseResumePluginButton; // button for only pusing the currently running plugin (PluginScheduleEntry), by SchedulerPlugin - private final JButton pauseResumeSchedulerButton; // button for pausing the whole scheduler -> all condition progress is paused for all PluginScheduleEntry currently running managed by SchedulerPlugin by SchedulerPlugin - // Added hard reset button to reset all user condition states for all scheduled plugins-> initial settings are applied again to all start and stop conditions for the curre - private final JButton hardResetButton; - - // Combined plugin information panel - private final JPanel pluginInfoPanel; - - // Current plugin components - private JLabel currentPluginNameLabel; - private JLabel currentPluginRuntimeLabel; - private JProgressBar stopConditionProgressBar; - private JLabel stopConditionStatusLabel; - private ZonedDateTime currentPluginStartTime; - - // Next plugin components - private JLabel nextUpComingPluginNameLabel; - private JLabel nextUpComingPluginTimeLabel; - private JLabel nextUpComingPluginScheduleLabel; - - // Previous plugin components - private JLabel prevPluginNameLabel; - private JLabel prevPluginDurationLabel; - private JTextArea prevPluginStatusLabel; - private JLabel prevPluginStopTimeLabel; - - // Player status components - private final JPanel playerStatusPanel; - private final JLabel activityLabel; - private final JLabel activityIntensityLabel; - private final JLabel idleTimeLabel; - private final JLabel loginTimeLabel; - private final JLabel breakStatusLabel; - private final JLabel nextBreakLabel; - private final JLabel breakDurationLabel; - - // Pre/Post Schedule Tasks components - private final PrePostScheduleTasksInfoPanel prePostTasksInfoPanel; - - // State tracking for optimized updates - private PluginScheduleEntry lastTrackedCurrentPlugin; - private PluginScheduleEntry lastTrackedPreviousPlugin; - private PluginScheduleEntry lastTrackedNextUpComingPlugin; - - - public SchedulerInfoPanel(SchedulerPlugin plugin) { - this.plugin = plugin; - // Use a box layout instead of BorderLayout - setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); - setBorder(new EmptyBorder(4, 4, 4, 4)); // Reduced padding from 8,8,8,8 for tighter layout - setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Add panels with some vertical spacing - JPanel statusPanel = UIUtils.createInfoPanel("Scheduler Status"); - GridBagConstraints gbc = UIUtils.createGbc(0, 0); - - statusPanel.add(new JLabel("Status:"), gbc); - gbc.gridx++; - statusLabel = UIUtils.createValueLabel("Not Running"); - statusPanel.add(statusLabel, gbc); - - gbc.gridx = 0; - gbc.gridy++; - statusPanel.add(new JLabel("Runtime:"), gbc); - gbc.gridx++; - runtimeLabel = UIUtils.createValueLabel("00:00:00"); - statusPanel.add(runtimeLabel, gbc); - - // Add current plugin field to status panel - gbc.gridx = 0; - gbc.gridy++; - statusPanel.add(new JLabel("Current Plugin:"), gbc); - gbc.gridx++; - currentPluginInStatusLabel = UIUtils.createValueLabel("None"); - statusPanel.add(currentPluginInStatusLabel, gbc); - - // Create control buttons panel - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - // Use GridLayout with 3 rows instead of 2x2 to properly fit all 5 buttons - JPanel buttonPanel = new JPanel(new GridLayout(3, 2, 5, 5)); - buttonPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Create run scheduler button - runSchedulerButton = createCompactButton("Run Scheduler", new Color(76, 175, 80)); - runSchedulerButton.addActionListener(e -> { - plugin.startScheduler(); - updateButtonStates(); - }); - buttonPanel.add(runSchedulerButton); - - // Create stop scheduler button - stopSchedulerButton = createCompactButton("Stop Scheduler", new Color(244, 67, 54)); - stopSchedulerButton.addActionListener(e -> { - SwingUtilities.invokeLater(() -> { - plugin.stopScheduler(); - updateButtonStates(); - }); - }); - buttonPanel.add(stopSchedulerButton); - - // Create login button - loginButton = createCompactButton("Login", new Color(33, 150, 243)); // Blue - loginButton.addActionListener(e -> { - SwingUtilities.invokeLater(() -> { - plugin.toggleManualLogin(); - }); - }); - buttonPanel.add(loginButton); - - // Create pause/resume button - pauseResumePluginButton = createCompactButton("Pause Plugin", new Color(0, 188, 212)); // Cyan color - pauseResumePluginButton.setVisible(false); // Initially hidden - pauseResumePluginButton.addActionListener(e -> { - // Toggle the pause state - - - // Update button text and color based on state - if (!plugin.isCurrentPluginPaused()) { - boolean pauseSuccess = plugin.pauseRunningPlugin(); - if (pauseSuccess){ - pauseResumePluginButton.setText("Resume Plugin"); - pauseResumePluginButton.setBackground(new Color(76, 175, 80)); // Green color - } - } else { - if(plugin.isCurrentPluginPaused()){ - plugin.resumeRunningPlugin(); - pauseResumePluginButton.setText("Pause Plugin"); - pauseResumePluginButton.setBackground(new Color(0, 188, 212)); // Cyan color - } - } - // updateCurrentPluginInfo(); // Commented out - moved to status section - updateButtonStates(); - }); - buttonPanel.add(pauseResumePluginButton); - - // Create pause/resume scheduler button - pauseResumeSchedulerButton = createCompactButton("Pause Scheduler", new Color(255, 152, 0)); // Orange color - pauseResumeSchedulerButton.addActionListener(e -> { - // Toggle the pause state using our new methods - if (plugin.isPaused() ) { - // Currently paused, so resume - plugin.resumeScheduler(); - pauseResumeSchedulerButton.setText("Pause Scheduler"); - pauseResumeSchedulerButton.setBackground(new Color(255, 152, 0)); // Orange color - }else if(plugin.isOnBreak() && (plugin.getCurrentState() == SchedulerState.BREAK) || - plugin.getCurrentState() == SchedulerState.PLAYSCHEDULE_BREAK){ - // If currently on break, resume the break - plugin.resumeBreak(); - }else { - // Currently running, so pause - plugin.pauseScheduler(); - pauseResumeSchedulerButton.setText("Resume Scheduler"); - pauseResumeSchedulerButton.setBackground(new Color(76, 175, 80)); // Green color - } - - // Update UI - // updateCurrentPluginInfo(); // Commented out - moved to status section - updateButtonStates(); - }); - buttonPanel.add(pauseResumeSchedulerButton); - - // Create hard reset button - hardResetButton = createCompactButton("Hard Reset", new Color(156, 39, 176)); // Purple color - hardResetButton.setToolTipText("Reset all user condition states for all scheduled plugins"); - hardResetButton.addActionListener(e -> showHardResetConfirmation()); - buttonPanel.add(hardResetButton); - - statusPanel.add(buttonPanel, gbc); - - add(statusPanel); - add(Box.createRigidArea(new Dimension(0, 10))); // Add spacing - - // Create the player status panel - playerStatusPanel = UIUtils.createInfoPanel("Player Status"); - gbc = UIUtils.createGbc(0, 0); - - playerStatusPanel.add(new JLabel("Activity:"), gbc); - gbc.gridx++; - activityLabel = UIUtils.createValueLabel("None"); - playerStatusPanel.add(activityLabel, gbc); - - gbc.gridx = 0; - gbc.gridy++; - playerStatusPanel.add(new JLabel("Intensity:"), gbc); - gbc.gridx++; - activityIntensityLabel = UIUtils.createValueLabel("None"); - playerStatusPanel.add(activityIntensityLabel, gbc); - - gbc.gridx = 0; - gbc.gridy++; - playerStatusPanel.add(new JLabel("Idle Time:"), gbc); - gbc.gridx++; - idleTimeLabel = UIUtils.createValueLabel("0 ticks"); - playerStatusPanel.add(idleTimeLabel, gbc); - - gbc.gridx = 0; - gbc.gridy++; - playerStatusPanel.add(new JLabel("Login Duration:"), gbc); - gbc.gridx++; - loginTimeLabel = UIUtils.createValueLabel("Not logged in"); - playerStatusPanel.add(loginTimeLabel, gbc); - - gbc.gridx = 0; - gbc.gridy++; - playerStatusPanel.add(new JLabel("Break Status:"), gbc); - gbc.gridx++; - breakStatusLabel = UIUtils.createValueLabel("Not on break"); - playerStatusPanel.add(breakStatusLabel, gbc); - - gbc.gridx = 0; - gbc.gridy++; - playerStatusPanel.add(new JLabel("Next Break:"), gbc); - gbc.gridx++; - nextBreakLabel = UIUtils.createValueLabel("--:--:--"); - playerStatusPanel.add(nextBreakLabel, gbc); - - gbc.gridx = 0; - gbc.gridy++; - playerStatusPanel.add(new JLabel("Current Break:"), gbc); - gbc.gridx++; - breakDurationLabel = UIUtils.createValueLabel("00:00:00"); - playerStatusPanel.add(breakDurationLabel, gbc); - - add(playerStatusPanel, BorderLayout.CENTER); - - // Create compact plugin information panel - pluginInfoPanel = createDynamicPluginInfoPanel(); - add(pluginInfoPanel); - - // Add spacing before pre/post schedule tasks panel - add(Box.createRigidArea(new Dimension(0, 5))); - - // Create pre/post schedule tasks info panel - prePostTasksInfoPanel = new PrePostScheduleTasksInfoPanel(); - add(prePostTasksInfoPanel); - - // Add spacing after pre/post schedule tasks panel for better layout - add(Box.createRigidArea(new Dimension(0, 5))); - - // Initial refresh - refresh(); - } - - /** - * Creates a dynamic, responsive plugin info panel that adapts to content and window size - * Now shows only previous and next plugin information (current plugin moved to status section) - */ - private JPanel createDynamicPluginInfoPanel() { - // Create sections using utility methods - removed current section - JPanel prevSection = UIUtils.createAdaptiveSection("Previous"); - JPanel nextSection = UIUtils.createAdaptiveSection("Next"); - - // Add content to sections using utility methods - addPreviousPluginContentWithUtils(prevSection); - addNextPluginContentWithUtils(nextSection); - - // Create bottom panel for progress and stop reason - JPanel bottomPanel = createDynamicBottomPanelWithUtils(); - - // Create the main panel using utility - only previous and next sections - JPanel[] sections = {prevSection, nextSection}; - return UIUtils.createDynamicInfoPanel("Previous & Next Plugins", sections, bottomPanel); - } - - /** - * Adds content to the previous plugin section using utility methods - */ - private void addPreviousPluginContentWithUtils(JPanel section) { - prevPluginNameLabel = UIUtils.createAdaptiveValueLabel("None"); - prevPluginDurationLabel = UIUtils.createAdaptiveValueLabel("00:00:00"); - prevPluginStopTimeLabel = UIUtils.createAdaptiveValueLabel("--:--:--"); - - UIUtils.LabelValuePair[] rows = { - new UIUtils.LabelValuePair("Name:", prevPluginNameLabel), - new UIUtils.LabelValuePair("Duration:", prevPluginDurationLabel), - new UIUtils.LabelValuePair("Stop Time:", prevPluginStopTimeLabel) - }; - - UIUtils.addContentToSection(section, rows); - } - - /** - * Adds content to the current plugin section using utility methods - * Currently commented out since current plugin info moved to status section - */ - /* - private void addCurrentPluginContentWithUtils(JPanel section) { - currentPluginNameLabel = UIUtils.createAdaptiveValueLabel("None"); - currentPluginRuntimeLabel = UIUtils.createAdaptiveValueLabel("00:00:00"); - stopConditionStatusLabel = UIUtils.createAdaptiveValueLabel("None"); - stopConditionStatusLabel.setToolTipText("Detailed stop condition information"); - - UIUtils.LabelValuePair[] rows = { - new UIUtils.LabelValuePair("Name:", currentPluginNameLabel), - new UIUtils.LabelValuePair("Runtime:", currentPluginRuntimeLabel), - new UIUtils.LabelValuePair("Conditions:", stopConditionStatusLabel) - }; - - UIUtils.addContentToSection(section, rows); - } - */ - - /** - * Adds content to the next plugin section using utility methods - */ - private void addNextPluginContentWithUtils(JPanel section) { - nextUpComingPluginNameLabel = UIUtils.createAdaptiveValueLabel("None"); - nextUpComingPluginTimeLabel = UIUtils.createAdaptiveValueLabel("--:--"); - nextUpComingPluginScheduleLabel = UIUtils.createAdaptiveValueLabel("None"); - - UIUtils.LabelValuePair[] rows = { - new UIUtils.LabelValuePair("Name:", nextUpComingPluginNameLabel), - new UIUtils.LabelValuePair("Time:", nextUpComingPluginTimeLabel), - new UIUtils.LabelValuePair("Schedule:", nextUpComingPluginScheduleLabel) - }; - - UIUtils.addContentToSection(section, rows); - } - - /** - * Creates the dynamic bottom panel using utility methods - */ - private JPanel createDynamicBottomPanelWithUtils() { - // Create progress bar - stopConditionProgressBar = new JProgressBar(0, 100); - stopConditionProgressBar.setStringPainted(true); - stopConditionProgressBar.setString("No conditions"); - stopConditionProgressBar.setForeground(new Color(76, 175, 80)); - stopConditionProgressBar.setBackground(ColorScheme.DARKER_GRAY_COLOR); - stopConditionProgressBar.setPreferredSize(new Dimension(0, 16)); - - // Create status text area - prevPluginStatusLabel = UIUtils.createAdaptiveTextArea("None"); - - return UIUtils.createDynamicBottomPanel(stopConditionProgressBar, prevPluginStatusLabel); - } - - /** - * Helper method to create and style a compact button - * @param text Button text - * @param bgColor Background color - * @return Styled JButton - */ - private JButton createCompactButton(String text, Color bgColor) { - JButton button = new JButton(text); - button.setBackground(bgColor); - button.setForeground(Color.WHITE); - button.setFocusPainted(false); - // Make buttons more compact - button.setFont(button.getFont().deriveFont(11f)); // Smaller font - button.setMargin(new Insets(2, 4, 2, 4)); // Smaller margins - return button; - } - - /** - * Helper method to create a text area for multi-line text display - * @param text Initial text for the text area - * @return A configured JTextArea - */ - private JTextArea createMultiLineTextArea(String text) { - JTextArea textArea = new JTextArea(text); - textArea.setLineWrap(true); - textArea.setWrapStyleWord(true); - textArea.setOpaque(false); - textArea.setEditable(false); - textArea.setFocusable(false); - textArea.setBackground(ColorScheme.DARKER_GRAY_COLOR); - textArea.setForeground(Color.WHITE); - textArea.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); - textArea.setFont(FontManager.getRunescapeFont()); - return textArea; - } - - /** - * Refreshes all displayed information with selective updates based on plugin state changes - */ - public void refresh() { - // Always update scheduler status and buttons for real-time feedback - updateSchedulerStatus(); - updateButtonStates(); - - // Always update player status as it changes frequently - updatePlayerStatusInfo(); - - // Get current plugin states - PluginScheduleEntry currentPlugin = plugin.getCurrentPlugin(); - PluginScheduleEntry previousPlugin = plugin.getLastPlugin(); - PluginScheduleEntry nextUpComingPlugin = plugin.getUpComingPlugin(); - - // Update current plugin info if it changed or is running (for runtime updates) - // Note: Current plugin display moved to status section, keeping runtime-only updates - if (currentPlugin != lastTrackedCurrentPlugin) { - // updateCurrentPluginInfo(); // Commented out - moved to status section - lastTrackedCurrentPlugin = currentPlugin; - } else if (currentPlugin != null && currentPlugin.isRunning()) { - // Always update runtime for running plugins even if plugin object hasn't changed - updateCurrentPluginRuntimeOnly(); - } - - // Update previous plugin info only if it changed - if (previousPlugin != lastTrackedPreviousPlugin) { - updatePreviousPluginInfo(); - lastTrackedPreviousPlugin = previousPlugin; - } - - // Update next plugin info if it changed - if (nextUpComingPlugin != lastTrackedNextUpComingPlugin) { - updateNextUpComingPluginInfo(); - lastTrackedNextUpComingPlugin = nextUpComingPlugin; - } else if (nextUpComingPlugin != null) { - // Always update time display for next plugin since countdown changes every second - updateNextUpComingPluginTimeDisplay(nextUpComingPlugin); - } - - // Update pre/post schedule tasks info for current plugin - updatePrePostTasksInfo(); - } - - /** - * Forces an immediate update of all plugin information. - * Useful when plugin states change and immediate UI refresh is needed. - */ - public void forcePluginInfoUpdate() { - lastTrackedCurrentPlugin = null; - lastTrackedPreviousPlugin = null; - lastTrackedNextUpComingPlugin = null; - // updateCurrentPluginInfo(); // Commented out - moved to status section - updatePreviousPluginInfo(); - updateNextUpComingPluginInfo(); - } - - /** - * Updates the button states based on scheduler state - */ - private void updateButtonStates() { - SchedulerState state = plugin.getCurrentState(); - boolean isActive = plugin.getCurrentState().isSchedulerActive(); - - // Only enable run button if we're in READY or HOLD state - runSchedulerButton.setEnabled((!isActive && (state == SchedulerState.READY || state == SchedulerState.HOLD)) && !state.isPaused()); - - runSchedulerButton.setToolTipText( - !runSchedulerButton.isEnabled() ? - "Scheduler cannot be started in " + state.getDisplayName() + " state" : - "Start running the scheduler"); - - // Only enable stop button if scheduler is active - stopSchedulerButton.setEnabled(isActive); - stopSchedulerButton.setToolTipText( - isActive ? "Stop the scheduler" : "Scheduler is not running"); - - // login/logout button logic - only allow in scheduling/waiting states or manual login active - boolean isInManualLoginState = state == SchedulerState.MANUAL_LOGIN_ACTIVE; - boolean isInSchedulingState = state == SchedulerState.SCHEDULING || state == SchedulerState.WAITING_FOR_SCHEDULE; - boolean canUseManualLogin = isInManualLoginState || isInSchedulingState || - state == SchedulerState.BREAK || state == SchedulerState.PLAYSCHEDULE_BREAK; - - loginButton.setEnabled(canUseManualLogin && state != SchedulerState.WAITING_FOR_LOGIN && state != SchedulerState.LOGIN); - - // update button text and tooltip based on current state - String buttonText; - String loginTooltip; - - if (isInManualLoginState) { - buttonText = "Logout"; - loginTooltip = "logout and resume automatic break handling"; - } else if (Microbot.isLoggedIn()) { - buttonText = "Logout"; - loginTooltip = "logout manually (will switch to manual login mode)"; - } else { - buttonText = "Login"; - if (plugin.isOnBreak()) { - loginTooltip = "login manually (will interrupt break and pause break handling)"; - } else { - loginTooltip = "login manually (will pause automatic break handling)"; - } - } - - loginButton.setText(buttonText); - loginButton.setToolTipText(loginTooltip); - - - - // Update pause/resume scheduler button state - pauseResumeSchedulerButton.setEnabled(isActive || plugin.getCurrentState()== SchedulerState.SCHEDULER_PAUSED || plugin.getCurrentState() == SchedulerState.BREAK); - - boolean isSchedulerPaused = plugin.isPaused(); - - if (isSchedulerPaused || plugin.isOnBreak()) { - if (plugin.isOnBreak()){ - pauseResumeSchedulerButton.setText("Resume Break"); - pauseResumeSchedulerButton.setBackground(new Color(76, 175, 80)); // Green color - pauseResumeSchedulerButton.setToolTipText("Resume the break"); - pauseResumePluginButton.setEnabled(false); // Disable plugin pause/resume while scheduler is paused - }else{ - pauseResumeSchedulerButton.setText("Resume Scheduler"); - pauseResumeSchedulerButton.setBackground(new Color(76, 175, 80)); // Green color - pauseResumeSchedulerButton.setToolTipText("Resume the paused scheduler"); - pauseResumePluginButton.setEnabled(false); // Disable plugin pause/resume while scheduler is paused - } - } else { - pauseResumeSchedulerButton.setText("Pause Scheduler"); - pauseResumeSchedulerButton.setBackground(new Color(255, 152, 0)); // Orange color - pauseResumeSchedulerButton.setToolTipText("Pause the scheduler without stopping it"); - pauseResumePluginButton.setEnabled(true); // Enable plugin pause/resume while scheduler is running - } - if ( state.isBreaking()){ - pauseResumeSchedulerButton.setEnabled(false); - } - boolean currentRunningPluginPaused = plugin.isCurrentPluginPaused(); - - // Only show the pause button when a plugin is actively running - pauseResumePluginButton.setVisible(state == SchedulerState.RUNNING_PLUGIN || - state == SchedulerState.RUNNING_PLUGIN_PAUSED); - - // If Scheulder PLugin is not Running any Plugin at the moment -> detect changed state and we're no longer running, ensure pause for scripts and plugin is reset - - if (state == SchedulerState.RUNNING_PLUGIN && !(state == SchedulerState.RUNNING_PLUGIN_PAUSED || - state == SchedulerState.SCHEDULER_PAUSED) && PluginPauseEvent.isPaused()) { - PluginPauseEvent.setPaused(false); - pauseResumePluginButton.setText("Pause Plugin"); - pauseResumePluginButton.setBackground(new Color(255, 152, 0)); - } - if(state == SchedulerState.RUNNING_PLUGIN || - state == SchedulerState.RUNNING_PLUGIN_PAUSED){ - pauseResumePluginButton.setEnabled(true); - // Update pause/resume plugin button state - if (currentRunningPluginPaused) { - pauseResumePluginButton.setText("Resume Plugin"); - pauseResumePluginButton.setBackground(new Color(76, 175, 80)); // Green color - pauseResumePluginButton.setToolTipText("Resume the currently paused plugin"); - pauseResumeSchedulerButton.setEnabled(false); // Disable scheduler pause/resume while a plugin is paused - } else { - pauseResumePluginButton.setText("Pause Plugin"); - pauseResumePluginButton.setBackground(new Color(0, 188, 212)); // Cyan color - pauseResumePluginButton.setToolTipText("Pause the currently running plugin"); - pauseResumeSchedulerButton.setEnabled(true); // Disable scheduler pause/resume while a plugin is paused - } - }else { - // If the scheduler is paused, disable the pause/resume plugin button - pauseResumePluginButton.setEnabled(false); - pauseResumePluginButton.setToolTipText("Cannot pause/resume plugin when scheduler is not running"); - } - - // Hard reset button is always enabled if there are plugins scheduled - boolean hasScheduledPlugins = !plugin.getScheduledPlugins().isEmpty(); - hardResetButton.setEnabled(hasScheduledPlugins); - hardResetButton.setToolTipText( - hasScheduledPlugins ? - "Hard reset all user condition states for scheduled plugins" : - "No plugins scheduled to reset"); - } - - /** - * Updates the scheduler status information - */ - private void updateSchedulerStatus() { - SchedulerState state = plugin.getCurrentState(); - // Update state information if available - String stateInfo = state.getStateInformation(); - if (stateInfo != null && !stateInfo.isEmpty()) { - statusLabel.setToolTipText(stateInfo); - } else { - statusLabel.setToolTipText(null); - } - // Update state display - statusLabel.setText(state.getDisplayName()); - statusLabel.setForeground(state.getColor()); - - // Update current plugin in status section - updateCurrentPluginInStatusSection(state); - - // Update runtime if active - if (plugin.getCurrentState().isSchedulerActive()) { - if (schedulerStartTime == null) { - schedulerStartTime = ZonedDateTime.now(); - } - - Duration runtime = Duration.between(schedulerStartTime, ZonedDateTime.now()); - long totalSeconds = runtime.getSeconds(); - long hours = totalSeconds / 3600; - long minutes = (totalSeconds % 3600) / 60; - long seconds = totalSeconds % 60; - - runtimeLabel.setText(String.format("%02d:%02d:%02d", hours, minutes, seconds)); - } else { - schedulerStartTime = null; - runtimeLabel.setText("00:00:00"); - } - } - - /** - * Updates the current plugin display in the status section - */ - private void updateCurrentPluginInStatusSection(SchedulerState state) { - PluginScheduleEntry currentPlugin = plugin.getCurrentPlugin(); - - // Only show current plugin name when in specific states - if (currentPlugin != null && (state.isPluginRunning() || state.isStopping() || state.isPaused())) { - String displayName = currentPlugin.getCleanName(); - - // Add pause indicator if plugin is paused - if (currentPlugin.isPaused()) { - displayName += " (PAUSED)"; - currentPluginInStatusLabel.setForeground(new Color(255, 152, 0)); // Orange - } else { - currentPluginInStatusLabel.setForeground(Color.WHITE); - } - - currentPluginInStatusLabel.setText(displayName); - } else { - currentPluginInStatusLabel.setText("None"); - currentPluginInStatusLabel.setForeground(Color.LIGHT_GRAY); - } - } - - /** - * Updates information about the currently running plugin - * NOTE: This method is commented out because current plugin display moved to status section - */ - /* - private void updateCurrentPluginInfo() { - PluginScheduleEntry currentPlugin = plugin.getCurrentPlugin(); - - if (currentPlugin != null && currentPlugin.isRunning()) { - // Update name with pause indicator if needed - String displayName = currentPlugin.getCleanName(); - String pauseTooltip = null; - - if (PluginPauseEvent.isPaused()) { - displayName += " [PAUSED]"; - currentPluginNameLabel.setForeground(new Color(255, 152, 0)); // Orange - - // Create detailed pause tooltip - pauseTooltip = createPauseTooltipForCurrentPlugin(currentPlugin); - } else { - currentPluginNameLabel.setForeground(Color.WHITE); - - // Check if any other plugins are paused and create tooltip - pauseTooltip = createPauseTooltipForAllPlugins(); - } - - currentPluginNameLabel.setText(displayName); - currentPluginNameLabel.setToolTipText(pauseTooltip); - - // Update runtime - if (currentPluginStartTime == null) { - currentPluginStartTime = ZonedDateTime.now(); - } - - Duration runtime = Duration.between(currentPluginStartTime, ZonedDateTime.now()); - long totalSeconds = runtime.getSeconds(); - long hours = totalSeconds / 3600; - long minutes = (totalSeconds % 3600) / 60; - long seconds = totalSeconds % 60; - - currentPluginRuntimeLabel.setText(String.format("%02d:%02d:%02d", hours, minutes, seconds)); - - // Update stop condition status - if (currentPlugin.hasAnyStopConditions()) { - int total = currentPlugin.getTotalStopConditionCount(); - int satisfied = currentPlugin.getSatisfiedStopConditionCount(); - - // Updates the stop condition status text - stopConditionStatusLabel.setText(String.format("%d/%d conditions met", satisfied, total)); - stopConditionStatusLabel.setToolTipText(currentPlugin.getDetailedStopConditionsStatus()); - - // Update progress bar - double progress = currentPlugin.getStopConditionProgress(); - stopConditionProgressBar.setValue((int) progress); - stopConditionProgressBar.setString(String.format("%.1f%%", progress)); - - // Color the progress bar based on progress - if (progress > 80) { - stopConditionProgressBar.setForeground(new Color(76, 175, 80)); // Green - } else if (progress > 50) { - stopConditionProgressBar.setForeground(new Color(255, 193, 7)); // Amber - } else { - stopConditionProgressBar.setForeground(new Color(33, 150, 243)); // Blue - } - - stopConditionProgressBar.setVisible(true); - } else { - stopConditionStatusLabel.setText("None"); - stopConditionStatusLabel.setToolTipText("No stop conditions defined"); - stopConditionProgressBar.setVisible(false); - } - } else { - // No current plugin - check for paused plugins and show in tooltip - String noneText = "None"; - String noneTooltip = null; - - if (plugin.anyPluginEntryPaused()) { - List pausedPlugins = plugin.getScheduledPlugins().stream() - .filter(PluginScheduleEntry::isPaused) - .collect(Collectors.toList()); - - if (!pausedPlugins.isEmpty()) { - noneText = "None (" + pausedPlugins.size() + " paused)"; - noneTooltip = createPauseTooltipForAllPlugins(); - } - } - - // Reset all fields with pause information - currentPluginNameLabel.setText(noneText); - currentPluginNameLabel.setToolTipText(noneTooltip); - currentPluginNameLabel.setForeground(plugin.anyPluginEntryPaused() ? new Color(255, 152, 0) : Color.WHITE); - currentPluginRuntimeLabel.setText("00:00:00"); - stopConditionStatusLabel.setText("None"); - stopConditionProgressBar.setValue(0); - stopConditionProgressBar.setString("No conditions"); - currentPluginStartTime = null; - } - } - */ - - /** - * Updates information about the next scheduled plugin - */ - private void updateNextUpComingPluginInfo() { - PluginScheduleEntry nextUpComingPlugin = plugin.getUpComingPlugin(); - - if (nextUpComingPlugin != null) { - // Update name - nextUpComingPluginNameLabel.setText(nextUpComingPlugin.getCleanName()); - - // Set the next run time display (already handles various condition types) - nextUpComingPluginTimeLabel.setText(nextUpComingPlugin.getNextRunDisplay()); - - // Create an enhanced schedule description - StringBuilder scheduleDesc = new StringBuilder(nextUpComingPlugin.getIntervalDisplay()); - - // Add information about one-time schedules - if (nextUpComingPlugin.hasAnyOneTimeStartConditions()) { - if (nextUpComingPlugin.hasTriggeredOneTimeStartConditions() && !nextUpComingPlugin.canStartTriggerAgain()) { - scheduleDesc.append(" (Completed)"); - } else { - scheduleDesc.append(" (One-time)"); - } - } - - nextUpComingPluginScheduleLabel.setText(scheduleDesc.toString()); - } else { - // Reset all fields - nextUpComingPluginNameLabel.setText("None"); - nextUpComingPluginTimeLabel.setText("--:--"); - nextUpComingPluginScheduleLabel.setText("None"); - } - } - - /** - * Updates information about the player's status - */ - private void updatePlayerStatusInfo() { - // Update activity info - Activity activity = plugin.getCurrentActivity(); - if (activity != null) { - activityLabel.setText(activity.toString()); - activityLabel.setForeground(Color.WHITE); - } else { - activityLabel.setText("None"); - activityLabel.setForeground(Color.GRAY); - } - - // Update activity intensity - ActivityIntensity intensity = plugin.getCurrentIntensity(); - if (intensity != null) { - activityIntensityLabel.setText(intensity.getName()); - activityIntensityLabel.setForeground(Color.WHITE); - } else { - activityIntensityLabel.setText("None"); - activityIntensityLabel.setForeground(Color.GRAY); - } - - // Update idle time - int idleTime = plugin.getIdleTime(); - if (idleTime >= 0) { - idleTimeLabel.setText(idleTime + " ticks"); - // Change color based on idle time - if (idleTime > 100) { - idleTimeLabel.setForeground(new Color(255, 106, 0)); // Orange for long idle - } else if (idleTime > 50) { - idleTimeLabel.setForeground(new Color(255, 193, 7)); // Yellow for medium idle - } else { - idleTimeLabel.setForeground(Color.WHITE); // Normal color - } - } else { - idleTimeLabel.setText("0 ticks"); - idleTimeLabel.setForeground(Color.WHITE); - } - - // Update login duration - Duration loginDuration = Microbot.getLoginTime(); - if (loginDuration.getSeconds() > 0 && Microbot.isLoggedIn()) { - long hours = loginDuration.toHours(); - long minutes = (loginDuration.toMinutes() % 60); - long seconds = (loginDuration.getSeconds() % 60); - loginTimeLabel.setText(String.format("%02d:%02d:%02d", hours, minutes, seconds)); - loginTimeLabel.setForeground(Color.WHITE); - } else { - loginTimeLabel.setText("Not logged in"); - loginTimeLabel.setForeground(Color.GRAY); - } - - // Update break status - boolean onBreak = plugin.isOnBreak(); - if (onBreak) { - breakStatusLabel.setText("On Break"); - breakStatusLabel.setForeground(new Color(255, 193, 7)); // Amber color - } else { - breakStatusLabel.setText("Not on break"); - breakStatusLabel.setForeground(Color.WHITE); - } - - // Update next break time - Duration timeUntilBreak = plugin.getTimeUntilNextBreak(); - if (timeUntilBreak.getSeconds() > 0) { - long hours = timeUntilBreak.toHours(); - long minutes = (timeUntilBreak.toMinutes() % 60); - long seconds = (timeUntilBreak.getSeconds() % 60); - nextBreakLabel.setText(String.format("%02d:%02d:%02d", hours, minutes, seconds)); - nextBreakLabel.setForeground(Color.WHITE); - } else { - nextBreakLabel.setText("--:--:--"); - nextBreakLabel.setForeground(Color.GRAY); - } - - // Update current break duration - Duration breakDuration = plugin.getCurrentBreakDuration(); - if (breakDuration.getSeconds() > 0) { - long hours = breakDuration.toHours(); - long minutes = (breakDuration.toMinutes() % 60); - long seconds = (breakDuration.getSeconds() % 60); - breakDurationLabel.setText(String.format("%02d:%02d:%02d", hours, minutes, seconds)); - breakDurationLabel.setForeground(onBreak ? new Color(255, 193, 7) : Color.WHITE); // Amber if on break - } else { - breakDurationLabel.setText("00:00:00"); - breakDurationLabel.setForeground(Color.GRAY); - } - } - - /** - * Updates information about the previously run plugin - */ - private void updatePreviousPluginInfo() { - PluginScheduleEntry lastPlugin = plugin.getLastPlugin(); - - if (lastPlugin != null) { - // Update name - prevPluginNameLabel.setText(lastPlugin.getCleanName()); - - // Update duration if available - if (lastPlugin.getLastRunDuration() != null && !lastPlugin.getLastRunDuration().isZero()) { - Duration duration = lastPlugin.getLastRunDuration(); - long hours = duration.toHours(); - long minutes = (duration.toMinutes() % 60); - long seconds = (duration.getSeconds() % 60); - prevPluginDurationLabel.setText(String.format("%02d:%02d:%02d", hours, minutes, seconds)); - } else { - prevPluginDurationLabel.setText("Unknown"); - } - - // Update stop reason - String stopReason = lastPlugin.getLastStopReason(); - PluginScheduleEntry.StopReason stopReasonType = lastPlugin.getLastStopReasonType(); - - if (stopReason != null && !stopReason.isEmpty()) { - // Define colors for different states - Color successColor = new Color(76, 175, 80); // Green for success - Color unsuccessfulColor = new Color(255, 152, 0); // Orange for unsuccessful - Color errorColor = new Color(244, 67, 54); // Red for error - Color defaultColor = Color.WHITE; // Default color - - // Determine message color based on stop reason type and success state - Color messageColor = defaultColor; - - if (stopReasonType != null) { - switch (stopReasonType) { - case PLUGIN_FINISHED: - messageColor = lastPlugin.isLastRunSuccessful() ? successColor : unsuccessfulColor; - break; - case ERROR: - messageColor = errorColor; - break; - case INTERRUPTED: - messageColor = unsuccessfulColor; - break; - default: - messageColor = defaultColor; - break; - } - } - - // Set the text and color for the status text area - prevPluginStatusLabel.setText(stopReason); - prevPluginStatusLabel.setForeground(messageColor); - } else { - prevPluginStatusLabel.setText("Unknown"); - prevPluginStatusLabel.setForeground(Color.WHITE); - } - - // Update stop time - if (lastPlugin.getLastRunEndTime() != null) { - ZonedDateTime stopTime = lastPlugin.getLastRunEndTime(); - prevPluginStopTimeLabel.setText(stopTime.format(DateTimeFormatter.ofPattern("HH:mm:ss"))); - } else { - prevPluginStopTimeLabel.setText("Unknown"); - } - } else { - // Reset all fields - prevPluginNameLabel.setText("None"); - prevPluginDurationLabel.setText("00:00:00"); - prevPluginStatusLabel.setText("N/A"); - prevPluginStatusLabel.setForeground(Color.WHITE); - prevPluginStopTimeLabel.setText("--:--:--"); - } - } - - /** - * Shows a confirmation dialog for hard resetting all user conditions - */ - private void showHardResetConfirmation() { - String message = - "" + - "

Hard Reset All User Conditions

" + - "

This will perform a complete reset of all user conditions for all scheduled plugins.

" + - "

This will reset:

" + - "
    " + - "
  • All accumulated state tracking variables
  • " + - "
  • Maximum trigger counters
  • " + - "
  • Daily/periodic usage limits
  • " + - "
  • Historical tracking data
  • " + - "
  • Time-based condition states
  • " + - "
" + - "

Are you sure you want to continue?

" + - ""; - - int result = JOptionPane.showConfirmDialog( - SwingUtilities.getWindowAncestor(this), - message, - "Hard Reset Confirmation", - JOptionPane.YES_NO_OPTION, - JOptionPane.WARNING_MESSAGE - ); - - if (result == JOptionPane.YES_OPTION) { - hardResetAllUserConditions(); - } - } - - /** - * Performs a hard reset on all user conditions for all scheduled plugins - */ - private void hardResetAllUserConditions() { - try { - // Delegate the hard reset operation to the SchedulerPlugin - List resetPlugins = plugin.hardResetAllUserConditions(); - - // Show success message with details - String resultMessage = String.format( - "" + - "

Hard Reset Complete

" + - "

Successfully reset %d user condition states.

", - resetPlugins.size()); - - if (!resetPlugins.isEmpty()) { - resultMessage += "

Reset conditions for:

    "; - for (String pluginName : resetPlugins) { - resultMessage += "
  • " + pluginName + "
  • "; - } - resultMessage += "
"; - } - - resultMessage += ""; - - JOptionPane.showMessageDialog( - SwingUtilities.getWindowAncestor(this), - resultMessage, - "Hard Reset Complete", - JOptionPane.INFORMATION_MESSAGE - ); - - log.info("Hard reset completed for {} user condition states", resetPlugins.size()); - - } catch (Exception e) { - // Show error message - JOptionPane.showMessageDialog( - SwingUtilities.getWindowAncestor(this), - "An error occurred while resetting user conditions: " + e.getMessage(), - "Hard Reset Error", - JOptionPane.ERROR_MESSAGE - ); - - log.error("Error during hard reset of user conditions", e); - } - } - - /** - * Updates only the time display for the next plugin without full refresh. - * This is used for regular time updates when the plugin hasn't changed. - * - * @param nextUpComingPlugin The next scheduled plugin - */ - private void updateNextUpComingPluginTimeDisplay(PluginScheduleEntry nextUpComingPlugin) { - if (nextUpComingPlugin != null) { - // Only update the time display, keep other fields unchanged - nextUpComingPluginTimeLabel.setText(nextUpComingPlugin.getNextRunDisplay()); - } - } - - /** - * Updates only the runtime display for the current plugin without refreshing other info. - * This is used for regular runtime updates when the plugin hasn't changed. - */ - private void updateCurrentPluginRuntimeOnly() { - PluginScheduleEntry currentPlugin = plugin.getCurrentPlugin(); - - if (currentPlugin != null && currentPlugin.isRunning()) { - // Update runtime - if (currentPluginStartTime != null) { - Duration runtime = Duration.between(currentPluginStartTime, ZonedDateTime.now()); - long totalSeconds = runtime.getSeconds(); - long hours = totalSeconds / 3600; - long minutes = (totalSeconds % 3600) / 60; - long seconds = totalSeconds % 60; - - currentPluginRuntimeLabel.setText(String.format("%02d:%02d:%02d", hours, minutes, seconds)); - } - - // Update stop condition progress (in case conditions have progressed) - if (currentPlugin.hasAnyStopConditions()) { - double progress = currentPlugin.getStopConditionProgress(); - stopConditionProgressBar.setValue((int) progress); - stopConditionProgressBar.setString(String.format("%.1f%%", progress)); - - // Color the progress bar based on progress - if (progress > 80) { - stopConditionProgressBar.setForeground(new Color(76, 175, 80)); // Green - } else if (progress > 50) { - stopConditionProgressBar.setForeground(new Color(255, 193, 7)); // Amber - } else { - stopConditionProgressBar.setForeground(new Color(33, 150, 243)); // Blue - } - } - } - } - - /** - * Creates an alternative tabbed layout for extremely small spaces - * This method provides a fallback when the regular layout doesn't fit - * @deprecated This method is no longer used. Use UIUtils methods instead. - */ - @Deprecated - @SuppressWarnings("unused") - private JPanel createTabbedPluginInfoPanelDeprecated() { - JPanel wrapperPanel = new JPanel(new BorderLayout()); - wrapperPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(1, 1, 1, 1, ColorScheme.DARK_GRAY_COLOR), - BorderFactory.createEmptyBorder(2, 2, 2, 2) - ), - "Plugin Information", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - wrapperPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Create tabbed pane for very small spaces - JTabbedPane tabbedPane = new JTabbedPane(); - tabbedPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - tabbedPane.setForeground(Color.WHITE); - tabbedPane.setFont(FontManager.getRunescapeSmallFont()); - - // Previous Plugin Tab - JPanel prevTab = new JPanel(new GridBagLayout()); - prevTab.setBackground(ColorScheme.DARKER_GRAY_COLOR); - GridBagConstraints gbc = UIUtils.createGbc(0, 0); - - prevTab.add(new JLabel("Name:"), gbc); - gbc.gridx++; - prevPluginNameLabel = UIUtils.createCompactValueLabel("None"); - prevTab.add(prevPluginNameLabel, gbc); - - gbc.gridx = 0; gbc.gridy++; - prevTab.add(new JLabel("Duration:"), gbc); - gbc.gridx++; - prevPluginDurationLabel = UIUtils.createCompactValueLabel("00:00:00"); - prevTab.add(prevPluginDurationLabel, gbc); - - gbc.gridx = 0; gbc.gridy++; - prevTab.add(new JLabel("Stop Time:"), gbc); - gbc.gridx++; - prevPluginStopTimeLabel = UIUtils.createCompactValueLabel("--:--:--"); - prevTab.add(prevPluginStopTimeLabel, gbc); - - gbc.gridx = 0; gbc.gridy++; - gbc.gridwidth = 2; - prevPluginStatusLabel = createMultiLineTextArea("None"); - prevPluginStatusLabel.setPreferredSize(new Dimension(0, 40)); - prevTab.add(prevPluginStatusLabel, gbc); - - // Current Plugin Tab - JPanel currentTab = new JPanel(new GridBagLayout()); - currentTab.setBackground(ColorScheme.DARKER_GRAY_COLOR); - gbc = UIUtils.createGbc(0, 0); - - currentTab.add(new JLabel("Name:"), gbc); - gbc.gridx++; - currentPluginNameLabel = UIUtils.createCompactValueLabel("None"); - currentTab.add(currentPluginNameLabel, gbc); - - gbc.gridx = 0; gbc.gridy++; - currentTab.add(new JLabel("Runtime:"), gbc); - gbc.gridx++; - currentPluginRuntimeLabel = UIUtils.createCompactValueLabel("00:00:00"); - currentTab.add(currentPluginRuntimeLabel, gbc); - - gbc.gridx = 0; gbc.gridy++; - currentTab.add(new JLabel("Conditions:"), gbc); - gbc.gridx++; - stopConditionStatusLabel = UIUtils.createCompactValueLabel("None"); - currentTab.add(stopConditionStatusLabel, gbc); - - gbc.gridx = 0; gbc.gridy++; - gbc.gridwidth = 2; - stopConditionProgressBar = new JProgressBar(0, 100); - stopConditionProgressBar.setStringPainted(true); - stopConditionProgressBar.setString("No conditions"); - stopConditionProgressBar.setForeground(new Color(76, 175, 80)); - stopConditionProgressBar.setBackground(ColorScheme.DARKER_GRAY_COLOR); - stopConditionProgressBar.setPreferredSize(new Dimension(0, 8)); - currentTab.add(stopConditionProgressBar, gbc); - - // Next Plugin Tab - JPanel nextTab = new JPanel(new GridBagLayout()); - nextTab.setBackground(ColorScheme.DARKER_GRAY_COLOR); - gbc = UIUtils.createGbc(0, 0); - - nextTab.add(new JLabel("Name:"), gbc); - gbc.gridx++; - nextUpComingPluginNameLabel = UIUtils.createCompactValueLabel("None"); - nextTab.add(nextUpComingPluginNameLabel, gbc); - - gbc.gridx = 0; gbc.gridy++; - nextTab.add(new JLabel("Time:"), gbc); - gbc.gridx++; - nextUpComingPluginTimeLabel = UIUtils.createCompactValueLabel("--:--"); - nextTab.add(nextUpComingPluginTimeLabel, gbc); - - gbc.gridx = 0; gbc.gridy++; - nextTab.add(new JLabel("Type:"), gbc); - gbc.gridx++; - nextUpComingPluginScheduleLabel = UIUtils.createCompactValueLabel("None"); - nextTab.add(nextUpComingPluginScheduleLabel, gbc); - - tabbedPane.addTab("Prev", prevTab); - tabbedPane.addTab("Current", currentTab); - tabbedPane.addTab("Next", nextTab); - - wrapperPanel.add(tabbedPane, BorderLayout.CENTER); - - return wrapperPanel; - } - - /** - * Creates a structured plugin info panel using BoxLayout for better vertical control - * This is an alternative to the FlowLayout approach if height issues persist - * @deprecated This method is no longer used. Use UIUtils methods instead. - */ - @Deprecated - @SuppressWarnings("unused") - private JPanel createStructuredPluginInfoPanelDeprecated() { - JPanel wrapperPanel = new JPanel(new BorderLayout()); - wrapperPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(1, 1, 1, 1, ColorScheme.DARK_GRAY_COLOR), - BorderFactory.createEmptyBorder(5, 5, 5, 5) - ), - "Plugin Information", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - wrapperPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Create main content panel with BoxLayout for better vertical control - JPanel contentPanel = new JPanel(); - contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); - contentPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Create sections panel using a more structured approach - JPanel sectionsPanel = new JPanel(new BorderLayout()); - sectionsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Create individual section panels with fixed heights - JPanel prevSection = createStructuredSection("Previous"); - JPanel currentSection = createStructuredSection("Current"); - JPanel nextSection = createStructuredSection("Next"); - - // Add content to sections - addPreviousPluginContent(prevSection); - addCurrentPluginContent(currentSection); - addNextPluginContent(nextSection); - - // Use a horizontal layout with equal weights - sectionsPanel.add(prevSection, BorderLayout.WEST); - sectionsPanel.add(currentSection, BorderLayout.CENTER); - sectionsPanel.add(nextSection, BorderLayout.EAST); - - contentPanel.add(sectionsPanel); - contentPanel.add(Box.createRigidArea(new Dimension(0, 5))); - - // Add progress panel - JPanel progressPanel = createProgressPanel(); - contentPanel.add(progressPanel); - contentPanel.add(Box.createRigidArea(new Dimension(0, 5))); - - // Add stop reason panel - JPanel stopReasonPanel = createStopReasonPanel(); - contentPanel.add(stopReasonPanel); - - // Wrap in scroll pane - JScrollPane scrollPane = new JScrollPane(contentPanel); - scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); - scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); - scrollPane.setBorder(null); - scrollPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - scrollPane.getViewport().setBackground(ColorScheme.DARKER_GRAY_COLOR); - - wrapperPanel.add(scrollPane, BorderLayout.CENTER); - return wrapperPanel; - } - - /** - * Creates a structured section with fixed height and proper spacing - */ - private JPanel createStructuredSection(String title) { - JPanel panel = new JPanel(); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - panel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(ColorScheme.DARK_GRAY_COLOR), - BorderFactory.createEmptyBorder(8, 6, 8, 6) - ), - title, - TitledBorder.CENTER, - TitledBorder.TOP, - FontManager.getRunescapeSmallFont(), - Color.LIGHT_GRAY - )); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - panel.setPreferredSize(new Dimension(140, 180)); // Fixed height - panel.setMinimumSize(new Dimension(120, 160)); - panel.setMaximumSize(new Dimension(160, 200)); - return panel; - } - - /** - * Adds content to the previous plugin section - */ - private void addPreviousPluginContent(JPanel section) { - section.add(UIUtils.createLabelValueRow("Name:", prevPluginNameLabel = UIUtils.createCompactValueLabel("None"))); - section.add(Box.createRigidArea(new Dimension(0, 5))); - section.add(UIUtils.createLabelValueRow("Duration:", prevPluginDurationLabel = UIUtils.createCompactValueLabel("00:00:00"))); - section.add(Box.createRigidArea(new Dimension(0, 5))); - section.add(UIUtils.createLabelValueRow("Stop Time:", prevPluginStopTimeLabel = UIUtils.createCompactValueLabel("--:--:--"))); - section.add(Box.createVerticalGlue()); - } - - /** - * Adds content to the current plugin section - */ - private void addCurrentPluginContent(JPanel section) { - section.add(UIUtils.createLabelValueRow("Name:", currentPluginNameLabel = UIUtils.createCompactValueLabel("None"))); - section.add(Box.createRigidArea(new Dimension(0, 5))); - section.add(UIUtils.createLabelValueRow("Runtime:", currentPluginRuntimeLabel = UIUtils.createCompactValueLabel("00:00:00"))); - section.add(Box.createRigidArea(new Dimension(0, 5))); - section.add(UIUtils.createLabelValueRow("Conditions:", stopConditionStatusLabel = UIUtils.createCompactValueLabel("None"))); - section.add(Box.createVerticalGlue()); - } - - /** - * Adds content to the next plugin section - */ - private void addNextPluginContent(JPanel section) { - section.add(UIUtils.createLabelValueRow("Name:", nextUpComingPluginNameLabel = UIUtils.createCompactValueLabel("None"))); - section.add(Box.createRigidArea(new Dimension(0, 5))); - section.add(UIUtils.createLabelValueRow("Time:", nextUpComingPluginTimeLabel = UIUtils.createCompactValueLabel("--:--"))); - section.add(Box.createRigidArea(new Dimension(0, 5))); - section.add(UIUtils.createLabelValueRow("Type:", nextUpComingPluginScheduleLabel = UIUtils.createCompactValueLabel("None"))); - section.add(Box.createVerticalGlue()); - } - - /** - * Creates the progress panel - */ - private JPanel createProgressPanel() { - JPanel panel = new JPanel(new BorderLayout()); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - panel.setBorder(new EmptyBorder(2, 0, 2, 0)); - panel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 20)); - - JLabel label = new JLabel("Progress:"); - label.setForeground(Color.WHITE); - label.setFont(FontManager.getRunescapeSmallFont()); - - stopConditionProgressBar = new JProgressBar(0, 100); - stopConditionProgressBar.setStringPainted(true); - stopConditionProgressBar.setString("No conditions"); - stopConditionProgressBar.setForeground(new Color(76, 175, 80)); - stopConditionProgressBar.setBackground(ColorScheme.DARKER_GRAY_COLOR); - stopConditionProgressBar.setPreferredSize(new Dimension(0, 12)); - - panel.add(label, BorderLayout.WEST); - panel.add(stopConditionProgressBar, BorderLayout.CENTER); - - return panel; - } - - /** - * Creates the stop reason panel - */ - private JPanel createStopReasonPanel() { - JPanel panel = new JPanel(new BorderLayout()); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - panel.setBorder(new EmptyBorder(2, 0, 0, 0)); - panel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 50)); - - JLabel label = new JLabel("Stop Reason:"); - label.setForeground(Color.WHITE); - label.setFont(FontManager.getRunescapeSmallFont()); - - prevPluginStatusLabel = createMultiLineTextArea("None"); - prevPluginStatusLabel.setPreferredSize(new Dimension(0, 40)); - - panel.add(label, BorderLayout.WEST); - panel.add(prevPluginStatusLabel, BorderLayout.CENTER); - - return panel; - } - - /** - * Creates a detailed tooltip for the current plugin pause status - */ - private String createPauseTooltipForCurrentPlugin(PluginScheduleEntry currentPlugin) { - StringBuilder tooltip = new StringBuilder(""); - tooltip.append("Current Plugin Paused:
"); - tooltip.append("Name: ").append(currentPlugin.getName()).append("
"); - tooltip.append("Priority: ").append(currentPlugin.getPriority()).append("
"); - tooltip.append("Enabled: ").append(currentPlugin.isEnabled() ? "Yes" : "No").append("
"); - tooltip.append("Running: ").append(currentPlugin.isRunning() ? "Yes" : "No").append("
"); - - if (currentPlugin.getLastRunStartTime() != null) { - Duration runtime = Duration.between(currentPlugin.getLastRunStartTime(), ZonedDateTime.now()); - tooltip.append("Runtime: ").append(formatDurationForTooltip(runtime)).append("
"); - } - - // Check for other paused plugins - List pausedPlugins = plugin.getScheduledPlugins().stream() - .filter(p -> p.isPaused() && !p.equals(currentPlugin)) - .collect(Collectors.toList()); - - if (!pausedPlugins.isEmpty()) { - tooltip.append("
Other Paused Plugins:
"); - for (PluginScheduleEntry pausedPlugin : pausedPlugins) { - tooltip.append("â€Ē ").append(pausedPlugin.getName()) - .append(" (Priority: ").append(pausedPlugin.getPriority()).append(")
"); - } - } - - tooltip.append(""); - return tooltip.toString(); - } - - /** - * Creates a tooltip showing all paused plugins when no plugin is currently running paused - */ - private String createPauseTooltipForAllPlugins() { - List pausedPlugins = plugin.getScheduledPlugins().stream() - .filter(PluginScheduleEntry::isPaused) - .collect(Collectors.toList()); - - if (pausedPlugins.isEmpty()) { - return null; // No tooltip needed - } - - StringBuilder tooltip = new StringBuilder(""); - - if (plugin.isPaused()) { - tooltip.append("Scheduler Paused
"); - } - - if (plugin.anyPluginEntryPaused()) { - tooltip.append("Paused Plugins (").append(pausedPlugins.size()).append("):
"); - for (PluginScheduleEntry pausedPlugin : pausedPlugins) { - tooltip.append("â€Ē ").append(pausedPlugin.getName()) - .append(" (Priority: ").append(pausedPlugin.getPriority()) - .append(", Enabled: ").append(pausedPlugin.isEnabled() ? "Yes" : "No") - .append(")
"); - } - } - - tooltip.append(""); - return tooltip.toString(); - } - - /** - * Formats duration for tooltip display - */ - private String formatDurationForTooltip(Duration duration) { - if (duration.isZero() || duration.isNegative()) { - return "00:00:00"; - } - - long hours = duration.toHours(); - long minutes = duration.toMinutesPart(); - long seconds = duration.toSecondsPart(); - - return String.format("%02d:%02d:%02d", hours, minutes, seconds); - } - - /** - * Updates the pre/post schedule tasks information panel - */ - private void updatePrePostTasksInfo() { - PluginScheduleEntry currentPlugin = plugin.getCurrentPlugin(); - - if (currentPlugin != null) { - // Get the schedulable plugin interface - net.runelite.client.plugins.Plugin pluginInstance = currentPlugin.getPlugin(); - - if (pluginInstance instanceof SchedulablePlugin) { - SchedulablePlugin schedulablePlugin = (SchedulablePlugin) pluginInstance; - - // Update the pre/post tasks panel with the current plugin - prePostTasksInfoPanel.updatePlugin(schedulablePlugin); - } else { - // Plugin doesn't implement SchedulablePlugin, clear the panel - prePostTasksInfoPanel.clear(); - } - } else { - // No current plugin, clear the panel - prePostTasksInfoPanel.clear(); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerPanel.java deleted file mode 100644 index 1c7992021d7..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerPanel.java +++ /dev/null @@ -1,792 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui; -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerPlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerState; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.util.events.PluginPauseEvent; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; -import net.runelite.client.ui.PluginPanel; - -import javax.swing.*; -import javax.swing.border.CompoundBorder; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.time.Duration; -import java.time.ZonedDateTime; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -public class SchedulerPanel extends PluginPanel { - private final SchedulerPlugin plugin; - - // Current plugin section - private final JLabel currentPluginLabel; - private final JLabel runtimeLabel; - - // Previous plugin section - private final JLabel prevPluginNameLabel; - private final JLabel prevPluginDurationLabel; - private final JLabel prevPluginStopReasonLabel; - private final JLabel prevPluginStopTimeLabel; - - // Next plugin section - private final JLabel nextUpComingPluginNameLabel; - private final JLabel nextUpComingPluginTimeLabel; - private final JLabel nextUpComingPluginScheduleLabel; - - // Scheduler status section - private final JLabel schedulerStatusLabel; - // Control buttons - private final JButton configButton; - private final JButton runButton; - private final JButton stopButton; - private final JButton pauseSchedulerButton; - private final JButton pauseResumePluginButton; - private final JButton antibanButton; - - // State tracking for optimized updates - private PluginScheduleEntry lastTrackedCurrentPlugin; - private PluginScheduleEntry lastTrackedNextUpComingPlugin; - private SchedulerState lastTrackedState; - - - public SchedulerPanel(SchedulerPlugin plugin) { - super(false); - this.plugin = plugin; - - setBorder(new EmptyBorder(8, 8, 8, 8)); - setBackground(ColorScheme.DARK_GRAY_COLOR); - setLayout(new BorderLayout()); - - // Create main panel - JPanel mainPanel = new JPanel(); - mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); - mainPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); - - // Current plugin info panel - JPanel infoPanel = createInfoPanel("Current Plugin"); - - JLabel pluginLabel = new JLabel("Plugin:"); - pluginLabel.setForeground(Color.WHITE); - pluginLabel.setFont(FontManager.getRunescapeFont()); - infoPanel.add(pluginLabel, createGbc(0, 0)); - - currentPluginLabel = createValueLabel("None"); - infoPanel.add(currentPluginLabel, createGbc(1, 0)); - - JLabel runtimeTitleLabel = new JLabel("Runtime:"); - runtimeTitleLabel.setForeground(Color.WHITE); - runtimeTitleLabel.setFont(FontManager.getRunescapeFont()); - infoPanel.add(runtimeTitleLabel, createGbc(0, 1)); - - runtimeLabel = createValueLabel("00:00:00"); - infoPanel.add(runtimeLabel, createGbc(1, 1)); - - // Previous plugin info panel - JPanel prevPluginPanel = createInfoPanel("Previous Plugin"); - JLabel prevPluginTitleLabel = new JLabel("Plugin:"); - prevPluginTitleLabel.setForeground(Color.WHITE); - prevPluginTitleLabel.setFont(FontManager.getRunescapeFont()); - prevPluginPanel.add(prevPluginTitleLabel, createGbc(0, 0)); - - prevPluginNameLabel = createValueLabel("None"); - prevPluginPanel.add(prevPluginNameLabel, createGbc(1, 0)); - - JLabel prevDurationLabel = new JLabel("Duration:"); - prevDurationLabel.setForeground(Color.WHITE); - prevDurationLabel.setFont(FontManager.getRunescapeFont()); - prevPluginPanel.add(prevDurationLabel, createGbc(0, 1)); - - prevPluginDurationLabel = createValueLabel("00:00:00"); - prevPluginPanel.add(prevPluginDurationLabel, createGbc(1, 1)); - - JLabel prevStopReasonLabel = new JLabel("Stop Reason:"); - prevStopReasonLabel.setForeground(Color.WHITE); - prevStopReasonLabel.setFont(FontManager.getRunescapeFont()); - prevPluginPanel.add(prevStopReasonLabel, createGbc(0, 2)); - - prevPluginStopReasonLabel = createValueLabel("None"); - prevPluginPanel.add(prevPluginStopReasonLabel, createGbc(1, 2)); - - JLabel prevStopTimeLabel = new JLabel("Stop Time:"); - prevStopTimeLabel.setForeground(Color.WHITE); - prevStopTimeLabel.setFont(FontManager.getRunescapeFont()); - prevPluginPanel.add(prevStopTimeLabel, createGbc(0, 3)); - - prevPluginStopTimeLabel = createValueLabel("--:--"); - prevPluginPanel.add(prevPluginStopTimeLabel, createGbc(1, 3)); - - // Next plugin info panel - JPanel nextUpComingPluginPanel = createInfoPanel("Next Scheduled Plugin"); - JLabel nextUpComingPluginTitleLabel = new JLabel("Plugin:"); - nextUpComingPluginTitleLabel.setForeground(Color.WHITE); - nextUpComingPluginTitleLabel.setFont(FontManager.getRunescapeFont()); - nextUpComingPluginPanel.add(nextUpComingPluginTitleLabel, createGbc(0, 0)); - - nextUpComingPluginNameLabel = createValueLabel("None"); - nextUpComingPluginPanel.add(nextUpComingPluginNameLabel, createGbc(1, 0)); - - JLabel nextRunLabel = new JLabel("Next Run:"); - nextRunLabel.setForeground(Color.WHITE); - nextRunLabel.setFont(FontManager.getRunescapeFont()); - nextUpComingPluginPanel.add(nextRunLabel, createGbc(0, 1)); - - nextUpComingPluginTimeLabel = createValueLabel("--:--"); - nextUpComingPluginPanel.add(nextUpComingPluginTimeLabel, createGbc(1, 1)); - - JLabel scheduleLabel = new JLabel("Schedule:"); - scheduleLabel.setForeground(Color.WHITE); - scheduleLabel.setFont(FontManager.getRunescapeFont()); - nextUpComingPluginPanel.add(scheduleLabel, createGbc(0, 2)); - - nextUpComingPluginScheduleLabel = createValueLabel("None"); - nextUpComingPluginPanel.add(nextUpComingPluginScheduleLabel, createGbc(1, 2)); - - // Scheduler status panel - JPanel statusPanel = createInfoPanel("Scheduler Status"); - JLabel statusLabel = new JLabel("Status:"); - statusLabel.setForeground(Color.WHITE); - statusLabel.setFont(FontManager.getRunescapeFont()); - statusPanel.add(statusLabel, createGbc(0, 0)); - - schedulerStatusLabel = createValueLabel("Inactive"); - schedulerStatusLabel.setForeground(Color.YELLOW); - statusPanel.add(schedulerStatusLabel, createGbc(1, 0)); - - // Button panel - vertical layout (one button per row) - JPanel buttonPanel = new JPanel(new GridLayout(6, 1, 0, 5)); // Changed to 6 rows for all buttons - buttonPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); - - // Add config button - JButton configButton = createButton("Open Scheduler"); - configButton.addActionListener(this::onOpenConfigButtonClicked); - this.configButton = configButton; - - // Control buttons - Color greenColor = new Color(76, 175, 80); - JButton runButton = createButton("Run Scheduler", greenColor); - runButton.addActionListener(e -> { - plugin.startScheduler(); - refresh(); - }); - this.runButton = runButton; - - Color redColor = new Color(244, 67, 54); - JButton stopButton = createButton("Stop Scheduler", redColor); - stopButton.addActionListener(e -> { - plugin.stopScheduler(); - refresh(); - }); - this.stopButton = stopButton; - - // Add Antiban button - uses a distinct purple color - Color purpleColor = new Color(156, 39, 176); - JButton antibanButton = createButton("Antiban Settings", purpleColor); - antibanButton.addActionListener(this::onAntibanButtonClicked); - antibanButton.setToolTipText("Open Antiban settings in a separate window"); - this.antibanButton = antibanButton; - - - // Add pause/resume button - uses orange color - Color orangeColor = new Color(255, 152, 0); - JButton pauseSchedulerButton = createButton("Pause Scheduler", orangeColor); - pauseSchedulerButton.addActionListener(e -> { - if (plugin.isPaused()) { - plugin.resumeScheduler(); - pauseSchedulerButton.setText("Pause Scheduler"); - pauseSchedulerButton.setBackground(orangeColor); - } else { - plugin.pauseScheduler(); - pauseSchedulerButton.setText("Resume Scheduler"); - pauseSchedulerButton.setBackground(greenColor); - } - refresh(); - }); - pauseSchedulerButton.setToolTipText("Pause or resume the scheduler without stopping it"); - this.pauseSchedulerButton = pauseSchedulerButton; - - // Add pause/resume button for the currently running plugin - use cyan color - Color cyanColor = new Color(0, 188, 212); // Material design cyan color - JButton pauseResumePluginButton = createButton("Pause Plugin", cyanColor); - pauseResumePluginButton.addActionListener(e -> { - // Toggle the pause state - boolean newPauseState = !PluginPauseEvent.isPaused(); - PluginPauseEvent.setPaused(newPauseState); - - // Update button text and color based on state - if (newPauseState) { - plugin.pauseRunningPlugin(); - pauseResumePluginButton.setText("Resume Plugin"); - pauseResumePluginButton.setBackground(greenColor); // Change to green for resume - } else { - plugin.resumeRunningPlugin(); - pauseResumePluginButton.setText("Pause Plugin"); - pauseResumePluginButton.setBackground(cyanColor); // Change back to cyan for pause - } - refresh(); - }); - pauseResumePluginButton.setToolTipText("Pause or resume the currently running plugin"); - this.pauseResumePluginButton = pauseResumePluginButton; - - buttonPanel.add(configButton); - buttonPanel.add(runButton); - buttonPanel.add(stopButton); - buttonPanel.add(pauseSchedulerButton); - buttonPanel.add(pauseResumePluginButton); - buttonPanel.add(antibanButton); - - - // Add components to main panel - mainPanel.add(infoPanel); - mainPanel.add(Box.createRigidArea(new Dimension(0, 8))); - mainPanel.add(prevPluginPanel); - mainPanel.add(Box.createRigidArea(new Dimension(0, 8))); - mainPanel.add(nextUpComingPluginPanel); - mainPanel.add(Box.createRigidArea(new Dimension(0, 8))); - mainPanel.add(statusPanel); - mainPanel.add(Box.createRigidArea(new Dimension(0, 8))); - mainPanel.add(buttonPanel); - mainPanel.add(Box.createRigidArea(new Dimension(0, 8))); - - // Wrap main panel in scroll pane for better fit in different sidebar sizes - JScrollPane scrollPane = new JScrollPane(mainPanel); - scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - scrollPane.setBorder(BorderFactory.createEmptyBorder()); - scrollPane.setBackground(ColorScheme.DARK_GRAY_COLOR); - scrollPane.getViewport().setBackground(ColorScheme.DARK_GRAY_COLOR); - - // Set the preferred width to maintain proper sidebar display - scrollPane.setPreferredSize(new Dimension(200, 400)); - - add(scrollPane, BorderLayout.CENTER); // Changed from NORTH to CENTER for proper filling - refresh(); - } - - private GridBagConstraints createGbc(int x, int y) { - GridBagConstraints gbc = new GridBagConstraints(); - gbc.gridx = x; - gbc.gridy = y; - gbc.gridwidth = 1; - gbc.gridheight = 1; - gbc.insets = new Insets(5, 5, 5, 5); - - gbc.anchor = (x == 0) ? GridBagConstraints.WEST : GridBagConstraints.EAST; - gbc.fill = (x == 0) ? GridBagConstraints.BOTH - : GridBagConstraints.HORIZONTAL; - - gbc.weightx = (x == 0) ? 0.1 : 1.0; - gbc.weighty = 1.0; - return gbc; - } - - private JPanel createInfoPanel(String title) { - JPanel panel = new JPanel(new GridBagLayout()); - - panel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(1, 1, 1, 1, ColorScheme.MEDIUM_GRAY_COLOR), - BorderFactory.createEmptyBorder(5, 5, 5, 5) - ), - title, - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - return panel; - } - - private JLabel createValueLabel(String text) { - JLabel label = new JLabel(text); - label.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - label.setFont(FontManager.getRunescapeFont()); - return label; - } - - private JButton createButton(String text) { - return createButton(text, ColorScheme.BRAND_ORANGE); - } - - private JButton createButton(String text, Color backgroundColor) { - JButton button = new JButton(text); - button.setFont(FontManager.getRunescapeSmallFont()); - button.setFocusPainted(false); - button.setForeground(Color.WHITE); - button.setBackground(backgroundColor); - button.setBorder(new CompoundBorder( - BorderFactory.createLineBorder(backgroundColor.darker(), 1), - BorderFactory.createEmptyBorder(5, 15, 5, 15) - )); - - // Add hover effect that maintains the button's color theme - button.addMouseListener(new java.awt.event.MouseAdapter() { - public void mouseEntered(java.awt.event.MouseEvent evt) { - button.setBackground(backgroundColor.brighter()); - } - - public void mouseExited(java.awt.event.MouseEvent evt) { - button.setBackground(backgroundColor); - } - }); - - return button; - } - - public void refresh() { - // Get current state information - SchedulerState currentState = plugin.getCurrentState(); - PluginScheduleEntry currentPlugin = plugin.getCurrentPlugin(); - PluginScheduleEntry nextUpComingPlugin = plugin.getUpComingPlugin(); - - // Update current plugin info if it changed - if (currentPlugin != lastTrackedCurrentPlugin) { - updatePluginInfo(); - lastTrackedCurrentPlugin = currentPlugin; - } else if (currentPlugin != null && currentPlugin.isRunning()) { - // Update only the runtime display without full refresh when plugin is running - updateCurrentPluginRuntimeOnly(); - } - - // Update next plugin info if it changed - if (nextUpComingPlugin != lastTrackedNextUpComingPlugin) { - updateNextPluginInfo(); - lastTrackedNextUpComingPlugin = nextUpComingPlugin; - } else if (nextUpComingPlugin != null && nextUpComingPlugin.isEnabled()) { - // Update only the time display without full refresh for real-time countdown - updateNextPluginTimeDisplayOnly(nextUpComingPlugin); - } - - // Update scheduler status and buttons if state changed - if (currentState != lastTrackedState) { - updateButtonStates(); - - // Update scheduler status with pause information - String statusText = currentState.getDisplayName(); - String statusTooltip = currentState.getDescription(); - - // Add pause indicator to status if any plugins are paused - if (plugin.anyPluginEntryPaused()) { - List pausedPlugins = plugin.getScheduledPlugins().stream() - .filter(PluginScheduleEntry::isPaused) - .collect(Collectors.toList()); - - if (!pausedPlugins.isEmpty()) { - statusText += " (" + pausedPlugins.size() + " paused)"; - statusTooltip = createAllPausedPluginsTooltip(); - } - } - - schedulerStatusLabel.setText(statusText); - schedulerStatusLabel.setForeground(currentState.getColor()); - schedulerStatusLabel.setToolTipText(statusTooltip); - - lastTrackedState = currentState; - } - } - - /** - * Updates button states based on plugin initialization status - */ - private void updateButtonStates() { - - - SchedulerState state = plugin.getCurrentState(); - boolean schedulerActive = plugin.getCurrentState().isSchedulerActive(); - boolean pluginRunning = plugin.getCurrentState().isPluginRunning(); - configButton.setEnabled(state != SchedulerState.UNINITIALIZED || state != SchedulerState.ERROR || state != SchedulerState.INITIALIZING); - - // Only enable run button if we're in READY or HOLD state - runButton.setEnabled(!schedulerActive && (state != SchedulerState.UNINITIALIZED || state != SchedulerState.ERROR || state != SchedulerState.INITIALIZING)); - - // Only enable stop button in certain states - stopButton.setEnabled(schedulerActive); - - // Update pause scheduler button state - pauseSchedulerButton.setEnabled(schedulerActive|| state == SchedulerState.SCHEDULER_PAUSED); - if (plugin.isPaused()) { - pauseSchedulerButton.setText("Resume Scheduler"); - pauseSchedulerButton.setBackground(new Color(76, 175, 80)); // Green color - pauseSchedulerButton.setToolTipText("Resume the paused scheduler"); - } else { - pauseSchedulerButton.setText("Pause Scheduler"); - pauseSchedulerButton.setBackground(new Color(255, 152, 0)); // Orange color - pauseSchedulerButton.setToolTipText("Pause the scheduler without stopping it"); - } - - // Update pause plugin button state - only visible and enabled when a plugin is running - boolean pluginCanBePaused = state == SchedulerState.RUNNING_PLUGIN || - state == SchedulerState.RUNNING_PLUGIN_PAUSED; - pauseResumePluginButton.setVisible(pluginCanBePaused); - pauseResumePluginButton.setEnabled(pluginCanBePaused); - - // Update button text and color based on plugin pause state - if (PluginPauseEvent.isPaused()) { - pauseResumePluginButton.setText("Resume Plugin"); - pauseResumePluginButton.setBackground(new Color(76, 175, 80)); // Green color - pauseResumePluginButton.setToolTipText("Resume the paused plugin"); - } else { - pauseResumePluginButton.setText("Pause Plugin"); - pauseResumePluginButton.setBackground(new Color(0, 188, 212)); // Cyan color - pauseResumePluginButton.setToolTipText("Pause the currently running plugin"); - } - - // Only enable antiban button when no plugin is running - antibanButton.setEnabled(!pluginRunning); - - - - // Add tooltips - if (state == SchedulerState.UNINITIALIZED || state == SchedulerState.ERROR || state == SchedulerState.INITIALIZING) { - configButton.setToolTipText("Plugin not initialized yet"); - runButton.setToolTipText("Plugin not initialized yet"); - stopButton.setToolTipText("Plugin not initialized yet"); - pauseSchedulerButton.setToolTipText("Plugin not initialized yet"); - } else { - configButton.setToolTipText("Open scheduler configuration"); - runButton.setToolTipText(!runButton.isEnabled() ? - "Cannot start scheduler in " + state.getDisplayName() + " state" : - "Start the scheduler"); - stopButton.setToolTipText(!stopButton.isEnabled() ? - "Cannot stop scheduler: not running" : - "Stop the scheduler"); - } - - - } - - void updatePluginInfo() { - PluginScheduleEntry currentPlugin = plugin.getCurrentPlugin(); - - if (currentPlugin != null) { - // Get start time for runtime calculation - ZonedDateTime startTimeZdt = currentPlugin.getLastRunStartTime(); - String pluginName = currentPlugin.getCleanName(); - - // Add pause indicator if current plugin is paused - if (currentPlugin.isRunning() && PluginPauseEvent.isPaused()) { - pluginName += " [PAUSED]"; - currentPluginLabel.setForeground(new Color(255, 152, 0)); // Orange - } else { - currentPluginLabel.setForeground(Color.WHITE); - } - - // Add the stop reason indicator to the plugin name if available - if (currentPlugin.getLastStopReasonType() != null && - currentPlugin.getLastStopReasonType() != PluginScheduleEntry.StopReason.NONE) { - String stopReason = formatStopReason(currentPlugin.getLastStopReasonType()); - pluginName += " (" + stopReason + ")"; - } - - currentPluginLabel.setText(pluginName); - - // Create and set tooltip with pause information - String pauseTooltip = createPauseTooltipForCurrentPlugin(currentPlugin); - currentPluginLabel.setToolTipText(pauseTooltip); - - // Show runtime - either current or last run duration - if (currentPlugin.isRunning()) { - // Calculate and display current runtime for active plugin - if (startTimeZdt != null) { - long startTimeMillis = startTimeZdt.toInstant().toEpochMilli(); - long runtimeMillis = System.currentTimeMillis() - startTimeMillis; - runtimeLabel.setText(formatDuration(runtimeMillis)); - } else { - runtimeLabel.setText("Running"); - } - } else if (currentPlugin.getLastRunDuration() != null && !currentPlugin.getLastRunDuration().isZero()) { - // Show the stored last run duration for completed plugins - runtimeLabel.setText(formatDuration(currentPlugin.getLastRunDuration().toMillis())); - } else { - runtimeLabel.setText("Not started"); - } - - // Update previous plugin information if it has run at least once - updatePreviousPluginInfo(currentPlugin); - } else { - // No current plugin - check for paused plugins and show in tooltip - String noneText = "None"; - String noneTooltip = null; - - if (plugin.anyPluginEntryPaused()) { - List pausedPlugins = plugin.getScheduledPlugins().stream() - .filter(PluginScheduleEntry::isPaused) - .collect(Collectors.toList()); - - if (!pausedPlugins.isEmpty()) { - noneText = "None (" + pausedPlugins.size() + " paused)"; - noneTooltip = createAllPausedPluginsTooltip(); - } - } - - currentPluginLabel.setText(noneText); - currentPluginLabel.setToolTipText(noneTooltip); - currentPluginLabel.setForeground(plugin.anyPluginEntryPaused() ? new Color(255, 152, 0) : Color.WHITE); - runtimeLabel.setText("00:00:00"); - - // Clear previous plugin info when there's no current plugin - prevPluginNameLabel.setText("None"); - prevPluginDurationLabel.setText("00:00:00"); - prevPluginStopReasonLabel.setText("None"); - prevPluginStopTimeLabel.setText("--:--"); - } - } - - /** - * Updates information about the previously run plugin - */ - private void updatePreviousPluginInfo(PluginScheduleEntry plugin) { - if (plugin == null) return; - - // Only show previous plugin info if the plugin has been run at least once - if (plugin.getLastRunEndTime() != null && plugin.getLastRunDuration() != null) { - // Set name - prevPluginNameLabel.setText(plugin.getCleanName()); - - // Set duration - long durationMillis = plugin.getLastRunDuration().toMillis(); - prevPluginDurationLabel.setText(formatDuration(durationMillis)); - - // Set stop reason - String stopReason = "None"; - if (plugin.getLastStopReasonType() != null && - plugin.getLastStopReasonType() != PluginScheduleEntry.StopReason.NONE) { - stopReason = formatStopReason(plugin.getLastStopReasonType()); - } - prevPluginStopReasonLabel.setText(stopReason); - - // Set stop time - ZonedDateTime endTime = plugin.getLastRunEndTime(); - if (endTime != null) { - prevPluginStopTimeLabel.setText( - endTime.format(PluginScheduleEntry.TIME_FORMATTER) - ); - } else { - prevPluginStopTimeLabel.setText("--:--"); - } - } - } - - /** - * Formats a duration in milliseconds as HH:MM:SS - */ - private String formatDuration(long durationMillis) { - long hours = TimeUnit.MILLISECONDS.toHours(durationMillis); - long minutes = TimeUnit.MILLISECONDS.toMinutes(durationMillis) % 60; - long seconds = TimeUnit.MILLISECONDS.toSeconds(durationMillis) % 60; - return String.format("%02d:%02d:%02d", hours, minutes, seconds); - } - - /** - * Returns a formatted stop reason - */ - private String formatStopReason(PluginScheduleEntry.StopReason stopReason) { - // Use the description from the enum if available - if (stopReason != null) { - switch (stopReason) { - case MANUAL_STOP: - return "Stopped"; - case PLUGIN_FINISHED: - return "Completed"; - case ERROR: - return "Error"; - case SCHEDULED_STOP: - return "Timed out"; - case INTERRUPTED: - return "Interrupted"; - case HARD_STOP: - return "Force stopped"; - default: - return stopReason.getDescription(); - } - } - return ""; - } - - void updateNextPluginInfo() { - PluginScheduleEntry nextUpComingPlugin = plugin.getUpComingPlugin(); - - if (nextUpComingPlugin != null) { - // Set the plugin name - nextUpComingPluginNameLabel.setText(nextUpComingPlugin.getCleanName()); - - // Set the next run time display (already handles various condition types) - nextUpComingPluginTimeLabel.setText(nextUpComingPlugin.getNextRunDisplay()); - - // Create an enhanced schedule description - StringBuilder scheduleDesc = new StringBuilder(nextUpComingPlugin.getIntervalDisplay()); - - // Add information about one-time schedules - if (nextUpComingPlugin.hasAnyOneTimeStartConditions()) { - if (nextUpComingPlugin.hasTriggeredOneTimeStartConditions() && !nextUpComingPlugin.canStartTriggerAgain()) { - scheduleDesc.append(" (Completed)"); - } else { - scheduleDesc.append(" (One-time)"); - } - } - - // Add condition status information if available - if (nextUpComingPlugin.hasAnyStartConditions()) { - int total = nextUpComingPlugin.getStartConditionManager().getConditions().size(); - long satisfied = nextUpComingPlugin.getStartConditionManager().getConditions().stream() - .filter(condition -> condition.isSatisfied()) - .count(); - - if (total > 1) { - scheduleDesc.append(String.format(" [%d/%d conditions met]", satisfied, total)); - } - } - - // Set the updated schedule description - nextUpComingPluginScheduleLabel.setText(scheduleDesc.toString()); - } else { - // No next plugin scheduled - nextUpComingPluginNameLabel.setText("None"); - nextUpComingPluginTimeLabel.setText("--:--"); - nextUpComingPluginScheduleLabel.setText("None"); - } - } - - /** - * Updates only the runtime display for the current plugin without refreshing other info. - * This is used for regular runtime updates when the plugin hasn't changed. - */ - private void updateCurrentPluginRuntimeOnly() { - PluginScheduleEntry currentPlugin = plugin.getCurrentPlugin(); - if (currentPlugin != null && currentPlugin.isRunning()) { - // Calculate and display current runtime for active plugin - ZonedDateTime startTimeZdt = currentPlugin.getLastRunStartTime(); - if (startTimeZdt != null) { - long startTimeMillis = startTimeZdt.toInstant().toEpochMilli(); - long runtimeMillis = System.currentTimeMillis() - startTimeMillis; - runtimeLabel.setText(formatDuration(runtimeMillis)); - } else { - runtimeLabel.setText("Running"); - } - } - } - - /** - * Updates only the time display for the next plugin without full refresh. - * This is used for regular time updates when the plugin hasn't changed. - * - * @param nextUpComingPlugin The next scheduled plugin - */ - private void updateNextPluginTimeDisplayOnly(PluginScheduleEntry nextUpComingPlugin) { - if (nextUpComingPlugin != null && nextUpComingPlugin.isEnabled()) { - // Update only the time display - this calls getCurrentStartTriggerTime() for real-time accuracy - nextUpComingPluginTimeLabel.setText(nextUpComingPlugin.getNextRunDisplay()); - } - } - - private void onOpenConfigButtonClicked(ActionEvent e) { - plugin.openSchedulerWindow(); - } - - private void onAntibanButtonClicked(ActionEvent e) { - plugin.openAntibanSettings(); - } - - /** - * Creates a tooltip showing pause status for the current plugin - * @param currentPlugin The current plugin (can be null) - * @return HTML formatted tooltip string - */ - private String createPauseTooltipForCurrentPlugin(PluginScheduleEntry currentPlugin) { - StringBuilder tooltip = new StringBuilder(""); - - if (currentPlugin == null) { - tooltip.append("No current plugin"); - tooltip.append(""); - return tooltip.toString(); - } - - // Current plugin info - tooltip.append("Current Plugin: ").append(currentPlugin.getName()).append("
"); - tooltip.append("Priority: ").append(currentPlugin.getPriority()).append("
"); - tooltip.append("Enabled: ").append(currentPlugin.isEnabled() ? "Yes" : "No").append("
"); - tooltip.append("Running: ").append(currentPlugin.isRunning() ? "Yes" : "No").append("
"); - - // Pause status - if (currentPlugin.isPaused()) { - tooltip.append("Status: PAUSED
"); - } else if (currentPlugin.isRunning()) { - tooltip.append("Status: RUNNING
"); - } else { - tooltip.append("Status: STOPPED
"); - } - - // Runtime info if available - if (currentPlugin.isRunning() && currentPlugin.getLastRunStartTime() != null) { - Duration runtime = Duration.between(currentPlugin.getLastRunStartTime(), ZonedDateTime.now()); - tooltip.append("Runtime: ").append(formatDurationForTooltip(runtime)).append("
"); - } - - // Add pause info for all plugins if any are paused - if (plugin.anyPluginEntryPaused()) { - tooltip.append("
Other Paused Plugins:
"); - List pausedPlugins = plugin.getScheduledPlugins().stream() - .filter(p -> p.isPaused() && !p.equals(currentPlugin)) - .collect(Collectors.toList()); - - if (pausedPlugins.isEmpty()) { - tooltip.append("None"); - } else { - for (PluginScheduleEntry pausedPlugin : pausedPlugins) { - tooltip.append("â€Ē ").append(pausedPlugin.getName()) - .append(" (Priority: ").append(pausedPlugin.getPriority()).append(")
"); - } - } - } - - tooltip.append(""); - return tooltip.toString(); - } - - /** - * Creates a tooltip showing all paused plugins - * @return HTML formatted tooltip string - */ - private String createAllPausedPluginsTooltip() { - StringBuilder tooltip = new StringBuilder("Paused Plugins:
"); - - List pausedPlugins = plugin.getScheduledPlugins().stream() - .filter(PluginScheduleEntry::isPaused) - .collect(Collectors.toList()); - - if (pausedPlugins.isEmpty()) { - tooltip.append("No plugins are currently paused"); - } else { - for (PluginScheduleEntry pausedPlugin : pausedPlugins) { - tooltip.append("â€Ē ").append(pausedPlugin.getName()).append("
"); - tooltip.append(" Priority: ").append(pausedPlugin.getPriority()).append("
"); - tooltip.append(" Enabled: ").append(pausedPlugin.isEnabled() ? "Yes" : "No").append("
"); - tooltip.append(" Running: ").append(pausedPlugin.isRunning() ? "Yes" : "No").append("
"); - - if (pausedPlugin.isRunning() && pausedPlugin.getLastRunStartTime() != null) { - Duration runtime = Duration.between(pausedPlugin.getLastRunStartTime(), ZonedDateTime.now()); - tooltip.append(" Runtime: ").append(formatDurationForTooltip(runtime)).append("
"); - } - tooltip.append("
"); - } - } - - tooltip.append(""); - return tooltip.toString(); - } - - /** - * Formats a duration for tooltip display - * @param duration The duration to format - * @return Formatted duration string (HH:MM:SS) - */ - private String formatDurationForTooltip(Duration duration) { - long hours = duration.toHours(); - long minutes = duration.toMinutesPart(); - long seconds = duration.toSecondsPart(); - return String.format("%02d:%02d:%02d", hours, minutes, seconds); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerWindow.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerWindow.java deleted file mode 100644 index 76ca99e5817..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerWindow.java +++ /dev/null @@ -1,999 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui; - -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerPlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerState; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LogicalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ui.ConditionConfigPanel; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ui.callback.ConditionUpdateCallback; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.PluginScheduleEntry.ScheduleFormPanel; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.PluginScheduleEntry.ScheduleTablePanel; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.util.SchedulerUIUtils; - -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.CompoundBorder; -import javax.swing.border.EmptyBorder; - -import java.awt.*; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.io.File; -import java.util.concurrent.CompletableFuture; - -import lombok.extern.slf4j.Slf4j; - -/** - * A graphical user interface for the Plugin Scheduler system. - *

- * This window provides a comprehensive interface for managing scheduled plugins including: - *

    - *
  • Viewing and managing the list of scheduled plugins
  • - *
  • Adding new plugins to the schedule
  • - *
  • Configuring plugin run parameters and stop conditions
  • - *
  • Monitoring scheduler status and currently running plugins
  • - *
- *

- * The UI is organized into tabbed sections: - *

    - *
  • Schedule Tab - Contains a table of scheduled plugins and a form for adding/editing entries
  • - *
  • Stop Conditions Tab - Allows configuration of complex stop conditions for plugins
  • - *
- *

- * An information panel on the right side displays real-time information about the scheduler state. - * - * @see SchedulerPlugin - * @see ScheduleTablePanel - * @see ScheduleFormPanel - * @see ConditionConfigPanel - * @see SchedulerInfoPanel - */ - -@Slf4j -public class SchedulerWindow extends JFrame implements ConditionUpdateCallback { - private final SchedulerPlugin plugin; - private JTabbedPane tabbedPane; - private final ScheduleTablePanel tablePanel; - private final ScheduleFormPanel formPanel; - private final ConditionConfigPanel stopConditionPanel; - private final ConditionConfigPanel startConditionPanel; - private final SchedulerInfoPanel infoPanel; - // Timer for refreshing the info panel - private Timer refreshTimer; - // Last used file for saving/loading conditions - private File lastSaveFile; - - public SchedulerWindow(SchedulerPlugin plugin) { - super("Plugin Scheduler"); - this.plugin = plugin; - - // Increase width to accommodate the info panel - setSize(1050, 600); - setLocationRelativeTo(null); - setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); - - // Create main components - tablePanel = new ScheduleTablePanel(plugin); - formPanel = new ScheduleFormPanel(plugin); - stopConditionPanel = new ConditionConfigPanel(true); - startConditionPanel = new ConditionConfigPanel(false); - infoPanel = new SchedulerInfoPanel(plugin); - - // Set up form panel actions - formPanel.setAddButtonAction(e -> onAddPlugin()); - formPanel.setUpdateButtonAction(e -> onUpdatePlugin()); - formPanel.setRemoveButtonAction(e -> onRemovePlugin()); - formPanel.setEditMode(false); - - // Set up form panel to clear table selection when ComboBox changes - formPanel.setSelectionChangeListener(() -> { - if (tablePanel.getSelectedPlugin() == null) { - return; - } - tablePanel.clearSelection(); - }); - - // Set up condition panels with the callback - stopConditionPanel.setConditionUpdateCallback(this); - startConditionPanel.setConditionUpdateCallback(this); - - - - // Create main content area using a better layout - JPanel mainContent = createMainContentPanel(); - // Add main content to the center of the window - add(mainContent, BorderLayout.CENTER); - - // Add tab change listener to sync selection - tabbedPane.addChangeListener(e -> { - PluginScheduleEntry selected = tablePanel.getSelectedPlugin(); - int tabIndex = tabbedPane.getSelectedIndex(); - - // When switching to either conditions tab, ensure the condition panel shows the currently selected plugin - if (tabIndex == 1 || tabIndex == 2) { // Start Conditions or Stop Conditions tab - if (selected == null) { - PluginScheduleEntry currentPlugin = plugin.getCurrentPlugin(); - if (currentPlugin != null) { - tablePanel.selectPlugin(currentPlugin); - selected = currentPlugin; - } else { - log.warn("No plugin selected for editing conditions and no current plugin found."); - - PluginScheduleEntry nextPlugin = plugin.getNextScheduledPluginEntry(false,null).orElse(selected); - if (nextPlugin == null) { - log.warn("No plugin selected for editing conditions and no next scheduled plugin found."); - }else{ - tablePanel.selectPlugin(nextPlugin); - log.warn("No plugin selected for editing conditions, taking next scheduled plugin: " + nextPlugin.getCleanName()); - } - selected = nextPlugin; - } - } - - if (tabIndex == 1) { // Start Conditions tab - startConditionPanel.setSelectScheduledPlugin(selected); - } else if (tabIndex == 2) { // Stop Conditions tab - stopConditionPanel.setSelectScheduledPlugin(selected); - } - - } - if (tabIndex == 0) { - formPanel.loadPlugin(selected); - - formPanel.setEditMode(selected != null); - - - - } - }); - - // Add table selection listener - //tablePanel.addSelectionListener(this::onPluginSelected); - // Modify the existing table selection listener to update ComboBox - tablePanel.addSelectionListener(pluginEntry -> { - onPluginSelected(pluginEntry); - // Synchronize form panel ComboBox with table selection - }); - // Create refresh timer to update info panel - refreshTimer = new Timer(1000, e -> infoPanel.refresh()); - - // Start timer when window is opened, stop when closed - addWindowListener(new WindowAdapter() { - @Override - public void windowOpened(WindowEvent e) { - refreshTimer.start(); - } - - @Override - public void windowClosing(WindowEvent e) { - refreshTimer.stop(); - } - }); - - // Style all comboboxes in the UI - styleAllComboBoxes(this); - - // Initialize with data - refresh(); - } - - /** - * Implementation of ConditionUpdateCallback interface. - * Called when conditions are updated in the UI and need to be saved. - */ - @Override - public void onConditionsUpdated( LogicalCondition logicalCondition, - PluginScheduleEntry plugin, - boolean isStopCondition) { - // Save to default configuration - onConditionsUpdated(logicalCondition, plugin, isStopCondition, null); - } - - /** - * Implementation of ConditionUpdateCallback interface. - * Called when conditions are updated and need to be saved to a specific file. - */ - @Override - public void onConditionsUpdated(LogicalCondition logicalCondition, PluginScheduleEntry pluginEntry, - boolean isStopCondition, File saveFile) { - if (pluginEntry == null) { - log.warn("Cannot save conditions: No plugin selected"); - return; - } - - - - try { - - // Update the plugin's condition manager with the new logical condition - /*if (isStopCondition) { - // For stop conditions - - this.plugin.saveConditionsToPlugin( - pluginEntry, - pluginEntry.getStopConditions(), - null, // No changes to start conditions - requireAll, - true, // Stop on conditions met - saveFile - ); - } else { - // For start conditions - this.plugin.saveConditionsToPlugin( - pluginEntry, - pluginEntry.getStopConditions(), // Keep existing stop conditions - pluginEntry.getStartConditions(), // Update start conditions - requireAll, - true, // Stop on conditions met - saveFile - ); - }*/ - - // Remember this file for future operations - if (saveFile != null) { - this.lastSaveFile = saveFile; - } - PluginScheduleEntry selected = tablePanel.getSelectedPlugin(); - if (selected != null) { - // Check if we're waiting to start a plugin - if ( isStopCondition && plugin.getCurrentState() == SchedulerState.WAITING_FOR_STOP_CONDITION && - plugin.getCurrentPlugin() == selected && - !selected.getStopConditionManager().getConditions().isEmpty()) { - - // Conditions added for the plugin we're waiting to start - continue starting - int result = JOptionPane.showConfirmDialog( - this, - "Stop conditions have been added. Would you like to start the plugin now?", - "Start Plugin", - JOptionPane.YES_NO_OPTION - ); - - if (result == JOptionPane.YES_OPTION) { - plugin.continuePendingStart(selected); - } else { - // User decided not to start - reset state - plugin.resetPendingStart(); - } - } - } - // Refresh UI elements - tablePanel.refreshTable(); - infoPanel.refresh(); - plugin.saveScheduledPlugins(); - log.info("Successfully saved {} conditions for plugin: {}", - isStopCondition ? "stop" : "start", - pluginEntry.getCleanName()); - } catch (Exception e) { - log.error("Error saving conditions for plugin: " + pluginEntry.getCleanName(), e); - JOptionPane.showMessageDialog( - this, - "Error saving conditions: " + e.getMessage(), - "Save Error", - JOptionPane.ERROR_MESSAGE - ); - } - } - - /** - * Implementation of ConditionUpdateCallback interface. - * Called when conditions are reset in the UI. - */ - @Override - public void onConditionsReset(PluginScheduleEntry pluginEntry, boolean isStopCondition) { - if (pluginEntry == null) { - log.warn("Cannot reset conditions: No plugin selected"); - return; - } - - log.info("Resetting {} conditions for plugin: {}", - isStopCondition ? "stop" : "start", - pluginEntry.getCleanName()); - - try { - // Clear conditions from the plugin's condition manager - if (isStopCondition) { - pluginEntry.getStopConditionManager().clearUserConditions(); - } else { - pluginEntry.getStartConditionManager().clearUserConditions(); - } - - // Save changes to config - this.plugin.saveScheduledPlugins(); - - // Refresh UI - tablePanel.refreshTable(); - infoPanel.refresh(); - - // If this was the start conditions tab, refresh also the start condition panel - if (!isStopCondition) { - CompletableFuture.runAsync(() -> { - try { - Thread.sleep(100); // Small delay to ensure changes are processed - SwingUtilities.invokeLater(() -> startConditionPanel.refreshConditions()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }); - } - // If this was the stop conditions tab, refresh also the stop condition panel - else { - CompletableFuture.runAsync(() -> { - try { - Thread.sleep(100); // Small delay to ensure changes are processed - SwingUtilities.invokeLater(() -> stopConditionPanel.refreshConditions()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }); - } - - log.info("Successfully reset {} conditions for plugin: {}", - isStopCondition ? "stop" : "start", - pluginEntry.getCleanName()); - } catch (Exception e) { - log.error("Error resetting conditions for plugin: " + pluginEntry.getCleanName(), e); - JOptionPane.showMessageDialog( - this, - "Error resetting conditions: " + e.getMessage(), - "Reset Error", - JOptionPane.ERROR_MESSAGE - ); - } - } - - /** - * Shows a dialog to choose a file for saving or loading conditions - * - * @param save True for save dialog, false for open dialog - * @return The selected file, or null if canceled - */ - public File showFileChooser(boolean save) { - JFileChooser fileChooser = new JFileChooser(); - fileChooser.setDialogTitle(save ? "Save Scheduler Plan" : "Load Scheduler Plan"); - fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); - - // Set initial directory to last used if available - if (lastSaveFile != null && lastSaveFile.getParentFile() != null && lastSaveFile.getParentFile().exists()) { - fileChooser.setCurrentDirectory(lastSaveFile.getParentFile()); - } - - // Add file extension filter - fileChooser.setFileFilter(new javax.swing.filechooser.FileFilter() { - @Override - public boolean accept(File f) { - return f.isDirectory() || f.getName().toLowerCase().endsWith(".json"); - } - - @Override - public String getDescription() { - return "JSON Files (*.json)"; - } - }); - - int result = save ? fileChooser.showSaveDialog(this) : fileChooser.showOpenDialog(this); - - if (result == JFileChooser.APPROVE_OPTION) { - File selectedFile = fileChooser.getSelectedFile(); - - // Add .json extension if missing for save dialogs - if (save && !selectedFile.getName().toLowerCase().endsWith(".json")) { - selectedFile = new File(selectedFile.getAbsolutePath() + ".json"); - } - - return selectedFile; - } - - return null; - } - - /** - * Saves the currently loaded scheduler plan to a file - */ - public void saveSchedulerPlanToFile() { - File saveFile = showFileChooser(true); - if (saveFile == null) { - return; - } - - // Check if file already exists - if (saveFile.exists()) { - int option = JOptionPane.showConfirmDialog( - this, - "File already exists. Overwrite?", - "File Exists", - JOptionPane.YES_NO_OPTION - ); - - if (option != JOptionPane.YES_OPTION) { - return; - } - } - - // Save the plan - boolean success = plugin.savePluginScheduleEntriesToFile(saveFile); - - if (success) { - lastSaveFile = saveFile; - JOptionPane.showMessageDialog( - this, - "Scheduler plan saved successfully!", - "Save Complete", - JOptionPane.INFORMATION_MESSAGE - ); - } else { - JOptionPane.showMessageDialog( - this, - "Failed to save scheduler plan. See log for details.", - "Save Failed", - JOptionPane.ERROR_MESSAGE - ); - } - } - - /** - * Loads a scheduler plan from a file - */ - public void loadSchedulerPlanFromFile() { - File loadFile = showFileChooser(false); - if (loadFile == null) { - return; - } - - // Confirm before loading - int option = JOptionPane.showConfirmDialog( - this, - "Loading will replace the current scheduler plan. Continue?", - "Load Scheduler Plan", - JOptionPane.YES_NO_OPTION - ); - - if (option != JOptionPane.YES_OPTION) { - return; - } - - // Load the plan - boolean success = plugin.loadPluginScheduleEntriesFromFile(loadFile); - - if (success) { - lastSaveFile = loadFile; - refresh(); - JOptionPane.showMessageDialog( - this, - "Scheduler plan loaded successfully!", - "Load Complete", - JOptionPane.INFORMATION_MESSAGE - ); - } else { - JOptionPane.showMessageDialog( - this, - "Failed to load scheduler plan. See log for details.", - "Load Failed", - JOptionPane.ERROR_MESSAGE - ); - } - } - - /** - * Recursively applies styling to all JComboBox components found in the container hierarchy. - * - * @param container The parent container to start searching from - */ - - private void styleAllComboBoxes(Container container) { - for (Component component : container.getComponents()) { - if (component instanceof JComboBox) { - SchedulerUIUtils.styleComboBox((JComboBox) component); - } - if (component instanceof Container) { - styleAllComboBoxes((Container) component); - } - } - } - - /** - * Creates the main content panel with tabbed interface and information sidebar. - * - * @return JPanel with the configured layout - */ - private JPanel createMainContentPanel() { - JPanel mainContent = new JPanel(new BorderLayout()); - mainContent.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Create a more flexible tabbed pane structure - tabbedPane = new JTabbedPane(); - tabbedPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - tabbedPane.setForeground(Color.WHITE); - - // Schedule tab - Split into table (top) and form (bottom) with adjustable divider - JPanel scheduleTab = new JPanel(new BorderLayout()); - scheduleTab.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Create a split pane for the table and form - use VERTICAL layout for better table visibility - JSplitPane scheduleSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); - scheduleSplitPane.setTopComponent(tablePanel); - scheduleSplitPane.setBottomComponent(formPanel); - - // Calculate the preferred size for showing 3 rows in the table - // Row height (30px) * 3 rows + header height (~25px) + legend panel (~30px) + borders/margins (~15px) - int preferredTableHeight = (30 * 3) + 25 + 30 + 15; // ~160px for 3 rows - - // Calculate the maximum size for showing 8 rows in the table - int maxTableHeight = (30 * 8) + 25 + 30 + 15; // ~310px for 8 rows - - // Set minimum size for the form panel to ensure it doesn't get too small - formPanel.setMinimumSize(new Dimension(0, 140)); - - // Set minimum size for the table panel to ensure at least 3 rows are visible - tablePanel.setMinimumSize(new Dimension(0, preferredTableHeight)); - - // Set preferred size for the table panel - tablePanel.setPreferredSize(new Dimension(0, preferredTableHeight)); - - // Set maximum size for the table panel to prevent expanding beyond 8 rows - tablePanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, maxTableHeight)); - - scheduleSplitPane.setResizeWeight(0.7); // Give 70% space to the table on top - scheduleSplitPane.setDividerLocation(preferredTableHeight); // Set initial divider position - scheduleSplitPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - scheduleTab.add(scheduleSplitPane, BorderLayout.CENTER); - - // Start Conditions tab - JPanel startConditionsTab = new JPanel(new BorderLayout()); - startConditionsTab.setBackground(ColorScheme.DARKER_GRAY_COLOR); - startConditionsTab.add(startConditionPanel, BorderLayout.CENTER); - - // Stop Conditions tab - JPanel stopConditionsTab = new JPanel(new BorderLayout()); - stopConditionsTab.setBackground(ColorScheme.DARKER_GRAY_COLOR); - stopConditionsTab.add(stopConditionPanel, BorderLayout.CENTER); - - // Add tabs to tabbed pane - tabbedPane.addTab("Schedule", scheduleTab); - tabbedPane.addTab("Start Conditions", startConditionsTab); - tabbedPane.addTab("Stop Conditions", stopConditionsTab); - - // Create a split pane for the tabs and info panel - JSplitPane mainSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); - mainSplitPane.setLeftComponent(tabbedPane); - - // Configure the info panel for proper scaling - configureInfoPanelForScrollPane(); - - // Wrap info panel in a scroll pane with proper configuration - JScrollPane infoScrollPane = createInfoScrollPane(); - - // Set up the split pane configuration - mainSplitPane.setRightComponent(infoScrollPane); - mainSplitPane.setResizeWeight(0.75); // Favor the main content (75% left, 25% right) - mainSplitPane.setDividerLocation(800); - mainSplitPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Add a single resize listener for proper scaling - mainSplitPane.addComponentListener(new ComponentAdapter() { - @Override - public void componentResized(ComponentEvent e) { - updateInfoPanelSize(mainSplitPane, infoScrollPane); - } - }); - - // Listen for divider location changes - mainSplitPane.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, evt -> { - SwingUtilities.invokeLater(() -> updateInfoPanelSize(mainSplitPane, infoScrollPane)); - }); - - // Add a top control panel for file operations with better visibility - JPanel controlPanel = new JPanel(); - controlPanel.setLayout(new BorderLayout()); - controlPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); - controlPanel.setBorder(new EmptyBorder(5, 8, 5, 8)); - - // Create button panel with FlowLayout for better spacing - JPanel fileButtonsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0)); - fileButtonsPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); - - // Create load button with improved styling - JButton loadButton = createStyledButton("Load Plan from File...", Color.BLUE, "load.png"); - loadButton.setToolTipText("Load a saved scheduler plan from a file"); - loadButton.addActionListener(e -> loadSchedulerPlanFromFile()); - - // Create save button with improved styling - JButton saveButton = createStyledButton("Save Plan to File...", ColorScheme.PROGRESS_COMPLETE_COLOR, "save.png"); - saveButton.setToolTipText("Save current scheduler plan to a file"); - saveButton.addActionListener(e -> saveSchedulerPlanToFile()); - - // Add buttons to the panel - fileButtonsPanel.add(loadButton); - fileButtonsPanel.add(saveButton); - - // Add the buttons panel to the control panel - controlPanel.add(fileButtonsPanel, BorderLayout.WEST); - - // Add optional heading/title if desired - JLabel controlPanelTitle = new JLabel("Scheduler Controls"); - controlPanelTitle.setForeground(Color.WHITE); - controlPanelTitle.setFont(FontManager.getRunescapeBoldFont().deriveFont(14f)); - controlPanel.add(controlPanelTitle, BorderLayout.EAST); - - // Add control panel to the top - mainContent.add(controlPanel, BorderLayout.NORTH); - - // Add main split pane to the center - mainContent.add(mainSplitPane, BorderLayout.CENTER); - - // Schedule initial size update after the window is displayed - SwingUtilities.invokeLater(() -> { - updateInfoPanelSize(mainSplitPane, infoScrollPane); - }); - - return mainContent; - } - - /** - * Creates a consistently styled button with icon and hover effects - */ - private JButton createStyledButton(String text, Color color, String iconName) { - JButton button = new JButton(text); - button.setFont(FontManager.getRunescapeSmallFont()); - button.setForeground(Color.WHITE); - button.setBackground(ColorScheme.DARKER_GRAY_COLOR); - button.setFocusPainted(false); - button.setBorder(new CompoundBorder( - BorderFactory.createLineBorder(color.darker(), 1), - BorderFactory.createEmptyBorder(5, 10, 5, 10))); - - // Add hover effect - button.addMouseListener(new java.awt.event.MouseAdapter() { - public void mouseEntered(java.awt.event.MouseEvent evt) { - button.setBackground(ColorScheme.DARK_GRAY_COLOR); - button.setBorder(new CompoundBorder( - BorderFactory.createLineBorder(color, 1), - BorderFactory.createEmptyBorder(5, 10, 5, 10))); - } - - public void mouseExited(java.awt.event.MouseEvent evt) { - button.setBackground(ColorScheme.DARKER_GRAY_COLOR); - button.setBorder(new CompoundBorder( - BorderFactory.createLineBorder(color.darker(), 1), - BorderFactory.createEmptyBorder(5, 10, 5, 10))); - } - }); - - return button; - } - - /** - * Refreshes all UI components with the latest data from the plugin. - * Updates the table, form state, condition panel, and information panel. - */ - public void refresh() { - tablePanel.refreshTable(); - if (formPanel != null) { - formPanel.refresh(); - } - if (stopConditionPanel != null) { - stopConditionPanel.refreshConditions(); - } - if (startConditionPanel != null) { - startConditionPanel.refreshConditions(); - } - - // Refresh info panel - if (infoPanel != null) { - infoPanel.refresh(); - } - } - /** - * Handles plugin selection events from the table. - * Updates the form panel and condition panel with the selected plugin's data. - * - * @param plugin The selected plugin or null if no selection - */ - private void onPluginSelected(PluginScheduleEntry plugin) { - if (tablePanel.getRowCount() == 0) { - log.info("No plugins in table."); - return; - } - - PluginScheduleEntry selected = tablePanel.getSelectedPlugin(); - - if (selected == null) { - formPanel.clearForm(); - formPanel.setEditMode(false); - // Update both condition panels when no plugin is selected - startConditionPanel.setSelectScheduledPlugin(null); - stopConditionPanel.setSelectScheduledPlugin(null); - - if (plugin == null) { - return; - } - - // Handle case where we have a plugin reference but nothing is selected - log.info("No plugin selected for editing. Plugin: {}", plugin.getCleanName()); - // Find if this is the next scheduled plugin - PluginScheduleEntry nextPlugin = this.plugin.getNextScheduledPluginEntry(false,null).orElse(selected); - if (nextPlugin == null) { - log.warn("No plugin selected for editing conditions and no next scheduled plugin found."); - } else { - tablePanel.selectPlugin(nextPlugin); - log.info("No plugin selected for editing conditions, selecting next scheduled plugin: {}", nextPlugin.getCleanName()); - } - selected = nextPlugin; - } - - // Update form panel with selection - formPanel.loadPlugin(selected); - formPanel.setEditMode(true); - - // Update condition panels based on the current tab - int currentTabIndex = tabbedPane.getSelectedIndex(); - if (currentTabIndex == 1) { // Start Conditions tab - startConditionPanel.setSelectScheduledPlugin(selected); - } else if (currentTabIndex == 2) { // Stop Conditions tab - stopConditionPanel.setSelectScheduledPlugin(selected); - } - - // Always update control button when selection changes - formPanel.updateControlButton(); - } - - /** - * Processes the addition of a new plugin from the form data. - * Checks for stop conditions and prompts user if none are configured. - */ - private void onAddPlugin() { - PluginScheduleEntry scheduledPlugin = formPanel.getPluginFromForm(null); - if (scheduledPlugin == null) return; - - // Check if the plugin has stop conditions - if (scheduledPlugin.getStopConditionManager().getUserConditions().isEmpty()) { - // Check if this plugin needs time-based stop conditions (from checkbox) - if (scheduledPlugin.isNeedsStopCondition()) { - int result = JOptionPane.showConfirmDialog(this, - "No stop conditions are set. The plugin will run until manually stopped.\n" + - "Would you like to configure stop conditions now?", - "No Stop Conditions", - JOptionPane.YES_NO_CANCEL_OPTION); - - if (result == JOptionPane.YES_OPTION) { - // Add the plugin first (disabled by default) so we can set conditions on it - scheduledPlugin.setEnabled(false); - plugin.addScheduledPlugin(scheduledPlugin); - plugin.saveScheduledPlugins(); - refresh(); - log.info("Plugin added without conditions: row count" + tablePanel.getRowCount()); - // Select the newly added plugin - tablePanel.selectPlugin(scheduledPlugin); - // Switch to stop conditions tab - tabbedPane.setSelectedIndex(2); - return; - } else if (result == JOptionPane.CANCEL_OPTION) { - scheduledPlugin.setEnabled(false); // Set to disabled by default - return; // Cancel the operation - } - // If NO, continue with adding plugin without conditions - } - } - - // Add the plugin (disabled by default for safety) - scheduledPlugin.setEnabled(false); - plugin.addScheduledPlugin(scheduledPlugin); - plugin.saveScheduledPlugins(); - refresh(); - - // Select the newly added plugin - tablePanel.selectPlugin(scheduledPlugin); - - // Show a hint about enabling the plugin - JOptionPane.showMessageDialog(this, - "Plugin added successfully (currently disabled).\n" + - "Enable it in the Properties tab when you're ready to schedule it.", - "Plugin Added", - JOptionPane.INFORMATION_MESSAGE); - - // Switch to the Properties tab in the form panel - if (formPanel.getComponent(0) instanceof JTabbedPane) { - ((JTabbedPane)formPanel.getComponent(0)).setSelectedIndex(1); - } - } - - /** - * Updates an existing plugin with data from the form. - * Preserves the plugin's identity while updating its configuration. - */ - private void onUpdatePlugin() { - PluginScheduleEntry selectedPlugin = tablePanel.getSelectedPlugin(); - if (selectedPlugin == null) { - return; - } - - try { - // Apply form values to the selected plugin - formPanel.getPluginFromForm(selectedPlugin); - - // Update the UI - plugin.saveScheduledPlugins(); - tablePanel.refreshTable(); - - // Clear edit mode and selection to encourage users to review the changes - formPanel.loadPlugin(selectedPlugin); - formPanel.setEditMode(false); - tablePanel.clearSelection(); - - JOptionPane.showMessageDialog(this, - "Plugin schedule updated successfully!", - "Update Success", - JOptionPane.INFORMATION_MESSAGE); - - } catch (Exception e) { - log.error("Error updating plugin: {}", e.getMessage(), e); - JOptionPane.showMessageDialog( - this, - "Error updating plugin: " + e.getMessage(), - "Update Error", - JOptionPane.ERROR_MESSAGE - ); - } - } - - /** - * Removes the currently selected plugin from the schedule. - */ - private void onRemovePlugin() { - PluginScheduleEntry selectedPlugin = tablePanel.getSelectedPlugin(); - if (selectedPlugin == null) { - return; - } - - // Confirm deletion - int result = JOptionPane.showConfirmDialog(this, - "Are you sure you want to remove '" + selectedPlugin.getCleanName() + "' from the schedule?", - "Confirm Removal", - JOptionPane.YES_NO_OPTION, - JOptionPane.WARNING_MESSAGE); - - if (result != JOptionPane.YES_OPTION) { - return; - } - - // Stop the plugin if it's running - if (plugin.getCurrentPlugin()!=null && - plugin.getCurrentPlugin().equals(selectedPlugin) && - selectedPlugin.isRunning()) { - - plugin.forceStopCurrentPluginScheduleEntry(false); - } - - // Remove from schedule - plugin.removeScheduledPlugin(selectedPlugin); - plugin.saveScheduledPlugins(); - - // Update UI - tablePanel.refreshTable(); - formPanel.clearForm(); - startConditionPanel.setSelectScheduledPlugin(null); - stopConditionPanel.setSelectScheduledPlugin(null); - formPanel.setEditMode(false); - JOptionPane.showMessageDialog(this, - "Plugin removed from schedule.", - "Plugin Removed", - JOptionPane.INFORMATION_MESSAGE); - } - /** - * Cleans up resources when window is closed. - * Stops the refresh timer before disposing the window. - */ - @Override - public void dispose() { - if (refreshTimer != null) { - refreshTimer.stop(); - } - super.dispose(); - } - /** - * Programmatically selects a plugin in the table. - * - * @param plugin The plugin entry to select - */ - public void selectPlugin(PluginScheduleEntry plugin) { - if (tablePanel != null) { - tablePanel.selectPlugin(plugin); - } - } - /** - * Switches the UI to display the stop conditions tab. - */ - public void switchToStopConditionsTab() { - if (tabbedPane != null) { - tabbedPane.setSelectedIndex(2); // Switch to stop conditions tab (now at index 2) - } - } - - /** - * Configures the info panel for optimal display within a scroll pane. - * This ensures the panel layout is properly prepared for dynamic resizing. - */ - private void configureInfoPanelForScrollPane() { - // Set a minimum width for the info panel to ensure readability - int minInfoPanelWidth = 240; - infoPanel.setMinimumSize(new Dimension(minInfoPanelWidth, 0)); - - // Allow the info panel to expand vertically but control horizontal size - infoPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); - - // Set an initial preferred size that will be adjusted by resize listeners - infoPanel.setPreferredSize(new Dimension(minInfoPanelWidth, 600)); - } - - /** - * Creates and configures the scroll pane for the info panel with optimal settings. - * - * @return A properly configured JScrollPane containing the info panel - */ - private JScrollPane createInfoScrollPane() { - JScrollPane scrollPane = new JScrollPane(infoPanel); - - // Configure scroll policies - scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - - // Style the scroll pane to match the application theme - scrollPane.setBorder(BorderFactory.createEmptyBorder()); - scrollPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - scrollPane.getViewport().setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Set initial size constraints - int minScrollPaneWidth = 250; - scrollPane.setMinimumSize(new Dimension(minScrollPaneWidth, 0)); - scrollPane.setPreferredSize(new Dimension(minScrollPaneWidth, 600)); - - return scrollPane; - } - - /** - * Updates the info panel size based on the current split pane configuration. - * This method ensures the info panel properly fits within the allocated space. - * - * @param splitPane The main split pane containing the info panel - * @param scrollPane The scroll pane wrapping the info panel - */ - private void updateInfoPanelSize(JSplitPane splitPane, JScrollPane scrollPane) { - if (splitPane == null || scrollPane == null) { - return; - } - - // Get the actual width allocated to the right component - Component rightComponent = splitPane.getRightComponent(); - if (rightComponent == null) { - return; - } - - int availableWidth = rightComponent.getWidth(); - if (availableWidth <= 0) { - // If width is not yet available, use the divider location to estimate - int dividerLocation = splitPane.getDividerLocation(); - int totalWidth = splitPane.getWidth(); - availableWidth = totalWidth - dividerLocation - splitPane.getDividerSize(); - } - - if (availableWidth > 0) { - // Account for potential scrollbar width and margins - int scrollBarWidth = 20; // Conservative estimate including margins - int contentWidth = Math.max(240, availableWidth - scrollBarWidth); // Minimum width of 240px - - // Update the info panel size - Dimension newSize = new Dimension(contentWidth, infoPanel.getPreferredSize().height); - infoPanel.setPreferredSize(newSize); - infoPanel.setMaximumSize(new Dimension(contentWidth, Integer.MAX_VALUE)); - - // Revalidate to apply the changes - infoPanel.revalidate(); - scrollPane.revalidate(); - - log.debug("Updated info panel size to {}px wide (available: {}px)", contentWidth, availableWidth); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DatePickerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DatePickerPanel.java deleted file mode 100644 index 4f23d0c3613..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DatePickerPanel.java +++ /dev/null @@ -1,254 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.components; - -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.event.PopupMenuEvent; -import javax.swing.event.PopupMenuListener; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.time.DayOfWeek; -import java.time.LocalDate; -import java.time.YearMonth; -import java.time.format.DateTimeFormatter; -import java.util.function.Consumer; - -/** - * A custom date picker component that shows a calendar popup for date selection - */ -public class DatePickerPanel extends JPanel { - private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - private JTextField dateField; - private LocalDate selectedDate; - private JPopupMenu calendarPopup; - private JPanel calendarPanel; - private JLabel monthYearLabel; - private YearMonth currentYearMonth; - private Consumer dateChangeListener; - - public DatePickerPanel() { - this(LocalDate.now()); - } - - public DatePickerPanel(LocalDate initialDate) { - this.selectedDate = initialDate; - this.currentYearMonth = YearMonth.from(initialDate); - setLayout(new BorderLayout(5, 0)); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setBorder(new EmptyBorder(0, 0, 0, 0)); - - initComponents(); - } - - private void initComponents() { - // Date text field with formatted date - dateField = new JTextField(selectedDate.format(dateFormatter), 10); - dateField.setForeground(Color.WHITE); - dateField.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - dateField.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - BorderFactory.createEmptyBorder(2, 5, 2, 5))); - - // Calendar button with ImageIcon - JButton calendarButton = new JButton(); - calendarButton.setFocusPainted(false); - calendarButton.setForeground(Color.WHITE); - calendarButton.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - calendarButton.setPreferredSize(new Dimension(30, dateField.getPreferredSize().height)); - - - try { - ImageIcon icon = new ImageIcon(getClass().getResource("/net/runelite/client/plugins/microbot/pluginscheduler/"+"calendar-icon.png")); - // Scale the icon to fit the button - Image img = icon.getImage().getScaledInstance(16, 16, Image.SCALE_SMOOTH); - calendarButton.setIcon(new ImageIcon(img)); - } catch (Exception e) { - // Fallback to simple text if icon can't be loaded - calendarButton.setText("▾"); - } - - // Initialize calendar popup - createCalendarPopup(); - - // Show calendar on button click - calendarButton.addActionListener(e -> { - Point location = dateField.getLocationOnScreen(); - calendarPopup.show(dateField, 0, dateField.getHeight()); - calendarPopup.setLocation(location.x, location.y + dateField.getHeight()); - }); - - // Update date when text field changes - dateField.addActionListener(e -> { - try { - LocalDate newDate = LocalDate.parse(dateField.getText(), dateFormatter); - setSelectedDate(newDate); - } catch (Exception ex) { - // If parsing fails, revert to current selection - dateField.setText(selectedDate.format(dateFormatter)); - } - }); - - // Add components to panel - add(dateField, BorderLayout.CENTER); - add(calendarButton, BorderLayout.EAST); - } - - private void createCalendarPopup() { - calendarPopup = new JPopupMenu(); - calendarPopup.setBorder(BorderFactory.createLineBorder(ColorScheme.DARK_GRAY_COLOR)); - - JPanel contentPanel = new JPanel(new BorderLayout()); - contentPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - contentPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); - - // Month navigation panel - JPanel navigationPanel = new JPanel(new BorderLayout()); - navigationPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JButton prevButton = new JButton("<"); - prevButton.setFocusPainted(false); - prevButton.setForeground(Color.WHITE); - prevButton.setBackground(ColorScheme.BRAND_ORANGE); - prevButton.addActionListener(e -> { - currentYearMonth = currentYearMonth.minusMonths(1); - updateCalendar(); - }); - - JButton nextButton = new JButton(">"); - nextButton.setFocusPainted(false); - nextButton.setForeground(Color.WHITE); - nextButton.setBackground(ColorScheme.BRAND_ORANGE); - nextButton.addActionListener(e -> { - currentYearMonth = currentYearMonth.plusMonths(1); - updateCalendar(); - }); - - monthYearLabel = new JLabel("", SwingConstants.CENTER); - monthYearLabel.setForeground(Color.WHITE); - monthYearLabel.setFont(FontManager.getRunescapeBoldFont()); - - navigationPanel.add(prevButton, BorderLayout.WEST); - navigationPanel.add(monthYearLabel, BorderLayout.CENTER); - navigationPanel.add(nextButton, BorderLayout.EAST); - - // Calendar panel (will be populated in updateCalendar()) - calendarPanel = new JPanel(new GridLayout(7, 7, 2, 2)); - calendarPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - contentPanel.add(navigationPanel, BorderLayout.NORTH); - contentPanel.add(calendarPanel, BorderLayout.CENTER); - - calendarPopup.add(contentPanel); - - // Update calendar when shown - calendarPopup.addPopupMenuListener(new PopupMenuListener() { - @Override - public void popupMenuWillBecomeVisible(PopupMenuEvent e) { - currentYearMonth = YearMonth.from(selectedDate); - updateCalendar(); - } - - @Override - public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {} - - @Override - public void popupMenuCanceled(PopupMenuEvent e) {} - }); - } - - private void updateCalendar() { - calendarPanel.removeAll(); - - // Update month/year label - monthYearLabel.setText(currentYearMonth.format(DateTimeFormatter.ofPattern("MMMM yyyy"))); - - // Day of week headers - String[] daysOfWeek = {"Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"}; - for (String day : daysOfWeek) { - JLabel label = new JLabel(day, SwingConstants.CENTER); - label.setForeground(Color.LIGHT_GRAY); - calendarPanel.add(label); - } - - // Get the first day of the month and adjust for Monday-based week - LocalDate firstOfMonth = currentYearMonth.atDay(1); - int dayOfWeekValue = firstOfMonth.getDayOfWeek().getValue(); // 1 for Monday, 7 for Sunday - - // Add empty cells before the first day of the month - for (int i = 1; i < dayOfWeekValue; i++) { - calendarPanel.add(new JLabel()); - } - - // Add day buttons - for (int day = 1; day <= currentYearMonth.lengthOfMonth(); day++) { - final int dayValue = day; - final LocalDate date = currentYearMonth.atDay(day); - - JButton dayButton = new JButton(String.valueOf(day)); - dayButton.setFocusPainted(false); - dayButton.setMargin(new Insets(2, 2, 2, 2)); - - // Highlight today - if (date.equals(LocalDate.now())) { - dayButton.setBackground(new Color(70, 130, 180)); // Steel blue - dayButton.setForeground(Color.WHITE); - } - // Highlight selected date - else if (date.equals(selectedDate)) { - dayButton.setBackground(ColorScheme.BRAND_ORANGE); - dayButton.setForeground(Color.WHITE); - } - // Weekend days - else if (date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY) { - dayButton.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - dayButton.setForeground(Color.LIGHT_GRAY); - } - // Regular days - else { - dayButton.setBackground(ColorScheme.DARK_GRAY_COLOR); - dayButton.setForeground(Color.WHITE); - } - - // Select this date and close popup when clicked - dayButton.addActionListener(e -> { - setSelectedDate(date); - calendarPopup.setVisible(false); - }); - - calendarPanel.add(dayButton); - } - - calendarPanel.revalidate(); - calendarPanel.repaint(); - } - - public LocalDate getSelectedDate() { - return selectedDate; - } - - public void setSelectedDate(LocalDate date) { - this.selectedDate = date; - dateField.setText(date.format(dateFormatter)); - - if (dateChangeListener != null) { - dateChangeListener.accept(date); - } - } - - public void setDateChangeListener(Consumer listener) { - this.dateChangeListener = listener; - } - - public void setEditable(boolean editable) { - dateField.setEditable(editable); - } - - public JTextField getTextField() { - return dateField; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DateRangePanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DateRangePanel.java deleted file mode 100644 index edf637f9be3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DateRangePanel.java +++ /dev/null @@ -1,160 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.components; - -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import java.awt.*; -import java.time.LocalDate; -import java.util.function.BiConsumer; - -/** - * A panel for selecting a date range with start and end dates - */ -public class DateRangePanel extends JPanel { - private final DatePickerPanel startDatePicker; - private final DatePickerPanel endDatePicker; - private BiConsumer rangeChangeListener; - - public DateRangePanel() { - this(LocalDate.now(), LocalDate.now().plusMonths(1)); - } - - public DateRangePanel(LocalDate startDate, LocalDate endDate) { - setLayout(new GridBagLayout()); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setBorder(new EmptyBorder(5, 5, 5, 5)); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 0; - gbc.anchor = GridBagConstraints.WEST; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.insets = new Insets(2, 2, 2, 2); - - JLabel startLabel = new JLabel("Start Date:"); - startLabel.setForeground(Color.WHITE); - startLabel.setFont(FontManager.getRunescapeSmallFont()); - add(startLabel, gbc); - - gbc.gridx = 1; - startDatePicker = new DatePickerPanel(startDate); - add(startDatePicker, gbc); - - gbc.gridx = 0; - gbc.gridy = 1; - JLabel endLabel = new JLabel("End Date:"); - endLabel.setForeground(Color.WHITE); - endLabel.setFont(FontManager.getRunescapeSmallFont()); - add(endLabel, gbc); - - gbc.gridx = 1; - endDatePicker = new DatePickerPanel(endDate); - add(endDatePicker, gbc); - - // Add common presets panel - gbc.gridx = 0; - gbc.gridy = 2; - gbc.gridwidth = 2; - gbc.insets = new Insets(10, 2, 2, 2); - - JPanel presetsPanel = createPresetsPanel(); - add(presetsPanel, gbc); - - // Set up change listeners - startDatePicker.setDateChangeListener(d -> { - // Ensure end date is not before start date - if (endDatePicker.getSelectedDate().isBefore(d)) { - endDatePicker.setSelectedDate(d); - } - notifyRangeChanged(); - }); - - endDatePicker.setDateChangeListener(d -> { - // Ensure start date is not after end date - if (startDatePicker.getSelectedDate().isAfter(d)) { - startDatePicker.setSelectedDate(d); - } - notifyRangeChanged(); - }); - } - - private JPanel createPresetsPanel() { - JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel presetsLabel = new JLabel("Quick Presets:"); - presetsLabel.setForeground(Color.WHITE); - panel.add(presetsLabel); - - // Common date range presets - LocalDate today = LocalDate.now(); - - addPresetButton(panel, "Today", today, today); - addPresetButton(panel, "This Week", today, today.plusDays(7 - today.getDayOfWeek().getValue())); - addPresetButton(panel, "This Month", today, today.withDayOfMonth(today.lengthOfMonth())); - addPresetButton(panel, "Next 7 Days", today, today.plusDays(7)); - addPresetButton(panel, "Next 30 Days", today, today.plusDays(30)); - addPresetButton(panel, "Next 90 Days", today, today.plusDays(90)); - - // Add unlimited option - addPresetButton(panel, "Unlimited", - net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition.UNLIMITED_START_DATE, - net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition.UNLIMITED_END_DATE); - - return panel; - } - - private void addPresetButton(JPanel panel, String label, LocalDate start, LocalDate end) { - JButton button = new JButton(label); - button.setFocusPainted(false); - button.setBackground(ColorScheme.DARK_GRAY_COLOR); - button.setForeground(Color.WHITE); - button.setFont(FontManager.getRunescapeSmallFont()); - button.addActionListener(e -> { - startDatePicker.setSelectedDate(start); - endDatePicker.setSelectedDate(end); - }); - panel.add(button); - } - - private void notifyRangeChanged() { - if (rangeChangeListener != null) { - rangeChangeListener.accept(getStartDate(), getEndDate()); - } - } - - public LocalDate getStartDate() { - return startDatePicker.getSelectedDate(); - } - - public LocalDate getEndDate() { - return endDatePicker.getSelectedDate(); - } - - public void setStartDate(LocalDate date) { - startDatePicker.setSelectedDate(date); - } - - public void setEndDate(LocalDate date) { - endDatePicker.setSelectedDate(date); - } - - public void setRangeChangeListener(BiConsumer listener) { - this.rangeChangeListener = listener; - } - - public void setEditable(boolean editable) { - startDatePicker.setEditable(editable); - endDatePicker.setEditable(editable); - } - - public DatePickerPanel getStartDatePicker() { - return startDatePicker; - } - - public DatePickerPanel getEndDatePicker() { - return endDatePicker; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DateTimePickerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DateTimePickerPanel.java deleted file mode 100644 index 0d2502d384d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DateTimePickerPanel.java +++ /dev/null @@ -1,88 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.components; - -import net.runelite.client.ui.ColorScheme; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import java.awt.*; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.util.function.Consumer; - -/** - * A combined date and time picker panel - */ -public class DateTimePickerPanel extends JPanel { - private final DatePickerPanel datePicker; - private final TimePickerPanel timePicker; - private Consumer dateTimeChangeListener; - - public DateTimePickerPanel() { - this(LocalDate.now(), LocalTime.now()); - } - - public DateTimePickerPanel(LocalDate date, LocalTime time) { - setLayout(new BorderLayout(10, 0)); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setBorder(new EmptyBorder(0, 0, 0, 0)); - - datePicker = new DatePickerPanel(date); - timePicker = new TimePickerPanel(time); - - JPanel container = new JPanel(new BorderLayout(5, 0)); - container.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - container.add(datePicker, BorderLayout.CENTER); - container.add(timePicker, BorderLayout.EAST); - - add(container, BorderLayout.CENTER); - - // Set up change listeners - datePicker.setDateChangeListener(d -> notifyDateTimeChanged()); - timePicker.setTimeChangeListener(t -> notifyDateTimeChanged()); - } - - private void notifyDateTimeChanged() { - if (dateTimeChangeListener != null) { - dateTimeChangeListener.accept(getDateTime()); - } - } - - public LocalDateTime getDateTime() { - return LocalDateTime.of(datePicker.getSelectedDate(), timePicker.getSelectedTime()); - } - - public void setDateTime(LocalDateTime dateTime) { - datePicker.setSelectedDate(dateTime.toLocalDate()); - timePicker.setSelectedTime(dateTime.toLocalTime()); - } - - public void setDateTimeChangeListener(Consumer listener) { - this.dateTimeChangeListener = listener; - } - - public void setEditable(boolean editable) { - datePicker.setEditable(editable); - timePicker.setEditable(editable); - } - - public DatePickerPanel getDatePicker() { - return datePicker; - } - - public TimePickerPanel getTimePicker() { - return timePicker; - } - - public void setDate(LocalDate date) { - datePicker.setSelectedDate(date); - notifyDateTimeChanged(); - } - - public void setTime(Integer hour, Integer minute) { - LocalTime time = LocalTime.of(hour, minute); - timePicker.setSelectedTime(time); - notifyDateTimeChanged(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/InitialDelayPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/InitialDelayPanel.java deleted file mode 100644 index ffc926d40d7..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/InitialDelayPanel.java +++ /dev/null @@ -1,156 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.components; - -import net.runelite.client.ui.ColorScheme; - -import javax.swing.*; -import java.awt.*; - -/** - * A reusable panel component for configuring an initial delay with hours, minutes, and seconds spinners. - */ -public class InitialDelayPanel extends JPanel { - private final JCheckBox initialDelayCheckBox; - private final JSpinner hoursSpinner; - private final JSpinner minutesSpinner; - private final JSpinner secondsSpinner; - - /** - * Creates a new InitialDelayPanel with default values. - */ - public InitialDelayPanel() { - setLayout(new FlowLayout(FlowLayout.LEFT)); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - - initialDelayCheckBox = new JCheckBox("Initial Delay"); - initialDelayCheckBox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - initialDelayCheckBox.setForeground(Color.WHITE); - add(initialDelayCheckBox); - - // Hours spinner - SpinnerNumberModel hoursModel = new SpinnerNumberModel(0, 0, 23, 1); - hoursSpinner = new JSpinner(hoursModel); - hoursSpinner.setPreferredSize(new Dimension(60, hoursSpinner.getPreferredSize().height)); - hoursSpinner.setEnabled(false); - add(hoursSpinner); - - JLabel hoursLabel = new JLabel("hr"); - hoursLabel.setForeground(Color.WHITE); - add(hoursLabel); - - // Minutes spinner - SpinnerNumberModel minutesModel = new SpinnerNumberModel(0, 0, 59, 1); - minutesSpinner = new JSpinner(minutesModel); - minutesSpinner.setPreferredSize(new Dimension(60, minutesSpinner.getPreferredSize().height)); - minutesSpinner.setEnabled(false); - add(minutesSpinner); - - JLabel minutesLabel = new JLabel("min"); - minutesLabel.setForeground(Color.WHITE); - add(minutesLabel); - - // Seconds spinner - SpinnerNumberModel secondsModel = new SpinnerNumberModel(0, 0, 59, 1); - secondsSpinner = new JSpinner(secondsModel); - secondsSpinner.setPreferredSize(new Dimension(60, secondsSpinner.getPreferredSize().height)); - secondsSpinner.setEnabled(false); - add(secondsSpinner); - - JLabel secondsLabel = new JLabel("sec"); - secondsLabel.setForeground(Color.WHITE); - add(secondsLabel); - - // Enable/disable spinners based on checkbox - initialDelayCheckBox.addActionListener(e -> { - boolean selected = initialDelayCheckBox.isSelected(); - hoursSpinner.setEnabled(selected); - minutesSpinner.setEnabled(selected); - secondsSpinner.setEnabled(selected); - }); - - // Add overflow logic for spinners - addSpinnerOverflowLogic(); - } - - private void addSpinnerOverflowLogic() { - // Seconds overflow - secondsSpinner.addChangeListener(e -> { - int seconds = (int) secondsSpinner.getValue(); - if (seconds > 59) { - secondsSpinner.setValue(0); - minutesSpinner.setValue((int) minutesSpinner.getValue() + 1); - } - }); - - // Minutes overflow - minutesSpinner.addChangeListener(e -> { - int minutes = (int) minutesSpinner.getValue(); - if (minutes > 59) { - minutesSpinner.setValue(0); - hoursSpinner.setValue((int) hoursSpinner.getValue() + 1); - } - }); - } - - /** - * Gets whether the initial delay is enabled. - * - * @return True if enabled, false otherwise. - */ - public boolean isInitialDelayEnabled() { - return initialDelayCheckBox.isSelected(); - } - - /** - * Gets the configured hours. - * - * @return The hours value. - */ - public int getHours() { - return (int) hoursSpinner.getValue(); - } - - /** - * Gets the configured minutes. - * - * @return The minutes value. - */ - public int getMinutes() { - return (int) minutesSpinner.getValue(); - } - - /** - * Gets the configured seconds. - * - * @return The seconds value. - */ - public int getSeconds() { - return (int) secondsSpinner.getValue(); - } - - /** - * Gets the hours spinner component. - * - * @return The hours spinner. - */ - public JSpinner getHoursSpinner() { - return hoursSpinner; - } - - /** - * Gets the minutes spinner component. - * - * @return The minutes spinner. - */ - public JSpinner getMinutesSpinner() { - return minutesSpinner; - } - - /** - * Gets the seconds spinner component. - * - * @return The seconds spinner. - */ - public JSpinner getSecondsSpinner() { - return secondsSpinner; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/IntervalPickerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/IntervalPickerPanel.java deleted file mode 100644 index ffd625a8a1b..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/IntervalPickerPanel.java +++ /dev/null @@ -1,583 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.components; - -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.ui.ColorScheme; -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import java.awt.*; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - -/** - * A reusable panel component for configuring time intervals with optional randomization. - * Allows setting fixed intervals or randomized intervals with min/max values. - */ -public class IntervalPickerPanel extends JPanel { - private JRadioButton fixedRadioButton; - private JRadioButton randomizedRadioButton; - private JPanel fixedPanel; - private JPanel randomizedPanel; - private JSpinner hoursSpinner; - private JSpinner minutesSpinner; - private JSpinner minHoursSpinner; - private JSpinner minMinutesSpinner; - private JSpinner maxHoursSpinner; - private JSpinner maxMinutesSpinner; - private JPanel presetPanel; - private JPanel randomPresetPanel; - private JComboBox fixedPresetComboBox; - private JComboBox randomPresetComboBox; - private List> changeListeners = new ArrayList<>(); - - /** - * Creates a new IntervalPickerPanel with default values - */ - public IntervalPickerPanel() { - this(true); - } - - /** - * Creates a new IntervalPickerPanel - * - * @param includePresets Whether to include preset buttons - */ - public IntervalPickerPanel(boolean includePresets) { - setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setBorder(new EmptyBorder(5, 5, 5, 5)); - - initComponents(includePresets); - } - - private void initComponents(boolean includePresets) { - // Interval type selection (fixed vs randomized) - JPanel typePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - typePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - ButtonGroup typeGroup = new ButtonGroup(); - - fixedRadioButton = new JRadioButton("Fixed Interval"); - fixedRadioButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - fixedRadioButton.setForeground(Color.WHITE); - fixedRadioButton.setSelected(true); - - randomizedRadioButton = new JRadioButton("Random Interval"); - randomizedRadioButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - randomizedRadioButton.setForeground(Color.WHITE); - - typeGroup.add(fixedRadioButton); - typeGroup.add(randomizedRadioButton); - - typePanel.add(fixedRadioButton); - typePanel.add(randomizedRadioButton); - - add(typePanel); - add(Box.createVerticalStrut(5)); - - // Fixed interval panel - fixedPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - fixedPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel hoursLabel = new JLabel("Hours:"); - hoursLabel.setForeground(Color.WHITE); - - SpinnerNumberModel hoursModel = new SpinnerNumberModel(0, 0, 24, 1); - hoursSpinner = new JSpinner(hoursModel); - hoursSpinner.setPreferredSize(new Dimension(60, hoursSpinner.getPreferredSize().height)); - - JLabel minutesLabel = new JLabel("Minutes:"); - minutesLabel.setForeground(Color.WHITE); - - SpinnerNumberModel minutesModel = new SpinnerNumberModel(30, 0, 59, 1); - minutesSpinner = new JSpinner(minutesModel); - minutesSpinner.setPreferredSize(new Dimension(60, minutesSpinner.getPreferredSize().height)); - - fixedPanel.add(hoursLabel); - fixedPanel.add(hoursSpinner); - fixedPanel.add(minutesLabel); - fixedPanel.add(minutesSpinner); - - add(fixedPanel); - - // Randomized interval panel - randomizedPanel = new JPanel(); - randomizedPanel.setLayout(new BoxLayout(randomizedPanel, BoxLayout.Y_AXIS)); - randomizedPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Min interval - JPanel minPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - minPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel minLabel = new JLabel("Min Interval - Hours:"); - minLabel.setForeground(Color.WHITE); - - SpinnerNumberModel minHoursModel = new SpinnerNumberModel(0, 0, 24, 1); - minHoursSpinner = new JSpinner(minHoursModel); - minHoursSpinner.setPreferredSize(new Dimension(60, minHoursSpinner.getPreferredSize().height)); - - JLabel minMinutesLabel = new JLabel("Minutes:"); - minMinutesLabel.setForeground(Color.WHITE); - - SpinnerNumberModel minMinutesModel = new SpinnerNumberModel(30, 0, 59, 1); - minMinutesSpinner = new JSpinner(minMinutesModel); - minMinutesSpinner.setPreferredSize(new Dimension(60, minMinutesSpinner.getPreferredSize().height)); - - minPanel.add(minLabel); - minPanel.add(minHoursSpinner); - minPanel.add(minMinutesLabel); - minPanel.add(minMinutesSpinner); - - // Max interval - JPanel maxPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - maxPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel maxLabel = new JLabel("Max Interval - Hours:"); - maxLabel.setForeground(Color.WHITE); - - SpinnerNumberModel maxHoursModel = new SpinnerNumberModel(1, 0, 24, 1); - maxHoursSpinner = new JSpinner(maxHoursModel); - maxHoursSpinner.setPreferredSize(new Dimension(60, maxHoursSpinner.getPreferredSize().height)); - - JLabel maxMinutesLabel = new JLabel("Minutes:"); - maxMinutesLabel.setForeground(Color.WHITE); - - SpinnerNumberModel maxMinutesModel = new SpinnerNumberModel(0, 0, 59, 1); - maxMinutesSpinner = new JSpinner(maxMinutesModel); - maxMinutesSpinner.setPreferredSize(new Dimension(60, maxMinutesSpinner.getPreferredSize().height)); - - maxPanel.add(maxLabel); - maxPanel.add(maxHoursSpinner); - maxPanel.add(maxMinutesLabel); - maxPanel.add(maxMinutesSpinner); - - randomizedPanel.add(minPanel); - randomizedPanel.add(maxPanel); - randomizedPanel.setVisible(false); - - add(randomizedPanel); - - // Add presets if requested - if (includePresets) { - add(Box.createVerticalStrut(5)); - - // Fixed presets - presetPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - presetPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel presetsLabel = new JLabel("Presets:"); - presetsLabel.setForeground(Color.WHITE); - presetPanel.add(presetsLabel); - - String[][] presets = { - {"Select a preset...", "0", "0"}, - {"15m", "0", "15"}, - {"30m", "0", "30"}, - {"45m", "0", "45"}, - {"1h", "1", "0"}, - {"1h30m", "1", "30"}, - {"2h", "2", "0"}, - {"2h30m", "2", "30"}, - {"3h", "3", "0"}, - {"3h30m", "3", "30"}, - {"4h", "4", "0"}, - {"4h30m", "4", "30"}, - {"6h", "6", "0"} - }; - - fixedPresetComboBox = new JComboBox<>(); - for (String[] preset : presets) { - fixedPresetComboBox.addItem(preset[0]); - } - fixedPresetComboBox.setBackground(ColorScheme.DARK_GRAY_COLOR); - fixedPresetComboBox.setForeground(Color.WHITE); - fixedPresetComboBox.setPreferredSize(new Dimension(150, fixedPresetComboBox.getPreferredSize().height)); - fixedPresetComboBox.addActionListener(e -> { - int selectedIndex = fixedPresetComboBox.getSelectedIndex(); - if (selectedIndex > 0) { // Skip the "Select a preset..." option - fixedRadioButton.setSelected(true); - updatePanelVisibility(); - - // Apply preset values - String[] preset = presets[selectedIndex]; - hoursSpinner.setValue(Integer.parseInt(preset[1])); - minutesSpinner.setValue(Integer.parseInt(preset[2])); - - // Also set reasonable min/max values - setMinMaxFromFixed(); - - // Notify listeners - notifyChangeListeners(); - } - }); - presetPanel.add(fixedPresetComboBox); - - add(presetPanel); - - // Random presets - randomPresetPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - randomPresetPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel randomPresetsLabel = new JLabel("Random Presets:"); - randomPresetsLabel.setForeground(Color.WHITE); - randomPresetPanel.add(randomPresetsLabel); - - String[][] randomPresets = { - {"Select a preset...", "0", "0", "0", "0"}, - {"1-5m", "0", "1", "0", "5"}, - {"5-10m", "0", "5", "0", "10"}, - {"10-15m", "0", "10", "0", "15"}, - {"15-30m", "0", "15", "0", "30"}, - {"30-60m", "0", "30", "1", "0"}, - {"45m-1h15m", "0", "45", "1", "15"}, - {"1-2h", "1", "0", "2", "0"}, - {"1-3h", "1", "0", "3", "0"}, - {"2-3h", "2", "0", "3", "0"}, - {"2-4h", "2", "0", "4", "0"}, - }; - - randomPresetComboBox = new JComboBox<>(); - for (String[] preset : randomPresets) { - randomPresetComboBox.addItem(preset[0]); - } - randomPresetComboBox.setBackground(ColorScheme.DARK_GRAY_COLOR); - randomPresetComboBox.setForeground(Color.WHITE); - randomPresetComboBox.setPreferredSize(new Dimension(150, randomPresetComboBox.getPreferredSize().height)); - randomPresetComboBox.addActionListener(e -> { - int selectedIndex = randomPresetComboBox.getSelectedIndex(); - if (selectedIndex > 0) { // Skip the "Select a preset..." option - randomizedRadioButton.setSelected(true); - updatePanelVisibility(); - - // Apply preset values to min - String[] preset = randomPresets[selectedIndex]; - minHoursSpinner.setValue(Integer.parseInt(preset[1])); - minMinutesSpinner.setValue(Integer.parseInt(preset[2])); - - // Apply preset values to max - maxHoursSpinner.setValue(Integer.parseInt(preset[3])); - maxMinutesSpinner.setValue(Integer.parseInt(preset[4])); - - // Set fixed to average - setFixedFromMinMax(); - - // Notify listeners - notifyChangeListeners(); - } - }); - randomPresetPanel.add(randomPresetComboBox); - - randomPresetPanel.setVisible(false); - add(randomPresetPanel); - } - - // Add toggle listeners - fixedRadioButton.addActionListener(e -> { - updatePanelVisibility(); - notifyChangeListeners(); - }); - - randomizedRadioButton.addActionListener(e -> { - updatePanelVisibility(); - - // Initialize min/max values if switching to random - if (randomizedRadioButton.isSelected()) { - setMinMaxFromFixed(); - } else { - setFixedFromMinMax(); - } - - notifyChangeListeners(); - }); - - // Add spinner change listeners - hoursSpinner.addChangeListener(e -> { - if (fixedRadioButton.isSelected()) { - setMinMaxFromFixed(); - } - notifyChangeListeners(); - }); - - minutesSpinner.addChangeListener(e -> { - if (fixedRadioButton.isSelected()) { - setMinMaxFromFixed(); - } - notifyChangeListeners(); - }); - - // Add min/max validation - minHoursSpinner.addChangeListener(e -> { - validateMinMaxInterval(true); - if (randomizedRadioButton.isSelected()) { - setFixedFromMinMax(); - } - notifyChangeListeners(); - }); - - minMinutesSpinner.addChangeListener(e -> { - validateMinMaxInterval(true); - if (randomizedRadioButton.isSelected()) { - setFixedFromMinMax(); - } - notifyChangeListeners(); - }); - - maxHoursSpinner.addChangeListener(e -> { - validateMinMaxInterval(false); - if (randomizedRadioButton.isSelected()) { - setFixedFromMinMax(); - } - notifyChangeListeners(); - }); - - maxMinutesSpinner.addChangeListener(e -> { - validateMinMaxInterval(false); - if (randomizedRadioButton.isSelected()) { - setFixedFromMinMax(); - } - notifyChangeListeners(); - }); - } - - /** - * Updates panel visibility based on the selected radio button - */ - private void updatePanelVisibility() { - boolean useFixed = fixedRadioButton.isSelected(); - fixedPanel.setVisible(useFixed); - randomizedPanel.setVisible(!useFixed); - - if (presetPanel != null && randomPresetPanel != null) { - presetPanel.setVisible(useFixed); - randomPresetPanel.setVisible(!useFixed); - - // Reset combo boxes to first item when switching modes - if (useFixed && fixedPresetComboBox != null) { - fixedPresetComboBox.setSelectedIndex(0); - } else if (!useFixed && randomPresetComboBox != null) { - randomPresetComboBox.setSelectedIndex(0); - } - } - - revalidate(); - repaint(); - } - - /** - * Validates that min <= max for the interval and adjusts if needed - * - * @param isMinUpdated Whether the min value was updated (to determine which value to adjust) - */ - private void validateMinMaxInterval(boolean isMinUpdated) { - int minHours = (Integer) minHoursSpinner.getValue(); - int minMinutes = (Integer) minMinutesSpinner.getValue(); - int maxHours = (Integer) maxHoursSpinner.getValue(); - int maxMinutes = (Integer) maxMinutesSpinner.getValue(); - - int minTotalMinutes = minHours * 60 + minMinutes; - int maxTotalMinutes = maxHours * 60 + maxMinutes; - - if (isMinUpdated) { - // If min was updated and exceeds max, adjust max - if (minTotalMinutes > maxTotalMinutes) { - maxHoursSpinner.setValue(minHours); - maxMinutesSpinner.setValue(minMinutes); - } - } else { - // If max was updated and is less than min, adjust min - if (maxTotalMinutes < minTotalMinutes) { - minHoursSpinner.setValue(maxHours); - minMinutesSpinner.setValue(maxMinutes); - } - } - } - - /** - * Sets min/max values based on the fixed value (with some variation) - */ - private void setMinMaxFromFixed() { - int hours = (Integer) hoursSpinner.getValue(); - int minutes = (Integer) minutesSpinner.getValue(); - int totalMinutes = hours * 60 + minutes; - - // Set min to ~70% of fixed value - int minTotalMinutes = Math.max(1, (int)(totalMinutes * 0.7)); - minHoursSpinner.setValue(minTotalMinutes / 60); - minMinutesSpinner.setValue(minTotalMinutes % 60); - - // Set max to ~130% of fixed value - int maxTotalMinutes = (int)(totalMinutes * 1.3); - maxHoursSpinner.setValue(maxTotalMinutes / 60); - maxMinutesSpinner.setValue(maxTotalMinutes % 60); - } - - /** - * Sets the fixed value based on the average of min and max - */ - private void setFixedFromMinMax() { - int minHours = (Integer) minHoursSpinner.getValue(); - int minMinutes = (Integer) minMinutesSpinner.getValue(); - int maxHours = (Integer) maxHoursSpinner.getValue(); - int maxMinutes = (Integer) maxMinutesSpinner.getValue(); - - int minTotalMinutes = minHours * 60 + minMinutes; - int maxTotalMinutes = maxHours * 60 + maxMinutes; - int avgTotalMinutes = (minTotalMinutes + maxTotalMinutes) / 2; - - hoursSpinner.setValue(avgTotalMinutes / 60); - minutesSpinner.setValue(avgTotalMinutes % 60); - } - - /** - * Creates an IntervalCondition based on the current settings - * - * @return The configured IntervalCondition - */ - public IntervalCondition createIntervalCondition() { - boolean useFixed = fixedRadioButton.isSelected(); - - if (useFixed) { - // Get fixed duration - int hours = (Integer) hoursSpinner.getValue(); - int minutes = (Integer) minutesSpinner.getValue(); - Duration duration = Duration.ofHours(hours).plusMinutes(minutes); - - return new IntervalCondition(duration); - } else { - // Get min/max durations for randomized interval - int minHours = (Integer) minHoursSpinner.getValue(); - int minMinutes = (Integer) minMinutesSpinner.getValue(); - int maxHours = (Integer) maxHoursSpinner.getValue(); - int maxMinutes = (Integer) maxMinutesSpinner.getValue(); - - Duration minDuration = Duration.ofHours(minHours).plusMinutes(minMinutes); - Duration maxDuration = Duration.ofHours(maxHours).plusMinutes(maxMinutes); - - return IntervalCondition.createRandomized(minDuration, maxDuration); - } - } - - /** - * Configures this panel based on an existing IntervalCondition - * - * @param condition The interval condition to use for configuration - */ - public void setIntervalCondition(IntervalCondition condition) { - if (condition == null) { - return; - } - - // Check if this is a randomized min-max interval or a fixed interval - boolean isRandomized = condition.isRandomize(); - - if (isRandomized) { - // Set to randomized mode - randomizedRadioButton.setSelected(true); - - // Get duration values for min interval - long minTotalMinutes = condition.getMinInterval().toMinutes(); - long minHours = minTotalMinutes / 60; - long minMinutes = minTotalMinutes % 60; - - // Get duration values for max interval - long maxTotalMinutes = condition.getMaxInterval().toMinutes(); - long maxHours = maxTotalMinutes / 60; - long maxMinutes = maxTotalMinutes % 60; - - // Set values on spinners - minHoursSpinner.setValue((int)minHours); - minMinutesSpinner.setValue((int)minMinutes); - maxHoursSpinner.setValue((int)maxHours); - maxMinutesSpinner.setValue((int)maxMinutes); - - // Also update the fixed spinner with the average value - long avgTotalMinutes = (minTotalMinutes + maxTotalMinutes) / 2; - hoursSpinner.setValue((int)(avgTotalMinutes / 60)); - minutesSpinner.setValue((int)(avgTotalMinutes % 60)); - } else { - // Use fixed mode - fixedRadioButton.setSelected(true); - - // Get duration values from the base interval - long totalMinutes = condition.getInterval().toMinutes(); - long hours = totalMinutes / 60; - long minutes = totalMinutes % 60; - - // Set values on fixed spinners - hoursSpinner.setValue((int)hours); - minutesSpinner.setValue((int)minutes); - - // Set min/max values based on fixed value - setMinMaxFromFixed(); - } - - // Update panel visibility - updatePanelVisibility(); - } - - /** - * Adds a change listener that will be notified when the interval configuration changes - * - * @param listener A consumer that receives the updated IntervalCondition - */ - public void addChangeListener(Consumer listener) { - changeListeners.add(listener); - } - - /** - * Notifies all change listeners with the current interval condition - */ - private void notifyChangeListeners() { - IntervalCondition condition = createIntervalCondition(); - for (Consumer listener : changeListeners) { - listener.accept(condition); - } - } - - /** - * Gets the current fixed hours value - */ - public int getFixedHours() { - return (Integer) hoursSpinner.getValue(); - } - - /** - * Gets the current fixed minutes value - */ - public int getFixedMinutes() { - return (Integer) minutesSpinner.getValue(); - } - - /** - * Gets whether randomized interval mode is selected - */ - public boolean isRandomized() { - return randomizedRadioButton.isSelected(); - } - - /** - * Sets the enabled state of all components - */ - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - fixedRadioButton.setEnabled(enabled); - randomizedRadioButton.setEnabled(enabled); - hoursSpinner.setEnabled(enabled && fixedRadioButton.isSelected()); - minutesSpinner.setEnabled(enabled && fixedRadioButton.isSelected()); - minHoursSpinner.setEnabled(enabled && randomizedRadioButton.isSelected()); - minMinutesSpinner.setEnabled(enabled && randomizedRadioButton.isSelected()); - maxHoursSpinner.setEnabled(enabled && randomizedRadioButton.isSelected()); - maxMinutesSpinner.setEnabled(enabled && randomizedRadioButton.isSelected()); - - // Update preset components if they exist - if (fixedPresetComboBox != null) { - fixedPresetComboBox.setEnabled(enabled); - } - - if (randomPresetComboBox != null) { - randomPresetComboBox.setEnabled(enabled); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/SingleDateTimePickerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/SingleDateTimePickerPanel.java deleted file mode 100644 index 29e68a739a1..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/SingleDateTimePickerPanel.java +++ /dev/null @@ -1,136 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.components; - -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import java.awt.*; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.util.function.Consumer; - -/** - * A panel for selecting a single date and time with presets - */ -public class SingleDateTimePickerPanel extends JPanel { - private final DateTimePickerPanel dateTimePicker; - private Consumer dateTimeChangeListener; - - public SingleDateTimePickerPanel() { - this(LocalDateTime.now().plusHours(1)); // Default to one hour from now - } - - public SingleDateTimePickerPanel(LocalDateTime initialDateTime) { - setLayout(new BorderLayout()); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setBorder(new EmptyBorder(5, 5, 5, 5)); - - dateTimePicker = new DateTimePickerPanel(initialDateTime.toLocalDate(), initialDateTime.toLocalTime()); - - // Create a main panel with title - JPanel mainPanel = new JPanel(new BorderLayout(0, 10)); - mainPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel titleLabel = new JLabel("Select Date and Time:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - - mainPanel.add(titleLabel, BorderLayout.NORTH); - mainPanel.add(dateTimePicker, BorderLayout.CENTER); - - // Add presets panel - JPanel presetsPanel = createPresetsPanel(); - mainPanel.add(presetsPanel, BorderLayout.SOUTH); - - add(mainPanel, BorderLayout.CENTER); - - // Set up change listener - dateTimePicker.setDateTimeChangeListener(this::notifyDateTimeChanged); - } - - private JPanel createPresetsPanel() { - JPanel panel = new JPanel(); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - panel.setBorder(new EmptyBorder(10, 0, 0, 0)); - - JLabel presetsLabel = new JLabel("Quick Presets:"); - presetsLabel.setForeground(Color.WHITE); - presetsLabel.setAlignmentX(Component.LEFT_ALIGNMENT); - panel.add(presetsLabel); - - // Flow panel for buttons - JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 5)); - buttonsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - buttonsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - - LocalDateTime now = LocalDateTime.now(); - - // Common time presets - addPresetButton(buttonsPanel, "In 1 hour", now.plusHours(1)); - addPresetButton(buttonsPanel, "In 3 hours", now.plusHours(3)); - addPresetButton(buttonsPanel, "Tomorrow", now.plusDays(1).withHour(9).withMinute(0)); - addPresetButton(buttonsPanel, "This evening", now.withHour(19).withMinute(0)); - addPresetButton(buttonsPanel, "Next week", now.plusWeeks(1)); - addPresetButton(buttonsPanel, "Next month", now.plusMonths(1)); - - panel.add(buttonsPanel); - return panel; - } - - private void addPresetButton(JPanel panel, String label, LocalDateTime dateTime) { - JButton button = new JButton(label); - button.setFocusPainted(false); - button.setBackground(ColorScheme.DARK_GRAY_COLOR); - button.setForeground(Color.WHITE); - button.setFont(FontManager.getRunescapeSmallFont()); - button.addActionListener(e -> setDateTime(dateTime)); - panel.add(button); - } - - private void notifyDateTimeChanged(LocalDateTime dateTime) { - if (dateTimeChangeListener != null) { - dateTimeChangeListener.accept(dateTime); - } - } - - public LocalDateTime getDateTime() { - return dateTimePicker.getDateTime(); - } - - public void setDateTime(LocalDateTime dateTime) { - if (dateTime == null) { - // Create a default time (1 hour from now) - LocalDateTime defaultDateTime = LocalDateTime.now().plusHours(1); - try { - // Try using individual setters if available - dateTimePicker.setDate(LocalDate.now()); - dateTimePicker.setTime( - Integer.valueOf(defaultDateTime.getHour()), - Integer.valueOf(defaultDateTime.getMinute()) - ); - } catch (Exception e) { - // Fall back to the combined setter if individual ones aren't available - dateTimePicker.setDateTime(defaultDateTime); - } - } else { - try { - // Try using individual setters if available - dateTimePicker.setDate(dateTime.toLocalDate()); - dateTimePicker.setTime( - Integer.valueOf(dateTime.getHour()), - Integer.valueOf(dateTime.getMinute()) - ); - } catch (Exception e) { - // Fall back to the combined setter - dateTimePicker.setDateTime(dateTime); - } - } - } - - public void setDateTimeChangeListener(Consumer listener) { - this.dateTimeChangeListener = listener; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/TimePickerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/TimePickerPanel.java deleted file mode 100644 index ed871675375..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/TimePickerPanel.java +++ /dev/null @@ -1,184 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.components; - -import net.runelite.client.ui.ColorScheme; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import java.awt.*; - -import java.time.LocalTime; -import java.time.format.DateTimeFormatter; -import java.util.function.Consumer; -import java.awt.event.FocusEvent; -import java.awt.event.FocusAdapter; -/** - * A custom time picker component with hours and minutes selection - */ -public class TimePickerPanel extends JPanel { - private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm"); - private final JTextField timeField; // Changed from JFormattedTextField - private LocalTime selectedTime; - private Consumer timeChangeListener; - - public TimePickerPanel() { - this(LocalTime.of(9, 0)); // Default to 9:00 - } - - public TimePickerPanel(LocalTime initialTime) { - this.selectedTime = initialTime; - setLayout(new BorderLayout(5, 0)); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setBorder(new EmptyBorder(0, 0, 0, 0)); - - // Create a regular text field instead of formatted - timeField = new JTextField(selectedTime.format(timeFormatter)); - timeField.setForeground(Color.WHITE); - timeField.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - timeField.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - BorderFactory.createEmptyBorder(2, 5, 2, 5))); - - // Button to show time picker popup - JButton timeButton = new JButton("🕒"); - timeButton.setFocusPainted(false); - timeButton.setForeground(Color.WHITE); - timeButton.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - timeButton.setPreferredSize(new Dimension(30, timeField.getPreferredSize().height)); - timeButton.addActionListener(e -> showTimePickerPopup()); - - add(timeField, BorderLayout.CENTER); - add(timeButton, BorderLayout.EAST); - - // Update time when text changes - timeField.addActionListener(e -> { - try { - String text = timeField.getText(); - LocalTime parsedTime = LocalTime.parse(text, timeFormatter); - setSelectedTime(parsedTime); - } catch (Exception ex) { - // Reset to current value if parsing fails - timeField.setText(selectedTime.format(timeFormatter)); - } - }); - - // Add a focus listener to validate when the field loses focus - timeField.addFocusListener(new FocusAdapter() { - @Override - public void focusLost(FocusEvent e) { - try { - String text = timeField.getText(); - LocalTime parsedTime = LocalTime.parse(text, timeFormatter); - setSelectedTime(parsedTime); - } catch (Exception ex) { - // Reset to current value if parsing fails - timeField.setText(selectedTime.format(timeFormatter)); - } - } - }); - } - - private void showTimePickerPopup() { - JPopupMenu popup = new JPopupMenu(); - popup.setBorder(BorderFactory.createLineBorder(ColorScheme.DARK_GRAY_COLOR)); - - JPanel contentPanel = new JPanel(); - contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); - contentPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); - - // Hour and minute spinners - JPanel timeControls = new JPanel(new FlowLayout()); - timeControls.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Hour spinner (0-23) - SpinnerNumberModel hourModel = new SpinnerNumberModel(selectedTime.getHour(), 0, 23, 1); - JSpinner hourSpinner = new JSpinner(hourModel); - JComponent hourEditor = new JSpinner.NumberEditor(hourSpinner, "00"); - hourSpinner.setEditor(hourEditor); - hourSpinner.setPreferredSize(new Dimension(60, 30)); - - // Minute spinner (0-59) - SpinnerNumberModel minuteModel = new SpinnerNumberModel(selectedTime.getMinute(), 0, 59, 1); - JSpinner minuteSpinner = new JSpinner(minuteModel); - JComponent minuteEditor = new JSpinner.NumberEditor(minuteSpinner, "00"); - minuteSpinner.setEditor(minuteEditor); - minuteSpinner.setPreferredSize(new Dimension(60, 30)); - - JLabel colonLabel = new JLabel(":"); - colonLabel.setForeground(Color.WHITE); - - timeControls.add(hourSpinner); - timeControls.add(colonLabel); - timeControls.add(minuteSpinner); - - // Common time presets - JPanel presets = new JPanel(new GridLayout(2, 3, 5, 5)); - presets.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - addTimePresetButton(presets, "00:00", popup); - addTimePresetButton(presets, "06:00", popup); - addTimePresetButton(presets, "09:00", popup); - addTimePresetButton(presets, "12:00", popup); - addTimePresetButton(presets, "18:00", popup); - addTimePresetButton(presets, "22:00", popup); - - // Apply button - JButton applyButton = new JButton("Apply"); - applyButton.setFocusPainted(false); - applyButton.setBackground(ColorScheme.PROGRESS_COMPLETE_COLOR); - applyButton.setForeground(Color.WHITE); - applyButton.addActionListener(e -> { - int hour = (Integer) hourSpinner.getValue(); - int minute = (Integer) minuteSpinner.getValue(); - setSelectedTime(LocalTime.of(hour, minute)); - popup.setVisible(false); - }); - - contentPanel.add(timeControls); - contentPanel.add(Box.createVerticalStrut(10)); - contentPanel.add(new JLabel("Presets:")); - contentPanel.add(presets); - contentPanel.add(Box.createVerticalStrut(10)); - contentPanel.add(applyButton); - - popup.add(contentPanel); - popup.show(this, 0, this.getHeight()); - } - - private void addTimePresetButton(JPanel panel, String timeText, JPopupMenu popup) { - JButton button = new JButton(timeText); - button.setFocusPainted(false); - button.setBackground(ColorScheme.DARK_GRAY_COLOR); - button.setForeground(Color.WHITE); - button.addActionListener(e -> { - setSelectedTime(LocalTime.parse(timeText)); - popup.setVisible(false); - }); - panel.add(button); - } - - public LocalTime getSelectedTime() { - return selectedTime; - } - - public void setSelectedTime(LocalTime time) { - this.selectedTime = time; - timeField.setText(time.format(timeFormatter)); - - if (timeChangeListener != null) { - timeChangeListener.accept(time); - } - } - - public void setTimeChangeListener(Consumer listener) { - this.timeChangeListener = listener; - } - - public void setEditable(boolean editable) { - timeField.setEditable(editable); - } - - public JTextField getTextField() { - return timeField; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/TimeRangePanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/TimeRangePanel.java deleted file mode 100644 index f2a5d232d8b..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/TimeRangePanel.java +++ /dev/null @@ -1,184 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.components; - -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import java.awt.*; -import java.time.LocalTime; -import java.util.function.BiConsumer; - -/** - * A panel for selecting a time range with start and end times - */ -public class TimeRangePanel extends JPanel { - private final TimePickerPanel startTimePicker; - private final TimePickerPanel endTimePicker; - private BiConsumer rangeChangeListener; - - public TimeRangePanel() { - this(LocalTime.of(9, 0), LocalTime.of(17, 0)); - } - - public TimeRangePanel(LocalTime startTime, LocalTime endTime) { - setLayout(new GridBagLayout()); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setBorder(new EmptyBorder(5, 5, 5, 5)); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 0; - gbc.anchor = GridBagConstraints.WEST; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.insets = new Insets(2, 2, 2, 2); - - JLabel startLabel = new JLabel("Start Time:"); - startLabel.setForeground(Color.WHITE); - startLabel.setFont(FontManager.getRunescapeSmallFont()); - add(startLabel, gbc); - - gbc.gridx = 1; - startTimePicker = new TimePickerPanel(startTime); - add(startTimePicker, gbc); - - gbc.gridx = 0; - gbc.gridy = 1; - JLabel endLabel = new JLabel("End Time:"); - endLabel.setForeground(Color.WHITE); - endLabel.setFont(FontManager.getRunescapeSmallFont()); - add(endLabel, gbc); - - gbc.gridx = 1; - endTimePicker = new TimePickerPanel(endTime); - add(endTimePicker, gbc); - - // Add common presets panel - gbc.gridx = 0; - gbc.gridy = 2; - gbc.gridwidth = 2; - gbc.insets = new Insets(10, 2, 2, 2); - - JPanel presetsPanel = createPresetsPanel(); - add(presetsPanel, gbc); - - // Add specialized time window presets - gbc.gridy = 3; - JPanel specialPresetsPanel = createSpecializedPresetsPanel(); - add(specialPresetsPanel, gbc); - - // Set up change listeners - startTimePicker.setTimeChangeListener(t -> notifyRangeChanged()); - endTimePicker.setTimeChangeListener(t -> notifyRangeChanged()); - } - - private JPanel createPresetsPanel() { - JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel presetsLabel = new JLabel("Quick Presets:"); - presetsLabel.setForeground(Color.WHITE); - panel.add(presetsLabel); - - // Common time range presets - addPresetButton(panel, "Morning (6-12)", LocalTime.of(6, 0), LocalTime.of(12, 0)); - addPresetButton(panel, "Afternoon (12-18)", LocalTime.of(12, 0), LocalTime.of(18, 0)); - addPresetButton(panel, "Evening (18-22)", LocalTime.of(18, 0), LocalTime.of(22, 0)); - addPresetButton(panel, "Night (22-6)", LocalTime.of(22, 0), LocalTime.of(6, 0)); - addPresetButton(panel, "Business Hours", LocalTime.of(9, 0), LocalTime.of(17, 0)); - - return panel; - } - - private JPanel createSpecializedPresetsPanel() { - JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel specialPresetsLabel = new JLabel("Special Presets:"); - specialPresetsLabel.setForeground(Color.WHITE); - panel.add(specialPresetsLabel); - - // Special presets for all day, start of day, and end of day - addSpecialPresetButton(panel, "All Day", LocalTime.of(0, 0), LocalTime.of(23, 59)); - addSpecialPresetButton(panel, "From Start of Day", LocalTime.of(0, 0), endTimePicker.getSelectedTime()); - addSpecialPresetButton(panel, "Until End of Day", startTimePicker.getSelectedTime(), LocalTime.of(23, 59)); - - return panel; - } - - private void addPresetButton(JPanel panel, String label, LocalTime start, LocalTime end) { - JButton button = new JButton(label); - button.setFocusPainted(false); - button.setBackground(ColorScheme.DARK_GRAY_COLOR); - button.setForeground(Color.WHITE); - button.setFont(FontManager.getRunescapeSmallFont()); - button.addActionListener(e -> { - startTimePicker.setSelectedTime(start); - endTimePicker.setSelectedTime(end); - }); - panel.add(button); - } - - private void addSpecialPresetButton(JPanel panel, String label, LocalTime start, LocalTime end) { - JButton button = new JButton(label); - button.setFocusPainted(false); - button.setBackground(ColorScheme.LIGHT_GRAY_COLOR); - button.setForeground(Color.BLACK); - button.setFont(FontManager.getRunescapeSmallFont()); - button.addActionListener(e -> { - if (label.equals("From Start of Day")) { - // Only update start time, keep current end time - startTimePicker.setSelectedTime(start); - // No need to update endTimePicker as we want to keep the current end time - } else if (label.equals("Until End of Day")) { - // Only update end time, keep current start time - endTimePicker.setSelectedTime(end); - // No need to update startTimePicker as we want to keep the current start time - } else { - // Update both times - startTimePicker.setSelectedTime(start); - endTimePicker.setSelectedTime(end); - } - }); - panel.add(button); - } - - private void notifyRangeChanged() { - if (rangeChangeListener != null) { - rangeChangeListener.accept(getStartTime(), getEndTime()); - } - } - - public LocalTime getStartTime() { - return startTimePicker.getSelectedTime(); - } - - public LocalTime getEndTime() { - return endTimePicker.getSelectedTime(); - } - - public void setStartTime(LocalTime time) { - startTimePicker.setSelectedTime(time); - } - - public void setEndTime(LocalTime time) { - endTimePicker.setSelectedTime(time); - } - - public void setRangeChangeListener(BiConsumer listener) { - this.rangeChangeListener = listener; - } - - public void setEditable(boolean editable) { - startTimePicker.setEditable(editable); - endTimePicker.setEditable(editable); - } - - public TimePickerPanel getStartTimePicker() { - return startTimePicker; - } - - public TimePickerPanel getEndTimePicker() { - return endTimePicker; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/layout/DynamicFlowLayout.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/layout/DynamicFlowLayout.java deleted file mode 100644 index 8fcf38cd307..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/layout/DynamicFlowLayout.java +++ /dev/null @@ -1,300 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.layout; - -import java.awt.*; -import javax.swing.JComponent; - -/** - * A custom FlowLayout that provides better control over wrapping, spacing, and dynamic sizing. - * This layout automatically adjusts component arrangements based on available space and content. - * - * Features: - * - Intelligent wrapping when space is limited - * - Dynamic spacing based on container size - * - Better minimum size calculations - * - Content-aware layout decisions - * - * @author Vox - */ -public class DynamicFlowLayout extends FlowLayout { - - private int minRowHeight = 120; - private int preferredRowHeight = 140; - private boolean adaptiveSpacing = true; - - /** - * Creates a new DynamicFlowLayout with default settings - */ - public DynamicFlowLayout() { - super(FlowLayout.CENTER, 8, 5); - } - - /** - * Creates a new DynamicFlowLayout with specified alignment and spacing - * - * @param align the alignment value - * @param hgap the horizontal gap between components - * @param vgap the vertical gap between components - */ - public DynamicFlowLayout(int align, int hgap, int vgap) { - super(align, hgap, vgap); - } - - /** - * Sets the minimum row height for components - * - * @param minRowHeight minimum height in pixels - */ - public void setMinRowHeight(int minRowHeight) { - this.minRowHeight = minRowHeight; - } - - /** - * Sets the preferred row height for components - * - * @param preferredRowHeight preferred height in pixels - */ - public void setPreferredRowHeight(int preferredRowHeight) { - this.preferredRowHeight = preferredRowHeight; - } - - /** - * Enables or disables adaptive spacing based on container size - * - * @param adaptiveSpacing true to enable adaptive spacing - */ - public void setAdaptiveSpacing(boolean adaptiveSpacing) { - this.adaptiveSpacing = adaptiveSpacing; - } - - @Override - public Dimension preferredLayoutSize(Container target) { - synchronized (target.getTreeLock()) { - Dimension dim = new Dimension(0, 0); - int nmembers = target.getComponentCount(); - boolean firstVisibleComponent = true; - - int maxWidth = 0; - int currentRowWidth = 0; - int currentRowHeight = 0; - int totalHeight = 0; - - // Calculate container width for wrapping decisions - int containerWidth = target.getWidth(); - if (containerWidth <= 0) { - containerWidth = Integer.MAX_VALUE; // No wrapping if width unknown - } - - for (int i = 0; i < nmembers; i++) { - Component m = target.getComponent(i); - if (m.isVisible()) { - Dimension d = m.getPreferredSize(); - - // Check if we need to wrap to next row - int neededWidth = currentRowWidth + (firstVisibleComponent ? 0 : getHgap()) + d.width; - - if (!firstVisibleComponent && neededWidth > containerWidth - getHgap() * 2) { - // Wrap to next row - maxWidth = Math.max(maxWidth, currentRowWidth); - totalHeight += currentRowHeight + getVgap(); - currentRowWidth = d.width; - currentRowHeight = d.height; - } else { - // Add to current row - if (!firstVisibleComponent) { - currentRowWidth += getHgap(); - } - currentRowWidth += d.width; - currentRowHeight = Math.max(currentRowHeight, d.height); - firstVisibleComponent = false; - } - } - } - - // Add the last row - if (currentRowWidth > 0) { - maxWidth = Math.max(maxWidth, currentRowWidth); - totalHeight += currentRowHeight; - } - - Insets insets = target.getInsets(); - dim.width = maxWidth + insets.left + insets.right + getHgap() * 2; - dim.height = Math.max(totalHeight + insets.top + insets.bottom + getVgap() * 2, - preferredRowHeight); - - return dim; - } - } - - @Override - public Dimension minimumLayoutSize(Container target) { - synchronized (target.getTreeLock()) { - Dimension dim = new Dimension(0, 0); - int nmembers = target.getComponentCount(); - - if (nmembers > 0) { - // Find the widest component for minimum width - int maxComponentWidth = 0; - for (int i = 0; i < nmembers; i++) { - Component m = target.getComponent(i); - if (m.isVisible()) { - Dimension d = m.getMinimumSize(); - maxComponentWidth = Math.max(maxComponentWidth, d.width); - } - } - - Insets insets = target.getInsets(); - dim.width = maxComponentWidth + insets.left + insets.right + getHgap() * 2; - dim.height = minRowHeight + insets.top + insets.bottom + getVgap() * 2; - } - - return dim; - } - } - - @Override - public void layoutContainer(Container target) { - synchronized (target.getTreeLock()) { - Insets insets = target.getInsets(); - int maxwidth = target.getWidth() - (insets.left + insets.right + getHgap() * 2); - int nmembers = target.getComponentCount(); - int x = 0, y = insets.top + getVgap(); - int rowh = 0, start = 0; - - boolean ltr = target.getComponentOrientation().isLeftToRight(); - - // Adaptive spacing based on container width - int effectiveHgap = getHgap(); - if (adaptiveSpacing && maxwidth > 0) { - int totalComponentWidth = 0; - int visibleComponents = 0; - - for (int i = 0; i < nmembers; i++) { - Component m = target.getComponent(i); - if (m.isVisible()) { - totalComponentWidth += m.getPreferredSize().width; - visibleComponents++; - } - } - - if (visibleComponents > 1) { - int availableSpaceForGaps = maxwidth - totalComponentWidth; - int optimalGap = Math.max(5, availableSpaceForGaps / (visibleComponents + 1)); - effectiveHgap = Math.min(effectiveHgap, optimalGap); - } - } - - for (int i = 0; i < nmembers; i++) { - Component m = target.getComponent(i); - if (m.isVisible()) { - Dimension d = m.getPreferredSize(); - m.setSize(d.width, d.height); - - if ((x == 0) || ((x + d.width) <= maxwidth)) { - if (x > 0) { - x += effectiveHgap; - } - x += d.width; - rowh = Math.max(rowh, d.height); - } else { - rowh = moveComponents(target, insets.left + effectiveHgap, y, - maxwidth - x, rowh, start, i, ltr); - x = d.width; - y += getVgap() + rowh; - rowh = d.height; - start = i; - } - } - } - - moveComponents(target, insets.left + effectiveHgap, y, maxwidth - x, - rowh, start, nmembers, ltr); - } - } - - /** - * Centers the elements in the specified row, if there is any slack. - * - * @param target the component which needs to be moved - * @param x the x coordinate - * @param y the y coordinate - * @param width the width dimensions - * @param height the height dimensions - * @param rowStart the beginning of the row - * @param rowEnd the the ending of the row - * @param useBaseline Whether or not to align on baseline. - * @param ascent Ascent for the components. This is only valid if useBaseline is true. - * @param descent Ascent for the components. This is only valid if useBaseline is true. - * @return actual row height - */ - private int moveComponents(Container target, int x, int y, int width, int height, - int rowStart, int rowEnd, boolean ltr) { - switch (getAlignment()) { - case LEFT: - x += ltr ? 0 : width; - break; - case CENTER: - x += width / 2; - break; - case RIGHT: - x += ltr ? width : 0; - break; - case LEADING: - break; - case TRAILING: - x += width; - break; - } - - int maxAscent = 0; - int maxDescent = 0; - boolean useBaseline = getAlignOnBaseline(); - int[] ascent = null; - int[] descent = null; - - if (useBaseline) { - ascent = new int[rowEnd - rowStart]; - descent = new int[rowEnd - rowStart]; - for (int i = rowStart; i < rowEnd; i++) { - Component m = target.getComponent(i); - if (m.isVisible()) { - if (m instanceof JComponent) { - JComponent jc = (JComponent) m; - int baseline = jc.getBaseline(m.getWidth(), m.getHeight()); - if (baseline >= 0) { - ascent[i - rowStart] = baseline; - descent[i - rowStart] = m.getHeight() - baseline; - } else { - ascent[i - rowStart] = -1; - } - } else { - ascent[i - rowStart] = -1; - } - if (ascent[i - rowStart] >= 0) { - maxAscent = Math.max(maxAscent, ascent[i - rowStart]); - maxDescent = Math.max(maxDescent, descent[i - rowStart]); - } - } - } - height = Math.max(maxAscent + maxDescent, height); - } - - for (int i = rowStart; i < rowEnd; i++) { - Component m = target.getComponent(i); - if (m.isVisible()) { - int cy; - if (useBaseline && ascent != null && ascent[i - rowStart] >= 0) { - cy = y + maxAscent - ascent[i - rowStart]; - } else { - cy = y + (height - m.getHeight()) / 2; - } - if (ltr) { - m.setLocation(x, cy); - } else { - m.setLocation(target.getWidth() - x - m.getWidth(), cy); - } - x += m.getWidth() + getHgap(); - } - } - return height; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/util/SchedulerUIUtils.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/util/SchedulerUIUtils.java deleted file mode 100644 index c5706b81500..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/util/SchedulerUIUtils.java +++ /dev/null @@ -1,155 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.util; - -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerConfig; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.util.security.LoginManager; -import net.runelite.client.ui.ColorScheme; - -import javax.swing.*; -import javax.swing.border.CompoundBorder; -import javax.swing.border.EmptyBorder; -import javax.swing.border.LineBorder; -import javax.swing.plaf.basic.BasicComboBoxRenderer; -import javax.swing.plaf.basic.BasicComboBoxUI; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.util.function.Consumer; - -import lombok.extern.slf4j.Slf4j; - -/** - * Utility class for consistent UI styling - */ -@Slf4j -public class SchedulerUIUtils { - /** - * Shows a dialog when user attempts to log into a members world without membership. - * The dialog runs on a separate thread to avoid blocking the client thread. - * - * @param currentPlugin The current plugin being run, can be null - * @param config The scheduler configuration - * @param resultCallback A callback that receives the dialog result: - * - true if the user chose to switch to free worlds - * - false if the user chose not to switch or the dialog timed out - */ - public static void showNonMemberWorldDialog(PluginScheduleEntry currentPlugin, SchedulerConfig config, Consumer resultCallback) { - Microbot.getClientThread().runOnSeperateThread(() -> { - // Create dialog with timeout - final JOptionPane optionPane = new JOptionPane( - "You do not have membership and tried to log into a members world.\n" + - "Would you like to switch to free worlds for this login?", - JOptionPane.QUESTION_MESSAGE, - JOptionPane.YES_NO_OPTION); - - final JDialog dialog = optionPane.createDialog("Membership Required"); - - // 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("Membership Required (Timeout: " + remainingSeconds + "s)"); - } else { - dialog.setTitle("Membership Required (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; - - if (result == JOptionPane.YES_OPTION) { - // User wants to switch to free worlds - if (Microbot.getConfigManager() != null) { - Microbot.getConfigManager().setConfiguration("AutoLoginConfig", "World", - LoginManager.getRandomWorld(false)); - Microbot.getConfigManager().setConfiguration("PluginScheduler", "worldType", 0); - } - // Notify caller that user chose to switch to free worlds - resultCallback.accept(true); - } else { - // User chose not to switch to free worlds or dialog timed out - log.info("Login to member world canceled"); - // Notify caller that user chose not to switch or timed out - resultCallback.accept(false); - } - return null; - }); - } - - /** - * Applies consistent styling to a JComboBox - */ - public static void styleComboBox(JComboBox comboBox) { - // Set colors - comboBox.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - comboBox.setForeground(Color.WHITE); - comboBox.setFont(comboBox.getFont().deriveFont(Font.PLAIN)); - - // Add a visible border to make it stand out - comboBox.setBorder(new CompoundBorder( - new LineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - new EmptyBorder(2, 4, 2, 0) - )); - - // Custom renderer for dropdown items - comboBox.setRenderer(new BasicComboBoxRenderer() { - @Override - public Component getListCellRendererComponent(JList list, Object value, - int index, boolean isSelected, boolean cellHasFocus) { - super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - - if (isSelected) { - setBackground(ColorScheme.BRAND_ORANGE); - setForeground(Color.WHITE); - } else { - setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - setForeground(Color.WHITE); - } - - // Add some padding - setBorder(new EmptyBorder(2, 5, 2, 5)); - return this; - } - }); - - // Use a custom UI to improve the arrow button appearance - comboBox.setUI(new BasicComboBoxUI() { - @Override - protected JButton createArrowButton() { - JButton button = super.createArrowButton(); - button.setBackground(ColorScheme.MEDIUM_GRAY_COLOR); - button.setBorder(BorderFactory.createEmptyBorder()); - return button; - } - }); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/util/UIUtils.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/util/UIUtils.java deleted file mode 100644 index d25e0aaaddd..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/util/UIUtils.java +++ /dev/null @@ -1,515 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.util; - -import net.runelite.client.plugins.microbot.pluginscheduler.ui.layout.DynamicFlowLayout; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import java.awt.*; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; - -/** - * Utility class for creating dynamic, responsive UI panels and components. - * Provides static methods for creating adaptive layouts and components that - * automatically adjust to content length and window size. - * - * Features: - * - Dynamic section creation with adaptive sizing - * - Text-aware component sizing - * - Responsive layouts with automatic wrapping - * - Consistent styling across components - * - * @author Vox - */ -public class UIUtils { - - // Default dimensions - further reduced heights for more compact layout - private static final Dimension DEFAULT_SECTION_MIN_SIZE = new Dimension(100, 60); - private static final Dimension DEFAULT_SECTION_PREF_SIZE = new Dimension(120, 70); - private static final int DEFAULT_HORIZONTAL_GAP = 6; - - /** - * Creates a dynamic plugin information panel that adapts to content and window size - * - * @param title the title for the panel - * @param sections array of section panels to include - * @param bottomPanel optional bottom panel (can be null) - * @return configured wrapper panel with scrolling and dynamic sizing - */ - public static JPanel createDynamicInfoPanel(String title, JPanel[] sections, JPanel bottomPanel) { - JPanel wrapperPanel = new JPanel(new BorderLayout()); - wrapperPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(1, 1, 1, 1, ColorScheme.DARK_GRAY_COLOR), - BorderFactory.createEmptyBorder(2, 2, 2, 2) // Reduced from 5,5,5,5 for tighter spacing - ), - title, - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - wrapperPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Create main content panel with dynamic layout - JPanel contentPanel = new JPanel(); - DynamicFlowLayout layout = new DynamicFlowLayout(); - layout.setMinRowHeight(60); // Further reduced from 80 to match smaller sections - layout.setPreferredRowHeight(70); // Further reduced from 90 to match smaller sections - layout.setAdaptiveSpacing(true); - contentPanel.setLayout(layout); - contentPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Add sections to content panel - if (sections != null) { - for (JPanel section : sections) { - if (section != null) { - contentPanel.add(section); - } - } - } - - // Create main container with proper layout - JPanel mainContainer = new JPanel(new BorderLayout()); - mainContainer.setBackground(ColorScheme.DARKER_GRAY_COLOR); - mainContainer.add(contentPanel, BorderLayout.CENTER); - - if (bottomPanel != null) { - mainContainer.add(bottomPanel, BorderLayout.SOUTH); - } - - // Wrap in scroll pane with both scrollbars as needed - JScrollPane scrollPane = new JScrollPane(mainContainer); - scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); - scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - scrollPane.setBorder(null); - scrollPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - scrollPane.getViewport().setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Add component listener for dynamic resizing - wrapperPanel.addComponentListener(new ComponentAdapter() { - @Override - public void componentResized(ComponentEvent e) { - SwingUtilities.invokeLater(() -> { - adjustSectionSizes(contentPanel); - contentPanel.revalidate(); - contentPanel.repaint(); - }); - } - }); - - wrapperPanel.add(scrollPane, BorderLayout.CENTER); - return wrapperPanel; - } - - /** - * Creates an adaptive section panel that resizes based on content length - * - * @param title the title for the section - * @return configured section panel - */ - public static JPanel createAdaptiveSection(String title) { - return createAdaptiveSection(title, DEFAULT_SECTION_MIN_SIZE, DEFAULT_SECTION_PREF_SIZE); - } - - /** - * Creates an adaptive section panel with custom dimensions - * - * @param title the title for the section - * @param minSize minimum size for the section - * @param prefSize preferred size for the section - * @return configured section panel - */ - public static JPanel createAdaptiveSection(String title, Dimension minSize, Dimension prefSize) { - JPanel panel = new JPanel(new GridBagLayout()); - panel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(ColorScheme.DARK_GRAY_COLOR), - BorderFactory.createEmptyBorder(2, 2, 2, 2) // Further reduced padding from 4,4,4,4 - ), - title, - TitledBorder.CENTER, - TitledBorder.TOP, - FontManager.getRunescapeSmallFont(), - Color.LIGHT_GRAY - )); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Set sizing constraints - allow the section to size to its content - panel.setMinimumSize(minSize); - // Don't enforce preferred size, let it size naturally to content - - return panel; - } - - /** - * Creates an adaptive label that adjusts its size based on content - * - * @param text the label text - * @return configured label - */ - public static JLabel createAdaptiveLabel(String text) { - JLabel label = new JLabel(text); - label.setForeground(Color.WHITE); - label.setFont(FontManager.getRunescapeSmallFont()); - return label; - } - - /** - * Creates an adaptive value label with HTML support for text wrapping - * - * @param text the label text - * @return configured label with HTML support - */ - public static JLabel createAdaptiveValueLabel(String text) { - JLabel label = new JLabel("" + text + ""); - label.setForeground(Color.WHITE); - label.setFont(FontManager.getRunescapeSmallFont()); - label.setHorizontalAlignment(SwingConstants.RIGHT); - label.setVerticalAlignment(SwingConstants.TOP); - return label; - } - - /** - * Creates an adaptive text area for multi-line content - * - * @param text initial text content - * @return configured text area - */ - public static JTextArea createAdaptiveTextArea(String text) { - JTextArea textArea = new JTextArea(text); - textArea.setLineWrap(true); - textArea.setWrapStyleWord(true); - textArea.setOpaque(false); - textArea.setEditable(false); - textArea.setFocusable(false); - textArea.setBackground(ColorScheme.DARKER_GRAY_COLOR); - textArea.setForeground(Color.WHITE); - textArea.setFont(FontManager.getRunescapeSmallFont()); - textArea.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); // Reduced from 2,2,2,2 - textArea.setRows(1); // Reduced from 2 rows to 1 for more compact display - return textArea; - } - - /** - * Creates a compact value label with smaller font - * - * @param text the initial text for the label - * @return configured compact value label - */ - public static JLabel createCompactValueLabel(String text) { - JLabel label = new JLabel(text); - label.setForeground(Color.WHITE); - label.setHorizontalAlignment(SwingConstants.RIGHT); - label.setFont(FontManager.getRunescapeSmallFont()); - return label; - } - - /** - * Creates a standard value label with regular font - * - * @param text the initial text for the label - * @return configured value label - */ - public static JLabel createValueLabel(String text) { - JLabel label = new JLabel(text); - label.setForeground(Color.WHITE); - label.setHorizontalAlignment(SwingConstants.RIGHT); - label.setFont(FontManager.getRunescapeFont()); - return label; - } - - /** - * Creates an info panel with titled border for status displays - * - * @param title the title for the panel border - * @return configured info panel with GridBagLayout - */ - public static JPanel createInfoPanel(String title) { - JPanel panel = new JPanel(new GridBagLayout()); - panel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(1, 1, 1, 1, ColorScheme.DARK_GRAY_COLOR), - BorderFactory.createEmptyBorder(5, 5, 5, 5) - ), - title, - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - return panel; - } - - /** - * Creates GridBagConstraints for label-value layouts - * - * @param x grid x position (0 for labels, 1 for values) - * @param y grid y position - * @return configured GridBagConstraints - */ - public static GridBagConstraints createGbc(int x, int y) { - GridBagConstraints gbc = new GridBagConstraints(); - gbc.gridx = x; - gbc.gridy = y; - gbc.gridwidth = 1; - gbc.gridheight = 1; - gbc.weightx = (x == 0) ? 0.0 : 1.0; // Labels (x=0) don't expand, values (x=1) do - gbc.weighty = 0.0; // Changed to 0.0 to prevent vertical compression - gbc.anchor = (x == 0) ? GridBagConstraints.WEST : GridBagConstraints.EAST; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.insets = new Insets(8, 4, 8, 4); // Increased vertical spacing - return gbc; - } - - /** - * Creates a label-value row with proper alignment - * - * @param labelText the text for the label - * @param valueLabel the value component - * @return configured row panel - */ - public static JPanel createLabelValueRow(String labelText, JLabel valueLabel) { - JPanel row = new JPanel(new BorderLayout()); - row.setBackground(ColorScheme.DARKER_GRAY_COLOR); - row.setMaximumSize(new Dimension(Integer.MAX_VALUE, 20)); - - JLabel label = new JLabel(labelText); - label.setForeground(Color.WHITE); - label.setFont(FontManager.getRunescapeSmallFont()); - - row.add(label, BorderLayout.WEST); - row.add(valueLabel, BorderLayout.EAST); - - return row; - } - - /** - * Adds content to a section using standard GridBagLayout configuration - * - * @param section the section panel to add content to - * @param rows array of LabelValuePair objects representing the rows - */ - public static void addContentToSection(JPanel section, LabelValuePair[] rows) { - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(1, 1, 1, 1); // Further reduced from 2,2,2,2 for tighter spacing - gbc.anchor = GridBagConstraints.WEST; - - for (int i = 0; i < rows.length; i++) { - LabelValuePair row = rows[i]; - - // Label column - gbc.gridx = 0; - gbc.gridy = i; - gbc.weightx = 0.3; - gbc.fill = GridBagConstraints.NONE; - section.add(createAdaptiveLabel(row.getLabel()), gbc); - - // Value column - gbc.gridx = 1; - gbc.weightx = 0.7; - gbc.fill = GridBagConstraints.HORIZONTAL; - section.add(row.getValueComponent(), gbc); - } - - // No vertical glue - let the section size naturally to its content - } - - /** - * Creates a dynamic bottom panel for progress bars and status information - * - * @param progressBar the progress bar component (can be null) - * @param statusTextArea the status text area component (can be null) - * @return configured bottom panel - */ - public static JPanel createDynamicBottomPanel(JProgressBar progressBar, JTextArea statusTextArea) { - JPanel bottomPanel = new JPanel(); - bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.Y_AXIS)); - bottomPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - bottomPanel.setBorder(new EmptyBorder(0, 0, 0, 0)); // Removed all padding for tighter spacing - - if (progressBar != null) { - // Progress panel - JPanel progressPanel = new JPanel(new BorderLayout()); - progressPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - progressPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 25)); - - JLabel progressLabel = new JLabel("Progress:"); - progressLabel.setForeground(Color.WHITE); - progressLabel.setFont(FontManager.getRunescapeSmallFont()); - - progressPanel.add(progressLabel, BorderLayout.WEST); - progressPanel.add(Box.createHorizontalStrut(5), BorderLayout.CENTER); - progressPanel.add(progressBar, BorderLayout.CENTER); - - bottomPanel.add(progressPanel); - bottomPanel.add(Box.createVerticalStrut(1)); // Further reduced spacing from 2 to 1 - } - - if (statusTextArea != null) { - // Status panel - JPanel statusPanel = new JPanel(new BorderLayout()); - statusPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - statusPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 60)); - - JLabel statusLabel = new JLabel("Status:"); - statusLabel.setForeground(Color.WHITE); - statusLabel.setFont(FontManager.getRunescapeSmallFont()); - - statusPanel.add(statusLabel, BorderLayout.NORTH); - statusPanel.add(Box.createVerticalStrut(3), BorderLayout.CENTER); - statusPanel.add(statusTextArea, BorderLayout.CENTER); - - bottomPanel.add(statusPanel); - } - - return bottomPanel; - } - - /** - * Dynamically adjusts section sizes based on content and available space - * - * @param contentPanel the panel containing the sections - */ - public static void adjustSectionSizes(JPanel contentPanel) { - if (contentPanel.getComponentCount() == 0) return; - - int availableWidth = contentPanel.getWidth(); - if (availableWidth <= 0) return; - - int minSectionWidth = 140; - int componentCount = contentPanel.getComponentCount(); - int totalMinWidth = minSectionWidth * componentCount + (DEFAULT_HORIZONTAL_GAP * (componentCount + 1)); - - if (availableWidth >= totalMinWidth) { - // Enough space for all sections horizontally - int sectionWidth = Math.max(minSectionWidth, (availableWidth - (DEFAULT_HORIZONTAL_GAP * (componentCount + 1))) / componentCount); - - for (int i = 0; i < componentCount; i++) { - Component comp = contentPanel.getComponent(i); - if (comp instanceof JPanel) { - JPanel section = (JPanel) comp; - // Calculate content-based height - int contentHeight = calculateOptimalSectionHeight(section); - Dimension sectionSize = new Dimension(sectionWidth, contentHeight); - section.setPreferredSize(sectionSize); - - // Adjust content based on component's content - adjustSectionContentSize(section); - } - } - } else { - // Not enough space, use minimum widths and let layout handle wrapping - for (int i = 0; i < componentCount; i++) { - Component comp = contentPanel.getComponent(i); - if (comp instanceof JPanel) { - JPanel section = (JPanel) comp; - // Calculate content-based height - int contentHeight = calculateOptimalSectionHeight(section); - Dimension sectionSize = new Dimension(minSectionWidth, contentHeight); - section.setPreferredSize(sectionSize); - - adjustSectionContentSize(section); - } - } - } - } - - /** - * Adjusts individual section size based on its content length - * - * @param section the section panel to adjust - */ - private static void adjustSectionContentSize(JPanel section) { - // Find labels in the section and check their text length - Component[] components = section.getComponents(); - int maxTextLength = 0; - - for (Component comp : components) { - if (comp instanceof JLabel) { - JLabel label = (JLabel) comp; - String text = label.getText(); - if (text != null) { - // Remove HTML tags for length calculation - String plainText = text.replaceAll("<[^>]*>", ""); - maxTextLength = Math.max(maxTextLength, plainText.length()); - } - } - } - - Dimension currentSize = section.getPreferredSize(); - int newWidth = currentSize.width; - - // Adjust width based on longest text length - if (maxTextLength > 25) { - newWidth = Math.max(currentSize.width, 200); - } else if (maxTextLength > 20) { - newWidth = Math.max(currentSize.width, 180); - } else if (maxTextLength > 15) { - newWidth = Math.max(currentSize.width, 160); - } else { - newWidth = Math.max(140, currentSize.width); - } - - section.setPreferredSize(new Dimension(newWidth, currentSize.height)); - } - - /** - * Calculates the optimal height for a section based on its content - * - * @param section the section panel to calculate height for - * @return optimal height in pixels - */ - private static int calculateOptimalSectionHeight(JPanel section) { - // Count components and calculate based on content - Component[] components = section.getComponents(); - int rows = 0; - int textAreaRows = 0; - - for (Component comp : components) { - if (comp instanceof JLabel) { - rows++; - } else if (comp instanceof JTextArea) { - JTextArea textArea = (JTextArea) comp; - textAreaRows += Math.max(1, textArea.getRows()); - } - } - - // Base height for the titled border and padding - int baseHeight = 25; // Reduced title bar space from 30 - - // Height per row of content (reduced for tighter layout) - int rowHeight = 16; // Further reduced from 18 for even tighter spacing - - // Calculate content height - int contentHeight = baseHeight + (rows * rowHeight) + (textAreaRows * rowHeight); - - // Minimum height to ensure usability, maximum to prevent excessive height - return Math.max(60, Math.min(contentHeight, 120)); - } - - /** - * Simple data class to hold label-value pairs for section content - */ - public static class LabelValuePair { - private final String label; - private final JComponent valueComponent; - - public LabelValuePair(String label, JComponent valueComponent) { - this.label = label; - this.valueComponent = valueComponent; - } - - public String getLabel() { - return label; - } - - public JComponent getValueComponent() { - return valueComponent; - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/util/PluginFilterUtil.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/util/PluginFilterUtil.java deleted file mode 100644 index 5d1e30edae5..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/util/PluginFilterUtil.java +++ /dev/null @@ -1,531 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.util; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.Plugin; -import net.runelite.client.plugins.PluginDescriptor; -import net.runelite.client.plugins.microbot.pluginscheduler.api.SchedulablePlugin; - -import java.lang.reflect.Field; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** - * Utility class for filtering and categorizing plugins based on their descriptors and metadata. - * Dynamically extracts creator information and tags without hardcoding. - */ -@Slf4j -public class PluginFilterUtil { - - // Primary filter categories - public static final String FILTER_ALL = "Show All"; - public static final String FILTER_INTERNAL = "Internal Plugins"; - public static final String FILTER_EXTERNAL = "External Plugins"; - public static final String FILTER_BY_CREATOR = "By Creator"; - public static final String FILTER_BY_TAGS = "By Tags"; - - // Pattern to extract creator prefixes from plugin names - private static final Pattern CREATOR_PATTERN = Pattern.compile("\\[([^\\]]+)\\]"); - - // OSRS Skill and Activity Groups - private static final Map> TAG_GROUPS = createTagGroups(); - - // Cache for creator constants to avoid repeated reflection - private static Set creatorConstantsCache = null; - - /** - * Creates the tag groups map using Java 8 compatible syntax - */ - private static Map> createTagGroups() { - Map> groups = new HashMap<>(); - - // Combat Skills - groups.put("Combat skills", new HashSet<>(Arrays.asList("combat","combat training", "attack", "strength", "defence", "hitpoints", "prayer", "magic", "ranged"))); - - // Gathering Skills - groups.put("Gathering Skills", new HashSet<>(Arrays.asList("farming", "fishing", "hunter","mininig", "woodcutting"))); - - // Artisan Skills - groups.put("Artisan Skills", new HashSet<>(Arrays.asList("cooking", "crafting", "fletching", "herblore", "runecrafting","runecraft", "smithing"))); - - // Support Skills - groups.put("Utility Skills", new HashSet<>(Arrays.asList("agility","construction", "firemaking", "thieving", "slayer"))); - - // PvM/Bossing - groups.put("PvM/Bossing", new HashSet<>(Arrays.asList("pvm", "boss", "bossing", "raids", "tob", "cox", "cm", "chambers", "theatre", - "zulrah", "vorkath", "hydra", "cerberus", "kraken", "thermonuclear", "barrows", - "jad", "inferno", "gauntlet", "corrupted gauntlet", "nightmare", "phosani", "nex", "bandos", - "armadyl", "zamorak", "saradomin", "dagannoth", "kalphite", "kq", "kbd", "chaos fanatic", - "archaeologist", "spider", "bear", "scorpia", "vetion", "callisto", - "venenatis", "rev", "revenant killer"))); - - // Money Making - groups.put("Money Making", new HashSet<>(Arrays.asList("money making", "mm", "gp per hour", "gold", "profit", "farming ", "merching", - "flipping", "moneymaking"))); - - // Minigames - groups.put("Minigames", new HashSet<>(Arrays.asList("minigame", "minigames", "wintertodt", "tempoross", "zalcano", "gotr", "guardians rift", - "tithe", "tithe farm", "mahogany homes", "pest control", "barbarian assault", - "castle wars", "fight caves", "duel arena", "last man standing", "lms"))); - - // Questing & Achievement - groups.put("Quests & Achievement", new HashSet<>(Arrays.asList("quest", "questing", "achievement", "diary", "clue", "clues", "treasure", - "trails", "casket", "scroll", "beginner", "easy", "medium", "hard", "elite", "master"))); - - // Utility & QoL - groups.put("Utility", new HashSet<>(Arrays.asList("utility", "qol", "quality", "life", "helper", "calculator", "timer", "notification", - "overlay", "highlight", "marker", "tracker", "counter", "solver", "automation"))); - - // PvP - groups.put("PvP", new HashSet<>(Arrays.asList("pvp", "player versus player", "pk", "pking", "bounty hunter", "edge", "edgeville", "anti-pk"))); - - // Bank & Trading - groups.put("Banking & Trading", new HashSet<>(Arrays.asList("bank", "banking", "ge", "grand", "exchange", "trade", "trading", - "merchant", "merch", "flip", "flipping", "sorter", "organization","muling"))); - - // Transportation - groups.put("Transportation", new HashSet<>(Arrays.asList("teleport", "transport", "fairy", "ring", "spirit", "tree", "home", "tab", - "house", "poh", "portal"))); - - return groups; - } - - /** - * Gets all available primary filter categories - */ - public static List getPrimaryFilterCategories() { - return Arrays.asList(FILTER_ALL, FILTER_INTERNAL, FILTER_EXTERNAL, FILTER_BY_CREATOR, FILTER_BY_TAGS); - } - - /** - * Gets secondary filter options based on the primary filter selection - */ - public static List getSecondaryFilterOptions(String primaryFilter, List plugins) { - List options = new ArrayList<>(); - - switch (primaryFilter) { - case FILTER_BY_CREATOR: - options = getAvailableCreators(plugins); - break; - case FILTER_BY_TAGS: - options = getAvailableTagGroups(plugins); - break; - case FILTER_INTERNAL: - case FILTER_EXTERNAL: - options.add("All"); - break; - case FILTER_ALL: - default: - options.add("All Plugins"); - break; - } - - return options.stream().sorted().collect(Collectors.toList()); - } - - /** - * Filters plugins based on the selected primary and secondary filters - */ - public static List filterPlugins(List plugins, String primaryFilter, String secondaryFilter) { - return plugins.stream() - .filter(plugin -> plugin instanceof SchedulablePlugin) - .filter(plugin -> matchesFilter(plugin, primaryFilter, secondaryFilter)) - .collect(Collectors.toList()); - } - - /** - * Checks if a plugin matches the given filters - */ - private static boolean matchesFilter(Plugin plugin, String primaryFilter, String secondaryFilter) { - if (FILTER_ALL.equals(primaryFilter) || secondaryFilter == null || "All Plugins".equals(secondaryFilter)) { - return true; - } - - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - if (descriptor == null) { - return false; - } - - switch (primaryFilter) { - case FILTER_BY_CREATOR: - return matchesCreator(descriptor, secondaryFilter); - case FILTER_BY_TAGS: - return matchesTagGroup(descriptor, secondaryFilter); - case FILTER_INTERNAL: - return !descriptor.isExternal(); - case FILTER_EXTERNAL: - return descriptor.isExternal(); - default: - return true; - } - } - - /** - * Checks if a plugin matches the selected creator by extracting creator from plugin name - */ - private static boolean matchesCreator(PluginDescriptor descriptor, String selectedCreator) { - String pluginCreator = extractCreatorFromName(descriptor.name()); - return pluginCreator.equals(selectedCreator); - } - - /** - * Checks if a plugin matches the selected tag group - */ - private static boolean matchesTagGroup(PluginDescriptor descriptor, String selectedTagGroup) { - String[] tags = descriptor.tags(); - Set groupTags = TAG_GROUPS.get(selectedTagGroup); - - if (groupTags == null) { - return false; - } - - return Arrays.stream(tags) - .filter(tag -> !isCreatorTag(tag)) // Exclude creator tags - .anyMatch(tag -> matchesAnyGroupTag(tag.toLowerCase(), groupTags)); - } - - /** - * Checks if a tag matches any of the group tags, including partial matches for multi-word phrases - */ - private static boolean matchesAnyGroupTag(String tag, Set groupTags) { - // Direct match - if (groupTags.contains(tag)) { - return true; - } - - // Check if tag contains any of the group tag words - for (String groupTag : groupTags) { - if (tag.contains(groupTag) || groupTag.contains(tag)) { - return true; - } - } - - // Split tag on spaces and check each word - String[] tagWords = tag.split("\\s+"); - for (String word : tagWords) { - if (groupTags.contains(word.trim())) { - return true; - } - } - - return false; - } - - /** - * Checks if a tag is likely a creator tag by checking against known creator constants - */ - private static boolean isCreatorTag(String tag) { - if (tag == null) { - return false; - } - - Set creators = getCreatorConstants(); - // Check against variable names (case-sensitive) - if (creators.contains(tag)) { - return true; - } - - // Check against variations (case-insensitive) for short tags - if (tag.length() <= 6) { - return creators.contains(tag.toUpperCase()) || creators.contains(tag.toLowerCase()); - } - - return false; - } - - /** - * Gets all available creators from the plugin list by analyzing plugin names and PluginDescriptor constants - */ - private static List getAvailableCreators(List plugins) { - Set creators = new HashSet<>(); - - // Extract creators from plugin names - for (Plugin plugin : plugins) { - if (!(plugin instanceof SchedulablePlugin)) { - continue; - } - - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - if (descriptor != null) { - String creator = extractCreatorFromName(descriptor.name()); - if (!"Unknown".equals(creator)) { // Filter out "Unknown" - creators.add(creator); - } - } - } - - return creators.stream() - .filter(creator -> !"Unknown".equals(creator)) - .sorted() - .collect(Collectors.toList()); - } - - /** - * Gets available tag groups that have matching plugins - */ - private static List getAvailableTagGroups(List plugins) { - Set availableGroups = new HashSet<>(); - - for (Plugin plugin : plugins) { - if (!(plugin instanceof SchedulablePlugin)) { - continue; - } - - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - if (descriptor != null) { - String[] tags = descriptor.tags(); - - // Check which tag groups this plugin belongs to - for (Map.Entry> groupEntry : TAG_GROUPS.entrySet()) { - String groupName = groupEntry.getKey(); - Set groupTags = groupEntry.getValue(); - - boolean hasMatchingTag = Arrays.stream(tags) - .filter(tag -> !isCreatorTag(tag)) // Exclude creator tags - .anyMatch(tag -> groupTags.contains(tag.toLowerCase())); - - if (hasMatchingTag) { - availableGroups.add(groupName); - } - } - } - } - - return new ArrayList<>(availableGroups); - } - - /** - * Gets creator constants with caching - */ - private static Set getCreatorConstants() { - if (creatorConstantsCache == null) { - creatorConstantsCache = extractCreatorConstantsFromPluginDescriptor(); - } - return creatorConstantsCache; - } - - /** - * Dynamically extracts creator constants from PluginDescriptor class using reflection - */ - private static Set extractCreatorConstantsFromPluginDescriptor() { - Set creators = new HashSet<>(); - - try { - Field[] fields = PluginDescriptor.class.getDeclaredFields(); - for (Field field : fields) { - if (field.getType() == String.class && - java.lang.reflect.Modifier.isStatic(field.getModifiers()) && - java.lang.reflect.Modifier.isFinal(field.getModifiers())) { - - try { - String value = (String) field.get(null); - if (value != null && value.contains("[") && value.contains("]")) { - // Use the field name as the creator identifier - String fieldName = field.getName(); - if (!fieldName.isEmpty()) { - creators.add(fieldName); - } - } - } catch (IllegalAccessException e) { - log.debug("Could not access field {}: {}", field.getName(), e.getMessage()); - } - } - } - } catch (Exception e) { - log.warn("Error extracting creator constants from PluginDescriptor: {}", e.getMessage()); - } - - return creators; - } - - /** - * Extracts creator name from PluginDescriptor constant value - */ - private static String extractCreatorFromValue(String value) { - // Pattern to match content between > and < in HTML format - Pattern pattern = Pattern.compile(">([^<]+)<"); - Matcher matcher = pattern.matcher(value); - - if (matcher.find()) { - String extracted = matcher.group(1); - // Handle special characters and emoji - if (extracted.length() <= 4) { // Most creator codes are short - return extracted; - } - } - - // Fallback: try to extract from brackets - Matcher bracketMatcher = CREATOR_PATTERN.matcher(value); - if (bracketMatcher.find()) { - return bracketMatcher.group(1); - } - - return ""; - } - - /** - * Extracts creator from plugin name using various patterns - */ - private static String extractCreatorFromName(String pluginName) { - if (pluginName == null) { - return "Unknown"; - } - - // Try to find the variable name from PluginDescriptor constants - String variableName = extractCreatorVariableName(pluginName); - if (variableName != null && !variableName.equals("Unknown")) { - return variableName; - } - - // Fallback: First try to extract from [Creator] pattern - Matcher matcher = CREATOR_PATTERN.matcher(pluginName); - if (matcher.find()) { - String creator = cleanHtmlTags(matcher.group(1)); - return creator.isEmpty() ? "Unknown" : creator; - } - - // Check if plugin name starts with HTML format and extract creator - if (pluginName.startsWith("")) { - String extracted = extractCreatorFromValue(pluginName); - if (!extracted.isEmpty()) { - return cleanHtmlTags(extracted); - } - } - - return "Unknown"; - } - - /** - * Extracts the variable name from PluginDescriptor by matching the plugin name against constant values - */ - private static String extractCreatorVariableName(String pluginName) { - try { - Field[] fields = PluginDescriptor.class.getDeclaredFields(); - for (Field field : fields) { - if (field.getType() == String.class && - java.lang.reflect.Modifier.isStatic(field.getModifiers()) && - java.lang.reflect.Modifier.isFinal(field.getModifiers())) { - - try { - String value = (String) field.get(null); - if (value != null && pluginName.startsWith(value)) { - // Found matching constant - return field name - return field.getName(); - } - } catch (IllegalAccessException e) { - log.debug("Could not access field {}: {}", field.getName(), e.getMessage()); - } - } - } - } catch (Exception e) { - log.warn("Error extracting creator variable name: {}", e.getMessage()); - } - - return "Unknown"; - } - - /** - * Removes HTML tags and cleans up text - */ - private static String cleanHtmlTags(String text) { - if (text == null) { - return ""; - } - - // Remove HTML tags - String cleaned = text.replaceAll("<[^>]*>", "").trim(); - - // Remove common HTML entities - cleaned = cleaned.replace(" ", " ") - .replace("<", "<") - .replace(">", ">") - .replace("&", "&") - .replace(""", "\""); - - return cleaned.trim(); - } - - /** - * Gets the creator name for a specific plugin - */ - public static String getPluginCreator(Plugin plugin) { - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - if (descriptor == null) { - return "Unknown"; - } - - String creator = extractCreatorFromName(descriptor.name()); - return "Unknown".equals(creator) ? "Unknown" : creator; - } - - /** - * Gets the tags for a specific plugin, excluding creator tags - */ - public static List getPluginTags(Plugin plugin) { - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - if (descriptor == null) { - return Collections.emptyList(); - } - - return Arrays.stream(descriptor.tags()) - .filter(tag -> !isCreatorTag(tag)) // Exclude creator tags - .collect(Collectors.toList()); - } - - /** - * Gets a clean display name for a plugin without HTML formatting - */ - public static String getPluginDisplayName(Plugin plugin) { - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - if (descriptor == null) { - return plugin.getName(); - } - - String name = descriptor.name(); - // Remove HTML tags and clean up the name - return cleanHtmlTags(name); - } - - /** - * Gets a formatted display name for a plugin including creator prefix in a clean format - */ - public static String getPluginFormattedDisplayName(Plugin plugin) { - String creator = getPluginCreator(plugin); - String cleanName = getPluginDisplayName(plugin); - - if ("Unknown".equals(creator)) { - return cleanName; - } - - return String.format("[%s] %s", creator, cleanName); - } - - /** - * Gets which tag group a plugin belongs to (if any) - */ - public static String getPluginTagGroup(Plugin plugin) { - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - if (descriptor == null) { - return "Other"; - } - - String[] tags = descriptor.tags(); - - // Check which tag group this plugin belongs to - for (Map.Entry> groupEntry : TAG_GROUPS.entrySet()) { - String groupName = groupEntry.getKey(); - Set groupTags = groupEntry.getValue(); - - boolean hasMatchingTag = Arrays.stream(tags) - .filter(tag -> !isCreatorTag(tag)) // Exclude creator tags - .anyMatch(tag -> groupTags.contains(tag.toLowerCase())); - - if (hasMatchingTag) { - return groupName; - } - } - - return "Other"; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/util/SchedulerPluginUtil.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/util/SchedulerPluginUtil.java deleted file mode 100644 index dd6571e498d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/util/SchedulerPluginUtil.java +++ /dev/null @@ -1,1223 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.util; -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.time.Duration; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import javax.swing.SwingUtilities; - -import org.slf4j.event.Level; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.config.ConfigManager; -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.accountselector.AutoLoginPlugin; -import net.runelite.client.plugins.microbot.breakhandler.BreakHandlerConfig; -import net.runelite.client.plugins.microbot.breakhandler.BreakHandlerPlugin; -import net.runelite.client.plugins.microbot.breakhandler.BreakHandlerScript; -import java.time.format.DateTimeFormatter; - -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerPlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.api.SchedulablePlugin; -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.condition.logical.PredicateCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.util.antiban.AntibanPlugin; -import net.runelite.client.plugins.microbot.util.antiban.Rs2Antiban; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -@Slf4j -public class SchedulerPluginUtil{ - /** - * Format a duration for display - * - * @param duration The duration to format - * @return Formatted string representation (e.g., "1h 30m 15s" or "45s") - */ - public static String formatDuration(Duration duration) { - long hours = duration.toHours(); - long minutes = duration.toMinutes() % 60; - long seconds = duration.getSeconds() % 60; - - if (hours > 0) { - return String.format("%dh %dm %ds", hours, minutes, seconds); - } else if (minutes > 0) { - return String.format("%dm %ds", minutes, seconds); - } else { - return String.format("%ds", seconds); - } - } - - /** - * Checks if a specific plugin is enabled - * - * @param pluginClass The class of the plugin to check - * @return True if the plugin is enabled, false otherwise - */ - public static boolean isPluginEnabled(Class pluginClass) { - return Microbot.isPluginEnabled(pluginClass); - } - - public static boolean isBreakHandlerEnabled() { - return isPluginEnabled(BreakHandlerPlugin.class); - } - - public static boolean isAntibanEnabled() { - return isPluginEnabled(AntibanPlugin.class); - } - - public static boolean isAutoLoginEnabled() { - return isPluginEnabled(AutoLoginPlugin.class); - } - - /** - * Enables a specific plugin - * - * @param pluginClass The class of the plugin to enable - * @return true if plugin was enabled successfully, false otherwise - */ - public static boolean enablePlugin(Class pluginClass) { - if (isPluginEnabled(pluginClass)) { - log.info("Plugin {} already enabled", pluginClass.getSimpleName()); - return true; // Already enabled - } - - Microbot.getClientThread().runOnSeperateThread(() -> { - Plugin plugin = Microbot.getPlugin(pluginClass.getName()); - log.info("Plugin {} suggested to be enabled", pluginClass.getSimpleName()); - if (plugin == null) { - log.error("Failed to find plugin {}", pluginClass.getSimpleName()); - return false; - } - log.info("Plugin {} starting", pluginClass.getSimpleName()); - Microbot.startPlugin(plugin); - return true; - }); - - log.info("Plugin {} wait", pluginClass.getSimpleName()); - sleepUntil(() -> isPluginEnabled(pluginClass), 500); - if (!isPluginEnabled(pluginClass)) { - log.error("Failed to enable plugin {}", pluginClass.getSimpleName()); - return false; - } - - log.info("Plugin {} enabled", pluginClass.getSimpleName()); - return true; - } - - /** - * Disables a specific plugin - * - * @param pluginClass The class of the plugin to disable - * @return true if plugin was disabled successfully, false otherwise - */ - public static boolean disablePlugin(Class pluginClass) { - if (!isPluginEnabled(pluginClass)) { - log.info("Plugin {} already disabled", pluginClass.getSimpleName()); - return true; // Already disabled - } - - log.info("disablePlugin {} - are we on client thread->; {}", - pluginClass.getSimpleName(), Microbot.getClient().isClientThread()); - - Microbot.getClientThread().runOnSeperateThread(() -> { - Plugin plugin = Microbot.getPlugin(pluginClass.getName()); - if (plugin == null) { - log.error("Failed to find plugin {}", pluginClass.getSimpleName()); - return false; - } - log.info("Plugin {} stopping", pluginClass.getSimpleName()); - Microbot.stopPlugin(plugin); - return true; - }); - - if (isPluginEnabled(pluginClass)) { - SwingUtilities.invokeLater(() -> { - disablePlugin(pluginClass); - }); - return false; - } - - log.info("Plugin {} disabled", pluginClass.getSimpleName()); - return true; - } - - /** - * Checks 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 - */ - public static boolean isAllSameTimestamp(List plugins) { - if (plugins == null || plugins.size() <= 1) { - return true; // Empty or single-element list has same timestamps by definition - } - - // Get first plugin's trigger time as reference - Optional firstTime = plugins.get(0).getCurrentStartTriggerTime(); - if (firstTime.isEmpty()) { - // If first plugin has no timestamp, check if all others also have no timestamp - return plugins.stream().allMatch(p -> p.getCurrentStartTriggerTime().isEmpty()); - } - - // Compare each plugin's trigger time with the first one - ZonedDateTime reference = firstTime.get().truncatedTo(ChronoUnit.MILLIS); - return plugins.stream() - .allMatch(p -> { - Optional time = p.getCurrentStartTriggerTime(); - return time.isPresent() && - time.get().truncatedTo(ChronoUnit.MILLIS).equals(reference); - }); - } - - /** - * Selects a plugin using weighted random selection. - * Plugins with lower run counts have higher probability of being selected. - * - * @param plugins List of plugins to select from - * @return The selected plugin - */ - public static PluginScheduleEntry selectPluginWeighted(List plugins) { - // Return the only plugin if there's just one - if (plugins.size() == 1) { - return plugins.get(0); - } - - // Calculate weights - plugins with lower run counts get higher weights - // Find the maximum run count - int maxRuns = plugins.stream() - .mapToInt(PluginScheduleEntry::getRunCount) - .max() - .orElse(0); - - // Add 1 to avoid division by zero and to ensure all plugins have some chance - maxRuns = maxRuns + 1; - - // Calculate weights as (maxRuns + 1) - runCount for each plugin - // This gives higher weight to plugins that have run less often - double[] weights = new double[plugins.size()]; - double totalWeight = 0; - - for (int i = 0; i < plugins.size(); i++) { - // Weight = (maxRuns + 1) - plugin's run count - weights[i] = maxRuns - plugins.get(i).getRunCount() + 1; - totalWeight += weights[i]; - } - - // Generate random value between 0 and totalWeight - double randomValue = Math.random() * totalWeight; - - // Select plugin based on random value and weights - double weightSum = 0; - for (int i = 0; i < plugins.size(); i++) { - weightSum += weights[i]; - if (randomValue < weightSum) { - // Log the selection for debugging - log.debug("Selected plugin '{}' with weight {}/{} (run count: {})", - plugins.get(i).getCleanName(), - weights[i], - totalWeight, - plugins.get(i).getRunCount()); - return plugins.get(i); - } - } - - // Fallback to the last plugin (shouldn't normally happen) - return plugins.get(plugins.size() - 1); - } - - /** - * Sort a group of randomizable plugins using a weighted approach based on run - * counts. - * Plugins with fewer runs get sorted ahead of plugins with more runs, following - * the weighting system used in the old selectPluginWeighted method. - * - * @param plugins A list of randomizable plugins with the same priority and - * default status - * @return A list sorted by weighted run count - */ - public static List applyWeightedSorting(List plugins) { - if (plugins.size() <= 1) { - return new ArrayList<>(plugins); - } - - // Similar to the old selectPluginWeighted, but we're sorting instead of - // selecting one - - // First, find the maximum run count - int maxRuns = plugins.stream() - .mapToInt(PluginScheduleEntry::getRunCount) - .max() - .orElse(0); - - // Add 1 to avoid division by zero and ensure all have a chance - maxRuns = maxRuns + 1; - - // Calculate weights for each plugin - final Map weights = new HashMap<>(); - double totalWeight = 0; - - for (PluginScheduleEntry plugin : plugins) { - // Weight = (maxRuns + 1) - plugin's run count - double weight = maxRuns - plugin.getRunCount() + 1; - weights.put(plugin, weight); - totalWeight += weight; - } - - // Create weighted comparison - Comparator weightedComparator = (p1, p2) -> { - // Higher weight (fewer runs) should come first - double weight1 = weights.getOrDefault(p1, 0.0); - double weight2 = weights.getOrDefault(p2, 0.0); - - if (Double.compare(weight1, weight2) != 0) { - // Higher weight first - return Double.compare(weight2, weight1); - } - - // If weights are equal, use name and identity for stable sorting - int nameCompare = p1.getName().compareTo(p2.getName()); - if (nameCompare != 0) { - return nameCompare; - } - - return Integer.compare(System.identityHashCode(p1), System.identityHashCode(p2)); - }; - - // Sort plugins based on weight - List sortedPlugins = new ArrayList<>(plugins); - sortedPlugins.sort(weightedComparator); - - if (log.isDebugEnabled()) { - for (int i = 0; i < sortedPlugins.size(); i++) { - PluginScheduleEntry plugin = sortedPlugins.get(i); - double weight = weights.get(plugin); - double weightPercentage = (weight / totalWeight) * 100.0; - log.debug("Weighted sorting position {}: '{}' with weight {:.2f}/{:.2f} ({:.2f}%) (run count: {})", - i, plugin.getCleanName(), weight, totalWeight, weightPercentage, plugin.getRunCount()); - } - } - - return sortedPlugins; - } - - /** - * Helper method to enable the BreakHandler plugin - */ - public static boolean enableBreakHandler() { - return enablePlugin(BreakHandlerPlugin.class); - } - - /** - * Helper method to disable the BreakHandler plugin - */ - public static boolean disableBreakHandler() { - if (isPluginEnabled(BreakHandlerPlugin.class)) { - BreakHandlerScript.setLockState(false); // Ensure we unlock before disabling - } - return disablePlugin(BreakHandlerPlugin.class); - } - - /** - * Helper method to enable the AutoLogin plugin - */ - public static boolean enableAutoLogin() { - return enablePlugin(AutoLoginPlugin.class); - } - - /** - * Helper method to enable the AutoLogin plugin with configuration - * - * @param randomWorld Whether to use a random world - * @param worldNumber The world number to use if not random - * @return true if plugin was enabled successfully, false otherwise - */ - public static boolean enableAutoLogin(boolean randomWorld, int worldNumber) { - ConfigManager configManager = Microbot.getConfigManager(); - if(configManager != null) { - configManager.setConfiguration("AutoLoginConfig", "RandomWorld", randomWorld); - configManager.setConfiguration("AutoLoginConfig", "World", worldNumber); - } - - return enableAutoLogin(); - } - - /** - * Helper method to disable the AutoLogin plugin - */ - public static boolean disableAutoLogin() { - return disablePlugin(AutoLoginPlugin.class); - } - - /** - * Helper method to enable the Antiban plugin - */ - public static boolean enableAntiban() { - return enablePlugin(AntibanPlugin.class); - } - - /** - * Helper method to disable the Antiban plugin - */ - public static boolean disableAntiban() { - return disablePlugin(AntibanPlugin.class); - } - - /** - * Checks if the bot is currently on a break - * - * @return true if on break, false otherwise - */ - public static boolean isOnBreak() { - // Check if BreakHandler is enabled and on a break - return isBreakHandlerEnabled() && BreakHandlerScript.isBreakActive(); - } - - /** - * Forces the bot to take a break immediately if BreakHandler is enabled - * - * @return true if break was initiated, false otherwise - */ - public static boolean forceBreak() { - if (!isBreakHandlerEnabled()) { - log.warn("Cannot force break: BreakHandler plugin not enabled"); - return false; - } - - // Set the breakNow config to true to trigger a break - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "breakNow", true); - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "logout", true); - log.info("Break requested via forceBreak()"); - return true; - } - - /** - * Attempts to take a micro break if conditions are favorable - * - * @param setState A callback function to update the scheduler state - * @return true if a micro break was taken, false otherwise - */ - public static boolean takeMicroBreak(Runnable setState) { - if (!isAntibanEnabled()) { - log.warn("Cannot take micro break: Antiban plugin not enabled"); - return false; - } - if (Rs2Player.isFullHealth()) { - if (Rs2Antiban.takeMicroBreakByChance() || BreakHandlerScript.isBreakActive()) { - if (setState != null) { - setState.run(); - } - return true; - } - } - return false; - } - - /** - * Locks the break handler to prevent breaks from occurring - * - * @return true if the break handler was successfully locked, false otherwise - */ - public static boolean lockBreakHandler() { - // Check if BreakHandler is enabled and not already locked - if (isBreakHandlerEnabled() && !BreakHandlerScript.isBreakActive() && !BreakHandlerScript.isLockState()) { - BreakHandlerScript.setLockState(true); - return true; // Successfully locked - } - return false; // Failed to lock - } - - /** - * Unlocks the break handler to allow breaks to occur - */ - public static void unlockBreakHandler() { - // Check if BreakHandler is enabled and not already unlocked - if (isBreakHandlerEnabled() && BreakHandlerScript.isLockState()) { - BreakHandlerScript.setLockState(false); - } - } - - /** - * Logs out the player if they are currently logged in - */ - public static void logout() { - if (Microbot.getClient().getGameState() == net.runelite.api.GameState.LOGGED_IN) { - if (isAutoLoginEnabled()) { - boolean successfulDisabled = disableAutoLogin(); - if (!successfulDisabled) { - Microbot.getClientThread().invokeLater(() -> { - logout(); - }); - return; - } - } - - Rs2Player.logout(); - } - } - - - - - - /** - * Sorts a list of plugins according to a consistent order, with weighted - * selection for randomizable plugins: - * 1. Enabled plugins first - * 2. Running status (running plugins first) - * 3. Due-to-run status (due plugins first) - prioritizes actionable plugins - * 4. Next run time (earliest first) - within due/not-due groups - * 5. Priority (highest first) - within due/runtime groups - * 6. Non-default status (non-default first) - * 7. Prefer non-randomizable plugins (for ties in timing) - * 8. For randomizable plugins with equal criteria: weighted by run count - * 9. Finally by name and object identity for stable ordering - * - * @param plugins The list of plugins to sort - * @param applyWeightedSelection Whether to apply weighted selection for - * randomizable plugins - * @return A sorted copy of the input list - */ - public static List sortPluginScheduleEntries(List plugins, - boolean applyWeightedSelection) { - if (plugins == null || plugins.isEmpty()) { - return new ArrayList<>(); - } - - List sortedPlugins = new ArrayList<>(plugins); - - // First, sort by all the stable criteria - sortedPlugins.sort((p1, p2) -> { - // First sort by enabled status (enabled plugins first) - if (p1.isEnabled() != p2.isEnabled()) { - return p1.isEnabled() ? -1 : 1; - } - - // For running plugins, prioritize current running plugin at the top - boolean p1IsRunning = p1.isRunning(); - boolean p2IsRunning = p2.isRunning(); - - if (p1IsRunning != p2IsRunning) { - return p1IsRunning ? -1 : 1; - } - - // Sort by due-to-run status first (due plugins first) - boolean p1IsDue = p1.isDueToRun(); - boolean p2IsDue = p2.isDueToRun(); - - if (p1IsDue != p2IsDue) { - return p1IsDue ? -1 : 1; - } - - // Then sort by next run time (earliest first) - within due/not-due groups - Optional time1 = p1.getCurrentStartTriggerTime(); - Optional time2 = p2.getCurrentStartTriggerTime(); - - if (time1.isPresent() && time2.isPresent()) { - ZonedDateTime t1 = time1.get().truncatedTo(ChronoUnit.SECONDS); - ZonedDateTime t2 = time2.get().truncatedTo(ChronoUnit.SECONDS); - int timeCompare = t1.compareTo(t2); - float timeDifference = Duration.between(t1, t2).toMillis(); - int priorityCompare = Integer.compare(p2.getPriority(), p1.getPriority()); - if (timeCompare != 0 && priorityCompare == 0) { - log.debug("Comparing times: {}() vs {}() -> result: {} ({} ms difference)", - t1.format(DateTimeFormatter.ISO_ZONED_DATE_TIME), - t2.format(DateTimeFormatter.ISO_ZONED_DATE_TIME), - timeCompare - , timeDifference); - return timeCompare; - } - } else if (time1.isPresent()) { - return -1; // p1 has time, p2 doesn't - } else if (time2.isPresent()) { - return 1; // p2 has time, p1 doesn't - } - - // Then sort by priority within due/runtime groups (highest first) - int priorityCompare = Integer.compare(p2.getPriority(), p1.getPriority()); - if (priorityCompare != 0) { - return priorityCompare; - } - - // Prefer non-default plugins - if (p1.isDefault() != p2.isDefault()) { - return p1.isDefault() ? 1 : -1; - } - - // Prefer non-randomizable plugins for deterministic behavior - if (p1.isAllowRandomScheduling() != p2.isAllowRandomScheduling()) { - return p1.isAllowRandomScheduling() ? 1 : -1; - } - - // As final tiebreakers use plugin name and object identity - int nameCompare = p1.getName().compareTo(p2.getName()); - if (nameCompare != 0) { - return nameCompare; - } - - // Last resort: use object identity hash code for stable ordering - return Integer.compare(System.identityHashCode(p1), System.identityHashCode(p2)); - }); - - // If we're not applying weighted selection, we're done - if (!applyWeightedSelection) { - return sortedPlugins; - } - - // Now we need to look for groups of randomizable plugins at the same priority, - // default status, and similar timing for weighted selection - List result = new ArrayList<>(); - List randomizableGroup = new ArrayList<>(); - Integer currentPriority = null; - boolean currentDefault = false; - ZonedDateTime currentTimeGroup = null; - final Duration TIME_GROUP_WINDOW = Duration.ofMinutes(5); // Group plugins within 5 minutes - - // Iterate through sorted plugins to find groups with the same priority, - // default status, and similar timing - for (int i = 0; i < sortedPlugins.size(); i++) { - PluginScheduleEntry current = sortedPlugins.get(i); - - // Skip non-randomizable plugins (they're already properly sorted by time) - if (!current.isAllowRandomScheduling()) { - // If we had a randomizable group, process it before adding this - // non-randomizable plugin - if (!randomizableGroup.isEmpty()) { - result.addAll(SchedulerPluginUtil.applyWeightedSorting(randomizableGroup)); - randomizableGroup.clear(); - } - - result.add(current); - continue; - } - - // Get the trigger time for timing group comparison - Optional triggerTime = current.getCurrentStartTriggerTime(); - ZonedDateTime currentTime = triggerTime.map(t -> t.truncatedTo(ChronoUnit.MINUTES)).orElse(null); - - // Check if this is part of an existing group (same priority, default status, and timing) - boolean sameGroup = currentPriority != null - && current.getPriority() == currentPriority - && current.isDefault() == currentDefault; - - // Add timing group check - plugins should be in same time window for randomization - if (sameGroup && currentTimeGroup != null && currentTime != null) { - Duration timeDifference = Duration.between(currentTimeGroup, currentTime).abs(); - sameGroup = timeDifference.compareTo(TIME_GROUP_WINDOW) <= 0; - } else if (sameGroup) { - // If one has time and other doesn't, they're not in the same group - sameGroup = (currentTimeGroup == null && currentTime == null); - } - - if (sameGroup) { - // Same group, add to current batch of randomizable plugins - randomizableGroup.add(current); - } else { - // New group - process previous group if it exists - if (!randomizableGroup.isEmpty()) { - result.addAll(SchedulerPluginUtil.applyWeightedSorting(randomizableGroup)); - randomizableGroup.clear(); - } - - // Start new group - randomizableGroup.add(current); - currentPriority = current.getPriority(); - currentDefault = current.isDefault(); - currentTimeGroup = currentTime; - } - } - - // Process any remaining group - if (!randomizableGroup.isEmpty()) { - result.addAll(SchedulerPluginUtil.applyWeightedSorting(randomizableGroup)); - } - - return result; - } - - - - /** - * Overloaded method that calls sortPluginScheduleEntries without weighted - * selection by default - */ - public static List sortPluginScheduleEntries(List plugins) { - return sortPluginScheduleEntries(plugins, false); - } - - - - public static Optional getScheduleInterval(PluginScheduleEntry plugin) { - if (plugin.hasAnyStartConditions()) { - Optional nextTrigger = plugin.getCurrentStartTriggerTime(); - if (nextTrigger.isPresent()) { - ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); - return Optional.of(Duration.between(now, nextTrigger.get())); - } - } - return Optional.empty(); - } - - - - /** - * Formats a reason message for better readability by splitting it into multiple lines - * if it's too long or contains natural break points. - * - * @param reason The original reason message - * @return A formatted reason message with appropriate line breaks - */ - public static String formatReasonMessage(String reason) { - if (reason == null || reason.isEmpty()) { - return ""; - } - - // Maximum line length before seeking a break point - final int MAX_LINE_LENGTH = 80; - - // If the reason is already short, return it as is - if (reason.length() <= MAX_LINE_LENGTH) { - return reason; - } - - StringBuilder formatted = new StringBuilder(); - - // First check if the message already contains a colon - // If so, we'll format differently - if (reason.contains(":")) { - String[] parts = reason.split(":", 2); - formatted.append(parts[0].trim()).append(":"); - - // Process the part after the colon - String afterColon = parts[1].trim(); - - // If what comes after the colon is still long, format it further - if (afterColon.length() > MAX_LINE_LENGTH) { - formatted.append("\n "); // Indent the continuation - formatted.append(formatLongText(afterColon, MAX_LINE_LENGTH)); - } else { - formatted.append("\n ").append(afterColon); - } - } else { - // No colon in the message, format as regular long text - formatted.append(formatLongText(reason, MAX_LINE_LENGTH)); - } - - return formatted.toString(); - } - - /** - * Helper method to format a long text by inserting line breaks at natural points - * - * @param text The text to format - * @param maxLineLength The maximum length for each line - * @return Formatted text with line breaks - */ - private static String formatLongText(String text, int maxLineLength) { - if (text == null || text.isEmpty() || text.length() <= maxLineLength) { - return text; - } - - StringBuilder result = new StringBuilder(); - int currentPosition = 0; - - while (currentPosition < text.length()) { - int endPosition = Math.min(currentPosition + maxLineLength, text.length()); - - // If we're not at the end of the text, look for a natural break point - if (endPosition < text.length()) { - // Look for natural break points: period, comma, space, etc. - int breakPoint = findBreakPoint(text, currentPosition, endPosition); - if (breakPoint > currentPosition) { - endPosition = breakPoint; - } - } - - // Add the current segment - if (result.length() > 0) { - result.append("\n "); // Indent continuation lines - } - result.append(text.substring(currentPosition, endPosition).trim()); - - // Move to next segment - currentPosition = endPosition; - } - - return result.toString(); - } - - /** - * Finds a suitable break point in text between start and end positions - * - * @param text The text to analyze - * @param start Start position to search from - * @param end End position to search to - * @return Position of a good break point, or end if none found - */ - private static int findBreakPoint(String text, int start, int end) { - // Search backward from the end position for a good break point - for (int i = end; i > start; i--) { - char c = text.charAt(i); - - // Good break points in priority order - if (c == '.' || c == '!' || c == '?') { - return i + 1; // Break after sentence-ending punctuation - } else if (c == ';' || c == ':') { - return i + 1; // Break after semicolons or colons - } else if (c == ',') { - return i + 1; // Break after commas - } else if (Character.isWhitespace(c)) { - return i + 1; // Break after whitespace - } - } - - // If no good break point found, just use the end position - return end; - } - - /** - * Logs detailed information about the sorted plugin list for debugging table ordering. - * This shows the order plugins appear in the schedule table and explains the sorting criteria. - * - * @param sortedPlugins The sorted list of plugins as they appear in the table - */ - public static void logPluginScheduleEntryList(List sortedPlugins) { - StringBuilder tableOrderLog = new StringBuilder(); - tableOrderLog.append("\n=== SCHEDULE TABLE ORDERING DEBUG ===\n"); - tableOrderLog.append("Plugins are sorted by priority order:\n"); - tableOrderLog.append("1. Enabled status (enabled first)\n"); - tableOrderLog.append("2. Running status (running first)\n"); - tableOrderLog.append("3. Due-to-run status (due first)\n"); - tableOrderLog.append("4. Next run time (earliest first)\n"); - tableOrderLog.append("5. Priority level (highest first)\n"); - tableOrderLog.append("6. Default status (non-default first)\n"); - tableOrderLog.append("7. Random scheduling (non-random first)\n"); - tableOrderLog.append("8. Plugin name (alphabetical)\n"); - tableOrderLog.append("9. Object identity (stable ordering)\n\n"); - tableOrderLog.append("Total plugins: ").append(sortedPlugins.size()).append("\n\n"); - - for (int i = 0; i < sortedPlugins.size(); i++) { - PluginScheduleEntry plugin = sortedPlugins.get(i); - tableOrderLog.append(String.format("Row %d: %s\n", i, plugin.getCleanName())); - - // Priority information - tableOrderLog.append(String.format(" Priority: %d %s\n", - plugin.getPriority(), - plugin.isDefault() ? "(DEFAULT - always priority 0)" : "")); - - // Status information - tableOrderLog.append(String.format(" Status: %s%s%s\n", - plugin.isEnabled() ? "ENABLED" : "DISABLED", - plugin.isRunning() ? " | RUNNING" : "", - plugin.isStopInitiated() ? " | STOPPING" : "")); - - // Next schedule time - Optional nextTime = plugin.getCurrentStartTriggerTime(); - if (nextTime.isPresent()) { - tableOrderLog.append(String.format(" Next Run: %s\n", - nextTime.get().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))); - } else { - tableOrderLog.append(" Next Run: No trigger time available\n"); - } - - // Condition information - boolean hasStartConditions = plugin.hasAnyStartConditions(); - boolean hasStopConditions = plugin.hasAnyStopConditions(); - tableOrderLog.append(String.format(" Conditions: Start=%s, Stop=%s\n", - hasStartConditions ? String.format("%d conditions", plugin.getStartConditionManager().getConditions().size()) : "None", - hasStopConditions ? String.format("%d conditions", plugin.getStopConditionManager().getConditions().size()) : "None")); - - // Start condition readiness - if (hasStartConditions) { - boolean canStart = plugin.canBeStarted(); - boolean isDue = plugin.isDueToRun(); - tableOrderLog.append(String.format(" Start Ready: %s | Due to Run: %s\n", - canStart ? "YES" : "NO", - isDue ? "YES" : "NO")); - } - - // Plugin properties - tableOrderLog.append(String.format(" Properties: RandomScheduling=%s, AllowContinue=%s, RunCount=%d\n", - plugin.isAllowRandomScheduling() ? "YES" : "NO", - plugin.isAllowContinue() ? "YES" : "NO", - plugin.getRunCount())); - - // One-time schedule information - if (plugin.hasAnyOneTimeStartConditions()) { - boolean hasTriggered = plugin.hasTriggeredOneTimeStartConditions(); - boolean canTriggerAgain = plugin.canStartTriggerAgain(); - tableOrderLog.append(String.format(" One-Time: HasTriggered=%s, CanTriggerAgain=%s\n", - hasTriggered ? "YES" : "NO", - canTriggerAgain ? "YES" : "NO")); - } - - // Last run information - if (plugin.getRunCount() > 0) { - tableOrderLog.append(String.format(" Last Run: %s (%s)\n", - plugin.getLastStopReasonType().getDescription(), - plugin.isLastRunSuccessful() ? "SUCCESS" : "FAILED")); - } - - tableOrderLog.append("\n"); - } - - - - log.info(tableOrderLog.toString()); - } - - - /** - * Detects if any enabled SchedulablePlugin has locked LockConditions or unsatisfied PredicateConditions. - * This prevents the break handler from taking breaks during critical plugin operations. - * - * @return true if any schedulable plugin has locked conditions or unsatisfied predicate conditions, false otherwise - */ - public static boolean hasLockedSchedulablePlugins() { - try { - // Get all enabled plugins from the plugin manager - return Microbot.getPluginManager().getPlugins().stream() - .filter(plugin -> Microbot.getPluginManager().isPluginEnabled(plugin)) - .filter(plugin -> plugin instanceof SchedulablePlugin) - .map(plugin -> (SchedulablePlugin) plugin) - .anyMatch(schedulablePlugin -> { - try { - // Get the stop condition from the schedulable plugin - LogicalCondition stopCondition = schedulablePlugin.getStopCondition(); - if (stopCondition != null) { - // Find all LockConditions in the logical condition structure using the utility method - List lockConditions = stopCondition.findAllLockConditions(); - // Check if any LockCondition is currently locked - boolean hasLockedConditions = lockConditions.stream().anyMatch(LockCondition::isLocked); - - // Find all PredicateConditions in the logical condition structure - List> predicateConditions = stopCondition.findAllPredicateConditions(); - // Check if any PredicateCondition is not satisfied - boolean hasUnsatisfiedPredicates = predicateConditions.stream() - .anyMatch(predicateCondition -> !predicateCondition.isSatisfied()); - - return hasLockedConditions || hasUnsatisfiedPredicates; - } - return false; - } catch (Exception e) { - log.error("Error checking stop conditions for schedulable plugin - " + e.getMessage()); - return false; - } - }); - } catch (Exception e) { - log.error("Error checking schedulable plugins for lock conditions: " + e.getMessage()); - return false; - } - } - - /** - * 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 - */ - public static Optional getTimeUntilUpComingScheduledPlugin() { - try { - // Get the SchedulerPlugin instance - SchedulerPlugin schedulerPlugin = (SchedulerPlugin) Microbot.getPlugin(SchedulerPlugin.class.getName()); - - // Check if scheduler plugin exists and is running - if (schedulerPlugin == null) { - Microbot.log("SchedulerPlugin is not loaded, cannot determine next plugin time", Level.DEBUG); - return Optional.empty(); - } - - // Check if the scheduler is in an active state - if (!schedulerPlugin.getCurrentState().isSchedulerActive()) { - Microbot.log("SchedulerPlugin is not in active state: " + schedulerPlugin.getCurrentState(), Level.DEBUG); - return Optional.empty(); - } - - // Get the upcoming plugin - PluginScheduleEntry upcomingPlugin = schedulerPlugin.getUpComingPlugin(); - if (upcomingPlugin == null) { - Microbot.log("No upcoming plugin found in scheduler", Level.DEBUG); - return Optional.empty(); - } - - // Get the time until the next run for this plugin - Optional timeUntilRun = upcomingPlugin.getTimeUntilNextRun(); - if (!timeUntilRun.isPresent()) { - Microbot.log("Cannot determine time until next run for plugin: " + upcomingPlugin.getCleanName(), Level.DEBUG); - return Optional.empty(); - } - - Duration duration = timeUntilRun.get(); - - // Log the result for debugging - Microbot.log("Next plugin '" + upcomingPlugin.getCleanName() + "' scheduled in: " + - formatDuration(duration), Level.DEBUG); - - return Optional.of(duration); - - } 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 - */ - public static Optional getUpComingScheduledPluginInfo() { - try { - SchedulerPlugin schedulerPlugin = (SchedulerPlugin) Microbot.getPlugin(SchedulerPlugin.class.getName()); - - if (schedulerPlugin == null || !schedulerPlugin.getCurrentState().isSchedulerActive()) { - return Optional.empty(); - } - - PluginScheduleEntry upcomingPlugin = schedulerPlugin.getUpComingPlugin(); - if (upcomingPlugin == null) { - return Optional.empty(); - } - - Optional timeUntilRun = upcomingPlugin.getTimeUntilNextRun(); - if (!timeUntilRun.isPresent()) { - return Optional.of("Next plugin: " + upcomingPlugin.getCleanName() + " (time unknown)"); - } - - String formattedTime = formatDuration(timeUntilRun.get()); - return Optional.of("Next plugin: " + upcomingPlugin.getCleanName() + " in " + formattedTime); - - } 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 - */ - public static Optional getNextUpComingPluginScheduleEntry() { - try { - SchedulerPlugin schedulerPlugin = (SchedulerPlugin) Microbot.getPlugin(SchedulerPlugin.class.getName()); - - if (schedulerPlugin == null || !schedulerPlugin.getCurrentState().isSchedulerActive()) { - return Optional.empty(); - } - - PluginScheduleEntry upcomingPlugin = schedulerPlugin.getUpComingPlugin(); - return Optional.ofNullable(upcomingPlugin); - - } catch (Exception e) { - Microbot.log("Error getting next scheduled plugin entry: " + e.getMessage(), Level.ERROR); - return Optional.empty(); - } - } - - /** - * Gets the estimated time until the next scheduled plugin will be ready to run. - * This method uses the enhanced estimation system to provide more accurate - * predictions by considering both current plugin stop conditions and upcoming - * plugin start conditions. - * - * @return Optional containing the estimated duration until the next plugin runs, - * or empty if no plugin is upcoming or time cannot be determined - */ - public static Optional getEstimatedTimeUntilNextScheduledPlugin() { - try { - // Get the SchedulerPlugin instance - SchedulerPlugin schedulerPlugin = (SchedulerPlugin) Microbot.getPlugin(SchedulerPlugin.class.getName()); - - // Check if scheduler plugin exists and is running - if (schedulerPlugin == null) { - Microbot.log("SchedulerPlugin is not loaded, cannot determine estimated next plugin time", Level.DEBUG); - return Optional.empty(); - } - - // Check if the scheduler is in an active state - if (!schedulerPlugin.getCurrentState().isSchedulerActive()) { - Microbot.log("SchedulerPlugin is not in active state: " + schedulerPlugin.getCurrentState(), Level.DEBUG); - return Optional.empty(); - } - - // Get the estimated schedule time using the new system - Optional estimatedTime = schedulerPlugin.getUpComingEstimatedScheduleTime(); - - if (estimatedTime.isPresent()) { - Duration duration = estimatedTime.get(); - - // Log the result for debugging - Microbot.log("Next plugin estimated to be scheduled in: " + - formatDuration(duration), Level.DEBUG); - - return Optional.of(duration); - } else { - Microbot.log("Cannot estimate time until next scheduled plugin", Level.DEBUG); - return Optional.empty(); - } - - } catch (Exception e) { - Microbot.log("Error getting estimated time until next scheduled plugin: " + e.getMessage(), Level.ERROR); - return Optional.empty(); - } - } - - /** - * Gets enhanced information about the next scheduled plugin using the estimation system. - * This method provides more accurate predictions by considering both current plugin - * stop conditions and upcoming plugin start conditions. - * - * @return Optional containing a formatted string with plugin name and estimated time until run, - * or empty if no plugin is upcoming - */ - public static Optional getNextScheduledPluginInfoWithEstimation() { - try { - SchedulerPlugin schedulerPlugin = (SchedulerPlugin) Microbot.getPlugin(SchedulerPlugin.class.getName()); - - if (schedulerPlugin == null || !schedulerPlugin.getCurrentState().isSchedulerActive()) { - return Optional.empty(); - } - - PluginScheduleEntry upcomingPlugin = schedulerPlugin.getUpComingPlugin(); - if (upcomingPlugin == null) { - return Optional.empty(); - } - - // Use the enhanced estimation system - Optional estimatedTime = schedulerPlugin.getUpComingEstimatedScheduleTime(); - if (!estimatedTime.isPresent()) { - return Optional.of("Next plugin: " + upcomingPlugin.getCleanName() + " (estimation unavailable)"); - } - - String formattedTime = formatDuration(estimatedTime.get()); - return Optional.of("Next plugin: " + upcomingPlugin.getCleanName() + " estimated in " + formattedTime); - - } catch (Exception e) { - Microbot.log("Error getting next scheduled plugin info with estimation: " + e.getMessage(), Level.ERROR); - return Optional.empty(); - } - } - - /** - * Gets enhanced information about the next scheduled plugin within a time window. - * This method provides predictions for plugins that will be ready within the specified timeframe. - * - * @param timeWindow The time window to look ahead for upcoming plugins - * @return Optional containing a formatted string with plugin name and estimated time until run, - * or empty if no plugin is upcoming within the window - */ - public static Optional getNextScheduledPluginInfoWithinTimeWindow(Duration timeWindow) { - try { - SchedulerPlugin schedulerPlugin = (SchedulerPlugin) Microbot.getPlugin(SchedulerPlugin.class.getName()); - - if (schedulerPlugin == null || !schedulerPlugin.getCurrentState().isSchedulerActive()) { - return Optional.empty(); - } - - // Get plugin within the time window - PluginScheduleEntry upcomingPlugin = schedulerPlugin.getUpComingPluginWithinTime(timeWindow); - if (upcomingPlugin == null) { - return Optional.empty(); - } - - // Use the enhanced estimation system for the time window - Optional estimatedTime = schedulerPlugin.getUpComingEstimatedScheduleTimeWithinTime(timeWindow); - if (!estimatedTime.isPresent()) { - return Optional.of("Next plugin within " + formatDuration(timeWindow) + ": " + - upcomingPlugin.getCleanName() + " (estimation unavailable)"); - } - - String formattedTime = formatDuration(estimatedTime.get()); - return Optional.of("Next plugin within " + formatDuration(timeWindow) + ": " + - upcomingPlugin.getCleanName() + " estimated in " + formattedTime); - - } catch (Exception e) { - Microbot.log("Error getting next scheduled plugin info within time window: " + e.getMessage(), Level.ERROR); - return Optional.empty(); - } - } - - public static void disableAllRunningNonEessentialPlugin() { - - - // Check if client is at login screen - List conditionProviders = new ArrayList<>(); - if (Microbot.getPluginManager() == null || Microbot.getClient() == null) { - return; - - } else { - // Find all plugins implementing ConditionProvider - conditionProviders = Microbot.getPluginManager().getPlugins().stream() - .filter(plugin -> plugin instanceof SchedulablePlugin) - .collect(Collectors.toList()); - - // Filter out essential plugins and disable non-essential enabled plugins - List enabledList = conditionProviders.stream() - .filter(plugin -> Microbot.getPluginManager().isPluginEnabled(plugin)) - .filter(plugin -> { - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - return descriptor != null && !descriptor.enabledByDefault(); - }) - .collect(Collectors.toList()); - - // Helper predicate to identify Microbot plugins - Predicate isMicrobotPlugin = plugin -> - plugin.getClass().getPackage().getName().toLowerCase().contains("microbot"); - - // Helper predicate to identify external plugins from Microbot Hub - Predicate isMicrobotExternalPlugin = plugin -> { - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - return descriptor != null && descriptor.isExternal() && - plugin.getClass().getPackage().getName().toLowerCase().contains("microbot"); - }; - - // Disable all non-essential plugins that are currently enabled, but exclude Microbot-related plugins - List allEnabledPlugins = Microbot.getPluginManager().getPlugins().stream() - .filter(plugin -> Microbot.getPluginManager().isPluginEnabled(plugin) - && !plugin.getClass().getName().equals("net.runelite.client.plugins.microbot.pluginscheduler.SchedulerPlugin")) - .collect(Collectors.toList()); - - for (Plugin plugin : allEnabledPlugins) { - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - - // Skip if it's not a Microbot plugin (internal or external from Microbot Hub) - if (!(isMicrobotPlugin.test(plugin) || isMicrobotExternalPlugin.test(plugin))) { - continue; - } - - // Only disable non-essential, Microbot - if (descriptor != null && !descriptor.enabledByDefault()) { - try { - Microbot.stopPlugin(plugin); - log.debug("Disabled non-essential Microbot plugin: {}", plugin.getName()); - } catch (Exception e) { - log.warn("Failed to disable plugin {}: {}", plugin.getName(), e.getMessage()); - } - } - } - } - } - - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java index fbb224c0c46..b672fdc954f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java @@ -13,7 +13,6 @@ import net.runelite.client.plugins.microbot.globval.enums.InterfaceTab; import net.runelite.client.plugins.microbot.shortestpath.*; import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.cache.Rs2SpiritTreeCache; import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; import net.runelite.client.plugins.microbot.util.magic.Rs2Magic; @@ -23,7 +22,6 @@ import net.runelite.client.plugins.microbot.util.poh.PohTeleports; import net.runelite.client.plugins.microbot.util.tabs.Rs2Tab; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; -import net.runelite.client.plugins.microbot.util.cache.Rs2SkillCache; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; @@ -260,9 +258,6 @@ private void refreshTransports(WorldPoint target) { usableTeleports.clear(); // Check spirit tree farming states for farmable spirit trees - Rs2SpiritTreeCache.getInstance().update(); - //Rs2SpiritTreeCache.logAllTreeStates(); - for (Map.Entry> entry : createMergedList().entrySet()) { WorldPoint point = entry.getKey(); Set usableTransports = new HashSet<>(entry.getValue().size()); @@ -477,8 +472,6 @@ private boolean useTransport(Transport transport) { log.debug("Transport ( O: {} D: {} ) requires quests {}", transport.getOrigin(), transport.getDestination(), transport.getQuests()); return false; } - // Check Spirit Tree specific requirements (farming state for farmable trees) - if (transport.getType() == TransportType.SPIRIT_TREE) return isSpiritTreeUsable(transport); // If the transport has varbit requirements & the varbits do not match if (!varbitChecks(transport)) { @@ -543,13 +536,7 @@ private boolean hasRequiredLevels(Transport transport) { Skill[] skills = Skill.values(); return IntStream.range(0, requiredLevels.length) .filter(i -> requiredLevels[i] > 0) - .allMatch(i -> { - if (Microbot.isRs2CacheEnabled()) { - return Rs2SkillCache.getBoostedSkillLevel(skills[i]) >= requiredLevels[i]; - } else { - return Microbot.getClient().getBoostedSkillLevel(skills[i]) >= requiredLevels[i]; - } - }); + .allMatch(i -> Microbot.getClient().getBoostedSkillLevel(skills[i]) >= requiredLevels[i]); } /** @@ -560,13 +547,7 @@ private boolean hasRequiredLevels(Restriction restriction) { Skill[] skills = Skill.values(); return IntStream.range(0, requiredLevels.length) .filter(i -> requiredLevels[i] > 0) - .allMatch(i -> { - if (Microbot.isRs2CacheEnabled()) { - return Rs2SkillCache.getBoostedSkillLevel(skills[i]) >= requiredLevels[i]; - } else { - return Microbot.getClient().getBoostedSkillLevel(skills[i]) >= requiredLevels[i]; - } - }); + .allMatch(i -> Microbot.getClient().getBoostedSkillLevel(skills[i]) >= requiredLevels[i]); } private void updateActionBasedOnQuestState(Transport transport) { @@ -732,19 +713,6 @@ private boolean hasChronicleCharges() { return charges != null && Integer.parseInt(charges) > 0; } - /** - * Check if a spirit tree transport is usable - * This method integrates with the farming system to determine if farmable spirit trees - * are planted and healthy enough for transportation - * - * @param transport The spirit tree transport to check - * @return true if the spirit tree is available for travel - */ - private boolean isSpiritTreeUsable(Transport transport) { - // Use the Rs2SpiritTreeCache directly for better performance and consistency - return Rs2SpiritTreeCache.isSpiritTreeTransportAvailable(transport); - } - @Deprecated(since = "1.6.2 - Add Restrictions to restrictions.tsv", forRemoval = true) public void setRestrictedTiles(Restriction... restrictions) { this.customRestrictions = List.of(restrictions); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/bank/Rs2Bank.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/bank/Rs2Bank.java index ee9d864877e..82492fa44de 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/bank/Rs2Bank.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/bank/Rs2Bank.java @@ -1,12 +1,10 @@ package net.runelite.client.plugins.microbot.util.bank; import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; import lombok.extern.slf4j.Slf4j; import net.runelite.api.*; import net.runelite.api.coords.WorldArea; import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.ItemContainerChanged; import net.runelite.api.gameval.InterfaceID; import net.runelite.api.gameval.ItemID; import net.runelite.api.gameval.VarbitID; @@ -21,7 +19,6 @@ import net.runelite.client.plugins.microbot.shortestpath.pathfinder.Pathfinder; import net.runelite.client.plugins.microbot.util.antiban.Rs2AntibanSettings; import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; -import net.runelite.client.plugins.microbot.util.cache.serialization.CacheSerializationManager; import net.runelite.client.plugins.microbot.util.coords.Rs2WorldPoint; import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; @@ -58,7 +55,6 @@ import static net.runelite.api.widgets.ComponentID.BANK_INVENTORY_ITEM_CONTAINER; import static net.runelite.api.widgets.ComponentID.BANK_ITEM_CONTAINER; -import static net.runelite.client.plugins.microbot.Microbot.updateItemContainer; import static net.runelite.client.plugins.microbot.util.Global.*; import static net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject.hoverOverObject; import static net.runelite.client.plugins.microbot.util.npc.Rs2Npc.hoverOverActor; @@ -2143,358 +2139,6 @@ public static boolean walkToBankAndUseBank(BankLocation bankLocation, boolean to return Rs2Bank.openBank(); } - /** - * Updates the bank items in memory based on the provided event. - * Thread-safe method called from the client thread via event handler. - * - * @param e The event containing the latest bank items. - */ - public static void updateLocalBank(ItemContainerChanged e) { - synchronized (lock) { - List list = updateItemContainer(InventoryID.BANK.getId(), e); - if (list != null) { - // Update the centralized bank data (Rs2BankData.set() is already synchronized) - updateCache(list); - } else { - log.debug("Bank data update skipped - no items received"); - } - } - } - - - /** - * Updates the cached bank data with the latest bank items and saves to config. - * - * @param items The current bank items - */ - private static void updateCache(List items) { - if (items != null) { - // save the current bank items before updating - if ( !rsProfileKey.get().isEmpty() && !rsProfileKey.get().equals(Microbot.getConfigManager().getRSProfileKey())){ - saveCacheToConfig(rsProfileKey.get()); - } - rs2BankData.set(items); - if (rsProfileKey.get().isEmpty() || !rsProfileKey.get().equals(Microbot.getConfigManager().getRSProfileKey())) { - rsProfileKey.set(Microbot.getConfigManager().getRSProfileKey()); - } - saveCacheToConfig(rsProfileKey.get()); - validLoadedCache.set(true); - } - } - public static void loadInitialCacheFromCurrentConfig() { - rsProfileKey.set(Microbot.getConfigManager().getRSProfileKey()); - loadCacheFromConfig(rsProfileKey.get()); - } - /** - * Loads the initial bank state from config. Should be called when a player logs in. - * Thread-safe method that synchronizes config loading. - */ - public static void loadCacheFromConfig(String newRsProfileKey) { - synchronized (lock) { - if (!validLoadedCache.get()) { - Player localPlayer = Microbot.getClient().getLocalPlayer(); - if (localPlayer != null && localPlayer.getName() != null) { - loadCache(newRsProfileKey); - log.debug("-load bank cache, bank items size: {}", rs2BankData.size()); - validLoadedCache.set(LoginManager.isLoggedIn()); - } - } - } - } - - /** - * Sets the initial state as unknown. Called when logging out or changing profiles. - * Thread-safe method that synchronizes state clearing. - */ - public static void setUnknownInitialCacheState() { - synchronized (lock) { - if (validLoadedCache.get() && !rsProfileKey.get().isEmpty() && Microbot.getConfigManager() != null && rsProfileKey.get().equals(Microbot.getConfigManager().getRSProfileKey())) { - saveCacheToConfig(rsProfileKey.get()); - } - markCacheStale(); - rsProfileKey.set(""); - } - } - - /** - * Handles profile changes by saving current cache and invalidating for the new profile. - * This ensures cache state is properly maintained across profile switches. - * - * @param newProfileKey the new profile key - * @param oldProfileKey the previous profile key (can be null) - */ - public static void handleProfileChange(String newProfileKey, String oldProfileKey) { - synchronized (lock) { - log.debug("Handling bank cache profile change from '{}' to '{}'", oldProfileKey, newProfileKey); - - // Save current cache state if valid - if (oldProfileKey != null && !oldProfileKey.isEmpty() && isCacheDataValid()) { - log.debug("Saving bank cache for previous profile: {}", oldProfileKey); - saveCacheToConfig(oldProfileKey); - } - - // Mark cache as stale for profile change - markCacheStale(); - - // Update profile key - rsProfileKey.set(newProfileKey != null ? newProfileKey : ""); - - // Load cache for new profile if available - if (newProfileKey != null && !newProfileKey.isEmpty()) { - loadCacheFromConfig(newProfileKey); - } - } - } - - /** - * Loads bank state from config, handling profile changes. - * Similar to QuestBank.loadState(). - */ - private static void loadCache(String newRsProfileKey ) { - // Only re-load from config if loading from a new profile - if (newRsProfileKey != null && !newRsProfileKey.equals(rsProfileKey.get())) { - // If we've hopped between profiles, save current state first - if (!rsProfileKey.get().isEmpty() && validLoadedCache.get()) { - saveCacheToConfig(rsProfileKey.get()); - } - - loadCacheFromConfigInternal(newRsProfileKey); - } - } - - /** - * Loads bank data from RuneLite config system. - * Updated to use character-specific caching. - */ - private static void loadCacheFromConfigInternal(String rsProfileKey) { - if (rsProfileKey == null || Microbot.getConfigManager() == null) { - log.warn("Cannot load bank data, rsProfileKey or config manager is null"); - return; - } - - // get current player name for character-specific loading - String playerName = getCurrentPlayerName(); - if (playerName == null) { - log.warn("Cannot load bank data - no player name available"); - return; - } - - Rs2Bank.rsProfileKey.set(rsProfileKey); - worldType = RuneScapeProfileType.getCurrent(Microbot.getClient()); - log.debug("Loading bank data for profile: {}, player: {}, world type: {}", rsProfileKey, playerName, worldType); - - // use character-specific key - String characterSpecificKey = CacheSerializationManager.createCharacterSpecificKey(BANK_KEY, playerName); - String json = Microbot.getConfigManager().getConfiguration(CONFIG_GROUP, rsProfileKey, characterSpecificKey); - - try { - if (json != null && !json.isEmpty()) { - int[] data = gson.fromJson(json, int[].class); - log.debug("Loaded {} bank items from config for player {}", data.length, playerName); - rs2BankData.setIdQuantityAndSlot(data); - log.debug("finished loading bank data for player {}, size: {}", playerName, rs2BankData.size()); - - // Load cached items if no live bank data - if (rs2BankData.getBankItems().isEmpty()) { - // Cache is already loaded via setIdQuantityAndSlot - log.debug("Loaded {} cached bank items from config for player {}", rs2BankData.size(), playerName); - } - log.debug("build data should now be valid for player {}, size: {}", playerName, rs2BankData.size()); - } else { - rs2BankData.setEmpty(); - log.debug("No cached bank data found in config for player {}", playerName); - } - } catch (JsonSyntaxException err) { - log.warn("Failed to parse cached bank data from config for player {}, resetting cache", playerName, err); - rs2BankData.setEmpty(); - saveCacheToConfig(Rs2Bank.rsProfileKey.get()); - } - } - - /** - * Saves the current bank state to RuneLite config system. - * Updated to use character-specific caching. - */ - public static void saveCacheToConfig(String newRsProfileKey) { - if (newRsProfileKey == null || Microbot.getConfigManager() == null) { - return; - } - - // get current player name for character-specific saving - String playerName = getCurrentPlayerName(); - if (playerName == null) { - log.warn("Cannot save bank data - no player name available"); - return; - } - - try { - // use character-specific key - String characterSpecificKey = CacheSerializationManager.createCharacterSpecificKey(BANK_KEY, playerName); - String json = gson.toJson(rs2BankData.getIdQuantityAndSlot()); - Microbot.getConfigManager().setConfiguration(CONFIG_GROUP, newRsProfileKey, characterSpecificKey, json); - log.debug("Saved {} bank items to config cache for player {}", rs2BankData.size(), playerName); - } catch (Exception e) { - log.error("Failed to save bank data to config for player {}", playerName, e); - } - } - - /** - * Clears the bank cache state. Called when logging out. - * Thread-safe method that synchronizes cache clearing. - */ - public static void emptyCacheState() { - synchronized (lock) { - rsProfileKey.set(""); - worldType = null; - rs2BankData.setEmpty(); - validLoadedCache.set(false); - // Rs2BankData handles its own cache states when emptied - log.debug("Emptied bank state and cache"); - } - } - - - /** - * Checks if we have cached bank data available. - * - * @return true if cached bank data is available, false otherwise - */ - public static boolean hasCachedBankData() { - return !rs2BankData.isEmpty(); - } - - /** - * Checks if the bank cache data is VALID (Profile-level validation). - * - * VALID = Rs2Bank profile state is consistent and trustworthy - * - validLoadedCache flag is true (Rs2Bank has processed cache data) - * - rsProfileKey matches current RuneLite profile (no profile switches) - * - ConfigManager is available for reading/writing cache - * - No stale cache from previous sessions or different characters - * - This is Rs2Bank's validation layer ON TOP OF Rs2BankData states - * - * NOTE: This does NOT check if cache is loaded or built - only profile consistency - * Use isCacheLoaded() to check complete cache readiness - * - * @return true if cache data is valid and current, false if stale or needs rebuild - */ - public static boolean isCacheDataValid() { - return validLoadedCache.get() - && !rsProfileKey.get().isEmpty() - && Microbot.getConfigManager() != null - && rsProfileKey.get().equals(Microbot.getConfigManager().getRSProfileKey()); - } - - /** - * Checks if the bank cache is COMPLETE AND READY for script usage. - * - * This is the MASTER CHECK that combines all validation layers: - * - * 1. VALID (Profile-level): Rs2Bank profile state is consistent - * - No profile switches, config manager available, flags consistent - * - * 2. LOADED (Data-level): Raw cache data exists from config - * - idQuantityAndSlot array populated with [id, quantity, slot] triplets - * - * 3. BUILT (Object-level): Rs2ItemModel objects are ready for use - * - rebuildBankItemsList() executed successfully on client thread - * - Items have proper names, properties, and are script-accessible - * - * Scripts should ONLY use bank data when this returns true. - * This prevents NPE, stale data, and incomplete cache issues. - * - * @return true if ALL cache layers are ready (valid + loaded + built), false otherwise - */ - public static boolean isCacheLoaded() { - return isCacheDataValid() && rs2BankData.isCacheReady(); - } - - /** - * Marks the cache as "stale" requiring rebuild on invalid cache data. - * This is called when cache data becomes inconsistent or profile changes. - */ - public static void markCacheStale() { - synchronized (lock) { - log.debug("Marking bank cache as stale - needs rebuild"); - rs2BankData.markForRebuild(); - validLoadedCache.set(false); - } - } - - /** - * Invalidates the bank cache, optionally saving current state first. - * Similar to Rs2CacheManager invalidation pattern. - * - * @param saveBeforeInvalidating if true, saves current cache state before invalidating - */ - public static void invalidateCache(boolean saveBeforeInvalidating) { - synchronized (lock) { - if (saveBeforeInvalidating && isCacheDataValid()) { - log.debug("Saving bank cache before invalidation"); - saveCacheToConfig(rsProfileKey.get()); - } - log.debug("Invalidating bank cache"); - rs2BankData.setEmpty(); - markCacheStale(); - } - } - - /** - * Forces a cache rebuild by marking it as stale and clearing data. - * This should be called when profile switches or data becomes inconsistent. - */ - public static void forceCacheRebuild() { - synchronized (lock) { - log.debug("Forcing bank cache rebuild due to inconsistent state"); - invalidateCache(true); - } - } - - /** - * Gets comprehensive cache state information for debugging. - * Includes both Rs2Bank and Rs2BankData states. - * - * @return formatted string with complete cache state details - */ - public static String getDetailedCacheState() { - return String.format("Rs2Bank[profileValid=%s, profileKey='%s'] + %s", - isCacheDataValid(), - rsProfileKey.get(), - rs2BankData.getCacheStateInfo()); - } - - /** - * Checks if the bank cache data is LOADED (Stage 1: Raw data from config). - * - * STAGE 1 LOADED = Raw integers available but NOT usable yet - * - idQuantityAndSlot array contains [id, quantity, slot] triplets - * - Data restored from RuneLite config on login/profile switch - * - Items are still just numbers - NO Rs2ItemModel objects yet - * - Client thread processing NOT required for this stage - * - Does NOT mean scripts can use the data yet - * - * @return true if raw cache data is loaded from config, false otherwise - */ - public static boolean isCacheDataLoaded() { - return rs2BankData.isCacheLoaded(); - } - - /** - * Checks if the bank cache is BUILT (Stage 2: Usable objects ready). - * - * STAGE 2 BUILT = Rs2ItemModel objects ready for script usage - * - rebuildBankItemsList() has completed successfully - * - Raw data converted to full Rs2ItemModel objects with names/properties - * - ItemManager validation completed on client thread - * - Scripts can immediately use hasItem(), count(), findBankItem(), etc. - * - No rebuild delays or client thread waiting required - * - * @return true if bankItems list is fully built and ready, false otherwise - */ - public static boolean isCacheDataBuilt() { - return rs2BankData.isCacheBuilt(); - } - /** * Handle bank pin boolean. * diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/CacheMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/CacheMode.java deleted file mode 100644 index 1afe1dcaaf0..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/CacheMode.java +++ /dev/null @@ -1,41 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -/** - * Enumeration defining different cache invalidation modes. - * Controls how and when cache entries are invalidated. - * - * @author Vox - * @version 1.0 - */ -public enum CacheMode { - /** - * Automatic invalidation mode. - * Cache entries are automatically invalidated based on TTL (Time-To-Live) - * and global invalidation intervals. This is the default behavior for - * most caches that need periodic refresh. - */ - AUTOMATIC_INVALIDATION, - - /** - * Event-driven invalidation mode. - * Cache entries are only invalidated when specific events occur. - * No automatic timeout-based invalidation is performed. - * - * This mode is ideal for entity caches (NPCs, Objects, Ground Items) - * where data should persist until: - * - GameState changes - * - Entity despawn events - * - Manual invalidation - */ - EVENT_DRIVEN_ONLY, - - /** - * Manual invalidation mode. - * Cache entries are never automatically invalidated. - * Invalidation must be triggered manually by calling invalidation methods. - * - * This mode provides maximum control over cache lifecycle - * and is suitable for data that rarely changes. - */ - MANUAL_ONLY -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/MemorySizeCalculator.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/MemorySizeCalculator.java deleted file mode 100644 index 9e3e733b5a7..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/MemorySizeCalculator.java +++ /dev/null @@ -1,514 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Quest; -import net.runelite.api.QuestState; -import net.runelite.api.Skill; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.util.cache.model.*; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; - -import java.lang.reflect.Array; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; - -/** - * Utility class for calculating approximate memory sizes of objects in cache. - * Provides both precise calculations using Java Instrumentation (when available) - * and estimates using reflection for fallback. - * - * For maximum accuracy, enable Java Instrumentation by adding JVM argument: - * -javaagent:microbot-agent.jar - */ -@Slf4j -public class MemorySizeCalculator { - - // Configuration flags - private static final boolean USE_INSTRUMENTATION_WHEN_AVAILABLE = true; - private static final boolean LOG_MEASUREMENT_METHOD = false; // Set to true for debugging - - // Object header overhead on 64-bit JVM with compressed OOPs - private static final int OBJECT_HEADER_SIZE = 12; - private static final int REFERENCE_SIZE = 4; // Compressed OOPs - private static final int ARRAY_HEADER_SIZE = 16; - - // Primitive type sizes - private static final int BOOLEAN_SIZE = 1; - private static final int BYTE_SIZE = 1; - private static final int CHAR_SIZE = 2; - private static final int SHORT_SIZE = 2; - private static final int INT_SIZE = 4; - private static final int FLOAT_SIZE = 4; - private static final int LONG_SIZE = 8; - private static final int DOUBLE_SIZE = 8; - - // Common object sizes (cached for performance) - private static final Map, Integer> KNOWN_SIZES = new ConcurrentHashMap<>(); - - static { - // Primitive wrapper sizes - KNOWN_SIZES.put(Boolean.class, OBJECT_HEADER_SIZE + BOOLEAN_SIZE); - KNOWN_SIZES.put(Byte.class, OBJECT_HEADER_SIZE + BYTE_SIZE); - KNOWN_SIZES.put(Character.class, OBJECT_HEADER_SIZE + CHAR_SIZE); - KNOWN_SIZES.put(Short.class, OBJECT_HEADER_SIZE + SHORT_SIZE); - KNOWN_SIZES.put(Integer.class, OBJECT_HEADER_SIZE + INT_SIZE); - KNOWN_SIZES.put(Float.class, OBJECT_HEADER_SIZE + FLOAT_SIZE); - KNOWN_SIZES.put(Long.class, OBJECT_HEADER_SIZE + LONG_SIZE); - KNOWN_SIZES.put(Double.class, OBJECT_HEADER_SIZE + DOUBLE_SIZE); - - // Common RuneLite types - KNOWN_SIZES.put(WorldPoint.class, OBJECT_HEADER_SIZE + (3 * INT_SIZE)); // x, y, plane - KNOWN_SIZES.put(Skill.class, OBJECT_HEADER_SIZE + INT_SIZE); // enum ordinal - KNOWN_SIZES.put(Quest.class, OBJECT_HEADER_SIZE + INT_SIZE); // enum ordinal - KNOWN_SIZES.put(QuestState.class, OBJECT_HEADER_SIZE + INT_SIZE); // enum ordinal - - // AtomicLong used in cache statistics - KNOWN_SIZES.put(AtomicLong.class, OBJECT_HEADER_SIZE + LONG_SIZE); - } - - /** - * Calculates the approximate memory size of a cache key. - * - * @param key The cache key - * @return Estimated memory size in bytes - */ - public static long calculateKeySize(Object key) { - if (key == null) return 0; - - Class keyClass = key.getClass(); - - // Check known sizes first - Integer knownSize = KNOWN_SIZES.get(keyClass); - if (knownSize != null) { - return knownSize; - } - - // Handle common key types - if (key instanceof String) { - String str = (String) key; - return OBJECT_HEADER_SIZE + INT_SIZE + // String object (hash field) - ARRAY_HEADER_SIZE + (str.length() * CHAR_SIZE); // char array - } - - if (key instanceof Integer) { - return OBJECT_HEADER_SIZE + INT_SIZE; - } - - if (key instanceof Long) { - return OBJECT_HEADER_SIZE + LONG_SIZE; - } - - // For unknown types, use reflection-based calculation - return calculateObjectSizeReflection(key, new HashSet<>()); - } - - /** - * Calculates the approximate memory size of a cache value. - * Optimized for known Microbot cache value types. - * - * @param value The cache value - * @return Estimated memory size in bytes - */ - public static long calculateValueSize(Object value) { - if (value == null) return 0; - - Class valueClass = value.getClass(); - - // Check known sizes first - Integer knownSize = KNOWN_SIZES.get(valueClass); - if (knownSize != null) { - return knownSize; - } - - // Handle known Microbot cache value types - if (value instanceof Rs2NpcModel) { - return calculateNpcModelSize((Rs2NpcModel) value); - } - - if (value instanceof Rs2ObjectModel) { - return calculateObjectModelSize((Rs2ObjectModel) value); - } - - if (value instanceof Rs2GroundItemModel) { - return calculateGroundItemModelSize((Rs2GroundItemModel) value); - } - - if (value instanceof VarbitData) { - return calculateVarbitDataSize((VarbitData) value); - } - - if (value instanceof SkillData) { - return calculateSkillDataSize((SkillData) value); - } - - if (value instanceof SpiritTreeData) { - return calculateSpiritTreeDataSize((SpiritTreeData) value); - } - - if (value instanceof QuestState) { - return OBJECT_HEADER_SIZE + INT_SIZE; // Enum ordinal - } - - if (value instanceof String) { - String str = (String) value; - return OBJECT_HEADER_SIZE + INT_SIZE + - ARRAY_HEADER_SIZE + (str.length() * CHAR_SIZE); - } - - // For collections - if (value instanceof Collection) { - return calculateCollectionSize((Collection) value); - } - - if (value instanceof Map) { - return calculateMapSize((Map) value); - } - - // For unknown types, use reflection-based calculation - return calculateObjectSizeReflection(value, new HashSet<>()); - } - - /** - * Calculates memory size for Rs2NpcModel objects. - */ - private static long calculateNpcModelSize(Rs2NpcModel npcModel) { - long size = OBJECT_HEADER_SIZE; // Base object - - // Primitive fields in Rs2NpcModel (estimated from typical NPC model) - size += INT_SIZE * 6; // id, index, combatLevel, hitpoints, maxHitpoints, interacting - size += BOOLEAN_SIZE * 4; // animating, moving, inCombat, dead - size += LONG_SIZE * 2; // spawnTick, lastUpdateTick - - // Reference fields - size += REFERENCE_SIZE * 8; // worldPoint, animation, graphic, overhead, etc. - - // WorldPoint - size += OBJECT_HEADER_SIZE + (3 * INT_SIZE); - - // String name (average NPC name ~10 characters) - size += OBJECT_HEADER_SIZE + INT_SIZE + ARRAY_HEADER_SIZE + (10 * CHAR_SIZE); - - return size; - } - - /** - * Calculates memory size for Rs2ObjectModel objects. - */ - private static long calculateObjectModelSize(Rs2ObjectModel objectModel) { - long size = OBJECT_HEADER_SIZE; // Base object - - // Primitive fields - size += INT_SIZE * 4; // id, orientation, type, flags - size += LONG_SIZE * 2; // spawnTick, lastUpdateTick - - // Reference fields - size += REFERENCE_SIZE * 4; // worldPoint, actions, etc. - - // WorldPoint - size += OBJECT_HEADER_SIZE + (3 * INT_SIZE); - - // Actions array (average 5 actions, 8 chars each) - size += ARRAY_HEADER_SIZE + (5 * REFERENCE_SIZE); - size += 5 * (OBJECT_HEADER_SIZE + INT_SIZE + ARRAY_HEADER_SIZE + (8 * CHAR_SIZE)); - - return size; - } - - /** - * Calculates memory size for Rs2GroundItemModel objects. - */ - private static long calculateGroundItemModelSize(Rs2GroundItemModel groundItem) { - long size = OBJECT_HEADER_SIZE; // Base object - - // Primitive fields - size += INT_SIZE * 3; // id, quantity, visibleTicks - size += LONG_SIZE * 2; // spawnTick, despawnTick - - // Reference fields - size += REFERENCE_SIZE * 3; // worldPoint, name, etc. - - // WorldPoint - size += OBJECT_HEADER_SIZE + (3 * INT_SIZE); - - // Item name (average item name ~15 characters) - size += OBJECT_HEADER_SIZE + INT_SIZE + ARRAY_HEADER_SIZE + (15 * CHAR_SIZE); - - return size; - } - - /** - * Calculates memory size for VarbitData objects. - */ - private static long calculateVarbitDataSize(VarbitData varbitData) { - long size = OBJECT_HEADER_SIZE; // Base object - - // Primitive fields - size += INT_SIZE * 2; // varbit, value - size += LONG_SIZE * 2; // lastUpdateTick, cacheTimestamp - - // Reference fields - size += REFERENCE_SIZE * 2; // worldPoint, metadata - - // WorldPoint (if present) - size += OBJECT_HEADER_SIZE + (3 * INT_SIZE); - - // Metadata map (small, average 2 entries) - size += calculateMapSize(2, String.class, Object.class); - - return size; - } - - /** - * Calculates memory size for SkillData objects. - */ - private static long calculateSkillDataSize(SkillData skillData) { - long size = OBJECT_HEADER_SIZE; // Base object - - // Primitive fields - size += INT_SIZE * 3; // level, experience, boostedLevel - size += LONG_SIZE * 2; // lastUpdateTick, cacheTimestamp - - // Reference fields - size += REFERENCE_SIZE * 2; // skill enum, metadata - - // Skill enum - size += OBJECT_HEADER_SIZE + INT_SIZE; - - return size; - } - - /** - * Calculates memory size for SpiritTreeData objects. - */ - private static long calculateSpiritTreeDataSize(SpiritTreeData spiritTreeData) { - long size = OBJECT_HEADER_SIZE; // Base object - - // Primitive fields - size += INT_SIZE * 3; // patchIndex, state, level - size += LONG_SIZE * 3; // plantedTick, harvestTick, lastUpdateTick - - // Reference fields - size += REFERENCE_SIZE * 4; // location, treeType, metadata, etc. - - // WorldPoint - size += OBJECT_HEADER_SIZE + (3 * INT_SIZE); - - // String treeType (average ~12 characters) - size += OBJECT_HEADER_SIZE + INT_SIZE + ARRAY_HEADER_SIZE + (12 * CHAR_SIZE); - - return size; - } - - /** - * Calculates memory size for collections. - */ - private static long calculateCollectionSize(Collection collection) { - if (collection.isEmpty()) { - return OBJECT_HEADER_SIZE + INT_SIZE; // Empty collection overhead - } - - long size = OBJECT_HEADER_SIZE; // Collection object - - if (collection instanceof ArrayList) { - size += REFERENCE_SIZE + INT_SIZE * 2; // elementData array ref, size, modCount - size += ARRAY_HEADER_SIZE + (collection.size() * REFERENCE_SIZE); // Array overhead - } else if (collection instanceof HashSet) { - size += REFERENCE_SIZE * 3 + INT_SIZE * 3; // HashMap backing, size, threshold, modCount - size += calculateMapSize(collection.size(), Object.class, Object.class); - } else { - // Generic collection estimate - size += INT_SIZE + (collection.size() * REFERENCE_SIZE); - } - - // Add estimated content size (sample first few elements) - Iterator iter = collection.iterator(); - long avgElementSize = 0; - int sampleCount = Math.min(3, collection.size()); - - for (int i = 0; i < sampleCount && iter.hasNext(); i++) { - avgElementSize += calculateValueSize(iter.next()); - } - - if (sampleCount > 0) { - avgElementSize /= sampleCount; - size += avgElementSize * collection.size(); - } - - return size; - } - - /** - * Calculates memory size for maps. - */ - private static long calculateMapSize(Map map) { - if (map.isEmpty()) { - return OBJECT_HEADER_SIZE + INT_SIZE * 3; // Empty map overhead - } - - long size = OBJECT_HEADER_SIZE; // Map object - - if (map instanceof HashMap || map instanceof ConcurrentHashMap) { - size += REFERENCE_SIZE + INT_SIZE * 3; // table array ref, size, threshold, modCount - size += ARRAY_HEADER_SIZE + (map.size() * REFERENCE_SIZE * 2); // Bucket array + entries - size += map.size() * (OBJECT_HEADER_SIZE + INT_SIZE + REFERENCE_SIZE * 3); // Node objects - } else { - // Generic map estimate - size += INT_SIZE + (map.size() * REFERENCE_SIZE * 2); - } - - // Add estimated content size (sample first few entries) - Iterator> iter = map.entrySet().iterator(); - long avgKeySize = 0, avgValueSize = 0; - int sampleCount = Math.min(3, map.size()); - - for (int i = 0; i < sampleCount && iter.hasNext(); i++) { - Map.Entry entry = iter.next(); - avgKeySize += calculateKeySize(entry.getKey()); - avgValueSize += calculateValueSize(entry.getValue()); - } - - if (sampleCount > 0) { - avgKeySize /= sampleCount; - avgValueSize /= sampleCount; - size += (avgKeySize + avgValueSize) * map.size(); - } - - return size; - } - - /** - * Calculates memory size for a map with estimated entry count and types. - */ - private static long calculateMapSize(int entryCount, Class keyType, Class valueType) { - if (entryCount == 0) { - return OBJECT_HEADER_SIZE + INT_SIZE * 3; - } - - long size = OBJECT_HEADER_SIZE; // Map object - size += REFERENCE_SIZE + INT_SIZE * 3; // HashMap structure - size += ARRAY_HEADER_SIZE + (entryCount * REFERENCE_SIZE * 2); // Bucket array - size += entryCount * (OBJECT_HEADER_SIZE + INT_SIZE + REFERENCE_SIZE * 3); // Node objects - - // Add estimated key/value sizes - long keySize = KNOWN_SIZES.getOrDefault(keyType, 24); // Default estimate - long valueSize = KNOWN_SIZES.getOrDefault(valueType, 32); // Default estimate - - size += (keySize + valueSize) * entryCount; - - return size; - } - - /** - * Reflection-based object size calculation for unknown types. - * Uses visited set to handle circular references. - */ - private static long calculateObjectSizeReflection(Object obj, Set visited) { - if (obj == null || visited.contains(obj)) { - return 0; - } - - visited.add(obj); - - try { - Class clazz = obj.getClass(); - long size = OBJECT_HEADER_SIZE; - - // Handle arrays - if (clazz.isArray()) { - int length = Array.getLength(obj); - size += ARRAY_HEADER_SIZE; - - if (clazz.getComponentType().isPrimitive()) { - size += getPrimitiveArraySize(clazz.getComponentType(), length); - } else { - size += length * REFERENCE_SIZE; - // Sample array elements for content size - for (int i = 0; i < Math.min(3, length); i++) { - Object element = Array.get(obj, i); - size += calculateObjectSizeReflection(element, visited) / Math.min(3, length) * length; - } - } - return size; - } - - // Handle regular objects - while (clazz != null) { - for (Field field : clazz.getDeclaredFields()) { - if (Modifier.isStatic(field.getModifiers())) { - continue; - } - - Class fieldType = field.getType(); - - if (fieldType.isPrimitive()) { - size += getPrimitiveSize(fieldType); - } else { - size += REFERENCE_SIZE; - - try { - field.setAccessible(true); - Object fieldValue = field.get(obj); - - // Only recurse for small objects to avoid deep recursion - if (fieldValue != null && visited.size() < 10) { - size += calculateObjectSizeReflection(fieldValue, visited); - } - } catch (Exception e) { - // Field access failed, add conservative estimate - size += 32; - } - } - } - clazz = clazz.getSuperclass(); - } - - return size; - - } catch (Exception e) { - log.warn("Error calculating object size for {}: {}", obj.getClass().getSimpleName(), e.getMessage()); - return 64; // Conservative fallback estimate - } finally { - visited.remove(obj); - } - } - - /** - * Gets the size of a primitive type. - */ - private static int getPrimitiveSize(Class primitiveType) { - if (primitiveType == boolean.class) return BOOLEAN_SIZE; - if (primitiveType == byte.class) return BYTE_SIZE; - if (primitiveType == char.class) return CHAR_SIZE; - if (primitiveType == short.class) return SHORT_SIZE; - if (primitiveType == int.class) return INT_SIZE; - if (primitiveType == float.class) return FLOAT_SIZE; - if (primitiveType == long.class) return LONG_SIZE; - if (primitiveType == double.class) return DOUBLE_SIZE; - return 0; - } - - /** - * Gets the size of a primitive array. - */ - private static long getPrimitiveArraySize(Class componentType, int length) { - return (long) getPrimitiveSize(componentType) * length; - } - - /** - * Formats memory size in human-readable format. - */ - public static String formatMemorySize(long bytes) { - if (bytes < 1024) { - return bytes + " B"; - } else if (bytes < 1024 * 1024) { - return String.format("%.1f KB", bytes / 1024.0); - } else { - return String.format("%.1f MB", bytes / (1024.0 * 1024.0)); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2Cache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2Cache.java deleted file mode 100644 index 3e90c16ed43..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2Cache.java +++ /dev/null @@ -1,1356 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Client; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.strategy.*; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Abstract base cache implementation following game cache framework guidelines. - * - * This abstract class provides common cache functionality and is designed to be extended - * by specific cache implementations like Rs2NpcCache, Rs2SkillCache, etc. - * - * Key improvements: - * - Composition over inheritance via strategy pattern - * - Pluggable invalidation, query, and wrapper strategies - * - Thread-safe reads with minimal locks - * - Unified interface for all cache types - * - Configurable eviction policies - * - Event-driven invalidation support - * - * @param The type of keys used in the cache - * @param The type of values stored in the cache - */ -@Slf4j -public abstract class Rs2Cache implements AutoCloseable, CacheOperations { - - // ============================================ - // UTC Timestamp Constants - // ============================================ - - /** UTC timestamp formatter for cache logging */ - private static final DateTimeFormatter UTC_TIMESTAMP_FORMATTER = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS 'UTC'") - .withZone(ZoneOffset.UTC); - - /** - * Gets the current UTC timestamp in milliseconds since epoch. - * - * @return Current UTC timestamp in milliseconds - */ - private static long getCurrentUtcTimestamp() { - return Instant.now().toEpochMilli(); - } - - /** - * Gets the current UTC time as ZonedDateTime. - * - * @return Current UTC time as ZonedDateTime - */ - public static ZonedDateTime getCurrentUtcTime() { - return ZonedDateTime.now(ZoneOffset.UTC); - } - - /** - * Formats a timestamp (in milliseconds since epoch) to a human-readable UTC string. - * - * @param timestampMillis The timestamp in milliseconds since epoch - * @return Formatted UTC timestamp string - */ - public static String formatUtcTimestamp(long timestampMillis) { - return UTC_TIMESTAMP_FORMATTER.format(Instant.ofEpochMilli(timestampMillis)); - } - - /** - * Formats a ZonedDateTime to a human-readable UTC string. - * - * @param zonedDateTime The ZonedDateTime to format - * @return Formatted UTC timestamp string - */ - public static String formatUtcTimestamp(ZonedDateTime zonedDateTime) { - return UTC_TIMESTAMP_FORMATTER.format(zonedDateTime.withZoneSameInstant(ZoneOffset.UTC)); - } - - // ============================================ - // POH Region Constants - // ============================================ - - /** POH region ID */ - private static final int POH_REGION_RIMMINGTON_1 = 7513; - private static final int POH_REGION_RIMMINGTON_2 = 7514; - private static final int POH_REGION_UNKNOWN__1 = 8025; - private static final int POH_REGION_ADVERTISEMENT_2 = 8026; - private static final int POH_REGION_ADVERTISEMENT_3 = 7769; - private static final int POH_REGION_ADVERTISEMENT_4 = 7770; - - /** All POH region IDs for easy checking (immutable list) */ - private static final List POH_REGIONS = Collections.unmodifiableList(Arrays.asList( - POH_REGION_RIMMINGTON_1, - POH_REGION_RIMMINGTON_2, - POH_REGION_UNKNOWN__1, - POH_REGION_ADVERTISEMENT_2, - POH_REGION_ADVERTISEMENT_3, - POH_REGION_ADVERTISEMENT_4 - )); - - - - private final String cacheName; - - @Getter - private final CacheMode cacheMode; - - // Core cache storage - private final ConcurrentHashMap cache; // Object to support wrapped values - private final ConcurrentHashMap cacheTimestamps; - private final AtomicLong lastGlobalInvalidation; - private final AtomicBoolean isShutdown; - private final AtomicLong cacheHits; - private final AtomicLong cacheMisses; - private final AtomicLong totalInvalidations; - - // Cache configuration - private final long ttlMillis; - private volatile boolean enableCustomTTLInvalidation = false; - private final long creationTime; - - // Strategy composition - following framework guidelines - private final List> updateStrategies; - private final List> queryStrategies; - @SuppressWarnings("rawtypes") - private volatile ValueWrapper valueWrapper; // Optional value wrapping - - // Periodic cleanup system - private final ScheduledExecutorService cleanupExecutor; - private ScheduledFuture cleanupTask; - private final long cleanupIntervalMs; - - // ============================================ - // Region Change Detection - Unified Support - // ============================================ - - // Track current regions to detect changes across all cache types - private static volatile int[] lastKnownRegions = null; - private static volatile int lastRegionCheckTick = -1; - - // Thread-safe flag to prevent multiple region checks per tick - private static final AtomicBoolean regionCheckInProgress = new AtomicBoolean(false); - - /** - * Checks for region changes and clears cache if regions have changed. - * This handles the issue where RuneLite doesn't fire despawn events on region changes. - * Optimized to only check once per game tick across all cache instances. - */ - public static boolean checkAndHandleRegionChange(CacheOperations cache) { - return checkAndHandleRegionChange(cache, false,false); - } - - /** - * Checks for region changes and clears cache if regions have changed. - * - * @param cache The cache to potentially clear on region change - * @param force Whether to force a region check regardless of tick optimization - * @return true if region changed and cache was cleared, false otherwise - */ - public static boolean checkAndHandleRegionChange(CacheOperations cache, boolean force, boolean withInvalidation) { - // Get current game tick from client - Client client = Microbot.getClient(); - if (client == null) { - return false; - } - - int currentGameTick = client.getTickCount(); - - // Only check once per game tick to avoid redundant checks during burst events - if (!force && lastRegionCheckTick == currentGameTick) { - return false; - } - - // Prevent multiple concurrent region checks - if (!regionCheckInProgress.compareAndSet(false, true)) { - return false; - } - - try { - // Skip if cache is empty and we're not forcing - if (!force && cache.size() == 0) { - lastRegionCheckTick = currentGameTick; - return false; - } - - @SuppressWarnings("deprecation") - int[] currentRegions = client.getMapRegions(); - if (currentRegions == null) { - lastRegionCheckTick = currentGameTick; - return false; - } - - // Check if regions have changed - if (lastKnownRegions == null || !Arrays.equals(lastKnownRegions, currentRegions)) { - if (lastKnownRegions != null) { - log.debug("Region change detected for cache {} - clearing cache. Old regions: {}, New regions: {}", - cache.getCacheName(), Arrays.toString(lastKnownRegions), Arrays.toString(currentRegions)); - if(withInvalidation) cache.invalidateAll(); - lastKnownRegions = currentRegions.clone(); - lastRegionCheckTick = currentGameTick; - return true; - } else { - // First time initialization - lastKnownRegions = currentRegions.clone(); - } - } - - // Mark that we've checked regions this tick - lastRegionCheckTick = currentGameTick; - return false; - - } finally { - regionCheckInProgress.set(false); - } - } - - - - /** - * Checks if player is currently in a Player-Owned House (POH). - * Uses instance detection, portal presence, and region-based detection for reliable POH detection. - * - * @return true if player is in POH, false otherwise - */ - public static boolean isInPOH() { - try { - // Check if player is in instance and portal object exists - boolean instanceAndPortal = Rs2Player.IsInInstance() && Rs2GameObject.getTileObject(4525) != null; - // Check if current region is standard POH region - int[] currentRegions = getCurrentRegions(); - boolean inStandardPoh = currentRegions != null && - Arrays.stream(currentRegions).anyMatch(region -> POH_REGIONS.contains(region)); - // Return true if any detection method confirms POH - return inStandardPoh; - - } catch (Exception e) { - log.warn("Error checking POH status: {}", e.getMessage()); - return false; - } - } - - /** - * Gets the current player's regions if available. - * - * @return Array of current regions, or null if not available - */ - public static int[] getCurrentRegions() { - try { - Client client = Microbot.getClient(); - if (client == null) { - return null; - } - - @SuppressWarnings("deprecation") - int[] regions = client.getMapRegions(); - return regions != null ? regions.clone() : null; - - } catch (Exception e) { - log.warn("Error getting current regions: {}", e.getMessage()); - return null; - } - } - - // ============================================ - // Serialization Support - // ============================================ - - private String configKey; - private boolean persistenceEnabled = false; - - /** - * Enables persistence for this cache with the specified config key. - * The cache will be automatically saved and loaded from RuneLite profile configuration. - * - * @param configKey The config key to use for persistence - * @return This cache for method chaining - */ - public Rs2Cache withPersistence(String configKey) { - this.configKey = configKey; - this.persistenceEnabled = true; - log.debug("Enabled persistence for cache {} with config key: {}", cacheName, configKey); - return this; - } - - /** - * Gets the config key for this cache. - * - * @return The config key, or null if persistence is not enabled - */ - public String getConfigKey() { - return configKey; - } - - /** - * Checks if persistence is enabled for this cache. - * - * @return true if persistence is enabled - */ - public boolean isPersistenceEnabled() { - return persistenceEnabled; - } - // ============================================ - // custom invalidation Support - // ============================================ - protected void setEnableCustomTTLInvalidation() { - this.enableCustomTTLInvalidation = true; - log.debug("Enabled custom TTL invalidation for cache {}", cacheName); - } - protected boolean isEnableCustomTTLInvalidation() { - return enableCustomTTLInvalidation; - } - - - /** - * Constructor for cache with default configuration. - * - * @param cacheName The name of this cache for logging and debugging - */ - public Rs2Cache(String cacheName) { - this(cacheName, CacheMode.AUTOMATIC_INVALIDATION, 30_000L); - } - - /** - * Constructor for cache with specific mode. - * - * @param cacheName The name of this cache for logging and debugging - * @param cacheMode The cache invalidation mode - */ - public Rs2Cache(String cacheName, CacheMode cacheMode) { - this(cacheName, cacheMode, 30_000L); - } - - - /** - * Constructor for cache with full configuration. - * - * @param cacheName The name of this cache for logging and debugging - * @param cacheMode The cache invalidation mode - * @param ttlMillis Time-to-live for individual cache entries in milliseconds - */ - public Rs2Cache(String cacheName, CacheMode cacheMode, long ttlMillis) { - this.cacheName = cacheName; - this.cacheMode = cacheMode; - this.ttlMillis = ttlMillis; - this.creationTime = getCurrentUtcTimestamp(); - - // Initialize cleanup system - this.cleanupIntervalMs = Math.min(ttlMillis / 4, 30000); // Quarter of TTL or max 30 seconds - this.cleanupExecutor = Executors.newSingleThreadScheduledExecutor(r -> { - Thread thread = new Thread(r, "Rs2Cache-Cleanup-" + cacheName); - thread.setDaemon(true); - return thread; - }); - - this.cache = new ConcurrentHashMap<>(); - this.cacheTimestamps = new ConcurrentHashMap<>(); - this.lastGlobalInvalidation = new AtomicLong(getCurrentUtcTimestamp()); - this.isShutdown = new AtomicBoolean(false); - this.cacheHits = new AtomicLong(0); - this.cacheMisses = new AtomicLong(0); - this.totalInvalidations = new AtomicLong(0); - - // Initialize strategy collections - thread-safe - this.updateStrategies = new CopyOnWriteArrayList<>(); - this.queryStrategies = new CopyOnWriteArrayList<>(); - this.valueWrapper = null; - - // Start periodic cleanup task for all cache modes - startPeriodicCleanup(); - - log.debug("Created unified cache: {} with mode: {}, TTL: {}ms, Global invalidation: {}ms, Cleanup interval: {}ms", - cacheName, cacheMode, ttlMillis, cleanupIntervalMs); - } - - // ============================================ - // Strategy Management - Composition Pattern - // ============================================ - - /** - * Adds an invalidation strategy to this cache. - * Follows framework guideline: "Pluggable invalidation strategies" - * - * @param strategy The invalidation strategy to add - * @return This cache for method chaining - */ - public Rs2Cache withUpdateStrategy(CacheUpdateStrategy strategy) { - updateStrategies.add(strategy); - strategy.onAttach(this); - log.debug("Added invalidation strategy {} to cache {}", strategy.getClass().getSimpleName(), cacheName); - return this; - } - - /** - * Adds a query strategy to this cache. - * Enables specialized queries without inheritance. - * - * @param strategy The query strategy to add - * @return This cache for method chaining - */ - public Rs2Cache withQueries(QueryStrategy strategy) { - queryStrategies.add(strategy); - log.debug("Added query strategy {} to cache {}", strategy.getClass().getSimpleName(), cacheName); - return this; - } - - /** - * Sets a value wrapper strategy for this cache. - * Enables entity tracking, metadata, etc. without inheritance. - * - * @param wrapper The value wrapper to use - * @return This cache for method chaining - */ - @SuppressWarnings("rawtypes") - public Rs2Cache withWrapper(ValueWrapper wrapper) { - this.valueWrapper = wrapper; - log.debug("Added value wrapper {} to cache {}", wrapper.getClass().getSimpleName(), cacheName); - return this; - } - - // ============================================ - // Core Cache Operations - Thread-Safe - // ============================================ - - /** - * Gets a value from the cache if it exists and is not expired. - * Thread-safe reads with minimal locks following framework guidelines. - * - * @param key The key to retrieve - * @return The cached value or null if not found or expired - */ - @Override - @SuppressWarnings("unchecked") - public V get(K key) { - if (isShutdown.get()) { - return null; - } - - // No longer need checkGlobalInvalidation() - handled by periodic cleanup - - Object cachedValue = cache.get(key); - Long timestamp = cacheTimestamps.get(key); - - // Check if value exists and is not expired (respect cache mode) - if (cachedValue != null && timestamp != null && !isExpired(key)) { - cacheHits.incrementAndGet(); - log.trace("Cache hit for key {} in cache {}", key, cacheName); - - // Unwrap value if wrapper is present - if (valueWrapper != null) { - return (V) valueWrapper.unwrap(cachedValue); - } else { - return (V) cachedValue; - } - } - - cacheMisses.incrementAndGet(); - log.trace("Cache miss for key {} in cache {}", key, cacheName); - return null; - } - - /** - * Gets a raw cached value without expiration checking or additional operations. - * This method is specifically designed for update strategies during scene synchronization - * to avoid triggering recursive cache operations like scene scans. - * - * @param key The key to retrieve - * @return The raw cached value or null if not present - */ - @Override - public V getRawValue(K key) { - return getRawCachedValue(key); - } - - /** - * Retrieves a value from the cache. If not present or expired, loads it using the provided supplier. - * - * @param key The key to retrieve - * @param valueLoader Supplier function to load the value if not cached or expired - * @return The cached or newly loaded value - */ - public V get(K key, Supplier valueLoader) { - if (isShutdown.get()) { - log.warn("Cache \"{}\" is shut down, loading value directly", cacheName); - return valueLoader.get(); - } - - V cachedValue = get(key); - if (cachedValue != null) { - return cachedValue; - } - - // Load new value - try { - V newValue = valueLoader.get(); - if (newValue != null) { - put(key, newValue); - log.trace("Loaded new value for key {} in cache {}", key, cacheName); - } - return newValue; - } catch (Exception e) { - log.error("Error loading value for key {} in cache {}: {}", key, cacheName, e.getMessage(), e); - return null; - } - } - - /** - * Puts a value into the cache with current timestamp. - * - * @param key The key to store - * @param value The value to store - */ - @Override - public void put(K key, V value) { - - if (isShutdown.get() || value == null) { - return; - } - - // Wrap value if wrapper is present - Object valueToStore = value; - if (valueWrapper != null) { - @SuppressWarnings("unchecked") - Object wrapped = valueWrapper.wrap(value, key); - valueToStore = wrapped; - } - - cache.put(key, valueToStore); - cacheTimestamps.put(key, getCurrentUtcTimestamp()); - - log.trace("Put value for key {} in cache {}", key, cacheName); - } - - /** - * Removes a specific key from the cache. - * - * @param key The key to remove - */ - @Override - public void remove(K key) { - cache.remove(key); - cacheTimestamps.remove(key); - log.trace("Removed key {} from cache {}", key, cacheName); - } - - /** - * Invalidates all cached data in a thread-safe manner. - * Uses synchronization to ensure atomicity of cache and timestamp clearing. - */ - @Override - public synchronized void invalidateAll() { - int sizeBefore = cache.size(); - cache.clear(); - cacheTimestamps.clear(); - lastGlobalInvalidation.set(getCurrentUtcTimestamp()); - totalInvalidations.incrementAndGet(); - log.debug("Invalidated all {} entries in cache {}", sizeBefore, cacheName); - } - - /** - * Gets the cache timestamp for a specific key. - * - * @param key The key to get the timestamp for - * @return The timestamp when the key was cached, or -1 if not found - */ - public long getCacheTimestamp(K key) { - Long timestamp = cacheTimestamps.get(key); - return timestamp != null ? timestamp : 0L; - } - - // ============================================ - // Query Strategy Support - // ============================================ - - /** - * Executes a query using registered query strategies. - * - * @param criteria The query criteria - * @return Stream of matching values - */ - public synchronized Stream query(QueryCriteria criteria) { - for (QueryStrategy strategy : queryStrategies) { - for (Class supportedType : strategy.getSupportedQueryTypes()) { - if (supportedType.isInstance(criteria)) { - return strategy.executeQuery(this, criteria); - } - } - } - - log.warn("No query strategy found for criteria type: {} in cache {}", - criteria.getClass().getSimpleName(), cacheName); - return Stream.empty(); - } - - /** - * Gets all non-expired values from the cache. - * - * @return Collection of cached values - */ - @SuppressWarnings("unchecked") - public synchronized Collection values() { - if (isShutdown.get()) { - return Collections.emptyList(); - } - - // No longer need checkGlobalInvalidation() - handled by periodic cleanup - - return cache.entrySet().stream() - .filter(entry -> { - Long timestamp = cacheTimestamps.get(entry.getKey()); - return timestamp != null && !isExpired(entry.getKey()); - }) - .map(entry -> { - if (valueWrapper != null) { - return (V) valueWrapper.unwrap(entry.getValue()); - } else { - return (V) entry.getValue(); - } - }) - .collect(Collectors.toList()); - - } - - // ============================================ - // Stream Support for Specialized Caches - // ============================================ - - /** - * Gets all values as a stream for specialized cache implementations. - * Each specialized cache can use this to implement its own domain-specific methods. - * - * @return Stream of all cached values - */ - - public synchronized Stream stream() { - if (isShutdown.get()) { - return Stream.empty(); - } - // No longer need checkGlobalInvalidation() - handled by periodic cleanup - - // Defensive copy for strong consistency, but less efficient: - List> entries = new ArrayList<>(cache.entrySet()); - return entries.stream() - .filter(entry -> { - Long timestamp = cacheTimestamps.get(entry.getKey()); - return timestamp != null && !isExpired(entry.getKey()); - }) - .map(entry -> { - if (valueWrapper != null) { - @SuppressWarnings("unchecked") - V unwrapped = (V) valueWrapper.unwrap(entry.getValue()); - return unwrapped; - } else { - @SuppressWarnings("unchecked") - V value = (V) entry.getValue(); - return value; - } - }) - .filter(Objects::nonNull); - } - - /** - * Returns statistics as a formatted string for legacy compatibility. - */ - public String getStatisticsString() { - CacheStatistics stats = getStatistics(); - return String.format( - "Cache: %s | Size: %d | Hits: %d | Misses: %d | Hit Rate: %.2f%% | Mode: %s | Invalidations: %d | Memory: %s", - stats.cacheName, - stats.currentSize, - stats.cacheHits, - stats.cacheMisses, - stats.getHitRate() * 100, - stats.cacheMode.toString(), - stats.totalInvalidations, - stats.getFormattedMemorySize() - ); - } - - // ============================================ - // Event Handling for Strategies - // ============================================ - - /** - * Handles an event by delegating to all registered invalidation strategies. - * - * @param event The event to handle - */ - public void handleEvent(Object event) { - for (CacheUpdateStrategy strategy : updateStrategies) { - for (Class eventType : strategy.getHandledEventTypes()) { - if (eventType.isInstance(event)) { - try { - strategy.handleEvent(event, this); - } catch (Exception e) { - log.error("Error handling event {} in strategy {} for cache {}: {}", - event.getClass().getSimpleName(), - strategy.getClass().getSimpleName(), - cacheName, e.getMessage(), e); - } - break; - } - } - } - } - - // ============================================ - // CacheOperations Interface Implementation - // ============================================ - - @Override - public boolean containsKey(K key) { - return cache.containsKey(key) && !isExpired(key); - } - - @Override - public int size() { - return cache.size(); - } - - @Override - public String getCacheName() { - return cacheName; - } - - @Override - public Stream keyStream() { - return entryStream().map(Map.Entry::getKey); - } - - @Override - public Stream valueStream() { - return entryStream().map(Map.Entry::getValue); - } - - // ============================================ - // Print Functions for Cache Information - // ============================================ - - /** - * Returns a detailed formatted string containing all cache information. - * Includes cache metadata, statistics, configuration, and stored data. - * - * @return Detailed multi-line string representation of the cache - */ - public String printDetailedCacheInfo() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") - .withZone(ZoneId.systemDefault()); - - sb.append("=".repeat(80)).append("\n"); - sb.append(" DETAILED CACHE INFORMATION\n"); - sb.append("=".repeat(80)).append("\n"); - - // Cache metadata - sb.append(String.format("Cache Name: %s\n", cacheName)); - sb.append(String.format("Cache Mode: %s\n", cacheMode)); - sb.append(String.format("Created: %s\n", formatter.format(Instant.ofEpochMilli(creationTime)))); - sb.append(String.format("Uptime: %d ms\n", getCurrentUtcTimestamp() - creationTime)); - sb.append(String.format("Is Shutdown: %s\n", isShutdown.get())); - sb.append("\n"); - - // Configuration - sb.append("CONFIGURATION:\n"); - sb.append(String.format("TTL (milliseconds): %d\n", ttlMillis)); - sb.append(String.format("Persistence Enabled: %s\n", persistenceEnabled)); - if (persistenceEnabled) { - sb.append(String.format("Config Key: %s\n", configKey)); - } - sb.append("\n"); - - // Statistics - CacheStatistics stats = getStatistics(); - sb.append("STATISTICS:\n"); - sb.append(String.format("Current Size: %d entries\n", stats.currentSize)); - sb.append(String.format("Cache Hits: %d\n", stats.cacheHits)); - sb.append(String.format("Cache Misses: %d\n", stats.cacheMisses)); - sb.append(String.format("Hit Rate: %.2f%%\n", stats.getHitRate() * 100)); - sb.append(String.format("Total Invalidations: %d\n", stats.totalInvalidations)); - sb.append(String.format("Last Global Invalidation: %s\n", - formatter.format(Instant.ofEpochMilli(lastGlobalInvalidation.get())))); - sb.append("\n"); - - // Strategies - sb.append("STRATEGIES:\n"); - sb.append(String.format("Update Strategies: %d\n", updateStrategies.size())); - for (CacheUpdateStrategy strategy : updateStrategies) { - sb.append(String.format(" - %s\n", strategy.getClass().getSimpleName())); - } - sb.append(String.format("Query Strategies: %d\n", queryStrategies.size())); - for (QueryStrategy strategy : queryStrategies) { - sb.append(String.format(" - %s\n", strategy.getClass().getSimpleName())); - } - sb.append(String.format("Value Wrapper: %s\n", - valueWrapper != null ? valueWrapper.getClass().getSimpleName() : "None")); - sb.append("\n"); - - // Cache entries - sb.append("-".repeat(80)).append("\n"); - sb.append(" CACHE ENTRIES\n"); - sb.append("-".repeat(80)).append("\n"); - - sb.append(String.format("%-20s %-30s %-19s\n", "KEY", "VALUE", "TIMESTAMP")); - sb.append("-".repeat(80)).append("\n"); - - cache.entrySet().stream() - .sorted((e1, e2) -> { - Long t1 = cacheTimestamps.get(e1.getKey()); - Long t2 = cacheTimestamps.get(e2.getKey()); - if (t1 == null || t2 == null) return 0; - return Long.compare(t2, t1); // Most recent first - }) - .forEach(entry -> { - String key = String.valueOf(entry.getKey()); - String value = String.valueOf(entry.getValue()); - Long timestamp = cacheTimestamps.get(entry.getKey()); - String timestampStr = timestamp != null ? - formatter.format(Instant.ofEpochMilli(timestamp)) : "Unknown"; - - // Truncate long values - if (key.length() > 20) key = key.substring(0, 17) + "..."; - if (value.length() > 30) value = value.substring(0, 27) + "..."; - - sb.append(String.format("%-20s %-30s %-19s\n", key, value, timestampStr)); - }); - - if (cache.isEmpty()) { - sb.append("No entries in cache\n"); - } - - sb.append("-".repeat(80)).append("\n"); - sb.append(String.format("Generated at: %s\n", formatter.format(Instant.now()))); - sb.append("=".repeat(80)); - - return sb.toString(); - } - - /** - * Returns a summary formatted string containing essential cache information. - * Compact view showing key metrics and status. - * - * @return Summary multi-line string representation of the cache - */ - public String printCacheSummary() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss"); - - sb.append("┌─ CACHE SUMMARY: ").append(cacheName).append(" ") - .append("─".repeat(Math.max(1, 45 - cacheName.length()))).append("┐\n"); - - CacheStatistics stats = getStatistics(); - - // Summary statistics - sb.append(String.format("│ Entries: %-3d │ Mode: %-12s │ Status: %-8s │\n", - stats.currentSize, cacheMode, isShutdown.get() ? "Shutdown" : "Active")); - - sb.append(String.format("│ Hits: %-6d │ Misses: %-6d │ Hit Rate: %5.1f%% │\n", - stats.cacheHits, stats.cacheMisses, stats.getHitRate() * 100)); - - // Configuration summary - sb.append(String.format("│ TTL: %-7d ms │ Invalidations: %-8d │\n", - ttlMillis, stats.totalInvalidations)); - - // Strategy summary - sb.append(String.format("│ Strategies: %-2d │ Persistence: %-8s │\n", - updateStrategies.size() + queryStrategies.size(), - persistenceEnabled ? "Enabled" : "Disabled")); - - // Uptime - long uptimeMs = getCurrentUtcTimestamp() - creationTime; - String uptimeStr; - if (uptimeMs < 60000) { - uptimeStr = String.format("%ds", uptimeMs / 1000); - } else if (uptimeMs < 3600000) { - uptimeStr = String.format("%dm %ds", uptimeMs / 60000, (uptimeMs % 60000) / 1000); - } else { - uptimeStr = String.format("%dh %dm", uptimeMs / 3600000, (uptimeMs % 3600000) / 60000); - } - - sb.append(String.format("│ Uptime: %-16s │ Generated: %-8s │\n", - uptimeStr, formatter.format(Instant.now()))); - - sb.append("└").append("─".repeat(63)).append("┘"); - - return sb.toString(); - } - - // ============================================ - // Entry Access for Serialization - // ============================================ - - /** - * Gets all cache entries for serialization purposes. - * Returns a stream of key-value entries that are not expired. - * - * @return Stream of cache entries suitable for serialization - */ - public Stream> entryStream() { - if (isShutdown.get()) { - return Stream.empty(); - } - - // No longer need checkGlobalInvalidation() - handled by periodic cleanup - - return cache.entrySet().stream() - .filter(entry -> { - Long timestamp = cacheTimestamps.get(entry.getKey()); - return timestamp != null && !isExpired(entry.getKey()); - }) - .map(entry -> { - V value; - if (valueWrapper != null) { - value = (V) valueWrapper.unwrap(entry.getValue()); - } else { - value = (V) entry.getValue(); - } - return new AbstractMap.SimpleEntry<>(entry.getKey(), value); - }); - } - - /** - * Gets all cache entries as a map for serialization purposes. - * Only returns non-expired entries. - * - * @return Map of all non-expired cache entries - */ - public Map getEntriesForSerialization() { - Map result = new ConcurrentHashMap<>(); - - if (isShutdown.get()) { - return result; - } - - // No longer need checkGlobalInvalidation() - handled by periodic cleanup - - for (Map.Entry entry : cache.entrySet()) { - K key = entry.getKey(); - Long timestamp = cacheTimestamps.get(key); - - // Only include non-expired entries - if (timestamp != null && !isExpired(key)) { - Object cachedValue = entry.getValue(); - - // Unwrap value if wrapper is present - V value; - if (valueWrapper != null) { - @SuppressWarnings("unchecked") - V unwrappedValue = (V) valueWrapper.unwrap(cachedValue); - value = unwrappedValue; - } else { - @SuppressWarnings("unchecked") - V castedValue = (V) cachedValue; - value = castedValue; - } - - result.put(key, value); - } - } - - return result; - } - - /** - * Gets all cache entries as a map for serialization. - * - * @return Map of all non-expired cache entries - */ - public Map entryMap() { - return entryStream().collect(Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue, - (existing, replacement) -> replacement, // Handle duplicates by keeping the latest - ConcurrentHashMap::new - )); - } - - // ============================================ - // Private Helper Methods - // ============================================ - - /** - * Checks if a cache entry is expired. - * This method can be overridden by subclasses to implement custom expiration logic. - * - * @param key The cache key to check for expiration - * @return true if the entry should be considered expired - */ - protected boolean isExpired(K key) { - Long timestamp = cacheTimestamps.get(key); - if (timestamp == null) { - return true; - } - - // EVENT_DRIVEN_ONLY mode: entries never expire by time (unless custom logic overrides) - if (cacheMode == CacheMode.EVENT_DRIVEN_ONLY && !enableCustomTTLInvalidation) { - return false; - } - - // MANUAL_ONLY mode: entries never expire automatically - if (cacheMode == CacheMode.MANUAL_ONLY) { - return false; - } - - // AUTOMATIC_INVALIDATION mode: check TTL and remove if expired - long currentTime = getCurrentUtcTimestamp(); - if (currentTime - timestamp > ttlMillis) { - // Item has expired - remove it immediately from cache - Object removedValue = cache.remove(key); - cacheTimestamps.remove(key); - if (removedValue != null) { - log.debug("Removed expired entry during TTL check: key={} in cache {}", key, cacheName); - } - return true; - } - - return false; - } - - /** - * Protected method to get raw cached value without TTL validation. - * Used by subclasses for custom expiration logic to avoid recursion. - * - * @param key The cache key - * @return The raw cached value or null if not present - */ - @SuppressWarnings("unchecked") - protected V getRawCachedValue(K key) { - Object cachedValue = cache.get(key); - if (cachedValue == null) { - return null; - } - - // Unwrap value if wrapper is present - if (valueWrapper != null) { - return (V) valueWrapper.unwrap(cachedValue); - } else { - return (V) cachedValue; - } - } - - /** - * Public method for strategies to access raw cached values without triggering - * additional cache operations (like scene scans). This prevents recursive scanning. - * - * @param key The cache key - * @return The raw cached value or null if not present - */ - public V getRawCachedValueForStrategy(K key) { - return getRawCachedValue(key); - } - - /** - * Starts the periodic cleanup task for all cache modes. - * AUTOMATIC_INVALIDATION: Removes expired entries based on TTL - * EVENT_DRIVEN_ONLY: Can be overridden by subclasses for custom cleanup (e.g., despawn checking) - * MANUAL_ONLY: Generally no cleanup, but available for override - */ - protected void startPeriodicCleanup() { - if (cleanupExecutor.isShutdown()) { - return; - } - - cleanupTask = cleanupExecutor.scheduleWithFixedDelay(() -> { - try { - performPeriodicCleanup(); - } catch (Exception e) { - log.warn("Error during periodic cleanup for cache {}: {}", cacheName, e.getMessage()); - } - }, cleanupIntervalMs, cleanupIntervalMs, TimeUnit.MILLISECONDS); - - log.debug("Started periodic cleanup for cache {} every {}ms", cacheName, cleanupIntervalMs); - } - - /** - * Performs the actual periodic cleanup. - * Can be overridden by subclasses to implement custom cleanup logic. - * Default implementation removes expired entries for AUTOMATIC_INVALIDATION mode. - */ - protected void performPeriodicCleanup() { - if (isShutdown.get()) { - return; - } - - if (cacheMode == CacheMode.AUTOMATIC_INVALIDATION) { - performTtlCleanup(); - } - // For other modes, subclasses can override this method for custom cleanup - } - - /** - * Removes expired entries based on TTL for AUTOMATIC_INVALIDATION mode. - * This replaces the global invalidation approach with per-entry checking. - */ - private void performTtlCleanup() { - if (cache.isEmpty()) { - return; - } - - long currentTime = getCurrentUtcTimestamp(); - List expiredKeys = new ArrayList<>(); - - // Collect expired keys - for (Map.Entry entry : cache.entrySet()) { - K key = entry.getKey(); - Long timestamp = cacheTimestamps.get(key); - - if (timestamp == null || (currentTime - timestamp) > ttlMillis) { - expiredKeys.add(key); - } - } - - // Remove expired entries - int removedCount = 0; - for (K key : expiredKeys) { - if (cache.remove(key) != null) { - cacheTimestamps.remove(key); - removedCount++; - } - } - - if (removedCount > 0) { - totalInvalidations.addAndGet(removedCount); - log.debug("Removed {} expired entries from cache {}", removedCount, cacheName); - } - } - - /** - * Calculates the estimated memory size of this cache in bytes. - * Includes keys, values, timestamps, and internal data structures. - * - * @return Estimated memory usage in bytes - */ - public long getEstimatedMemorySize() { - if (cache.isEmpty()) { - return getEmptyCacheMemorySize(); - } - - long totalSize = 0; - - // Base cache object overhead - totalSize += getBaseCacheMemorySize(); - - // Calculate size of stored entries - long keySize = 0; - long valueSize = 0; - long timestampSize = 0; - - // Sample a few entries to estimate average sizes - int sampleSize = Math.min(5, cache.size()); - int sampledEntries = 0; - - for (Map.Entry entry : cache.entrySet()) { - if (sampledEntries >= sampleSize) break; - - keySize += MemorySizeCalculator.calculateKeySize(entry.getKey()); - valueSize += MemorySizeCalculator.calculateValueSize(entry.getValue()); - timestampSize += Long.BYTES; // Long timestamp - - sampledEntries++; - } - - if (sampledEntries > 0) { - // Calculate average sizes and multiply by total entry count - long avgKeySize = keySize / sampledEntries; - long avgValueSize = valueSize / sampledEntries; - long avgTimestampSize = timestampSize / sampledEntries; - - totalSize += (avgKeySize + avgValueSize + avgTimestampSize) * cache.size(); - - // Add ConcurrentHashMap overhead per entry (Node objects, buckets) - totalSize += cache.size() * 64; // Estimated overhead per map entry - } - - return totalSize; - } - - /** - * Gets the base memory size of an empty cache. - */ - private long getEmptyCacheMemorySize() { - long size = 0; - - // Object header + all instance fields - size += 12; // Object header (64-bit JVM with compressed OOPs) - size += 4 * 8; // 8 reference fields (String, CacheMode, 2 ConcurrentHashMaps, 4 AtomicLong, AtomicBoolean) - size += 8 * 4; // 4 long fields - size += 4 * 2; // 2 int fields (if any) - size += 4 * 2; // 2 CopyOnWriteArrayList references - size += 4; // ValueWrapper reference - - // Empty ConcurrentHashMap overhead (x2 for cache and timestamps) - size += 2 * (12 + 4 + 4*3 + 16 + 16*4); // Object + fields + empty bucket array - - // AtomicLong objects (6 total) - size += 6 * (12 + 8); // Object header + long value - - // AtomicBoolean object - size += 12 + 1; // Object header + boolean value - - // CopyOnWriteArrayList objects (2 total) - size += 2 * (12 + 4*3 + 16); // Object + fields + empty array - - // String objects (cacheName) - if (cacheName != null) { - size += 12 + 4 + 16 + (cacheName.length() * 2); // String + char array - } - - return size; - } - - /** - * Gets the base memory size of cache infrastructure. - */ - private long getBaseCacheMemorySize() { - long size = getEmptyCacheMemorySize(); - - // Add strategy collections overhead - size += updateStrategies.size() * 4; // Reference per strategy - size += queryStrategies.size() * 4; // Reference per strategy - - // Add minimal strategy object overhead (strategies are typically small) - size += (updateStrategies.size() + queryStrategies.size()) * 32; // Estimated per strategy - - return size; - } - - /** - * Returns memory usage information as a formatted string. - */ - public String getMemoryUsageString() { - long memoryBytes = getEstimatedMemorySize(); - return String.format("Memory: %s (%d bytes)", - MemorySizeCalculator.formatMemorySize(memoryBytes), memoryBytes); - } - - /** - * Gets cache statistics for monitoring, including memory usage. - */ - public CacheStatistics getStatistics() { - return new CacheStatistics( - cacheName, - cacheMode, - size(), - cacheHits.get(), - cacheMisses.get(), - totalInvalidations.get(), - getCurrentUtcTimestamp() - creationTime, - ttlMillis, - getEstimatedMemorySize() - ); - } - - /** - * Cache statistics data class. - */ - public static class CacheStatistics { - public final String cacheName; - public final CacheMode cacheMode; - public final int currentSize; - public final long cacheHits; - public final long cacheMisses; - public final long totalInvalidations; - public final long uptime; - public final long ttlMillis; - public final long estimatedMemoryBytes; - - public CacheStatistics(String cacheName, CacheMode cacheMode, int currentSize, - long cacheHits, long cacheMisses, long totalInvalidations, - long uptime, long ttlMillis, - long estimatedMemoryBytes) { - this.cacheName = cacheName; - this.cacheMode = cacheMode; - this.currentSize = currentSize; - this.cacheHits = cacheHits; - this.cacheMisses = cacheMisses; - this.totalInvalidations = totalInvalidations; - this.uptime = uptime; - this.ttlMillis = ttlMillis; - this.estimatedMemoryBytes = estimatedMemoryBytes; - } - - public double getHitRate() { - long total = cacheHits + cacheMisses; - return total == 0 ? 0.0 : (double) cacheHits / total; - } - - public String getFormattedMemorySize() { - return MemorySizeCalculator.formatMemorySize(estimatedMemoryBytes); - } - } - - // ============================================ - // Abstract Methods for Specialized Cache Updates - // ============================================ - - /** - * Updates all cached data by retrieving fresh values from the game client. - * Each cache implementation must provide its own strategy for refreshing cached data. - * This method should iterate over existing cache entries and refresh them with current data. - */ - public abstract void update(); - - @Override - public void close() { - if (isShutdown.compareAndSet(false, true)) { - if (cleanupTask != null) { - cleanupTask.cancel(true); - } - - // Shutdown cleanup executor - if (cleanupExecutor != null && !cleanupExecutor.isShutdown()) { - cleanupExecutor.shutdown(); - try { - if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) { - cleanupExecutor.shutdownNow(); - } - } catch (InterruptedException e) { - cleanupExecutor.shutdownNow(); - Thread.currentThread().interrupt(); - } - } - - // Detach and close all strategies - for (CacheUpdateStrategy strategy : updateStrategies) { - try { - strategy.onDetach(this); - strategy.close(); // Close the strategy to release resources - } catch (Exception e) { - log.warn("Error detaching/closing strategy {} from cache {}: {}", - strategy.getClass().getSimpleName(), cacheName, e.getMessage()); - } - } - - cache.clear(); - cacheTimestamps.clear(); - log.debug("Closed cache: {}", cacheName); - } - } - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2CacheManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2CacheManager.java deleted file mode 100644 index 594ee2d2630..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2CacheManager.java +++ /dev/null @@ -1,1184 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Player; -import net.runelite.api.gameval.VarPlayerID; -import net.runelite.client.config.ConfigProfile; -import net.runelite.client.eventbus.EventBus; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.cache.serialization.CacheSerializationManager; -import net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executors; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -/** - * Central manager for all Rs2UnifiedCache instances in the Microbot framework. - * Handles lifecycle coordination, EventBus registration, cache persistence, and provides unified cache statistics. - * Updated to work with the new unified cache architecture where caches are static utility classes. - */ -@Slf4j -public class Rs2CacheManager implements AutoCloseable { - private static AtomicBoolean isEventRegistered = new AtomicBoolean (false); // Flag to track if event handlers are registered - private static Rs2CacheManager instance; - private static EventBus eventBus; - - private final ScheduledExecutorService cleanupExecutor; - private final ExecutorService cacheManagerExecutor; - private final AtomicBoolean isShutdown; - - // Profile management - similar to Rs2Bank - private static AtomicReference rsProfileKey = new AtomicReference<>(""); - private static AtomicBoolean loggedInCacheStateKnown = new AtomicBoolean(false); - - // Last known player name for shutdown saves when player may no longer be available - private static AtomicReference lastKnownPlayerName = new AtomicReference<>(""); - - // Cache loading retry configuration - private static final int MAX_CACHE_LOAD_ATTEMPTS = 10; // Configurable max retry attempts - private static final long CACHE_LOAD_RETRY_DELAY_MS = 1000; // 1 second between retries - private static final AtomicBoolean cacheLoadingInProgress = new AtomicBoolean(false); - - // Async operation tracking - private static final AtomicReference> currentSaveOperation = new AtomicReference<>(); - private static final AtomicReference> currentLoadOperation = new AtomicReference<>(); - - /** - * Checks if cache data is VALID at the manager level (profile consistency). - * - * VALID = Cache manager state is consistent and trustworthy - * - loggedInCacheStateKnown: Cache manager has initialized properly - * - rsProfileKey exists and matches current RuneLite profile - * - ConfigManager is available for cache operations - * - No cache loading operations in progress (atomic check) - * - No profile switches or stale manager state - * - * This is the TOP-LEVEL validation for all cache systems. - * Individual caches (Rs2Bank, etc.) add their own validation layers. - * - * @return true if cache manager state is valid and consistent, false otherwise - */ - public static boolean isCacheDataValid() { - return loggedInCacheStateKnown.get() && rsProfileKey != null && !rsProfileKey.get().isEmpty() - && Microbot.getConfigManager() != null - && rsProfileKey.get().equals(Microbot.getConfigManager().getRSProfileKey()) && cacheLoadingInProgress.get() == false; - } - - /** - * Checks if bank cache is COMPLETELY READY (all validation layers). - * - * This combines Rs2CacheManager validation + Rs2Bank validation + Rs2BankData states: - * 1. VALID: Cache manager profile consistency (this class) - * 2. VALID: Rs2Bank profile validation - * 3. LOADED: Raw cache data from config (Rs2BankData) - * 4. BUILT: Rs2ItemModel objects ready for scripts (Rs2BankData) - * - * Scripts should use this as the MASTER CHECK before using bank data. - * - * @return true if all cache layers are ready, false otherwise - */ - public static boolean isBankCacheLoaded() { - return Rs2Bank.isCacheLoaded(); - } - - /** - * Checks if bank cache is BUILT (Stage 2: Usable objects ready). - * - * BUILT = Rs2ItemModel objects created and ready for script usage - * - rebuildBankItemsList() completed successfully - * - Raw data converted to full objects with names/properties - * - ItemManager validation done on client thread - * - Scripts can immediately use hasItem(), count(), etc. - * - * @return true if bank items are built and script-ready, false otherwise - */ - public static boolean isBankCacheDataBuild() { - return Rs2Bank.isCacheDataBuilt(); - } - - /** - * Checks if bank cache is LOADED (Stage 1: Raw data from config). - * - * LOADED = Raw cache data exists but NOT yet usable - * - idQuantityAndSlot array contains [id, quantity, slot] triplets - * - Data restored from RuneLite config persistence - * - Items are still integers - NO Rs2ItemModel objects yet - * - Client thread processing NOT required for this stage - * - * @return true if raw cache data is loaded, false otherwise - */ - public static boolean isBankCacheDataLoaded() { - return Rs2Bank.isCacheDataLoaded(); - } - - - /** - * Private constructor for singleton pattern. - */ - private Rs2CacheManager() { - this.cleanupExecutor = Executors.newScheduledThreadPool(1, r -> { - Thread thread = new Thread(r, "Rs2CacheCleanup"); - thread.setDaemon(true); - return thread; - }); - this.cacheManagerExecutor = Executors.newFixedThreadPool(2, runnable -> { - Thread cacheThread = new Thread(runnable, "Rs2Cache-Persistence"); - cacheThread.setDaemon(true); - return cacheThread; - }); - this.isShutdown = new AtomicBoolean(false); - - log.debug("Rs2CacheManager (Unified) initialized with async operations support"); - } - - /** - * Gets the singleton instance of Rs2CacheManager. - * - * @return The singleton instance - */ - public static synchronized Rs2CacheManager getInstance() { - if (instance == null) { - instance = new Rs2CacheManager(); - } - return instance; - } - - /** - * Sets the EventBus instance and registers all cache event handlers. - * This method should be called during plugin startup to ensure all cache events are properly handled. - * Does NOT load persistent caches - that should be done when the profile is available. - * - * @param eventBus The RuneLite EventBus instance - */ - public static void setEventBus(EventBus eventBus) { - Rs2CacheManager.eventBus = eventBus; - - - } - - /** - * Registers all cache event handlers with the EventBus. - */ - public static void registerEventHandlers() { - if (eventBus == null || isEventRegistered.get()) { - log.warn("EventBus is null, cannot register cache event handlers"); - return; - } - - try { - // Register NPC cache events - - eventBus.register(Rs2NpcCache.getInstance()); - //Rs2NpcCache.getInstance().update(); - // Register Object cache events - eventBus.register(Rs2ObjectCache.getInstance()); - Rs2ObjectCache.getInstance().update(600*10); - // Register GroundItem cache events - eventBus.register(Rs2GroundItemCache.getInstance()); - //Rs2GroundItemCache.getInstance().update(); - // Register Varbit cache events - eventBus.register(Rs2VarbitCache.getInstance()); - - // Register VarPlayer cache events - eventBus.register(Rs2VarPlayerCache.getInstance()); - - // Register Skill cache events - eventBus.register(Rs2SkillCache.getInstance()); - - // Register Quest cache events - eventBus.register(Rs2QuestCache.getInstance()); - - // Register SpiritTree cache events - eventBus.register(Rs2SpiritTreeCache.getInstance()); - - Rs2CacheManager.isEventRegistered.set(true); // Set registration flag - log.info("All cache event handlers registered with EventBus"); - } catch (Exception e) { - log.error("Failed to register cache event handlers", e); - } - } - - /** - * Unregisters all cache event handlers from the EventBus. - */ - public static void unregisterEventHandlers() { - if (eventBus == null || !Rs2CacheManager.isEventRegistered.get()) { - return; - } - - try { - eventBus.unregister(Rs2NpcCache.getInstance()); - eventBus.unregister(Rs2ObjectCache.getInstance()); - eventBus.unregister(Rs2GroundItemCache.getInstance()); - eventBus.unregister(Rs2VarbitCache.getInstance()); - eventBus.unregister(Rs2VarPlayerCache.getInstance()); - eventBus.unregister(Rs2SkillCache.getInstance()); - eventBus.unregister(Rs2QuestCache.getInstance()); - eventBus.unregister(Rs2SpiritTreeCache.getInstance()); - Rs2CacheManager.isEventRegistered.set(false); // Reset registration flag - log.debug("All cache event handlers unregistered from EventBus"); - } catch (Exception e) { - log.error("Failed to unregister cache event handlers", e); - } - } - - /** - * Checks if cache event handlers are currently registered with the EventBus. - * - * @return true if event handlers are registered, false otherwise - */ - public static boolean isEventHandlersRegistered() { - return isEventRegistered.get(); - } - - /** - * Invalidates all known unified caches. - */ - public static void invalidateAllCaches(boolean savePersistentCaches) { - try { - if (savePersistentCaches) { - savePersistentCaches(Microbot.getConfigManager().getRSProfileKey()); - } - Rs2NpcCache.getInstance().invalidateAll(); - Rs2GroundItemCache.getInstance().invalidateAll(); - Rs2ObjectCache.getInstance().invalidateAll(); - Rs2VarbitCache.getInstance().invalidateAll(); - Rs2VarPlayerCache.getInstance().invalidateAll(); - Rs2SkillCache.getInstance().invalidateAll(); - Rs2QuestCache.getInstance().invalidateAll(); - Rs2SpiritTreeCache.getInstance().invalidateAll(); - } catch (Exception e) { - log.error("Error invalidating caches: {}", e.getMessage(), e); - } - } - - /** - * Triggers scene scans for all entity caches to repopulate them after clearing. - * This ensures caches are immediately synchronized with the current game scene. - * Should be called after invalidating caches to provide immediate data availability. - */ - public static void triggerSceneScansForAllCaches() { - try { - if (!Microbot.isLoggedIn() || Microbot.getClient() == null || Microbot.getClient().getLocalPlayer() == null) { - log.debug("Cannot trigger scene scans - not logged in"); - return; - } - - log.debug("Triggering scene scans for all entity caches after cache invalidation"); - - // Trigger scene scans for all entity caches with small delays to stagger the operations - Rs2NpcCache.requestSceneScan(); - Rs2GroundItemCache.requestSceneScan(); - Rs2ObjectCache.requestSceneScan(); - - log.debug("Scene scan requests sent to all entity caches"); - } catch (Exception e) { - log.error("Error triggering scene scans: {}", e.getMessage(), e); - } - } - - /** - * Updates the last known player name for use in situations where current player may not be available. - * This is crucial for shutdown saves when the player may have logged out. - * - * @param playerName The current player name to remember - */ - private static void updateLastKnownPlayerName(String playerName) { - if (playerName != null && !playerName.trim().isEmpty()) { - lastKnownPlayerName.set(playerName); - log.debug("Updated last known player name to: {}", playerName); - } - } - - /** - * Gets the current player name with improved tracking and caching. - * This method provides reliable player name access for cache operations. - * - * @return Current player name or null if not available - */ - public static String getCurrentPlayerName() { - try { - if (Microbot.isLoggedIn() && Microbot.getClient() != null) { - String currentPlayerName = Microbot.getClient().getLocalPlayer() != null ? - Microbot.getClient().getLocalPlayer().getName() : null; - if (currentPlayerName != null && !currentPlayerName.trim().isEmpty()) { - updateLastKnownPlayerName(currentPlayerName); - return currentPlayerName; - } - } - } catch (Exception e) { - log.debug("Error getting current player name: {}", e.getMessage()); - } - - return null; - } - - /** - * Gets the last known player name, falling back to current player if available. - * This ensures we can perform character-specific operations even during shutdown. - * - * @return Last known player name or null if never set - */ - public static String getLastKnownPlayerName() { - // try to get current player name first - String currentName = getCurrentPlayerName(); - if (currentName != null) { - return currentName; - } - - // fallback to last known player name - String lastName = lastKnownPlayerName.get(); - return (lastName != null && !lastName.trim().isEmpty()) ? lastName : null; - } - - /** - * Clears the last known player name. Should be called when switching profiles. - */ - private static void clearLastKnownPlayerName() { - lastKnownPlayerName.set(""); - log.debug("Cleared last known player name"); - } - - /** - * Gets the total number of entries across all unified caches. - * - * @return The total cache entry count - */ - public int getTotalCacheSize() { - try { - return Rs2NpcCache.getInstance().size() + - Rs2GroundItemCache.getInstance().size() + - Rs2ObjectCache.getInstance().size() + - Rs2VarbitCache.getInstance().size() + - Rs2VarPlayerCache.getInstance().size() + - Rs2SkillCache.getInstance().size() + - Rs2QuestCache.getInstance().size() + - Rs2SpiritTreeCache.getInstance().size(); - } catch (Exception e) { - log.error("Error calculating total cache size: {}", e.getMessage(), e); - return 0; - } - } - - /** - * Gets the total estimated memory usage across all unified caches. - * - * @return The total estimated memory usage in bytes - */ - public long getTotalMemoryUsage() { - try { - return Rs2NpcCache.getInstance().getEstimatedMemorySize() + - Rs2GroundItemCache.getInstance().getEstimatedMemorySize() + - Rs2ObjectCache.getInstance().getEstimatedMemorySize() + - Rs2VarbitCache.getInstance().getEstimatedMemorySize() + - Rs2VarPlayerCache.getInstance().getEstimatedMemorySize() + - Rs2SkillCache.getInstance().getEstimatedMemorySize() + - Rs2QuestCache.getInstance().getEstimatedMemorySize() + - Rs2SpiritTreeCache.getInstance().getEstimatedMemorySize(); - } catch (Exception e) { - log.error("Error calculating total memory usage: {}", e.getMessage(), e); - return 0; - } - } - - /** - * Provides unified cache statistics for debugging. - * - * @return A string containing cache statistics - */ - public String getCacheStatistics() { - StringBuilder stats = new StringBuilder(); - stats.append("Rs2CacheManager (Unified) Statistics:\n"); - - try { - int npcCount = Rs2NpcCache.getInstance().size(); - int groundItemCount = Rs2GroundItemCache.getInstance().size(); - int objectCount = Rs2ObjectCache.getInstance().size(); - int varbitCount = Rs2VarbitCache.getInstance().size(); - int varPlayerCount = Rs2VarPlayerCache.getInstance().size(); - int skillCount = Rs2SkillCache.getInstance().size(); - int questCount = Rs2QuestCache.getInstance().size(); - int spiritTreeCount = Rs2SpiritTreeCache.getInstance().size(); - - int totalEntries = npcCount + groundItemCount + objectCount + varbitCount + varPlayerCount + skillCount + questCount + spiritTreeCount; - - stats.append("Total entries: ").append(totalEntries).append("\n"); - stats.append("Individual cache sizes:\n"); - stats.append(" NpcCache (EVENT_DRIVEN): ").append(npcCount).append(" entries\n"); - stats.append(" GroundItemCache (EVENT_DRIVEN): ").append(groundItemCount).append(" entries\n"); - stats.append(" ObjectCache (EVENT_DRIVEN): ").append(objectCount).append(" entries\n"); - stats.append(" VarbitCache (AUTO_INVALIDATION): ").append(varbitCount).append(" entries\n"); - stats.append(" VarPlayerCache (EVENT_DRIVEN): ").append(varPlayerCount).append(" entries\n"); - stats.append(" SkillCache (AUTO_INVALIDATION): ").append(skillCount).append(" entries\n"); - stats.append(" QuestCache (AUTO_INVALIDATION): ").append(questCount).append(" entries\n"); - stats.append(" SpiritTreeCache (EVENT_DRIVEN_ONLY): ").append(spiritTreeCount).append(" entries\n"); - } catch (Exception e) { - stats.append("Error collecting statistics: ").append(e.getMessage()).append("\n"); - log.error("Error collecting cache statistics: {}", e.getMessage(), e); - } - - return stats.toString(); - } - - /** - * Gets detailed cache statistics for a specific cache. - * - * @param cacheName The name of the cache to get statistics for - * @return Cache statistics or null if cache not found - */ - public Rs2Cache.CacheStatistics getCacheStatistics(String cacheName) { - try { - switch (cacheName.toLowerCase()) { - case "npccache": - return Rs2NpcCache.getInstance().getStatistics(); - case "grounditemcache": - return Rs2GroundItemCache.getInstance().getStatistics(); - case "objectcache": - return Rs2ObjectCache.getInstance().getStatistics(); - case "varbitcache": - return Rs2VarbitCache.getInstance().getStatistics(); - case "varplayercache": - return Rs2VarPlayerCache.getInstance().getStatistics(); - case "skillcache": - return Rs2SkillCache.getInstance().getStatistics(); - case "questcache": - return Rs2QuestCache.getInstance().getStatistics(); - case "spirittreecache": - return Rs2SpiritTreeCache.getInstance().getStatistics(); - default: - log.warn("Unknown cache name: {}", cacheName); - return null; - } - } catch (Exception e) { - log.error("Error getting statistics for cache {}: {}", cacheName, e.getMessage(), e); - return null; - } - } - - /** - * Triggers cache cleanup for all known caches that support periodic cleanup. - */ - public void triggerCacheCleanup() { - // Note: With the unified architecture, caches handle their own cleanup - // based on their CacheMode. This method is kept for compatibility. - log.debug("Cache cleanup triggered (unified caches handle their own cleanup)"); - } - - /** - * Shuts down the cache manager and all managed caches. - */ - @Override - public void close() { - if (isShutdown.compareAndSet(false, true)) { - log.info("Shutting down Rs2CacheManager"); - - // - // Empty cache state and Save persistent caches before shutdown - emptyCacheState(); - // Unregister event handlers first - unregisterEventHandlers(); - // Close all cache instances to ensure proper resource cleanup - closeAllCaches(); - - - - // Wait for any ongoing cache operations before shutting down - try { - CompletableFuture ongoingSave = currentSaveOperation.get(); - CompletableFuture ongoingLoad = currentLoadOperation.get(); - - if (ongoingSave != null || ongoingLoad != null) { - log.info("Waiting for ongoing cache operations to complete during shutdown"); - if (ongoingSave != null) { - ongoingSave.get(15, TimeUnit.SECONDS); - } - if (ongoingLoad != null) { - ongoingLoad.get(15, TimeUnit.SECONDS); - } - log.info("Cache operations completed during shutdown"); - } - } catch (Exception e) { - log.error("Error waiting for cache operations during shutdown: {}", e.getMessage(), e); - } - - // Shutdown executors - cacheManagerExecutor.shutdown(); - try { - if (!cacheManagerExecutor.awaitTermination(5, TimeUnit.SECONDS)) { - cacheManagerExecutor.shutdownNow(); - } - } catch (InterruptedException e) { - cacheManagerExecutor.shutdownNow(); - Thread.currentThread().interrupt(); - } - - cleanupExecutor.shutdown(); - try { - if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) { - cleanupExecutor.shutdownNow(); - } - } catch (InterruptedException e) { - cleanupExecutor.shutdownNow(); - Thread.currentThread().interrupt(); - } - - log.debug("Rs2CacheManager shutdown complete"); - } - } - - /** - * Closes all cache instances to ensure proper resource cleanup. - * This includes shutting down any schedulers or background tasks. - */ - private void closeAllCaches() { - try { - log.debug("Closing all cache instances"); - - // Close object cache (includes ObjectUpdateStrategy shutdown) - Rs2ObjectCache.getInstance().close(); - - // Close other caches - Rs2NpcCache.getInstance().close(); - Rs2GroundItemCache.getInstance().close(); - Rs2VarbitCache.getInstance().close(); - Rs2VarPlayerCache.getInstance().close(); - Rs2SkillCache.getInstance().close(); - Rs2QuestCache.getInstance().close(); - Rs2SpiritTreeCache.getInstance().close(); - - log.debug("All cache instances closed successfully"); - } catch (Exception e) { - log.error("Error closing cache instances", e); - } - } - - /** - * Resets the singleton instance. Used for testing. - */ - public static synchronized void resetInstance() { - if (instance != null) { - instance.close(); - instance = null; - } - } - - /** - * Loads all persistent caches from RuneLite profile configuration. - * This method should be called when the RS profile is available (on login or profile change). - * Currently unused but kept for potential future use. - */ - @SuppressWarnings("unused") - private static void loadPersistentCaches() { - try { - rsProfileKey.set( Microbot.getConfigManager().getRSProfileKey()); - if (rsProfileKey == null || rsProfileKey.get().isEmpty()) { - log.warn("Cannot load persistent caches: profile key is null"); - return; - } - loadPersistentCaches(rsProfileKey.get()); - } catch (Exception e) { - log.error("Failed to load persistent caches", e); - } - } - - public static void loadVarPlayerCache(){ - try { - if (Microbot.getConfigManager() == null) { - log.warn("Cannot load persistent varplayer cache: ConfigManager is null"); - return; - } - String profileKey = Microbot.getConfigManager().getRSProfileKey(); - if(rsProfileKey == null || rsProfileKey.get().isEmpty()){ - log.warn("Cannot load persistent varplayer cache: profile key is null"); - return; - } - rsProfileKey.set( profileKey); - if (rsProfileKey == null || rsProfileKey.get().isEmpty()) { - log.warn("Cannot load persistent varplayer cache: profile key is null"); - return; - } - } catch (Exception e) { - log.error("Failed to load persistent varplayer cache", e); - return; - } - // Load VarPlayer cache - if (Rs2VarPlayerCache.getCache().isPersistenceEnabled()) { - String playerName = getLastKnownPlayerName(); - if (playerName != null) { - CacheSerializationManager.loadCache(Rs2VarPlayerCache.getCache(), Rs2VarPlayerCache.getCache().getConfigKey(), rsProfileKey.get(), playerName, false); - log.debug ("Loaded VarPlayer cache from configuration for player {}, new cache size: {}", - playerName, Rs2VarPlayerCache.getCache().size()); - } else { - log.warn("Cannot load VarPlayer cache - no player name available"); - } - } - - } - /** - * Loads persistent caches for a specific profile. - * - * @param profileKey The RuneLite profile key to load caches for - */ - private static void loadPersistentCaches(String profileKey) { - try { - if (profileKey == null) { - log.warn("Cannot load persistent caches: profile key is null"); - return; - } - - // get the last known player name for character-specific loading - String playerName = getLastKnownPlayerName(); - if (playerName == null) { - log.warn("Cannot load persistent caches - no player name available"); - return; - } - - Rs2CacheManager.rsProfileKey.set(profileKey); - - log.info("Loading persistent caches from configuration for profile: {} player: {}", profileKey, playerName); - - // Load Skills cache - if (Rs2SkillCache.getCache().isPersistenceEnabled()) { - CacheSerializationManager.loadCache(Rs2SkillCache.getCache(), Rs2SkillCache.getCache().getConfigKey(), profileKey, playerName, false); - log.info("Loaded Skills cache from configuration for player {}, new cache size: {}", - playerName, Rs2SkillCache.getCache().size()); - } - - // Load Quest cache - if (Rs2QuestCache.getCache().isPersistenceEnabled()) { - CacheSerializationManager.loadCache(Rs2QuestCache.getCache(), Rs2QuestCache.getCache().getConfigKey(), profileKey, playerName, false); - // Schedule an async update to populate quest states from client without blocking initialization - //Rs2QuestCache.updateAllFromClientAsync(); - log.debug ("Loaded Quest cache from configuration, new cache size: {}", - Rs2QuestCache.getCache().size()); - } - - // Load Varbit cache - if (Rs2VarbitCache.getCache().isPersistenceEnabled()) { - CacheSerializationManager.loadCache(Rs2VarbitCache.getCache(), Rs2VarbitCache.getCache().getConfigKey(), profileKey, playerName, false); - log.debug ("Loaded Varbit cache from configuration for player {}, new cache size: {}", - playerName, Rs2VarbitCache.getCache().size()); - } - - // Load VarPlayer cache - if (Rs2VarPlayerCache.getCache().isPersistenceEnabled()) { - CacheSerializationManager.loadCache(Rs2VarPlayerCache.getCache(), Rs2VarPlayerCache.getCache().getConfigKey(), profileKey, playerName, false); - log.debug ("Loaded VarPlayer cache from configuration for player {}, new cache size: {}", - playerName, Rs2VarPlayerCache.getCache().size()); - } - if (Rs2SpiritTreeCache.getCache().isPersistenceEnabled()) { - CacheSerializationManager.loadCache(Rs2SpiritTreeCache.getCache(), Rs2SpiritTreeCache.getCache().getConfigKey(), profileKey, playerName, false); - // Update spirit tree cache with current farming handler data after initial load - try { - Rs2SpiritTreeCache.getInstance().update(); - if(Microbot.isDebug()) Rs2SpiritTreeCache.logState(LogOutputMode.CONSOLE_ONLY); - log.debug("Spirit tree cache updated from FarmingHandler after initial load"); - } catch (Exception e) { - log.warn("Failed to update spirit tree cache from FarmingHandler after initial load: {}", e.getMessage()); - } - log.debug ("Loaded SpiritTree cache from configuration, new cache size: {}", - Rs2SpiritTreeCache.getCache().size()); - } - log.info("Finished Try to loaded all persistent caches from configuration for profile: {} - player {}", profileKey, playerName); - } catch (Exception e) { - log.error("Failed to load persistent caches from configuration for profile: {}", profileKey, e); - } - } - - - /** - * Loads the initial cache state from config. Should be called when a player logs in. - * Similar to Rs2Bank.loadInitialCacheFromCurrentConfig(). - * This method handles both Rs2Bank and other cache systems. - */ - public static void loadCacheStateFromCurrentProfile() { - String rsProfileKey = Microbot.getConfigManager().getRSProfileKey(); - loadCacheStateFromConfig(rsProfileKey); - - } - - /** - * Loads the initial cache state from config. Should be called when a player logs in. - * Similar to Rs2Bank.loadCacheFromConfig(). - * This method handles both Rs2Bank and other cache systems. - * Implements retry logic to ensure player is valid before loading. - */ - public static void loadCacheStateFromConfig(String newRsProfileKey) { - if (!isCacheDataValid()) { - // Use async loading to avoid blocking client thread - loadCachesAsync(newRsProfileKey).whenComplete((result, ex) -> { - if (ex != null) { - log.error("Failed to load cache state async for profile: {}", newRsProfileKey, ex); - } else { - log.info("Successfully loaded cache state async for profile: {}", newRsProfileKey); - } - }); - } - } - - /** - * Retries loading cache with player validation up to MAX_CACHE_LOAD_ATTEMPTS times. - * - * @param newRsProfileKey The profile key to load cache for - * @param attemptCount Current attempt number (0-based) - */ - private static void retryLoadCacheWithValidation(String newRsProfileKey, int attemptCount) { - try { - // Check if player is valid - Player localPlayer = Microbot.getClient() != null ? Microbot.getClient().getLocalPlayer() : null; - String playerName = localPlayer != null ? localPlayer.getName() : null; - - if (localPlayer != null && playerName != null && !playerName.trim().isEmpty()) { - log.info("Player validation successful on attempt {}, loading cache state for player: {}", - attemptCount + 1, playerName); - // update last known player name for later use (e.g., shutdown saves) - updateLastKnownPlayerName(playerName); - loadCaches(newRsProfileKey); - cacheLoadingInProgress.set(false); // Reset flag on success - return; - } - - // Player not valid yet, check if we should retry - if (attemptCount < MAX_CACHE_LOAD_ATTEMPTS - 1) { - log.debug("Player not valid on attempt {} (player: {}), retrying in {}ms", - attemptCount + 1, localPlayer != null ? "not null but no name" : "null", CACHE_LOAD_RETRY_DELAY_MS); - - // Schedule next retry - getInstance().cleanupExecutor.schedule(() -> { - retryLoadCacheWithValidation(newRsProfileKey, attemptCount + 1); - }, CACHE_LOAD_RETRY_DELAY_MS, TimeUnit.MILLISECONDS); - } else { - log.warn("Failed to load cache after {} attempts - player validation failed", MAX_CACHE_LOAD_ATTEMPTS); - cacheLoadingInProgress.set(false); // Reset flag on failure - } - - } catch (Exception e) { - log.error("Error during cache loading retry attempt {}: {}", attemptCount + 1, e.getMessage(), e); - cacheLoadingInProgress.set(false); // Reset flag on error - } - } - - /** - * Sets the initial cache state as unknown. Called when logging out or changing profiles. - * Similar to Rs2Bank.setUnknownInitialCacheState(). - * This method handles both Rs2Bank and other cache systems. - */ - public static void setUnknownInitialCacheState() { - if ( isCacheDataValid() - && rsProfileKey != null - && Microbot.getConfigManager() != null - && rsProfileKey.get() == Microbot.getConfigManager().getRSProfileKey()) { - log.info("In Setting initial cache state as unknown for profile \'{}\', saving current cache state", rsProfileKey); - savePersistentCaches(rsProfileKey.get()); - } - // Also handle Rs2Bank cache state - Rs2Bank.setUnknownInitialCacheState(); - loggedInCacheStateKnown.set( false); - rsProfileKey.set(""); - } - - /** - * Loads cache state asynchronously without blocking the client thread. - * Returns immediately and performs load operations in background threads. - * - * @param newRsProfileKey The profile key to load cache for - * @return CompletableFuture that completes when load is done - */ - public static CompletableFuture loadCachesAsync(String newRsProfileKey) { - // Ensure only one load operation at a time - if (cacheLoadingInProgress.compareAndSet(false, true)) { - CompletableFuture loadOperation = CompletableFuture.runAsync(() -> { - try { - retryLoadCacheWithValidation(newRsProfileKey, 0); - } catch (Exception e) { - log.error("Failed during async cache loading for profile: {}", newRsProfileKey, e); - cacheLoadingInProgress.set(false); // reset flag on unexpected error - throw new RuntimeException("Async cache load failed", e); - } - }, getInstance().cacheManagerExecutor); - - // Track the current operation - currentLoadOperation.set(loadOperation); - - return loadOperation.whenComplete((result, ex) -> { - // Clear the operation reference when done - currentLoadOperation.compareAndSet(loadOperation, null); - if (ex != null) { - log.error("Async cache loading failed for profile: {}", newRsProfileKey, ex); - } else { - log.info("Async cache loading completed for profile: {}", newRsProfileKey); - } - }); - } else { - log.debug("Cache loading already in progress, returning existing operation"); - CompletableFuture existingOperation = currentLoadOperation.get(); - return existingOperation != null ? existingOperation : CompletableFuture.completedFuture(null); - } - } - - /** - * Loads cache state from config, handling profile changes. - * This method handles both Rs2Bank and other cache systems. - */ - private static void loadCaches(String newRsProfileKey) { - // Only re-load from config if loading from a new profile - if (newRsProfileKey != null && !newRsProfileKey.equals(rsProfileKey.get())) { - // If we've hopped between profiles, save current state first - if (rsProfileKey != null&& !rsProfileKey.get().isEmpty() && isCacheDataValid()) { - log.info("Saving current cache state before loading new profile: {}, we have valid cache", rsProfileKey.get()); - savePersistentCaches(rsProfileKey.get()); - } - // Load persistent caches - loadPersistentCaches(newRsProfileKey); - // Also handle Rs2Bank cache loading - Rs2Bank.loadCacheFromConfig(newRsProfileKey); - loggedInCacheStateKnown.set(true); - } - } - - /** - * Handles cache state during profile changes. - * Saves current cache state before loading new profile caches. - * Similar to Rs2Bank.handleProfileChange(). - */ - public static void handleProfileChange(String newRsProfileKey, String prvProfile) { - log.info("Handling profile change from '{}' to '{}' with async operations", prvProfile, newRsProfileKey); - - // Save current cache state before loading new profile (async) - CompletableFuture saveOperation = savePersistentCachesAsync(prvProfile); - - // Chain the profile switch operations - saveOperation.thenRunAsync(() -> { - setUnknownInitialCacheState(); - // Load cache state for new profile (this will use async internally) - loadCacheStateFromConfig(newRsProfileKey); - }, getInstance().cacheManagerExecutor) - .whenComplete((result, ex) -> { - if (ex != null) { - log.error("Failed to complete async profile change from '{}' to '{}'", prvProfile, newRsProfileKey, ex); - } else { - log.info("Successfully completed async profile change from '{}' to '{}'", prvProfile, newRsProfileKey); - } - }); - } - - /** - * Clears all cache state. Called when logging out. - * Similar to Rs2Bank.emptyBankState(). - * This method handles both Rs2Bank and other cache systems. - */ - public static void emptyCacheState() { - // Save current state before clearing - if (rsProfileKey != null && !rsProfileKey.get().isEmpty() && isCacheDataValid()) { - // check when the cache was last saved to validate membership expiry - long cacheTimestamp = Rs2VarPlayerCache.getInstance().getCacheTimestamp(VarPlayerID.ACCOUNT_CREDIT); - if (cacheTimestamp <= 0) { - log.info("No valid cache timestamp found for membership validation"); - cacheTimestamp = 0L; - } - - // calculate days since cache was saved - long currentTime = System.currentTimeMillis(); - long daysSinceCached = (currentTime - cacheTimestamp) / (24 * 60 * 60 * 1000); - - // get cached membership days from when data was saved - int cachedMembershipDays = Rs2VarPlayerCache.getVarPlayerValue(VarPlayerID.ACCOUNT_CREDIT); - ConfigProfile profile = Microbot.getConfigManager().getProfile(); - Microbot.getConfigManager().setMemberExpireDays(profile, cachedMembershipDays); - Microbot.getConfigManager().setMemberExpireDaysTimeStemp(profile, currentTime); - log.debug("Saving current cache state before clearing for profile: {}, cached membership days: {}, days since cached: {}, current time: {}", - rsProfileKey.get(), cachedMembershipDays, daysSinceCached , currentTime); - - } - // Clear Rs2Bank state - Rs2Bank.emptyCacheState(); - // Clear cache manager state - rsProfileKey.set(""); - loggedInCacheStateKnown.set(false); - Rs2CacheManager.invalidateAllCaches(false); - log.info("Emptied all cache states"); - } - - /** - * Saves all persistent caches asynchronously without blocking the client thread. - * Returns immediately and performs save operations in background threads. - * - * @return CompletableFuture that completes when all saves are done - */ - public static CompletableFuture savePersistentCachesAsync() { - try { - if (rsProfileKey != null && !rsProfileKey.get().isEmpty()) { - String playerName = getLastKnownPlayerName(); - if (playerName == null) { - log.warn("Cannot save persistent caches - no player name available"); - return CompletableFuture.completedFuture(null); - } - return savePersistentCachesAsync(rsProfileKey.get()); - } - return CompletableFuture.completedFuture(null); - } catch (Exception e) { - log.error("Failed to start async save of persistent caches", e); - return CompletableFuture.failedFuture(e); - } - } - - /** - * Saves all persistent caches asynchronously for a specific profile. - * - * @param profileKey The RuneLite profile key to save caches for - * @return CompletableFuture that completes when all saves are done - */ - public static CompletableFuture savePersistentCachesAsync(String profileKey) { - if (profileKey == null) { - log.warn("Cannot save persistent caches: profile key is null"); - return CompletableFuture.completedFuture(null); - } - - // if we're saving a "previous" profile during a switch, proceed even if ConfigManager has - // already moved to the new profile. otherwise ensure current state is valid. - if (profileKey.equals(rsProfileKey.get())) { - if (!isCacheDataValid()) { - log.warn("Cache data is not valid for profile '{}', cannot save persistent caches", profileKey); - return CompletableFuture.completedFuture(null); - } - } else { - log.debug("Saving caches for previous profile '{}' (active='{}')", profileKey, rsProfileKey.get()); - } - - String playerName = getLastKnownPlayerName(); - if (playerName == null) { - log.warn("Cannot save persistent caches - no player name available"); - return CompletableFuture.completedFuture(null); - } - - log.info("Starting async save of all persistent caches for profile: {}", profileKey); - - // Ensure only one save operation at a time - CompletableFuture saveOperation = CompletableFuture.runAsync(() -> { - try { - // Save Rs2Bank cache first - Rs2Bank.saveCacheToConfig(profileKey); - - // Save other persistent caches - savePersistentCachesInternal(profileKey); - - log.info("Successfully completed async save of all persistent caches for profile: {}", profileKey); - } catch (Exception e) { - log.error("Failed during async save of persistent caches for profile: {}", profileKey, e); - throw new RuntimeException("Async cache save failed", e); - } - }, getInstance().cacheManagerExecutor); - - // Track the current operation - currentSaveOperation.set(saveOperation); - - return saveOperation.whenComplete((result, ex) -> { - // Clear the operation reference when done - currentSaveOperation.compareAndSet(saveOperation, null); - }); - } - - /** - * Saves all persistent caches to RuneLite profile configuration. - * This method handles both Rs2Bank and other cache systems. - */ - public static void savePersistentCaches() { - try { - if (rsProfileKey != null && !rsProfileKey.get().isEmpty()) { - // additional validation - ensure we have a valid player name - String playerName = getLastKnownPlayerName(); - if (playerName == null) { - log.warn("Cannot save persistent caches - no player name available"); - return; - } - savePersistentCaches(rsProfileKey.get()); - } - } catch (Exception e) { - log.error("Failed to save persistent caches", e); - } - } - - /** - * Saves persistent caches for a specific profile. - * This method handles both Rs2Bank and other cache systems. - * - * @param profileKey The RuneLite profile key to save caches for - */ - public static void savePersistentCaches(String profileKey) { - try { - if (!isCacheDataValid() ) { - log.warn("Cache data is not valid, cannot save persistent caches"); - return; - } - if (profileKey == null) { - log.warn("Cannot save persistent caches: profile key is null"); - return; - } - - log.info("Saving all persistent caches to configuration for profile: {}", profileKey); - - // Save Rs2Bank cache first - Rs2Bank.saveCacheToConfig(profileKey); - - // Save other persistent caches - savePersistentCachesInternal(profileKey); - - log.info("Successfully saved all persistent caches to configuration for profile: {}", profileKey); - } catch (Exception e) { - log.error("Failed to save persistent caches to configuration for profile: {}", profileKey, e); - } - } - - /** - * Internal method to save persistent caches (excluding Rs2Bank). - * - * @param profileKey The RuneLite profile key to save caches for - */ - private static void savePersistentCachesInternal(String profileKey) { - try { - // get the last known player name for character-specific saving - String playerName = getLastKnownPlayerName(); - if (playerName == null) { - log.warn("Cannot save persistent caches - no player name available"); - return; - } - - // Save Skills cache - if (Rs2SkillCache.getCache().isPersistenceEnabled()) { - log.info("Saving Skills cache to configuration for player {}, current size: {}", - playerName, Rs2SkillCache.getCache().size()); - CacheSerializationManager.saveCache(Rs2SkillCache.getCache(), Rs2SkillCache.getCache().getConfigKey(), profileKey, playerName); - } - - // Save Quest cache - if (Rs2QuestCache.getCache().isPersistenceEnabled()) { - log.info("Saving Quest cache to configuration for player {}, current size: {}", - playerName, Rs2QuestCache.getCache().size()); - - CacheSerializationManager.saveCache(Rs2QuestCache.getCache(), Rs2QuestCache.getCache().getConfigKey(), profileKey, playerName); - } - - // Save Varbit cache - if (Rs2VarbitCache.getCache().isPersistenceEnabled()) { - CacheSerializationManager.saveCache(Rs2VarbitCache.getCache(), Rs2VarbitCache.getCache().getConfigKey(), profileKey, playerName); - Rs2VarbitCache.printDetailedVarbitInfo(); - log.info("Saving Varbit cache to configuration for player {}, current size: {}", - playerName, Rs2VarbitCache.getCache().size()); - } - - // Save VarPlayer cache - if (Rs2VarPlayerCache.getCache().isPersistenceEnabled()) { - CacheSerializationManager.saveCache(Rs2VarPlayerCache.getCache(), Rs2VarPlayerCache.getCache().getConfigKey(), profileKey, playerName); - log.info("Saving VarPlayer cache to configuration for player {}, current size: {}", - playerName, Rs2VarPlayerCache.getCache().size()); - } - // Save SpiritTree cache - if (Rs2SpiritTreeCache.getCache().isPersistenceEnabled()) { - CacheSerializationManager.saveCache(Rs2SpiritTreeCache.getCache(), Rs2SpiritTreeCache.getCache().getConfigKey(), profileKey, playerName); - log.info("Saving SpiritTree cache to configuration for player {}, current size: {}", - playerName, Rs2SpiritTreeCache.getCache().size()); - } - - } catch (Exception e) { - log.error("Failed to save internal persistent caches for profile: {}", profileKey, e); - } - } - - /** - * Gets statistics for all unified caches as a formatted string with memory usage. - * @return Formatted string containing statistics for all caches including memory usage - */ - public static String getAllCacheStatisticsString() { - StringBuilder sb = new StringBuilder(); - try { - sb.append("=== MICROBOT CACHE STATISTICS ===\n"); - - // Individual cache statistics with memory usage - appendCacheStats(sb, "NPC", Rs2NpcCache.getInstance()); - appendCacheStats(sb, "GroundItems", Rs2GroundItemCache.getInstance()); - appendCacheStats(sb, "Objects", Rs2ObjectCache.getInstance()); - appendCacheStats(sb, "Varbits", Rs2VarbitCache.getInstance()); - appendCacheStats(sb, "VarPlayers", Rs2VarPlayerCache.getInstance()); - appendCacheStats(sb, "Skills", Rs2SkillCache.getInstance()); - appendCacheStats(sb, "Quests", Rs2QuestCache.getInstance()); - appendCacheStats(sb, "SpiritTrees", Rs2SpiritTreeCache.getInstance()); - - sb.append("\n=== SUMMARY ===\n"); - - // Calculate totals - int totalEntries = getInstance().getTotalCacheSize(); - long totalMemoryBytes = getInstance().getTotalMemoryUsage(); - String formattedMemory = MemorySizeCalculator.formatMemorySize(totalMemoryBytes); - - sb.append("Total Cache Entries: ").append(totalEntries).append("\n"); - sb.append("Total Memory Usage: ").append(formattedMemory) - .append(" (").append(totalMemoryBytes).append(" bytes)\n"); - - // Memory breakdown by cache type - sb.append("\n=== MEMORY BREAKDOWN ===\n"); - appendMemoryBreakdown(sb); - - } catch (Exception e) { - log.error("Error getting cache statistics: {}", e.getMessage(), e); - return "Error retrieving cache statistics: " + e.getMessage(); - } - return sb.toString(); - } - - /** - * Appends formatted cache statistics for a single cache. - */ - private static void appendCacheStats(StringBuilder sb, String cacheName, Rs2Cache cache) { - try { - Rs2Cache.CacheStatistics stats = cache.getStatistics(); - sb.append(String.format("%-12s: Size=%-4d | Hits=%-6d | Hit Rate=%5.1f%% | Memory=%s\n", - cacheName, - stats.currentSize, - stats.cacheHits, - stats.getHitRate() * 100, - stats.getFormattedMemorySize())); - } catch (Exception e) { - sb.append(String.format("%-12s: ERROR - %s\n", cacheName, e.getMessage())); - } - } - - /** - * Appends memory usage breakdown by cache type. - */ - private static void appendMemoryBreakdown(StringBuilder sb) { - try { - // Entity caches (volatile) - long entityMemory = Rs2NpcCache.getInstance().getEstimatedMemorySize() + - Rs2ObjectCache.getInstance().getEstimatedMemorySize() + - Rs2GroundItemCache.getInstance().getEstimatedMemorySize(); - - // Player caches (persistent) - long playerMemory = Rs2VarbitCache.getInstance().getEstimatedMemorySize() + - Rs2VarPlayerCache.getInstance().getEstimatedMemorySize() + - Rs2SkillCache.getInstance().getEstimatedMemorySize() + - Rs2QuestCache.getInstance().getEstimatedMemorySize() + - Rs2SpiritTreeCache.getInstance().getEstimatedMemorySize(); - - sb.append("Entity Caches (Volatile): ").append(MemorySizeCalculator.formatMemorySize(entityMemory)).append("\n"); - sb.append("Player Caches (Persistent): ").append(MemorySizeCalculator.formatMemorySize(playerMemory)).append("\n"); - - // Memory efficiency metrics - long totalMemory = entityMemory + playerMemory; - int totalEntries = getInstance().getTotalCacheSize(); - - if (totalEntries > 0) { - long avgMemoryPerEntry = totalMemory / totalEntries; - sb.append("Average Memory per Entry: ").append(MemorySizeCalculator.formatMemorySize(avgMemoryPerEntry)).append("\n"); - } - - } catch (Exception e) { - sb.append("Memory breakdown calculation failed: ").append(e.getMessage()).append("\n"); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2GroundItemCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2GroundItemCache.java deleted file mode 100644 index 0fe2c7f4aae..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2GroundItemCache.java +++ /dev/null @@ -1,787 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Constants; -import net.runelite.api.TileItem; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.ItemDespawned; -import net.runelite.api.events.ItemSpawned; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.strategy.entity.GroundItemUpdateStrategy; -import net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2CacheLoggingUtils; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; - -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Thread-safe cache for tracking ground items using the unified cache architecture. - * Returns Rs2GroundItemModel objects for enhanced item handling. - * Uses EVENT_DRIVEN_ONLY mode to persist items until despawn or game state changes. - * - * This class extends Rs2UnifiedCache and provides specific ground item caching functionality - * with proper EventBus integration for @Subscribe methods. - */ -@Slf4j -public class Rs2GroundItemCache extends Rs2Cache { - - private static Rs2GroundItemCache instance; - - // Reference to the update strategy for scene scanning - private GroundItemUpdateStrategy updateStrategy; - - /** - * Private constructor for singleton pattern. - */ - private Rs2GroundItemCache() { - super("GroundItemCache", CacheMode.EVENT_DRIVEN_ONLY); - this.updateStrategy = new GroundItemUpdateStrategy(); - this.withUpdateStrategy(this.updateStrategy); - } - - /** - * Gets the singleton instance of Rs2GroundItemCache. - * - * @return The singleton ground item cache instance - */ - public static synchronized Rs2GroundItemCache getInstance() { - if (instance == null) { - instance = new Rs2GroundItemCache(); - } - return instance; - } - - /** - * Requests a scene scan to be performed when appropriate. - * This is more efficient than immediate scanning. - */ - public static void requestSceneScan() { - getInstance().updateStrategy.requestSceneScan(getInstance()); - } - - /** - * Starts periodic scene scanning to keep the cache fresh. - * This is useful for long-running scripts that need up-to-date ground item data. - * - * @param intervalSeconds How often to scan the scene in seconds - */ - public static void startPeriodicSceneScan(long intervalSeconds) { - getInstance().updateStrategy.schedulePeriodicSceneScan(getInstance(), intervalSeconds); - } - - /** - * Stops periodic scene scanning. - */ - public static void stopPeriodicSceneScan() { - getInstance().updateStrategy.stopPeriodicSceneScan(); - } - - /** - * Overrides the get method to provide fallback scene scanning when cache is empty or key not found. - * This ensures that even if events are missed, we can still retrieve ground items from the scene. - * - * @param key The unique String key for the ground item - * @return The ground item model if found in cache or scene, null otherwise - */ - @Override - public Rs2GroundItemModel get(String key) { - // First try the regular cache lookup - Rs2GroundItemModel cachedResult = super.get(key); - if (cachedResult != null) { - return cachedResult; - } - - if (Microbot.getClient() == null || Microbot.getClient().getLocalPlayer() == null) { - log.warn("Client or local player is null, cannot perform scene scan"); - return null; - } - - // If not in cache and cache is very small, request and perform scene scan - if (updateStrategy.requestSceneScan(this)) { - log.debug("Cache miss for ground item key '{}' (size: {}), performing scene scan", key, this.size()); - // Try again after scene scan - return super.get(key); - }else { - log.debug("Cache miss for ground item key '{}' but scene scan not successful (size: {})", key, this.size()); - } - - return null; - } - - /** - * Gets a ground item by its unique key. - * - * @param key The unique key for the ground item - * @return Optional containing the ground item model if found - */ - public static Optional getGroundItemByKey(String key) { - return Optional.ofNullable(getInstance().get(key)); - } - - /** - * Gets all ground items matching a specific item ID. - * - * @param itemId The item ID to search for - * @return Stream of matching Rs2GroundItemModel objects - */ - public static Stream getGroundItemsById(int itemId) { - return getInstance().stream() - .filter(item -> item.getId() == itemId); - } - - /** - * Gets all ground items matching a specific name (case-insensitive). - * - * @param name The item name to search for - * @return Stream of matching Rs2GroundItemModel objects - */ - public static Stream getGroundItemsByName(String name) { - return getInstance().stream() - .filter(item -> item.getName() != null && - item.getName().toLowerCase().contains(name.toLowerCase())); - } - - /** - * Gets all ground items within a certain distance from a location. - * - * @param location The center location - * @param maxDistance The maximum distance in tiles - * @return Stream of ground items within the specified distance - */ - public static Stream getGroundItemsWithinDistance(WorldPoint location, int maxDistance) { - return getInstance().stream() - .filter(item -> item.getLocation() != null && - item.getLocation().distanceTo(location) <= maxDistance); - } - - /** - * Gets the first ground item matching the specified ID. - * - * @param itemId The item ID - * @return Optional containing the first matching ground item model - */ - public static Optional getFirstGroundItemById(int itemId) { - return getGroundItemsById(itemId).findFirst(); - } - - /** - * Gets the first ground item matching the specified name. - * - * @param name The item name - * @return Optional containing the first matching ground item model - */ - public static Optional getFirstGroundItemByName(String name) { - return getGroundItemsByName(name).findFirst(); - } - - /** - * Gets all cached ground items as Rs2GroundItemModel objects. - * - * @return Stream of all cached ground items - */ - public static Stream getAllGroundItems() { - return getInstance().stream(); - } - - /** - * Gets ground items with a specific quantity. - * - * @param quantity The quantity to search for - * @return Stream of matching ground items - */ - public static Stream getGroundItemsByQuantity(int quantity) { - return getAllGroundItems() - .filter(item -> item.getQuantity() == quantity); - } - - /** - * Gets ground items within a specific value range. - * - * @param minValue The minimum value (inclusive) - * @param maxValue The maximum value (inclusive) - * @return Stream of matching ground items - */ - public static Stream getGroundItemsByValueRange(int minValue, int maxValue) { - return getAllGroundItems() - .filter(item -> { - int totalValue = item.getHaPrice() * item.getQuantity(); - return totalValue >= minValue && totalValue <= maxValue; - }); - } - - /** - * Gets the closest ground item to the player with the specified ID. - * - * @param itemId The item ID to search for - * @return Optional containing the closest ground item - */ - public static Optional getClosestGroundItemById(int itemId) { - WorldPoint playerLocation = null; - try { - if (Microbot.getClient() != null && Microbot.getClient().getLocalPlayer() != null) { - playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - } catch (Exception e) { - log.debug("Could not get player location for distance calculation: {}", e.getMessage()); - } - - if (playerLocation == null) { - return getGroundItemsById(itemId).findFirst(); - } - - final WorldPoint finalPlayerLocation = playerLocation; - return getGroundItemsById(itemId) - .min((a, b) -> { - try { - int distA = a.getLocation() != null ? a.getLocation().distanceTo(finalPlayerLocation) : Integer.MAX_VALUE; - int distB = b.getLocation() != null ? b.getLocation().distanceTo(finalPlayerLocation) : Integer.MAX_VALUE; - return Integer.compare(distA, distB); - } catch (Exception e) { - return 0; - } - }); - } - - /** - * Gets the closest ground item to the player with the specified name. - * - * @param name The item name to search for - * @return Optional containing the closest ground item - */ - public static Optional getClosestGroundItemByName(String name) { - WorldPoint playerLocation = null; - try { - if (Microbot.getClient() != null && Microbot.getClient().getLocalPlayer() != null) { - playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - } catch (Exception e) { - log.debug("Could not get player location for distance calculation: {}", e.getMessage()); - } - - if (playerLocation == null) { - return getGroundItemsByName(name).findFirst(); - } - - final WorldPoint finalPlayerLocation = playerLocation; - return getGroundItemsByName(name) - .min((a, b) -> { - try { - int distA = a.getLocation() != null ? a.getLocation().distanceTo(finalPlayerLocation) : Integer.MAX_VALUE; - int distB = b.getLocation() != null ? b.getLocation().distanceTo(finalPlayerLocation) : Integer.MAX_VALUE; - return Integer.compare(distA, distB); - } catch (Exception e) { - return 0; - } - }); - } - - /** - * Gets the closest ground item to a specific anchor point with the specified ID. - * - * @param itemId The item ID to search for - * @param anchorPoint The anchor point to calculate distance from - * @return Optional containing the closest ground item - */ - public static Optional getClosestGroundItemById(int itemId, WorldPoint anchorPoint) { - return getGroundItemsById(itemId) - .min((a, b) -> Integer.compare( - a.getLocation().distanceTo(anchorPoint), - b.getLocation().distanceTo(anchorPoint) - )); - } - - /** - * Gets ground items sorted by value (highest first). - * - * @return List of ground items sorted by total value descending - */ - public static List getGroundItemsSortedByValue() { - return getAllGroundItems() - .sorted((a, b) -> Integer.compare( - b.getHaPrice() * b.getQuantity(), - a.getHaPrice() * a.getQuantity() - )) - .collect(Collectors.toList()); - } - - /** - * Gets valuable ground items above a certain threshold. - * - * @param minValue The minimum total value threshold - * @return Stream of valuable ground items - */ - public static Stream getValuableGroundItems(int minValue) { - return getAllGroundItems() - .filter(item -> (item.getHaPrice() * item.getQuantity()) >= minValue); - } - - /** - * Gets the total number of cached ground items. - * - * @return The total ground item count - */ - public static int getGroundItemCount() { - return getInstance().size(); - } - - /** - * Gets the total number of ground items by ID. - * - * @param itemId The item ID to count - * @return The count of ground items with the specified ID - */ - public static long getGroundItemCountById(int itemId) { - return getGroundItemsById(itemId).count(); - } - - /** - * Manually adds a ground item to the cache. - * - * @param tileItem The tile item to add - * @param tile The tile containing the item - */ - public static void addGroundItem(TileItem tileItem, net.runelite.api.Tile tile) { - if (tileItem != null && tile != null) { - String key = generateKey(tileItem, tile.getWorldLocation()); - Rs2GroundItemModel groundItem = new Rs2GroundItemModel(tileItem, tile); - getInstance().put(key, groundItem); - log.debug("Manually added ground item: {} at {}", tileItem.getId(), tile.getWorldLocation()); - } - } - - /** - * Manually removes a ground item from the cache. - * - * @param key The ground item key to remove - */ - public static void removeGroundItem(String key) { - getInstance().remove(key); - log.debug("Manually removed ground item with key: {}", key); - } - - /** - * Invalidates all ground item cache entries. - */ - public static void invalidateAllGroundItems() { - getInstance().invalidateAll(); - log.debug("Invalidated all ground item cache entries"); - } - - /** - - - /** - * Generates a unique key for ground items based on item ID, quantity, and location. - * - * @param item The tile item - * @param location The world location - * @return Unique key string - */ - public static String generateKey(TileItem item, WorldPoint location) { - return String.format("%d_%d_%d_%d_%d", - item.getId(), - item.getQuantity(), - location.getX(), - location.getY(), - location.getPlane()); - } - - /** - * Event handler registration for the unified cache. - * The unified cache handles events through its strategy automatically. - */ - - - @Subscribe(priority = 10) - public void onItemSpawned(ItemSpawned event) { - getInstance().handleEvent(event); - } - - @Subscribe(priority = 20) // Ensure despawn events are handled first - public void onItemDespawned(ItemDespawned event) { - getInstance().handleEvent(event); - } - - @Subscribe(priority = 40) - public void onGameStateChanged(final GameStateChanged event) { - // Removed old region detection - now handled by unified Rs2Cache system - // Also let the strategy handle the event - getInstance().handleEvent(event); - } - - - /** - * Resets the singleton instance. Used for testing. - */ - public static synchronized void resetInstance() { - if (instance != null) { - instance.invalidateAll(); - - instance = null; - } - } - - // ============================================ - // Legacy API Compatibility Methods - // ============================================ - - /** - * Gets ground items by their game ID - Legacy compatibility method. - * - * @param itemId The item ID - * @return Stream of matching ground items - */ - public static Stream getItemsByGameId(int itemId) { - return getInstance().stream() - .filter(item -> item.getId() == itemId); - } - - /** - * Gets first ground item by game ID - Legacy compatibility method. - * - * @param itemId The item ID - * @return Optional containing the first matching ground item - */ - public static Optional getFirstItemByGameId(int itemId) { - return getItemsByGameId(itemId).findFirst(); - } - - /** - * Gets closest ground item by game ID - Legacy compatibility method. - * - * @param itemId The item ID - * @return Optional containing the closest ground item - */ - public static Optional getClosestItemByGameId(int itemId) { - WorldPoint playerLocation = null; - try { - if (Microbot.getClient() != null && Microbot.getClient().getLocalPlayer() != null) { - playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - } catch (Exception e) { - log.debug("Could not get player location for distance calculation: {}", e.getMessage()); - } - - if (playerLocation == null) { - return getItemsByGameId(itemId).findFirst(); - } - - final WorldPoint finalPlayerLocation = playerLocation; - return getItemsByGameId(itemId) - .min((a, b) -> { - try { - int distA = a.getLocation() != null ? a.getLocation().distanceTo(finalPlayerLocation) : Integer.MAX_VALUE; - int distB = b.getLocation() != null ? b.getLocation().distanceTo(finalPlayerLocation) : Integer.MAX_VALUE; - return Integer.compare(distA, distB); - } catch (Exception e) { - return 0; - } - }); - } - - /** - * Gets all ground items - Legacy compatibility method. - * - * @return Stream of all ground items - */ - public static Stream getAllItems() { - return getInstance().stream(); - } - - /** - * Gets item count - Legacy compatibility method. - * - * @return Total number of cached ground items - */ - public static int getItemCount() { - return getInstance().size(); - } - - /** - * Gets cache mode - Legacy compatibility method. - * - * @return The cache mode - */ - public static CacheMode getGroundItemCacheMode() { - return getInstance().getCacheMode(); - } - - /** - * Gets cache statistics - Legacy compatibility method. - * - * @return Statistics string for debugging - */ - public static String getGroundItemCacheStatistics() { - return getInstance().getStatisticsString(); - } - - /** - * Gets closest ground item by name - Legacy compatibility method. - * - * @param itemName The item name to search for - * @return Optional containing the closest ground item - */ - public static Optional getClosestItemByName(String itemName) { - WorldPoint playerLocation = null; - try { - if (Microbot.getClient() != null && Microbot.getClient().getLocalPlayer() != null) { - playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - } catch (Exception e) { - log.debug("Could not get player location for distance calculation: {}", e.getMessage()); - } - - if (playerLocation == null) { - return getGroundItemsByName(itemName).findFirst(); - } - - final WorldPoint finalPlayerLocation = playerLocation; - return getGroundItemsByName(itemName) - .min((a, b) -> { - try { - int distA = a.getLocation() != null ? a.getLocation().distanceTo(finalPlayerLocation) : Integer.MAX_VALUE; - int distB = b.getLocation() != null ? b.getLocation().distanceTo(finalPlayerLocation) : Integer.MAX_VALUE; - return Integer.compare(distA, distB); - } catch (Exception e) { - return 0; - } - }); - } - @Override - public void update(){ - update(Constants.CLIENT_TICK_LENGTH*2); - } - - public void update(long delay) { - log.debug("Starting ground item cache update - clearing cache and performing scene scan, delay: {}ms", delay); - int sizeBefore = this.size(); - - // Clear the entire cache - this.invalidateAll(); - - // Perform a complete scene scan to repopulate the cache - updateStrategy.performSceneScan(this, delay); - - int sizeAfter = this.size(); - log.debug("Ground item cache update completed - items before: {}, after: {}", sizeBefore, sizeAfter); - } - - /** - * Logs the current state of all cached ground items for debugging. - * - * @param dumpToFile Whether to also dump the information to a file - */ - public static void logState(LogOutputMode mode) { - var cache = getInstance(); - var stats = cache.getStatistics(); - - // Create the log content - StringBuilder logContent = new StringBuilder(); - - String header = String.format("=== Ground Item Cache State (%d entries) ===", cache.size()); - logContent.append(header).append("\n"); - - String statsInfo = Rs2CacheLoggingUtils.formatCacheStatistics( - stats.getHitRate(), stats.cacheHits, stats.cacheMisses, stats.cacheMode.toString()); - logContent.append(statsInfo).append("\n\n"); - - if (cache.size() == 0) { - String emptyMsg = "Cache is empty"; - logContent.append(emptyMsg).append("\n"); - } else { - // Table format for ground items with enhanced timing information - String[] headers = {"Name", "Quantity", "ID", "Location", "Distance", "GE Price", "HA Price", "Owned", "Spawn Time UTC", "Despawn Time UTC", "Should Despawn?", "Ticks Left", "Cache Timestamp"}; - int[] columnWidths = {20, 8, 8, 18, 8, 10, 10, 6, 22, 22, 14, 10, 22}; - - String tableHeader = Rs2CacheLoggingUtils.formatTableHeader(headers, columnWidths); - logContent.append("\n").append(tableHeader); - - // Get player location once for distance calculations - WorldPoint playerLocation = null; - try { - if (Microbot.getClient() != null && Microbot.getClient().getLocalPlayer() != null) { - playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - } catch (Exception e) { - log.debug("Could not get player location for distance calculations: {}", e.getMessage()); - } - - final WorldPoint finalPlayerLocation = playerLocation; - - // Convert to list and sort by total value (highest first) - List items = cache.stream() - .limit(50) // Limit early to avoid processing too many items - .collect(Collectors.toList()); - - // Sort by total value with safe calculation - items.sort((a, b) -> { - try { - int valueA = a.getTotalGeValue(); - int valueB = b.getTotalGeValue(); - return Integer.compare(valueB, valueA); // Highest first - } catch (Exception e) { - return 0; // If value calculation fails, consider them equal - } - }); - int maxRows = mode == LogOutputMode.CONSOLE_ONLY ? 50 : cache.size(); - // Process each item safely - for (Rs2GroundItemModel item : items) { - try { - // Calculate distance safely - String distanceStr = "N/A"; - if (finalPlayerLocation != null && item.getLocation() != null) { - try { - int distance = item.getLocation().distanceTo(finalPlayerLocation); - distanceStr = String.valueOf(distance); - } catch (Exception e) { - distanceStr = "Error"; - } - } - - // Get values safely - String geValueStr = "N/A"; - String haValueStr = "N/A"; - try { - geValueStr = String.valueOf(item.getTotalGeValue()); - haValueStr = String.valueOf(item.getTotalHaValue()); - } catch (Exception e) { - log.debug("Error getting item values: {}", e.getMessage()); - } - - // Get cache timestamp for this ground item - String cacheTimestampStr = "N/A"; - try { - // Generate key manually using the same format as generateKey method - String itemKey = String.format("%d_%d_%d_%d_%d", - item.getId(), - item.getQuantity(), - item.getLocation().getX(), - item.getLocation().getY(), - item.getLocation().getPlane()); - Long cacheTimestamp = cache.getCacheTimestamp(itemKey); - if (cacheTimestamp != null) { - cacheTimestampStr = Rs2Cache.formatUtcTimestamp(cacheTimestamp); - } - } catch (Exception e) { - log.debug("Error getting cache timestamp: {}", e.getMessage()); - } - - // Get despawn information safely with enhanced UTC timing - String despawnTimeStr = "N/A"; - String shouldDespawnStr = "No"; - String spawnTimeStr = "N/A"; - String ticksLeftStr = "N/A"; - try { - // Get spawn time in UTC - if (item.getSpawnTimeUtc() != null) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM HH:mm:ss") - .withZone(ZoneOffset.UTC); - spawnTimeStr = formatter.format(item.getSpawnTimeUtc()) + " UTC"; - } - - // Get despawn time in UTC - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM HH:mm:ss") - .withZone(ZoneOffset.UTC); - despawnTimeStr = formatter.format(item.getDespawnTime().atZone(ZoneOffset.UTC)) + " UTC"; - - // Use tick-based despawn detection for accuracy - shouldDespawnStr = item.isDespawned() ? "Yes" : "No"; - ticksLeftStr = String.valueOf(item.getTicksUntilDespawn()); - - } catch (Exception e) { - log.debug("Error getting despawn information: {}", e.getMessage()); - } - - String[] values = { - Rs2CacheLoggingUtils.truncate(item.getName() != null ? item.getName() : "Unknown", 19), - String.valueOf(item.getQuantity()), - String.valueOf(item.getId()), - Rs2CacheLoggingUtils.formatLocation(item.getLocation()), - distanceStr, - geValueStr, - haValueStr, - item.isOwned() ? "Yes" : "No", - Rs2CacheLoggingUtils.truncate(spawnTimeStr, 21), - Rs2CacheLoggingUtils.truncate(despawnTimeStr, 21), - shouldDespawnStr, - ticksLeftStr, - Rs2CacheLoggingUtils.truncate(cacheTimestampStr, 21) - }; - - String row = Rs2CacheLoggingUtils.formatTableRow(values, columnWidths); - logContent.append(row); - } catch (Exception e) { - log.debug("Error processing ground item for logging: {}", e.getMessage()); - // Skip this item and continue with the next one - } - } - - String tableFooter = Rs2CacheLoggingUtils.formatTableFooter(columnWidths); - logContent.append(tableFooter); - - String limitMsg = Rs2CacheLoggingUtils.formatLimitMessage(cache.size(), maxRows); - if (!limitMsg.isEmpty()) { - logContent.append(limitMsg).append("\n"); - } - } - - String footer = "=== End Ground Item Cache State ==="; - logContent.append(footer).append("\n"); - - // Dump to file if requested - Rs2CacheLoggingUtils.outputCacheLog(getInstance().getCacheName(), logContent.toString(), mode); - - } - - /** - * Override periodic cleanup to check for despawned ground items. - * This method is called by the ScheduledExecutorService in the base cache - * to remove items that have naturally despawned based on their game timer. - */ - @Override - protected void performPeriodicCleanup() { - updateStrategy.performSceneScan(instance, Constants.CLIENT_TICK_LENGTH /2); - } - - /** - * Override isExpired to use ground item despawn timing instead of generic TTL. - * This integrates the despawn logic directly with the cache's expiration system. - * - * @param key The cache key to check for expiration - * @return true if the ground item should be considered expired (despawned) - */ - @Override - protected boolean isExpired(String key) { - // For EVENT_DRIVEN_ONLY mode with ground items, check despawn status directly - if (getCacheMode() == CacheMode.EVENT_DRIVEN_ONLY) { - // Access the cached value directly using the protected method to avoid recursion - Rs2GroundItemModel groundItem = getRawCachedValue(key); - if (groundItem != null && groundItem.isDespawned()) { - // Item has despawned - remove it immediately from cache - remove(key); - log.debug("Removed despawned ground item during expiration check: {} (ID: {}) at {}", - groundItem.getName(), groundItem.getId(), groundItem.getLocation()); - return true; - } - // If item is not in cache, consider it expired - if (groundItem == null) { - return true; - } - // Item exists and is not despawned - return false; - } - - // For other modes, fall back to the default TTL behavior - return super.isExpired(key); - } - - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2NpcCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2NpcCache.java deleted file mode 100644 index 655c9583f2d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2NpcCache.java +++ /dev/null @@ -1,502 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Constants; -import net.runelite.api.NPC; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.NpcDespawned; -import net.runelite.api.events.NpcSpawned; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.strategy.entity.NpcUpdateStrategy; -import net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2CacheLoggingUtils; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; - -import java.util.Optional; -import java.util.stream.Stream; - -/** - * Thread-safe cache for tracking NPCs using the unified cache architecture. - * Returns Rs2NpcModel objects for enhanced NPC handling. - * Uses EVENT_DRIVEN_ONLY mode to persist NPCs until despawn or game state changes. - * - * This class extends Rs2UnifiedCache and provides specific NPC caching functionality - * with proper EventBus integration for @Subscribe methods. - */ -@Slf4j -public class Rs2NpcCache extends Rs2Cache { - - private static Rs2NpcCache instance; - - // Reference to the update strategy for scene scanning - private NpcUpdateStrategy updateStrategy; - - /** - * Private constructor for singleton pattern. - */ - private Rs2NpcCache() { - super("NpcCache", CacheMode.EVENT_DRIVEN_ONLY); - this.updateStrategy = new NpcUpdateStrategy(); - this.withUpdateStrategy(this.updateStrategy); - } - - /** - * Gets the singleton instance of Rs2NpcCache. - * - * @return The singleton NPC cache instance - */ - public static synchronized Rs2NpcCache getInstance() { - if (instance == null) { - instance = new Rs2NpcCache(); - } - return instance; - } - - /** - * Requests a scene scan to be performed when appropriate. - * This is more efficient than immediate scanning. - */ - public static void requestSceneScan() { - getInstance().updateStrategy.requestSceneScan(getInstance()); - } - - /** - * Starts periodic scene scanning to keep the cache fresh. - * This is useful for long-running scripts that need up-to-date NPC data. - * - * @param intervalSeconds How often to scan the scene in seconds - */ - public static void startPeriodicSceneScan(long intervalSeconds) { - getInstance().updateStrategy.schedulePeriodicSceneScan(getInstance(), intervalSeconds); - } - - /** - * Stops periodic scene scanning. - */ - public static void stopPeriodicSceneScan() { - getInstance().updateStrategy.stopPeriodicSceneScan(); - } - - /** - * Overrides the get method to provide fallback scene scanning when cache is empty or key not found. - * This ensures that even if events are missed, we can still retrieve NPCs from the scene. - * - * @param key The NPC index key - * @return The NPC model if found in cache or scene, null otherwise - */ - @Override - public Rs2NpcModel get(Integer key) { - // First try the regular cache lookup - Rs2NpcModel cachedResult = super.get(key); - if (cachedResult != null) { - return cachedResult; - } - - if (Microbot.getClient() == null || Microbot.getClient().getLocalPlayer() == null) { - log.warn("Client or local player is null, cannot perform scene scan"); - return null; - } - - // If not in cache and cache is very small, request and perform scene scan - if (updateStrategy.requestSceneScan(this)) { - log.debug("Cache miss for NPC index '{}' (size: {}), performing scene scan", key, this.size()); - // Try again after scene scan if still missing, the npc is not in the scene - return super.get(key); - }else { - log.debug("Cache miss for NPC index '{}' but scene scan not a successful request, returning null", key); - } - - return null; - } - - // ============================================ - // Legacy API Compatibility Methods - // ============================================ - - /** - * Gets an NPC by its index. - * - * @param index The NPC index - * @return Optional containing the NPC model if found - */ - public static Optional getNpcByIndex(int index) { - return Optional.ofNullable(getInstance().get(index)); - } - - - /** - * Gets NPCs by ID - Legacy compatibility method. - * - * @param npcId The NPC ID - * @return Stream of matching NPCs - */ - public static Stream getNpcsById(int npcId) { - return getInstance().stream() - .filter(npc -> npc.getId() == npcId); - } - - /** - * Gets first NPC by ID - Legacy compatibility method. - * - * @param npcId The NPC ID - * @return Optional containing the first matching NPC - */ - public static Optional getFirstNpcById(int npcId) { - return getNpcsById(npcId).findFirst(); - } - - /** - * Gets all NPCs - Legacy compatibility method. - * - * @return Stream of all NPCs - */ - public static Stream getAllNpcs() { - return getInstance().stream(); - } - - /** - * Gets all NPCs matching a specific name (case-insensitive). - * - * @param name The NPC name to search for - * @return Stream of matching Rs2NpcModel objects - */ - public static Stream getNpcsByName(String name) { - return getInstance().stream() - .filter(npc -> npc.getName() != null && - npc.getName().toLowerCase().contains(name.toLowerCase())); - } - - /** - * Gets all NPCs within a certain distance from a location. - * - * @param location The center location - * @param maxDistance The maximum distance in tiles - * @return Stream of NPCs within the specified distance - */ - public static Stream getNpcsWithinDistance(net.runelite.api.coords.WorldPoint location, int maxDistance) { - return getInstance().stream() - .filter(npc -> npc.getWorldLocation() != null && - npc.getWorldLocation().distanceTo(location) <= maxDistance); - } - - /** - * Gets the first NPC matching the specified name. - * - * @param name The NPC name - * @return Optional containing the first matching NPC model - */ - public static Optional getFirstNpcByName(String name) { - return getNpcsByName(name).findFirst(); - } - - /** - * Gets NPCs matching a specific combat level. - * - * @param combatLevel The combat level to search for - * @return Stream of matching NPCs - */ - public static Stream getNpcsByCombatLevel(int combatLevel) { - return getAllNpcs() - .filter(npc -> npc.getCombatLevel() == combatLevel); - } - - /** - * Gets NPCs that are currently in combat. - * - * @return Stream of NPCs in combat - */ - public static Stream getNpcsInCombat() { - return getAllNpcs() - .filter(npc -> npc.isInteracting()); - } - - /** - * Gets the closest NPC to the player with the specified ID. - * - * @param npcId The NPC ID to search for - * @return Optional containing the closest NPC - */ - public static Optional getClosestNpcByGameId(int npcId) { - return getNpcsById(npcId) - .min((a, b) -> Integer.compare(a.getDistanceFromPlayer(), b.getDistanceFromPlayer())); - } - - /** - * Gets the closest NPC to the player with the specified name. - * - * @param name The NPC name to search for - * @return Optional containing the closest NPC - */ - public static Optional getClosestNpcByName(String name) { - return getNpcsByName(name) - .min((a, b) -> Integer.compare(a.getDistanceFromPlayer(), b.getDistanceFromPlayer())); - } - - /** - * Gets the closest NPC to a specific anchor point with the specified ID. - * - * @param npcId The NPC ID to search for - * @param anchorPoint The anchor point to calculate distance from - * @return Optional containing the closest NPC - */ - public static Optional getClosestNpcByGameId(int npcId, net.runelite.api.coords.WorldPoint anchorPoint) { - return getNpcsById(npcId) - .min((a, b) -> Integer.compare( - a.getWorldLocation().distanceTo(anchorPoint), - b.getWorldLocation().distanceTo(anchorPoint) - )); - } - - /** - * Gets the closest NPC to a specific anchor point with the specified name. - * - * @param name The NPC name to search for - * @param anchorPoint The anchor point to calculate distance from - * @return Optional containing the closest NPC - */ - public static Optional getClosestNpcByName(String name, net.runelite.api.coords.WorldPoint anchorPoint) { - return getNpcsByName(name) - .min((a, b) -> Integer.compare( - a.getWorldLocation().distanceTo(anchorPoint), - b.getWorldLocation().distanceTo(anchorPoint) - )); - } - - /** - * Gets the total number of cached NPCs. - * - * @return The total NPC count - */ - public static int getNpcCount() { - return getInstance().size(); - } - - /** - * Manually adds an NPC to the cache. - * - * @param npc The NPC to add - */ - public static void addNpc(NPC npc) { - if (npc != null) { - Rs2NpcModel npcModel = new Rs2NpcModel(npc); - getInstance().put(npc.getIndex(), npcModel); - log.debug("Manually added NPC: {} [{}] at {}", npc.getName(), npc.getId(), npc.getWorldLocation()); - } - } - - /** - * Manually removes an NPC from the cache. - * - * @param index The NPC index to remove - */ - public static void removeNpc(int index) { - getInstance().remove(index); - log.debug("Manually removed NPC with index: {}", index); - } - - /** - * Invalidates all NPC cache entries. - */ - public static void invalidateAllNpcs() { - getInstance().invalidateAll(); - log.debug("Invalidated all NPC cache entries"); - } - - /** - * Event handler registration for the unified cache. - * The unified cache handles events through its strategy automatically. - */ - - - @Subscribe(priority = 10) // High priority to ensure we capture all NPC events - public void onNpcSpawned(final NpcSpawned event) { - getInstance().handleEvent(event); - } - - @Subscribe(priority = 20) // first handle despawn events to ensure NPCs are removed before any other processing - public void onNpcDespawned(final NpcDespawned event) { - getInstance().handleEvent(event); - } - @Subscribe(priority = 40) - public void onGameStateChanged(final GameStateChanged event) { - // Also let the strategy handle the event, region changes and loading of a map trigger despawn events for NPCs correctly - getInstance().handleEvent(event); - } - - /** - * Resets the singleton instance. Used for testing. - */ - public static synchronized void resetInstance() { - if (instance != null) { - instance.invalidateAll(); - instance = null; - } - } - - /** - * Gets cache mode - Legacy compatibility method. - * - * @return The cache mode - */ - public static CacheMode getNpcCacheMode() { - return getInstance().getCacheMode(); - } - - /** - * Gets cache statistics - Legacy compatibility method. - * - * @return Statistics string for debugging - */ - public static String getNpcCacheStatistics() { - return getInstance().getStatisticsString(); - } - - /** - * Immediately updates the cache by invoking the update method with a default parameter of 0. - * This method overrides the parent implementation to provide a default update behavior. - */ - @Override - public void update() { - update(Constants.CLIENT_TICK_LENGTH*2); - - } - /** - * Updates the NPC cache by clearing it and performing a scene scan. - * This is useful for refreshing the cache after significant game state changes. - * - * @param delayMs Optional delay in milliseconds before performing the update - */ - public void update(long delayMs) { - log.debug("Starting NPC cache update - clearing cache and performing scene scan, delay: {} ms", delayMs); - int sizeBefore = this.size(); - - // Clear the entire cache - this.invalidateAll(); - - // Perform a complete scene scan to repopulate the cache - updateStrategy.performSceneScan(this,delayMs); - - int sizeAfter = this.size(); - log.debug("NPC cache update completed - NPCs before: {}, after: {}", sizeBefore, sizeAfter); - } - - /** - * Logs the current state of all cached NPCs for debugging. - * - * @param outputMode Where to direct the output (CONSOLE_ONLY, FILE_ONLY, or BOTH) - */ - public static void logState(LogOutputMode outputMode) { - var cache = getInstance(); - var stats = cache.getStatistics(); - // Create the log content - StringBuilder logContent = new StringBuilder(); - String header = String.format("=== NPC Cache State (%d entries) ===", cache.size()); - logContent.append(header).append("\n"); - - String statsInfo = Rs2CacheLoggingUtils.formatCacheStatistics( - stats.getHitRate(), stats.cacheHits, stats.cacheMisses, stats.cacheMode.toString()); - - logContent.append(statsInfo).append("\n\n"); - - if (cache.size() == 0) { - String emptyMsg = "Cache is empty"; - - logContent.append(emptyMsg).append("\n"); - } else { - // Table format for NPCs - String[] headers = {"Name", "ID", "Combat Level", "Distance", "Location", "Health", "Cache Timestamp"}; - int[] columnWidths = {25, 8, 12, 8, 18, 8, 22}; - - String tableHeader = Rs2CacheLoggingUtils.formatTableHeader(headers, columnWidths); - - logContent.append("\n").append(tableHeader); - int maxRows = outputMode == LogOutputMode.CONSOLE_ONLY ? 50 : cache.size(); - // Sort NPCs by distance (closest first) - cache.stream() - .filter(npc -> { - try { - // Filter out NPCs with invalid distance calculations - return npc != null && npc.getDistanceFromPlayer() < Integer.MAX_VALUE; - } catch (Exception e) { - return false; // Exclude NPCs that cause exceptions - } - }) - .sorted((a, b) -> { - try { - // Ensure both NPCs have valid distance data - if (a == null && b == null) return 0; - if (a == null) return 1; - if (b == null) return -1; - - int distanceA = a.getDistanceFromPlayer(); - int distanceB = b.getDistanceFromPlayer(); - - // Handle negative distances (invalid) by treating them as maximum distance - if (distanceA < 0) distanceA = Integer.MAX_VALUE; - if (distanceB < 0) distanceB = Integer.MAX_VALUE; - - return Integer.compare(distanceA, distanceB); - } catch (Exception e) { - // If comparison fails, use index as fallback to maintain consistency - return Integer.compare( - a != null ? a.getIndex() : Integer.MAX_VALUE, - b != null ? b.getIndex() : Integer.MAX_VALUE - ); - } - }) - .forEach(npc -> { - // Get cache timestamp for this NPC - Long cacheTimestamp = cache.getCacheTimestamp(Integer.valueOf(npc.getIndex())); - String cacheTimestampStr = cacheTimestamp != null ? - Rs2Cache.formatUtcTimestamp(cacheTimestamp) : "N/A"; - - String[] values = { - Rs2CacheLoggingUtils.truncate(npc.getName(), 24), - String.valueOf(npc.getId()), - String.valueOf(npc.getCombatLevel()), - String.valueOf(npc.getDistanceFromPlayer()), - Rs2CacheLoggingUtils.formatLocation(npc.getWorldLocation()), - npc.getHealthRatio() != -1 ? String.valueOf(npc.getHealthRatio()) : "N/A", - Rs2CacheLoggingUtils.truncate(cacheTimestampStr, 21) - }; - - String row = Rs2CacheLoggingUtils.formatTableRow(values, columnWidths); - - logContent.append(row); - }); - - String tableFooter = Rs2CacheLoggingUtils.formatTableFooter(columnWidths); - - logContent.append(tableFooter); - - String limitMsg = Rs2CacheLoggingUtils.formatLimitMessage(cache.size(), maxRows); - if (!limitMsg.isEmpty()) { - - logContent.append(limitMsg).append("\n"); - } - } - - String footer = "=== End NPC Cache State ==="; - logContent.append(footer).append("\n"); - // Use the new output mode utility - Rs2CacheLoggingUtils.outputCacheLog("npc", logContent.toString(), outputMode); - } - - /** - * Logs the current state of all cached NPCs for debugging. - * - * @param dumpToFile Whether to also dump the information to a file - */ - public static void logState(boolean dumpToFile) { - net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode outputMode = - dumpToFile ? net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode.BOTH - : net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode.CONSOLE_ONLY; - logState(outputMode); - } /** - * Logs the current state of all cached NPCs for debugging (console only). - */ - public static void logState() { - logState(false); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2ObjectCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2ObjectCache.java deleted file mode 100644 index 7f62749ae79..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2ObjectCache.java +++ /dev/null @@ -1,604 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Constants; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.*; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.strategy.entity.ObjectUpdateStrategy; -import net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2CacheLoggingUtils; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel.ObjectType; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Thread-safe cache for tracking game objects using the unified cache architecture. - * Handles GameObject, GroundObject, WallObject, and DecorativeObject types. - * Returns Rs2ObjectModel objects for enhanced object handling. - * Uses EVENT_DRIVEN_ONLY mode to persist objects until despawn or game state changes. - * - * This class extends Rs2Cache and provides specific object caching functionality - * with proper EventBus integration for @Subscribe methods. - * - * Key Changes: - * - Uses String-based keys for better tracking across region changes - * - Implements region change detection to clear stale objects - * - Handles the fact that RuneLite doesn't fire despawn events on region changes - */ -@Slf4j -public class Rs2ObjectCache extends Rs2Cache { - - private static Rs2ObjectCache instance; - - // Reference to the update strategy for scene scanning - private ObjectUpdateStrategy updateStrategy; - - /** - * Private constructor for singleton pattern. - */ - private Rs2ObjectCache() { - super("ObjectCache", CacheMode.EVENT_DRIVEN_ONLY); - this.updateStrategy = new ObjectUpdateStrategy(); - this.withUpdateStrategy(this.updateStrategy); - - log.debug("Rs2ObjectCache initialized with String-based keys, region change detection, and scene scanning"); - } - - /** - * Gets the singleton instance of Rs2ObjectCache. - * - * @return The singleton object cache instance - */ - public static synchronized Rs2ObjectCache getInstance() { - if (instance == null) { - instance = new Rs2ObjectCache(); - } - return instance; - } - - /** - * Requests an scene scan to be performed when appropriate. - * This is more efficient than immediate scanning. - */ - public static boolean requestSceneScan() { - return getInstance().updateStrategy.requestSceneScan(getInstance()); - } - - - /** - * Starts periodic scene scanning to keep the cache fresh. - * This is useful for long-running scripts that need up-to-date object data. - * - * @param intervalSeconds How often to scan the scene in seconds - */ - public static void startPeriodicSceneScan(long intervalSeconds) { - getInstance().updateStrategy.schedulePeriodicSceneScan(getInstance(), intervalSeconds); - } - - /** - * Stops periodic scene scanning. - */ - public static void stopPeriodicSceneScan() { - getInstance().updateStrategy.stopPeriodicSceneScan(); - } - - - - - /** - * Overrides the get method to provide fallback scene scanning when cache is empty or key not found. - * This ensures that even if events are missed, we can still retrieve objects from the scene. - * - * @param key The unique String key for the object - * @return The object model if found in cache or scene, null otherwise - */ - @Override - public Rs2ObjectModel get(String key) { - // First try the regular cache lookup - Rs2ObjectModel cachedResult = super.get(key); - if (cachedResult != null) { - return cachedResult; - } - if (Microbot.getClient() == null || Microbot.getClient().getLocalPlayer() == null) { - log.warn("Client or local player is null, cannot perform scene scan"); - return null; - } - // If not in cache and cache is very small, request and perform scene scan - if (updateStrategy.requestSceneScan(this)) { - log.debug("Cache miss for key '{}' (size: {}), performing scene scan", key, this.size()); - //updateStrategy.performSceneScan(this, false); - // Try again after scene scan - return super.get(key); - }else { - log.debug("Cache miss for key '{}' (size: {}), but scene scan not requested not successful", key, this.size()); - } - - return null; - } - - /** - * Gets an object by its unique String-based key. - * - * @param key The unique String key for the object - * @return Optional containing the object model if found - */ - public static Optional getObjectByKey(String key) { - return Optional.ofNullable(getInstance().get(key)); - } - - /** - * Gets all objects matching a specific ID. - * - * @param objectId The object ID to search for - * @return Stream of matching Rs2ObjectModel objects - */ - public static Stream getObjectsById(int objectId) { - return getInstance().stream() - .filter(obj -> obj.getId() == objectId); - } - - /** - * Gets all objects matching a specific name (case-insensitive). - * - * @param name The object name to search for - * @return Stream of matching Rs2ObjectModel objects - */ - public static Stream getObjectsByName(String name) { - return getInstance().stream() - .filter(obj -> obj.getName() != null && - obj.getName().toLowerCase().contains(name.toLowerCase())); - } - - /** - * Gets all objects of a specific type. - * - * @param objectType The type of objects to search for - * @return Stream of matching Rs2ObjectModel objects - */ - public static Stream getObjectsByType(ObjectType objectType) { - return getInstance().stream() - .filter(obj -> obj.getObjectType() == objectType); - } - - /** - * Gets all objects within a certain distance from a location. - * - * @param location The center location - * @param maxDistance The maximum distance in tiles - * @return Stream of objects within the specified distance - */ - public static Stream getObjectsWithinDistance(WorldPoint location, int maxDistance) { - return getInstance().stream() - .filter(obj -> obj.getWorldLocation() != null && - obj.getWorldLocation().distanceTo(location) <= maxDistance); - } - - /** - * Gets the first object matching the specified ID. - * - * @param objectId The object ID - * @return Optional containing the first matching object model - */ - public static Optional getFirstObjectById(int objectId) { - return getObjectsById(objectId).findFirst(); - } - - /** - * Gets the first object matching the specified name. - * - * @param name The object name - * @return Optional containing the first matching object model - */ - public static Optional getFirstObjectByName(String name) { - return getObjectsByName(name).findFirst(); - } - - - - /** - * Gets all objects - Legacy compatibility method. - * - * @return Stream of all objects - */ - public static Stream getAllObjects() { - return getInstance().values().stream(); - } - - /** - * Gets all GameObjects from the cache. - * - * @return Stream of GameObject models - */ - public static Stream getGameObjects() { - return getObjectsByType(ObjectType.GAME_OBJECT); - } - - /** - * Gets all GroundObjects from the cache. - * - * @return Stream of GroundObject models - */ - public static Stream getGroundObjects() { - return getObjectsByType(ObjectType.GROUND_OBJECT); - } - - /** - * Gets all WallObjects from the cache. - * - * @return Stream of WallObject models - */ - public static Stream getWallObjects() { - return getObjectsByType(ObjectType.WALL_OBJECT); - } - - /** - * Gets all DecorativeObjects from the cache. - * - * @return Stream of DecorativeObject models - */ - public static Stream getDecorativeObjects() { - return getObjectsByType(ObjectType.DECORATIVE_OBJECT); - } - - /** - * Gets the closest object to the player with the specified ID. - * - * @param objectId The object ID to search for - * @return Optional containing the closest object - */ - public static Optional getClosestObjectById(int objectId) { - return getClosestObjectById(objectId, Microbot.getClient().getLocalPlayer().getWorldLocation()); - } - - /** - * Gets the closest object to the player with the specified name. - * - * @param name The object name to search for - * @return Optional containing the closest object - */ - public static Optional getClosestObjectByName(String name) { - return getClosestObjectByName(name, Microbot.getClient().getLocalPlayer().getWorldLocation()); - } - - /** - * Gets the closest object to a specific anchor point with the specified ID. - * - * @param objectId The object ID to search for - * @param anchorPoint The anchor point to calculate distance from - * @return Optional containing the closest object - */ - public static Optional getClosestObjectById(int objectId, WorldPoint anchorPoint) { - return getObjectsById(objectId) - .min((o1, o2) -> Integer.compare( - o1.getWorldLocation().distanceTo(anchorPoint), - o2.getWorldLocation().distanceTo(anchorPoint) - )); - } - - /** - * Gets the closest object to a specific anchor point with the specified name. - * - * @param name The object name to search for - * @param anchorPoint The anchor point to calculate distance from - * @return Optional containing the closest object - */ - public static Optional getClosestObjectByName(String name, WorldPoint anchorPoint) { - return getObjectsByName(name) - .min((o1, o2) -> Integer.compare( - o1.getWorldLocation().distanceTo(anchorPoint), - o2.getWorldLocation().distanceTo(anchorPoint) - )); - } - - /** - * Gets objects sorted by distance from player (closest first). - * - * @return List of objects sorted by distance ascending - */ - public static List getObjectsSortedByDistance() { - WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - return getInstance().values().stream() - .sorted((o1, o2) -> Integer.compare( - o1.getWorldLocation().distanceTo(playerLocation), - o2.getWorldLocation().distanceTo(playerLocation) - )) - .collect(Collectors.toList()); - } - - /** - * Gets the total number of cached objects. - * - * @return The total object count - */ - public static int getObjectCount() { - return getInstance().size(); - } - - /** - * Gets the total number of objects by ID. - * - * @param objectId The object ID to count - * @return The count of objects with the specified ID - */ - public static long getObjectCountById(int objectId) { - return getObjectsById(objectId).count(); - } - - /** - * Gets the count of objects by type. - * - * @param objectType The object type to count - * @return The count of objects with the specified type - */ - public static long getObjectCountByType(ObjectType objectType) { - return getObjectsByType(objectType).count(); - } - - - - /** - * Invalidates all object cache entries and performs a fresh scene scan. - */ - public static void invalidateAllObjectsAndScanScene() { - getInstance().invalidateAll(); - requestSceneScan(); - log.debug("Invalidated all object cache entries and triggered scene scan"); - } - - - - /** - * Event handler registration for the unified cache. - * The unified cache handles events through its strategy automatically. - * Region change detection is now handled primarily in onGameTick() to prevent - * redundant checks during burst spawn events. - */ - - @Subscribe(priority = 50) - public void onGameObjectSpawned(final GameObjectSpawned event) { - // Region change check now handled in onGameStateChanged() to prevent redundant checks - getInstance().handleEvent(event); - } - - @Subscribe(priority = 60) - public void onGameObjectDespawned(final GameObjectDespawned event) { - getInstance().handleEvent(event); - } - - @Subscribe(priority = 50) - public void onGroundObjectSpawned(final GroundObjectSpawned event) { - // Region change check now handled in onGameStateChanged() to prevent redundant checks - getInstance().handleEvent(event); - } - - @Subscribe(priority = 60) - public void onGroundObjectDespawned(final GroundObjectDespawned event) { - getInstance().handleEvent(event); - } - - @Subscribe(priority = 50) - public void onWallObjectSpawned(final WallObjectSpawned event) { - // Region change check now handled in onGameStateChanged() to prevent redundant checks - getInstance().handleEvent(event); - } - - @Subscribe(priority = 60) - public void onWallObjectDespawned(final WallObjectDespawned event) { - getInstance().handleEvent(event); - } - - @Subscribe(priority = 50) - public void onDecorativeObjectSpawned(final DecorativeObjectSpawned event) { - // Region change check now handled in onGameStateChanged() to prevent redundant checks - getInstance().handleEvent(event); - } - - @Subscribe(priority = 60) - public void onDecorativeObjectDespawned(final DecorativeObjectDespawned event) { - getInstance().handleEvent(event); - } - @Subscribe(priority = 40) - public void onGameStateChanged(final GameStateChanged event) { - // Removed old region detection - now handled by unified Rs2Cache system - // Also let the strategy handle the event - getInstance().handleEvent(event); - } - - @Subscribe(priority = 110) - public void onGameTick(final GameTick event) { - // Let the strategy handle scanning - getInstance().handleEvent(event); - } - - /** - * Resets the singleton instance. Used for testing. - */ - public static synchronized void resetInstance() { - if (instance != null) { - instance.invalidateAll(); - instance = null; - log.debug("Rs2ObjectCache instance reset"); - } - } - - /** - * Gets object type statistics for display in overlays. - * - * @return Statistics string with object type counts - */ - public static String getObjectTypeStatistics() { - Rs2ObjectCache cache = getInstance(); - - // Count objects by type - int gameObjectCount = 0; - int wallObjectCount = 0; - int decorativeObjectCount = 0; - int groundObjectCount = 0; - int tileObjectCount = 0; - - for (Rs2ObjectModel objectModel : cache.values()) { - switch (objectModel.getObjectType()) { - case GAME_OBJECT: - gameObjectCount++; - break; - case WALL_OBJECT: - wallObjectCount++; - break; - case DECORATIVE_OBJECT: - decorativeObjectCount++; - break; - case GROUND_OBJECT: - groundObjectCount++; - break; - case TILE_OBJECT: - tileObjectCount++; - break; - } - } - - return String.format("Objects by type - Game: %d, Wall: %d, Decorative: %d, Ground: %d, Tile: %d (Total: %d)", - gameObjectCount, wallObjectCount, decorativeObjectCount, groundObjectCount, tileObjectCount, - cache.size()); - } - - /** - * Logs the current state of all cached objects for debugging. - * - * @param dumpToFile Whether to also dump the information to a file - */ - public static void logState(LogOutputMode mode) { - var cache = getInstance(); - var stats = cache.getStatistics(); - - // Create the log content - StringBuilder logContent = new StringBuilder(); - - String header = String.format("=== Object Cache State (%d entries) ===", cache.size()); - logContent.append(header).append("\n"); - - String statsInfo = Rs2CacheLoggingUtils.formatCacheStatistics( - stats.getHitRate(), stats.cacheHits, stats.cacheMisses, stats.cacheMode.toString()); - logContent.append(statsInfo).append("\n\n"); - - if (cache.size() == 0) { - logContent.append("Cache is empty\n"); - } else { - // Table format for objects - final String[] headers = {"Name", "Type", "ID", "Location", "Distance", "Actions"}; - final int[] columnWidths = {25, 12, 8, 18, 8, 30}; - logContent.append("\n").append(Rs2CacheLoggingUtils.formatTableHeader(headers, columnWidths)); - - // Get player location once for distance calculations - WorldPoint playerLocation = null; - try { - if (Microbot.getClient() != null && Microbot.getClient().getLocalPlayer() != null) { - playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - } catch (Exception e) { - log.debug("Could not get player location for distance calculations: {}", e.getMessage()); - } - final WorldPoint finalPlayerLocation = playerLocation; - - // Use a fixed-size buffer to avoid excessive StringBuilder growth - int maxRows = mode == LogOutputMode.CONSOLE_ONLY ? 50 : cache.size(); - int rowCount = 0; - // Precompute distances and actions in parallel for performance - class ObjectLogInfo { - Rs2ObjectModel obj; - int distance; - String actionsStr; - ObjectLogInfo(Rs2ObjectModel obj, int distance, String actionsStr) { - this.obj = obj; - this.distance = distance; - this.actionsStr = actionsStr; - } - } - - List objects = cache.values().parallelStream().limit(50) - .map(obj -> { - int distance = Integer.MAX_VALUE; - if (finalPlayerLocation != null && obj.getLocation() != null) { - try { - distance = obj.getLocation().distanceTo(finalPlayerLocation); - } catch (Exception ignored) {} - } - String actionsStr = ""; - try { - String[] actions = obj.getActions(); - if (actions != null && actions.length > 0) { - actionsStr = Arrays.stream(actions) - .filter(Objects::nonNull) - .filter(action -> !action.trim().isEmpty()) - .collect(Collectors.joining(",")); - } - } catch (Exception ignored) {} - return new ObjectLogInfo(obj, distance, actionsStr); - }) - .collect(Collectors.toList()); - - // Sort by distance (single-threaded, but fast on precomputed values) - if (finalPlayerLocation != null) { - objects.sort(Comparator.comparingInt(info -> info.distance)); - } - - for (ObjectLogInfo info : objects) { - if (rowCount++ >= maxRows) break; - try { - String[] values = { - Rs2CacheLoggingUtils.truncate(info.obj.getName() != null ? info.obj.getName() : "Unknown", 24), - info.obj.getObjectType() != null ? info.obj.getObjectType().name() : "Unknown", - String.valueOf(info.obj.getId()), - Rs2CacheLoggingUtils.formatLocation(info.obj.getLocation()), - info.distance == Integer.MAX_VALUE ? "N/A" : String.valueOf(info.distance), - Rs2CacheLoggingUtils.truncate(info.actionsStr, 29) - }; - logContent.append(Rs2CacheLoggingUtils.formatTableRow(values, columnWidths)); - } catch (Exception e) { - log.debug("Error processing object for logging: {}", e.getMessage()); - } - } - - logContent.append(Rs2CacheLoggingUtils.formatTableFooter(columnWidths)); - String limitMsg = Rs2CacheLoggingUtils.formatLimitMessage(cache.size(), maxRows); - if (!limitMsg.isEmpty()) { - logContent.append(limitMsg).append("\n"); - } - } - - String footer = "=== End Object Cache State ==="; - logContent.append(footer).append("\n"); - Rs2CacheLoggingUtils.outputCacheLog(getInstance().getCacheName(), logContent.toString(), mode); - } - - - - /** - * Implementation of abstract update method from Rs2Cache. - * Clears the cache and performs a complete scene scan to reload all objects from the scene. - * This ensures the cache is fully refreshed with current scene data. - */ - @Override - public void update() { - // Call the update method with a default delay of 0 - update(Constants.CLIENT_TICK_LENGTH /2); - } - /** - * Updates the object cache by clearing it and performing a scene scan. - * This is useful for refreshing the cache after significant game state changes. - * - * @param delayMs Optional delay in milliseconds before performing the update - */ - public void update(long delayMs) { - log.debug("Starting object cache update - clearing cache and performing scene scan after delay: {}ms", delayMs); - // Clear the entire cache - this.invalidateAll(); - // Perform a complete scene scan to repopulate the cache - updateStrategy.performSceneScan(this, delayMs ); - - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2QuestCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2QuestCache.java deleted file mode 100644 index 2c5c08dd30d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2QuestCache.java +++ /dev/null @@ -1,669 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import java.util.function.Supplier; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Quest; -import net.runelite.api.QuestState; -import net.runelite.api.events.ChatMessage; -import net.runelite.api.events.VarbitChanged; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.serialization.CacheSerializable; -import net.runelite.client.plugins.microbot.util.cache.strategy.simple.QuestUpdateStrategy; -import net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2CacheLoggingUtils; - -/** - * Thread-safe cache for quest states using the unified cache architecture. - * Automatically updates when quest-related events are received and supports persistence. - * - * This class extends Rs2UnifiedCache and provides specific quest caching functionality - * with proper EventBus integration for @Subscribe methods. - * - * Threading Strategy: - * - All quest state loading uses async approach with invokeLater() for deferred execution - * - Event handlers: Use invokeLater() to defer execution and avoid blocking event processing - * - Async methods: Use invokeLater() for deferred execution during initialization scenarios - * - * The cache uses only asynchronous operations to avoid nested thread issues - * that can occur when events (already on client thread) need to load quest states. - */ -@Slf4j -public class Rs2QuestCache extends Rs2Cache implements CacheSerializable { - private static Rs2QuestCache instance; - // Async update tracking - private static final AtomicInteger pendingAsyncUpdates = new java.util.concurrent.atomic.AtomicInteger(0); - private static final Set> updateCompletionCallbacks = java.util.concurrent.ConcurrentHashMap.newKeySet(); - - // Quest-specific update coordination to prevent deadlocks and race conditions - private static final java.util.concurrent.ConcurrentHashMap questUpdatesInProgress = new java.util.concurrent.ConcurrentHashMap<>(); - private static final long UPDATE_TIMEOUT_MS = 10000; // 10 seconds timeout for update tracking - - /** - * Private constructor for singleton pattern. - */ - private Rs2QuestCache() { - super("QuestCache", CacheMode.EVENT_DRIVEN_ONLY); - this.withUpdateStrategy(new QuestUpdateStrategy()) - .withPersistence("quests"); - } - - /** - * Gets the singleton instance of Rs2QuestCache. - * - * @return The singleton quest cache instance - */ - public static synchronized Rs2QuestCache getInstance() { - if (instance == null) { - instance = new Rs2QuestCache(); - } - return instance; - } - - /** - * Gets the cache instance for backward compatibility. - * - /** - * Gets the cache instance for backward compatibility. - * - * @return The singleton unified cache instance - */ - public static Rs2Cache getCache() { - return getInstance(); - } - - /** - * Checks if a quest update is currently in progress. - * Cleans up stale update entries older than UPDATE_TIMEOUT_MS. - * - * @param quest The quest to check - * @return true if update is in progress, false otherwise - */ - private static boolean isUpdateInProgress(Quest quest) { - Long updateStartTime = questUpdatesInProgress.get(quest); - if (updateStartTime == null) { - return false; - } - - // Check if update has timed out - long currentTime = System.currentTimeMillis(); - if (currentTime - updateStartTime > UPDATE_TIMEOUT_MS) { - // Remove stale update entry - questUpdatesInProgress.remove(quest); - log.warn("Removed stale quest update entry for {} (timeout after {}ms)", - quest.getName(), UPDATE_TIMEOUT_MS); - return false; - } - - return true; - } - - /** - * Marks a quest as having an update in progress. - * - * @param quest The quest to mark - * @return true if successfully marked (no existing update), false if update already in progress - */ - private static boolean markUpdateInProgress(Quest quest) { - long currentTime = System.currentTimeMillis(); - Long previousTime = questUpdatesInProgress.putIfAbsent(quest, currentTime); - - if (previousTime != null) { - // Check if existing update has timed out - if (currentTime - previousTime > UPDATE_TIMEOUT_MS) { - // Replace stale entry - questUpdatesInProgress.put(quest, currentTime); - log.debug("Replaced stale quest update entry for {}", quest.getName()); - return true; - } - return false; // Update already in progress - } - - return true; // Successfully marked - } - - /** - * Marks a quest update as completed. - * - * @param quest The quest to mark as completed - */ - private static void markUpdateCompleted(Quest quest) { - questUpdatesInProgress.remove(quest); - } - - /** - * Cleans up stale quest update entries. - */ - private static void cleanupStaleUpdates() { - long currentTime = System.currentTimeMillis(); - questUpdatesInProgress.entrySet().removeIf(entry -> { - if (currentTime - entry.getValue() > UPDATE_TIMEOUT_MS) { - log.debug("Cleaned up stale quest update entry for {}", entry.getKey().getName()); - return true; - } - return false; - }); - } - - - - - - - - /** - * Asynchronously loads quest state using invokeLater for deferred execution. - * This is the core method for all quest state loading to ensure consistent async behavior. - * Prevents duplicate updates for the same quest using quest-specific coordination. - * - * @param quest The quest to load state for - * @param callback Callback to handle the loaded quest state - */ - private static void loadQuestStateFromClientAsync(Quest quest, Consumer callback) { - try { - log.debug("Setting up async quest state loading for {}", quest.getName()); - - if (Microbot.getClient() == null) { - log.warn("Client is null when loading quest state for {}", quest); - executeCallback(callback, QuestState.NOT_STARTED); - return; - } - - // Check if update is already in progress for this quest - if (!markUpdateInProgress(quest)) { - log.debug("Quest update already in progress for {}, rejecting duplicate request", quest.getName()); - return; // Simply reject the duplicate update request - } - - // Increment pending updates counter - pendingAsyncUpdates.incrementAndGet(); - - // Always use invokeLater for consistency, even when on client thread - Microbot.getClientThread().invokeLater(() -> { - executeQuestStateLoad(quest, callback); - }); - - } catch (Exception e) { - log.error("Error setting up async quest state loading for {}: {}", quest, e.getMessage(), e); - handleLoadFailure(quest, callback); - } - } - - /** - * Executes the actual quest state loading on the client thread. - * - * @param quest The quest to load state for - * @param callback Callback to handle the loaded quest state - */ - private static void executeQuestStateLoad(Quest quest, Consumer callback) { - try { - QuestState state = quest.getState(Microbot.getClient()); - log.debug("Loaded quest state: {} = {}", quest.getName(), state); - - // Update cache with new state - updateQuestState(quest, state); - - // Execute callback - executeCallback(callback, state); - - } catch (Exception e) { - log.error("Error in quest state loading for {}: {}", quest, e.getMessage(), e); - executeCallback(callback, QuestState.NOT_STARTED); - } finally { - // Always clean up tracking and notify completion - handleLoadCompletion(quest); - } - } - - /** - * Handles cleanup and notification when a quest load completes. - * - * @param quest The quest that completed loading - */ - private static void handleLoadCompletion(Quest quest) { - // Mark quest update as completed - markUpdateCompleted(quest); - - // Decrement pending updates and check for completion - int remaining = pendingAsyncUpdates.decrementAndGet(); - if (remaining == 0) { - // Clean up stale updates - cleanupStaleUpdates(); - - // Notify all completion callbacks - notifyCompletionCallbacks(); - } - } - - /** - * Handles load failure scenarios. - * - * @param quest The quest that failed to load - * @param callback The callback to notify of failure - */ - private static void handleLoadFailure(Quest quest, Consumer callback) { - markUpdateCompleted(quest); - executeCallback(callback, QuestState.NOT_STARTED); - pendingAsyncUpdates.decrementAndGet(); - } - - /** - * Safely executes a callback with error handling. - * - * @param callback The callback to execute - * @param state The quest state to pass to the callback - */ - private static void executeCallback(Consumer callback, QuestState state) { - if (callback != null) { - try { - callback.accept(state); - } catch (Exception e) { - log.error("Error in quest state callback: {}", e.getMessage(), e); - } - } - } - - /** - * Notifies all completion callbacks safely. - */ - private static void notifyCompletionCallbacks() { - updateCompletionCallbacks.forEach(completionCallback -> { - try { - completionCallback.accept(true); - } catch (Exception e) { - log.error("Error in update completion callback: {}", e.getMessage()); - } - }); - } - - /** - * Gets quest state from the cache. If not cached, returns NOT_STARTED and triggers async loading. - * This prevents blocking behavior and deadlocks by not waiting for completion. - * - * @param quest The quest to retrieve state for - * @return The cached QuestState or NOT_STARTED if not in cache - */ - public static QuestState getQuestState(Quest quest) { - // Use the base cache get method directly - QuestState cachedState = getInstance().get(quest); - if (cachedState != null ) { - return cachedState; - } - if ( isUpdateInProgress(quest)) { - //log.info("Quest update in progress for {}, returning NOT_STARTED", quest.getName()); - return QuestState.NOT_STARTED; // Return NOT_STARTED if update is in progress - } - - // Trigger async loading if not cached - updateQuestStateAsync(quest); - return getCache().get(quest); // Default state if not cached - - - - - } - - /** - * Asynchronously updates quest state in cache. Useful during initialization or event processing - * where you want to ensure quest state loading doesn't block current execution. - * - * @param quest The quest to update - */ - public static void updateQuestStateAsync(Quest quest) { - loadQuestStateFromClientAsync(quest, state -> { - log.debug("Async quest update completed: {} = {}", quest.getName(), state); - }); - } - - /** - * Gets quest state asynchronously with a callback. Preferred method for async operations. - * If cached, callback is executed immediately. Otherwise, loads asynchronously. - * - * @param quest The quest to retrieve state for - * @param callback Callback to receive the quest state - */ - public static void getQuestStateAsync(Quest quest, Consumer callback) { - QuestState cachedState = getInstance().get(quest); - if (cachedState != null) { - executeCallback(callback, cachedState); - } else { - loadQuestStateFromClientAsync(quest, callback); - } - } - - /** - * Registers a callback to be notified when all pending async updates are complete. - * - * @param callback Callback to be called with true when all updates are complete - */ - public static void onAllAsyncUpdatesComplete(Consumer callback) { - if (callback != null) { - updateCompletionCallbacks.add(callback); - // If no updates are pending, call immediately - if (pendingAsyncUpdates.get() == 0) { - try { - callback.accept(true); - } catch (Exception e) { - log.error("Error in immediate completion callback: {}", e.getMessage()); - } - } - } - } - - /** - * Gets the number of pending async quest state updates. - * - * @return The number of pending updates - */ - public static int getPendingAsyncUpdates() { - return pendingAsyncUpdates.get(); - } - - /** - * Clears all completion callbacks. Useful for cleanup. - */ - public static void clearCompletionCallbacks() { - updateCompletionCallbacks.clear(); - } - - /** - * Gets quest state from the cache or loads it with a custom supplier. - * - * @param quest The quest to retrieve state for - * @param valueLoader Custom supplier for loading the quest state - * @return The QuestState - */ - public static QuestState getQuestState(Quest quest, Supplier valueLoader) { - return getInstance().get(quest, valueLoader); - } - - /** - * Manually updates a quest state in the cache. - * - * @param quest The quest to update - * @param state The new quest state - */ - private static void updateQuestState(Quest quest, QuestState state) { - getInstance().put(quest, state); - log.debug("Updated quest cache: {} = {}", quest, state); - } - - /** - * Checks if a quest is started (not NOT_STARTED). - * - * @param quest The quest to check - * @return true if the quest is started - */ - public static boolean isQuestStarted(Quest quest) { - return getQuestState(quest) != QuestState.NOT_STARTED; - } - - /** - * Checks if a quest is completed (FINISHED). - * - * @param quest The quest to check - * @return true if the quest is completed - */ - public static boolean isQuestCompleted(Quest quest) { - return getQuestState(quest) == QuestState.FINISHED; - } - - /** - * Checks if a quest is in progress (IN_PROGRESS). - * - * @param quest The quest to check - * @return true if the quest is in progress - */ - public static boolean isQuestInProgress(Quest quest) { - return getQuestState(quest) == QuestState.IN_PROGRESS; - } - - /** - * Schedules an asynchronous update of all cached quests using invokeLater. - * This is useful during initialization or when you want to ensure quest updates - * don't block current event processing, even when already on the client thread. - */ - public static void updateAllFromClientAsync() { - - try { - log.debug("Starting asynchronous quest cache update..."); - getInstance().update(); - log.debug("Completed asynchronous quest cache update"); - } catch (Exception e) { - log.error("Error during asynchronous quest cache update: {}", e.getMessage(), e); - } - - } - - /** - * Updates all cached data by retrieving fresh values from the game client asynchronously. - * Implements the abstract method from Rs2Cache. - * - * Iterates over all currently cached quest keys and refreshes their states asynchronously. - */ - @Override - public void update() { - log.debug("Updating all cached quests from client asynchronously..."); - - if (Microbot.getClient() == null) { - log.warn("Cannot update quests - client is null"); - return; - } - - int beforeSize = size(); - - // Get all currently cached quest keys and update them asynchronously - java.util.Set cachedQuests = entryStream() - .map(java.util.Map.Entry::getKey) - .collect(java.util.stream.Collectors.toSet()); - - if (cachedQuests.isEmpty()) { - log.debug("No cached quests to update"); - return; - } - - log.info("Starting async update of {} cached quests", cachedQuests.size()); - - for (Quest quest : cachedQuests) { - loadQuestStateFromClientAsync(quest, freshState -> { - if (freshState != null) { - put(quest, freshState); - log.debug("Updated quest {} with fresh state: {}", quest.getName(), freshState); - } - }); - } - - log.info("Initiated async update for {} quests (cache had {} entries total)", - cachedQuests.size(), beforeSize); - } - - - - /** - * Event handler registration for the unified cache. - * The unified cache handles events through its strategy automatically. - */ - - @Subscribe - public void onVarbitChanged(final VarbitChanged event) { - try { - getInstance().handleEvent(event); - } catch (Exception e) { - log.error("Error handling VarbitChanged event: {}", e.getMessage(), e); - } - } - - @Subscribe - public void onChatMessage(final ChatMessage chatMessage) { - try { - getInstance().handleEvent(chatMessage); - } catch (Exception e) { - log.error("Error handling ChatMessage event: {}", e.getMessage(), e); - } - } - - /** - * Resets the singleton instance. Used for testing. - */ - public static synchronized void resetInstance() { - if (instance != null) { - instance.invalidateAll(); - instance = null; - } - } - - // ============================================ - // CacheSerializable Implementation - // ============================================ - - @Override - public String getConfigKey() { - return "quests"; - } - - @Override - public String getConfigGroup() { - return "microbot"; - } - - @Override - public boolean shouldPersist() { - return true; // Quest states should be persisted for progress tracking - } - - /** - * Logs the current state of all cached quests for debugging. - * - * @param dumpToFile Whether to also dump the information to a file - */ - public static void logState(LogOutputMode mode) { - var cache = getInstance(); - var stats = cache.getStatistics(); - - // Create the log content - StringBuilder logContent = new StringBuilder(); - - String header = String.format("=== Quest Cache State (%d entries) ===", cache.size()); - logContent.append(header).append("\n"); - - String statsInfo = Rs2CacheLoggingUtils.formatCacheStatistics( - stats.getHitRate(), stats.cacheHits, stats.cacheMisses, stats.cacheMode.toString()); - logContent.append(statsInfo).append("\n\n"); - - if (cache.size() == 0) { - String emptyMsg = "Cache is empty"; - logContent.append(emptyMsg).append("\n"); - } else { - // Table format for quests - String[] headers = {"Quest", "State", "ID", "Cache Timestamp", "Varbit ID", "VarPlayer ID"}; - int[] columnWidths = {40, 15, 8, 22, 10, 12}; - - String tableHeader = Rs2CacheLoggingUtils.formatTableHeader(headers, columnWidths); - logContent.append("\n").append(tableHeader); - - // Sort quests by state (completed first, then in progress, then not started) - cache.entryStream() - .sorted((a, b) -> { - try { - // Handle null entries - if (a == null && b == null) return 0; - if (a == null) return 1; - if (b == null) return -1; - if (a.getKey() == null && b.getKey() == null) return 0; - if (a.getKey() == null) return 1; - if (b.getKey() == null) return -1; - if (a.getValue() == null && b.getValue() == null) return 0; - if (a.getValue() == null) return 1; - if (b.getValue() == null) return -1; - - // Sort by state priority: FINISHED > IN_PROGRESS > NOT_STARTED - int aOrder = getQuestStateOrder(a.getValue()); - int bOrder = getQuestStateOrder(b.getValue()); - if (aOrder != bOrder) { - return Integer.compare(aOrder, bOrder); - } - // Then sort alphabetically by quest name - String nameA = a.getKey().getName(); - String nameB = b.getKey().getName(); - if (nameA == null && nameB == null) return 0; - if (nameA == null) return 1; - if (nameB == null) return -1; - return nameA.compareTo(nameB); - } catch (Exception e) { - // Fallback to ID comparison if anything goes wrong - try { - return Integer.compare( - a != null && a.getKey() != null ? a.getKey().getId() : Integer.MAX_VALUE, - b != null && b.getKey() != null ? b.getKey().getId() : Integer.MAX_VALUE - ); - } catch (Exception e2) { - return 0; // Last resort - consider equal - } - } - }) - .forEach(entry -> { - Quest quest = entry.getKey(); - QuestState questState = entry.getValue(); - - // Get cache timestamp for this quest - Long cacheTimestamp = cache.getCacheTimestamp(quest); - String cacheTimestampStr = cacheTimestamp != null ? - Rs2Cache.formatUtcTimestamp(cacheTimestamp) : "N/A"; - - // Get varbit/varPlayer IDs for this quest - Integer varbitId = QuestUpdateStrategy.getVarbitIdByQuest(quest); - Integer varPlayerId = QuestUpdateStrategy.getVarPlayerIdByQuest(quest); - - String[] values = { - Rs2CacheLoggingUtils.truncate(quest.getName(), 39), - questState.toString(), - String.valueOf(quest.getId()), - Rs2CacheLoggingUtils.truncate(cacheTimestampStr, 21), - varbitId != null ? String.valueOf(varbitId) : "N/A", - varPlayerId != null ? String.valueOf(varPlayerId) : "N/A" - }; - - String row = Rs2CacheLoggingUtils.formatTableRow(values, columnWidths); - logContent.append(row); - }); - - String tableFooter = Rs2CacheLoggingUtils.formatTableFooter(columnWidths); - logContent.append(tableFooter); - - // Summary statistics - long completedCount = cache.entryStream().filter(e -> e.getValue() == QuestState.FINISHED).count(); - long inProgressCount = cache.entryStream().filter(e -> e.getValue() == QuestState.IN_PROGRESS).count(); - long notStartedCount = cache.entryStream().filter(e -> e.getValue() == QuestState.NOT_STARTED).count(); - - String summaryMsg = String.format("Quest Summary: %d Completed, %d In Progress, %d Not Started", - completedCount, inProgressCount, notStartedCount); - logContent.append(summaryMsg).append("\n"); - } - - String footer = "=== End Quest Cache State ==="; - logContent.append(footer).append("\n"); - Rs2CacheLoggingUtils.outputCacheLog(getInstance().getCacheName(), logContent.toString(), mode); - } - - /** - * Helper method to define quest state sorting order. - */ - private static int getQuestStateOrder(QuestState state) { - switch (state) { - case FINISHED: - return 0; // Highest priority - case IN_PROGRESS: - return 1; // Medium priority - case NOT_STARTED: - default: - return 2; // Lowest priority - } - } - - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2SkillCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2SkillCache.java deleted file mode 100644 index f0f944ec859..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2SkillCache.java +++ /dev/null @@ -1,523 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Skill; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.StatChanged; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.model.SkillData; -import net.runelite.client.plugins.microbot.util.cache.strategy.simple.SkillUpdateStrategy; -import net.runelite.client.plugins.microbot.util.cache.serialization.CacheSerializable; -import net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2CacheLoggingUtils; - -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.function.Supplier; - -/** - * Thread-safe cache for skill levels and experience using the unified cache architecture. - * Automatically updates when StatChanged events are received and supports persistence. - * - * This class extends Rs2UnifiedCache and provides specific skill caching functionality - * with proper EventBus integration for @Subscribe methods. - */ -@Slf4j -public class Rs2SkillCache extends Rs2Cache implements CacheSerializable { - - private static Rs2SkillCache instance; - - /** - * Private constructor for singleton pattern. - */ - private Rs2SkillCache() { - super("SkillCache", CacheMode.EVENT_DRIVEN_ONLY); - this.withUpdateStrategy(new SkillUpdateStrategy()) - .withPersistence("skills"); - } - - /** - * Gets the singleton instance of Rs2SkillCache. - * - * @return The singleton skill cache instance - */ - public static synchronized Rs2SkillCache getInstance() { - if (instance == null) { - instance = new Rs2SkillCache(); - } - return instance; - } - - /** - * Gets the cache instance for backward compatibility. - * - * @return The singleton unified cache instance - */ - public static Rs2Cache getCache() { - return getInstance(); - } - - /** - * Loads skill data from the client for a specific skill. - * - * @param skill The skill to load data for - * @return The SkillData containing level, boosted level, and experience - */ - private static SkillData loadSkillDataFromClient(Skill skill) { - try { - if (Microbot.getClient() == null) { - log.warn("Client is null when loading skill data for {}", skill); - return new SkillData(1, 1, 0); - } - final int[] skillValues = new int[3]; // [level, boostedLevel, experience] - boolean loadedSuccessfully = Microbot.getClientThread().runOnClientThreadOptional( () -> { - skillValues[0] = Microbot.getClient().getRealSkillLevel(skill); - skillValues[1] = Microbot.getClient().getBoostedSkillLevel(skill); - skillValues[2] = Microbot.getClient().getSkillExperience(skill); - if (skillValues[0] < 0 || skillValues[1] < 0 || skillValues[2] < 0) { - log.warn("Invalid skill data for {}: level={}, boosted={}, exp={}", skill, skillValues[0], skillValues[1], skillValues[2]); - return false; // Skip if invalid - } - return true; // Ensure this runs on the client thread - }).orElse(false); - - if (!loadedSuccessfully) { - log.warn("Failed to load skill data for {}, using default values", skill); - return new SkillData(1, 1, 0); - } - - log.trace("Loaded skill data from client: {} (level: {}, boosted: {}, exp: {})", - skill, skillValues[0], skillValues[1], skillValues[2]); - return new SkillData(skillValues[0], skillValues[1], skillValues[2]); - } catch (Exception e) { - log.error("Error loading skill data for {}: {}", skill, e.getMessage(), e); - return new SkillData(1, 1, 0); - } - } - - /** - * Gets skill data from the cache or loads it from the client. - * - * @param skill The skill to retrieve data for - * @return The SkillData containing level, boosted level, and experience - */ - public static SkillData getSkillData(Skill skill) { - return getInstance().get(skill, () -> loadSkillDataFromClient(skill)); - } - - /** - * Gets skill data from the cache or loads it with a custom supplier. - * - * @param skill The skill to retrieve data for - * @param valueLoader Custom supplier for loading the skill data - * @return The SkillData - */ - public static SkillData getSkillData(Skill skill, Supplier valueLoader) { - return getInstance().get(skill, valueLoader); - } - - /** - * Gets the real (unboosted) level for a skill from the cache. - * - * @param skill The skill to get the level for - * @return The real skill level - */ - public static int getRealSkillLevel(Skill skill) { - return getSkillData(skill).getLevel(); - } - - /** - * Gets the boosted level for a skill from the cache. - * - * @param skill The skill to get the boosted level for - * @return The boosted skill level - */ - public static int getBoostedSkillLevel(Skill skill) { - return getSkillData(skill).getBoostedLevel(); - } - - /** - * Gets the experience for a skill from the cache. - * - * @param skill The skill to get the experience for - * @return The skill experience - */ - public static int getSkillExperience(Skill skill) { - return getSkillData(skill).getExperience(); - } - - /** - * Manually updates skill data in the cache. - * - * @param skill The skill to update - * @param skillData The new skill data - */ - public static void updateSkillData(Skill skill, SkillData skillData) { - getInstance().put(skill, skillData); - } - - /** - * Manually updates skill data in the cache. - * - * @param skill The skill to update - * @param level The real skill level - * @param boostedLevel The boosted skill level - * @param experience The skill experience - */ - public static void updateSkillData(Skill skill, int level, int boostedLevel, int experience) { - updateSkillData(skill, new SkillData(level, boostedLevel, experience)); - } - - /** - * Updates all cached skills by retrieving fresh data from the game client. - * Implements the abstract method from Rs2Cache. - * - * Iterates over all currently cached skill keys and refreshes their data from the client. - */ - @Override - public void update() { - log.debug("Updating all cached skills from client..."); - - if (Microbot.getClient() == null) { - log.warn("Cannot update skills - client is null"); - return; - } - - int beforeSize = size(); - int updatedCount = 0; - - // Get all currently cached skill keys and update them - java.util.Set cachedSkills = entryStream() - .map(java.util.Map.Entry::getKey) - .collect(java.util.stream.Collectors.toSet()); - - for (Skill skill : cachedSkills) { - try { - // Refresh the skill data from client using the private method - SkillData freshData = loadSkillDataFromClient(skill); - if (freshData != null) { - put(skill, freshData); - updatedCount++; - log.debug("Updated skill {} with fresh data: level={}, boosted={}, xp={}", - skill, freshData.getLevel(), freshData.getBoostedLevel(), freshData.getExperience()); - } - } catch (Exception e) { - log.warn("Failed to update skill {}: {}", skill, e.getMessage()); - } - } - - log.debug("Updated {} skills from client (cache had {} entries total)", - updatedCount, beforeSize); - } - - // ============================================ - // Print Functions for Cache Information - // ============================================ - - /** - * Returns a detailed formatted string containing all skill cache information. - * Includes complete skill data with temporal tracking and change information. - * - * @return Detailed multi-line string representation of all cached skills - */ - public static String printDetailedSkillInfo() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") - .withZone(ZoneId.systemDefault()); - - sb.append("=".repeat(80)).append("\n"); - sb.append(" DETAILED SKILL CACHE INFORMATION\n"); - sb.append("=".repeat(80)).append("\n"); - - // Cache metadata - Rs2Cache cache = getInstance(); - sb.append(String.format("Cache Name: %s\n", cache.getCacheName())); - sb.append(String.format("Cache Mode: %s\n", cache.getCacheMode())); - sb.append(String.format("Total Cached Skills: %d\n", cache.size())); - - // Cache statistics - var stats = cache.getStatistics(); - sb.append(String.format("Cache Hits: %d\n", stats.cacheHits)); - sb.append(String.format("Cache Misses: %d\n", stats.cacheMisses)); - sb.append(String.format("Hit Ratio: %.2f%%\n", stats.getHitRate() * 100)); - sb.append(String.format("Total Invalidations: %d\n", stats.totalInvalidations)); - sb.append(String.format("Uptime: %d ms\n", stats.uptime)); - sb.append(String.format("TTL: %d ms\n", stats.ttlMillis)); - sb.append("\n"); - - sb.append("-".repeat(80)).append("\n"); - sb.append(" SKILL DETAILS\n"); - sb.append("-".repeat(80)).append("\n"); - - // Headers - sb.append(String.format("%-15s %-6s %-8s %-12s %-12s %-10s %-19s\n", - "SKILL", "LEVEL", "BOOSTED", "EXPERIENCE", "EXP GAINED", "LEVEL UP", "LAST UPDATED")); - sb.append("-".repeat(80)).append("\n"); - - // Iterate through all skills - for (Skill skill : Skill.values()) { - SkillData data = cache.get(skill); - if (data != null) { - String lastUpdated = formatter.format(Instant.ofEpochMilli(data.getLastUpdated())); - String expGained = data.getExperienceGained() > 0 ? - String.format("+%d", data.getExperienceGained()) : "-"; - String levelUp = data.isLevelUp() ? "YES" : "-"; - - sb.append(String.format("%-15s %-6d %-8d %-12d %-12s %-10s %-19s\n", - skill.name(), - data.getLevel(), - data.getBoostedLevel(), - data.getExperience(), - expGained, - levelUp, - lastUpdated)); - - // Additional details for skills with changes - if (data.getPreviousLevel() != null || data.getPreviousExperience() != null) { - sb.append(String.format(" └─ Previous: Level %s, Experience %s\n", - data.getPreviousLevel() != null ? data.getPreviousLevel() : "Unknown", - data.getPreviousExperience() != null ? data.getPreviousExperience() : "Unknown")); - } - } - } - - sb.append("-".repeat(80)).append("\n"); - sb.append(String.format("Generated at: %s\n", formatter.format(Instant.now()))); - sb.append("=".repeat(80)); - - return sb.toString(); - } - - /** - * Returns a summary formatted string containing essential skill cache information. - * Compact view showing key metrics and recent changes. - * - * @return Summary multi-line string representation of skill cache - */ - public static String printSkillSummary() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss"); - - sb.append("┌─ SKILL CACHE SUMMARY ").append("─".repeat(45)).append("┐\n"); - - Rs2Cache cache = getInstance(); - var stats = cache.getStatistics(); - - // Summary statistics - sb.append(String.format("│ Skills Cached: %-3d │ Hits: %-6d │ Hit Rate: %5.1f%% │\n", - cache.size(), stats.cacheHits, stats.getHitRate() * 100)); - - // Combat level calculation - int attack = getRealSkillLevel(Skill.ATTACK); - int strength = getRealSkillLevel(Skill.STRENGTH); - int defence = getRealSkillLevel(Skill.DEFENCE); - int hitpoints = getRealSkillLevel(Skill.HITPOINTS); - int prayer = getRealSkillLevel(Skill.PRAYER); - int ranged = getRealSkillLevel(Skill.RANGED); - int magic = getRealSkillLevel(Skill.MAGIC); - - double combatLevel = (defence + hitpoints + Math.floor(prayer / 2)) * 0.25 + - Math.max(attack + strength, Math.max(ranged * 1.5, magic * 1.5)) * 0.325; - - // Calculate total level - int totalLevel = 0; - for (Skill skill : Skill.values()) { - totalLevel += getRealSkillLevel(skill); - } - - sb.append(String.format("│ Combat Level: %-6.1f │ Total Level: %-8d │\n", - combatLevel, totalLevel)); - - sb.append("├─ RECENT CHANGES ").append("─".repeat(46)).append("â”Ī\n"); - - // Show skills with recent changes (level ups or significant exp gains) - boolean hasChanges = false; - for (Skill skill : Skill.values()) { - SkillData data = cache.get(skill); - if (data != null && (data.isLevelUp() || data.getExperienceGained() > 0)) { - hasChanges = true; - String timeStr = formatter.format(Instant.ofEpochMilli(data.getLastUpdated())); - if (data.isLevelUp()) { - sb.append(String.format("│ %-12s LEVEL UP! %d → %d (%s) %-14s │\n", - skill.name(), data.getPreviousLevel(), data.getLevel(), timeStr, "")); - } else if (data.getExperienceGained() > 1000) { - sb.append(String.format("│ %-12s +%-8d exp (%s) %-20s │\n", - skill.name(), data.getExperienceGained(), timeStr, "")); - } - } - } - - if (!hasChanges) { - sb.append("│ No recent skill changes detected ").append(" ".repeat(27)).append("│\n"); - } - - sb.append("└").append("─".repeat(63)).append("┘"); - - return sb.toString(); - } - - // ============================================ - // Legacy API Compatibility Methods - // ============================================ - - /** - * Checks if skill meets level requirement - Legacy compatibility method. - * - * @param skill The skill to check - * @param levelRequired The required level - * @param boosted Whether to use boosted level (true) or real level (false) - * @return true if the requirement is met - */ - public static boolean hasSkillRequirement(Skill skill, int levelRequired, boolean boosted) { - int currentLevel = boosted ? getBoostedSkillLevel(skill) : getRealSkillLevel(skill); - return currentLevel >= levelRequired; - } - - /** - * Invalidates all skill cache entries. - */ - public static void invalidateAllSkills() { - getInstance().invalidateAll(); - log.debug("Invalidated all skill cache entries"); - } - - /** - * Invalidates a specific skill cache entry. - * - * @param skill The skill to invalidate - */ - public static void invalidateSkill(Skill skill) { - getInstance().remove(skill); - log.debug("Invalidated skill cache entry: {}", skill); - } - - /** - * Event handler registration for the unified cache. - * The unified cache handles events through its strategy automatically. - */ - - @Subscribe - public void onStatChanged(StatChanged event) { - try { - getInstance().handleEvent(event); - } catch (Exception e) { - log.error("Error handling StatChanged event: {}", e.getMessage(), e); - } - } - - @Subscribe - public void onGameStateChanged(GameStateChanged event) { - try { - switch (event.getGameState()) { - case LOGGED_IN: - case HOPPING: - case LOGIN_SCREEN: - case CONNECTION_LOST: - // Let the strategy handle cache invalidation - break; - default: - break; - } - } catch (Exception e) { - log.error("Error handling GameStateChanged event: {}", e.getMessage(), e); - } - } - - /** - * Resets the singleton instance. Used for testing. - */ - public static synchronized void resetInstance() { - if (instance != null) { - instance.invalidateAll(); - instance = null; - } - } - - /** - * Logs the current state of all cached skills for debugging. - * - * @param dumpToFile Whether to also dump the information to a file - */ - public static void logState(LogOutputMode mode) { - var cache = getInstance(); - var stats = cache.getStatistics(); - - // Create the log content - StringBuilder logContent = new StringBuilder(); - - String header = String.format("=== Skill Cache State (%d entries) ===", cache.size()); - logContent.append(header).append("\n"); - - String statsInfo = Rs2CacheLoggingUtils.formatCacheStatistics( - stats.getHitRate(), stats.cacheHits, stats.cacheMisses, stats.cacheMode.toString()); - logContent.append(statsInfo).append("\n\n"); - - if (cache.size() == 0) { - String emptyMsg = "Cache is empty"; - logContent.append(emptyMsg).append("\n"); - } else { - // Table format for skills - String[] headers = {"Skill", "Level", "Boosted", "Experience", "Previous", "Last Updated"}; - int[] columnWidths = {15, 8, 8, 12, 20, 12}; - - String tableHeader = Rs2CacheLoggingUtils.formatTableHeader(headers, columnWidths); - logContent.append("\n").append(tableHeader); - - // Sort skills by name for consistent ordering - for (Skill skill : Skill.values()) { - SkillData data = cache.get(skill); - if (data != null) { - String previousInfo = ""; - if (data.getPreviousLevel() != null || data.getPreviousExperience() != null) { - previousInfo = String.format("L%s E%s", - data.getPreviousLevel() != null ? data.getPreviousLevel() : "?", - data.getPreviousExperience() != null ? data.getPreviousExperience() : "?"); - } - - String[] values = { - skill.name(), - String.valueOf(data.getLevel()), - String.valueOf(data.getBoostedLevel()), - String.valueOf(data.getExperience()), - Rs2CacheLoggingUtils.truncate(previousInfo, 19), - Rs2CacheLoggingUtils.formatTimestamp(data.getLastUpdated()) - }; - - String row = Rs2CacheLoggingUtils.formatTableRow(values, columnWidths); - logContent.append(row); - } - } - - String tableFooter = Rs2CacheLoggingUtils.formatTableFooter(columnWidths); - logContent.append(tableFooter); - } - - String footer = "=== End Skill Cache State ==="; - logContent.append(footer).append("\n"); - - - Rs2CacheLoggingUtils.writeCacheLogFile("skill", logContent.toString(), true); - } - - - // ============================================ - // CacheSerializable Implementation - // ============================================ - - @Override - public String getConfigKey() { - return "skills"; - } - - @Override - public String getConfigGroup() { - return "microbot"; - } - - @Override - public boolean shouldPersist() { - return true; // Skills should always be persisted for progress tracking - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2SpiritTreeCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2SpiritTreeCache.java deleted file mode 100644 index 01319ed81b4..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2SpiritTreeCache.java +++ /dev/null @@ -1,688 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.GameState; -import net.runelite.api.Skill; -import net.runelite.api.GameState; -import net.runelite.api.Skill; -import net.runelite.api.coords.WorldArea; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.GameStateChanged; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.questhelper.helpers.mischelpers.farmruns.CropState; -import net.runelite.client.plugins.microbot.shortestpath.Transport; -import net.runelite.client.plugins.microbot.shortestpath.TransportType; -import net.runelite.client.plugins.microbot.util.cache.model.SpiritTreeData; -import net.runelite.client.plugins.microbot.util.cache.serialization.CacheSerializable; -import net.runelite.client.plugins.microbot.util.cache.strategy.farming.SpiritTreeUpdateStrategy; -import net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2CacheLoggingUtils; -import net.runelite.client.plugins.microbot.util.farming.SpiritTree; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -/** - * Thread-safe cache for spirit tree farming states and travel availability using the unified cache architecture. - * Automatically updates when WidgetLoaded, VarbitChanged, and GameObjectSpawned events are received and supports persistence. - * - * This cache tracks both built-in spirit trees (quest unlocked) and farmable spirit trees (player grown), - * storing comprehensive state information including crop states, travel availability, and detection context. - * - * The cache integrates with: - * - Spirit tree widget detection (Adventure Log - Spirit Tree Locations) - * - Farming varbit monitoring for spiritTree state changes - * - Game object spawning for real-time availability detection - * - Persistent storage for cross-session state preservation - */ -@Slf4j -public class Rs2SpiritTreeCache extends Rs2Cache implements CacheSerializable { - - private static Rs2SpiritTreeCache instance; - - // Cache configuration constants - private static final long SPIRIT_TREE_DATA_TTL = 30 * 60 * 1000L; // 30 minutes - private static final long STALE_DATA_THRESHOLD = 10 * 60 * 1000L; // 10 minutes - - /** - * Private constructor for singleton pattern. - */ - private Rs2SpiritTreeCache() { - super("SpiritTreeCache", CacheMode.EVENT_DRIVEN_ONLY); - this.withUpdateStrategy(new SpiritTreeUpdateStrategy()) - .withPersistence("spiritTrees"); - } - - /** - * Gets the singleton instance of Rs2SpiritTreeCache. - * - * @return The singleton spirit tree cache instance - */ - public static synchronized Rs2SpiritTreeCache getInstance() { - if (instance == null) { - instance = new Rs2SpiritTreeCache(); - } - return instance; - } - - /** - * Gets the cache instance for backward compatibility. - * - * @return The singleton cache instance - */ - public static Rs2Cache getCache() { - return getInstance(); - } - - // ============================================ - // Core Spirit Tree Cache Operations - // ============================================ - - /** - * Gets spirit tree data from the cache or initializes it with current state. - * - * @param spiritTree The spirit tree spiritTree to retrieve data for - * @return The SpiritTreeData containing state and availability information - */ - public static SpiritTreeData getSpiritTreeData(SpiritTree spiritTree) { - return getInstance().get(spiritTree, () -> { - try { - // Determine initial state based on spiritTree type - CropState cropState = CropState.HARVESTABLE; - boolean availableForTravel = spiritTree.hasQuestRequirements(); - - if (spiritTree.getType() == SpiritTree.SpiritTreeType.FARMABLE) { - cropState = spiritTree.getPatchState(); - availableForTravel &= spiritTree.isAvailableForTravel(); - } else if (spiritTree.getType() == SpiritTree.SpiritTreeType.POH) { - availableForTravel &= spiritTree.hasLevelRequirement(); - } - - log.debug("Initial spirit tree data for {}: \n\tcropState={}, available={}", - spiritTree.name(), cropState, availableForTravel); - - return new SpiritTreeData(spiritTree, cropState, availableForTravel); - - } catch (Exception e) { - log.error("Error loading initial spirit tree data for {}: {}", spiritTree.name(), e.getMessage(), e); - // Return default state - assume unavailable to be safe - return new SpiritTreeData(spiritTree, null, false); - } - }); - } - - /** - * Gets all available spirit tree locations for travel. - * These are the origins where spirit trees are available for use. - * - * @return Set of world points where spirit trees are available for travel - */ - public static Set getAvailableOrigins() { - return getInstance().stream() - .filter(SpiritTreeData::isAvailableForTravel) - .map(data -> data.getSpiritTree().getLocation()) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - } - - /** - * Gets all available spirit tree destinations for travel. - * This is an alias for getAvailableOrigins() since in spirit tree context, - * available origins can also serve as destinations. - * - * @return Set of world points where spirit trees are available as destinations - */ - public static Set getAvailableDestinations() { - return getAvailableOrigins(); - } - - /** - * Gets all spirit tree patches that are currently available for travel. - * - * @return List of available spirit tree patches - */ - public static List getAvailableSpiritTrees() { - return getInstance().stream() - .filter(SpiritTreeData::isAvailableForTravel) - .map(SpiritTreeData::getSpiritTree) - .collect(Collectors.toList()); - } - - /** - * Gets all farmable spirit tree patches and their current states. - * - * @return List of spirit tree data for farmable patches only - */ - public static List getFarmableTreeStates() { - return getInstance().stream() - .filter(data -> data.getSpiritTree().getType() == SpiritTree.SpiritTreeType.FARMABLE) - .collect(Collectors.toList()); - } - - /** - * Gets farmable patches that require attention (diseased, dead, or ready for harvest). - * - * @return List of patches requiring farming attention - */ - public static List getPatchesRequiringAttention() { - return getInstance().stream() - .filter(data -> data.getSpiritTree().getType() == SpiritTree.SpiritTreeType.FARMABLE) - .filter(data -> { - CropState state = data.getCropState(); - return state == CropState.DISEASED || - state == CropState.DEAD; - }) - .collect(Collectors.toList()); - } - - /** - * Gets patches that are ready for planting (empty). - * - * @return List of empty farmable patches - */ - public static List getEmptyPatches() { - return getInstance().stream() - .filter(data -> data.getSpiritTree().getType() == SpiritTree.SpiritTreeType.FARMABLE) - .filter(data -> data.getCropState() == CropState.EMPTY) - .collect(Collectors.toList()); - } - - /** - * Checks if a spirit tree is available for travel at the given origin location. - * Uses WorldArea for robust location matching around both the spirit tree spiritTree - * and the query location to handle slight coordinate variations. - * - * @param origin The world point to check (where a spirit tree should be standing) - * @return true if a spirit tree is available at this location - */ - public static boolean isOriginAvailable(WorldPoint origin) { - if (origin == null) { - return false; - } - - // Create a search area around the query point for robust matching - WorldArea queryArea = new WorldArea(origin, 3, 3); // 3x3 area around query point - - return getInstance().stream() - .filter(SpiritTreeData::isAvailableForTravel) - .map(data -> data.getSpiritTree().getLocation()) - .filter(Objects::nonNull) - .anyMatch(location -> { - // Create an area around each spirit tree location (accounting for multi-tile objects) - WorldArea spiritTreeArea = new WorldArea(location, 3, 3); // 3x3 area around spirit tree - // Check if the areas intersect (on same plane) - return queryArea.intersectsWith2D(spiritTreeArea) && - queryArea.getPlane() == spiritTreeArea.getPlane(); - }); - } - - /** - * Checks if a spirit tree is available as a destination for travel. - * This is an alias for isOriginAvailable() since any available origin can serve as a destination. - * - * @param destination The world point to check (where you want to travel TO) - * @return true if a spirit tree is available at this destination - */ - public static boolean isDestinationAvailable(WorldPoint destination) { - return isOriginAvailable(destination); - } - - /** - * Checks if a spirit tree transport is available for pathfinding. - * This method is specifically designed for pathfinder integration. - * Validates that the transport is of type SPIRIT_TREE and that both the origin and destination are available. - * - * @param transport The transport object to check - * @return true if the transport is a valid spirit tree and both ends are available for travel - */ - public static boolean isSpiritTreeTransportAvailable(Transport transport) { - if (transport == null) { - return false; - } - - if (transport.getType() != TransportType.SPIRIT_TREE) { - log.warn("Transport type {} is not SPIRIT_TREE, cannot check availability", transport.getType()); - return false; - } - - return isOriginAvailable(transport.getOrigin()) & isDestinationAvailable(transport.getDestination()); - } - - /** - * Gets the closest available spirit tree to a specific location. - * - * @param fromLocation The location to measure distance from - * @return Optional containing the closest available spirit tree data - */ - public static Optional getClosestAvailableTree(WorldPoint fromLocation) { - if (fromLocation == null) { - return Optional.empty(); - } - - return getInstance().stream() - .filter(data -> data.isAvailableForTravel()) - .filter(data -> data.getSpiritTree().getLocation() != null) - .min((data1, data2) -> { - int dist1 = data1.getSpiritTree().getLocation().distanceTo(fromLocation); - int dist2 = data2.getSpiritTree().getLocation().distanceTo(fromLocation); - return Integer.compare(dist1, dist2); - }); - } - - // ============================================ - // Static Update Methods - // ============================================ - - /** - * Static update method that loads and updates spirit tree cache based on FarmingHandler predictions. - * This method checks all spirit tree patches using FarmingHandler to get the most up-to-date information - * and updates the cache accordingly. It handles both initial cache population and updates based on - * current cached state, ensuring the most accurate information is preserved. - * - * This method should be called during cache initialization and when fresh farming data is needed. - * It uses the same logic as dynamic updates but provides static access for initial loading. - */ - @Override - public void update() { - try { - log.debug("Starting static update from FarmingHandler for spirit tree cache"); - - Rs2SpiritTreeCache cache = getInstance(); - - // Get current player context - WorldPoint playerLocation = getPlayerLocationSafely(); - Integer farmingLevel = getFarmingLevelSafely(); - - // Initialize update counters - int updatedCount = 0; - int newEntriesCount = 0; - int preservedCount = 0; - - // Process all spirit tree patches - for (SpiritTree spiritTree : SpiritTree.values()) { - try { - // Get existing cached data - SpiritTreeData existingData = getSpiritTreeData(spiritTree); - // Determine update strategy based on spiritTree type and existing data - SpiritTreeData updatedData = createUpdatedSpiritTreeData( - spiritTree, existingData, playerLocation, farmingLevel); - - // Update cache if we have new or updated data - if (updatedData != null) { - // Check if this is new data or an update - if (existingData == null) { - newEntriesCount++; - log.debug("Added new spirit tree data for {}: {}", - spiritTree.name(), getDataSummary(updatedData)); - } else if (!isDataEquivalent(existingData, updatedData)) { - updatedCount++; - log.debug("Updated spirit tree data for {}: {} -> {}", - spiritTree.name(), getDataSummary(existingData), getDataSummary(updatedData)); - } else { - preservedCount++; - log.debug("Preserved existing spirit tree data for {}: {}", - spiritTree.name(), getDataSummary(existingData)); - continue; // Skip update if data is equivalent - } - - cache.put(spiritTree, updatedData); - } else if (existingData != null) { - // Keep existing data if no new information is available - preservedCount++; - log.trace("Preserved existing spirit tree data for {} (no new information): {}", - spiritTree.name(), getDataSummary(existingData)); - } - - } catch (Exception e) { - log.warn("Failed to update spirit tree data for spiritTree {}: {}", spiritTree.name(), e.getMessage()); - } - } - List farambleTrees = getFarmableTreeStates(); - int availabilityFarmableTrees = (int) farambleTrees.stream() - .filter(SpiritTreeData::isAvailableForTravel) - .count(); - log.debug(getFarmingStatusSummary()); - log.debug("Static spirit tree cache update completed: \n\t{} new entries, {} updated, {} preserved entries, {} farmable trees (available for travel: {})", - newEntriesCount, updatedCount, preservedCount, farambleTrees.size(), availabilityFarmableTrees); - - } catch (Exception e) { - log.error("Failed to update spirit tree cache from FarmingHandler: {}", e.getMessage(), e); - } - } - - /** - * Creates updated spirit tree data based on current farming information and existing cache data. - * This method combines FarmingHandler predictions with existing cache data, - * preserving the most recent and accurate information. - */ - private static SpiritTreeData createUpdatedSpiritTreeData(SpiritTree spiritTree, - SpiritTreeData existingData, - WorldPoint playerLocation, - Integer farmingLevel) { - try { - // For built-in trees, check accessibility based on quest requirements - if (spiritTree.getType() == SpiritTree.SpiritTreeType.BUILT_IN) { - boolean accessible = spiritTree.hasQuestRequirements(); - // If we have existing data and it's recent, preserve travel availability info - if (existingData == null || existingData.isAvailableForTravel() != accessible) { - - // Update with quest accessibility but preserve recent travel information - return new SpiritTreeData( - spiritTree, - null, // Built-in trees don't have crop states - accessible, // Must be both accessible and available - playerLocation, - false, // Not detected via widget in static update - false // Not detected via game object in static update - ); - } - } - // For farmable trees, use FarmingHandler to predict state - else if (spiritTree.getType() == SpiritTree.SpiritTreeType.FARMABLE ) { - CropState predictedState = spiritTree.getPatchState(); // Uses Rs2Farming.predictPatchState internally - CropState lastCropState = existingData != null ? existingData.getCropState() : null; - if (predictedState != null && !predictedState.equals(lastCropState)) { - // Determine travel availability based on crop state - boolean detectedViaWidget = existingData != null && existingData.isDetectedViaWidget(); - boolean detectedViaNearPatch = existingData != null && existingData.isDetectedViaNearBy(); - boolean availableForTravelLast = existingData != null && existingData.isAvailableForTravel(); - boolean availableForTravel = spiritTree.isAvailableForTravel(); - if ((availableForTravel != availableForTravelLast) && - (lastCropState!=null && (lastCropState == CropState.UNCHECKED || lastCropState == CropState.GROWING))) { - log.info("Spirit tree {} is now available, last available for travel was false, and tree was predicted updating to true", spiritTree.name()); - // Use the more specific information: dynamic detection for travel, farming handler for crop state - return new SpiritTreeData( - spiritTree, - predictedState, // Always update with latest farming prediction - availableForTravel && farmingLevel >= 83, // Preserve recent dynamic travel detection - playerLocation, - false, - false - ); - - } else { - log.info("Spirit tree {} not updated, farm state: {}, available for travel last: {}, detected via widget: {}, detected via near patch: {}, last farming state: {}", - spiritTree.name(), predictedState, availableForTravelLast, detectedViaWidget, detectedViaNearPatch, lastCropState); - - } - - - } else { - // If FarmingHandler can't predict the state, preserve existing data if available - if (existingData != null) { - log.trace("No farming prediction available for {}, preserving existing data", spiritTree.name()); - return existingData; // Return existing data unchanged - } else { - log.trace("No farming prediction or existing data for {}, skipping", spiritTree.name()); - return null; // No data to work with - } - } - } - - return null; // Unknown spiritTree type or no data available - - } catch (Exception e) { - log.warn("Failed to create updated spirit tree data for {}: {}", spiritTree.name(), e.getMessage()); - return existingData; // Fallback to existing data on error - } - } - - /** - * Checks if two SpiritTreeData objects are equivalent for update purposes. - * This avoids unnecessary cache updates when data hasn't meaningfully changed. - */ - private static boolean isDataEquivalent(SpiritTreeData data1, SpiritTreeData data2) { - if (data1 == null || data2 == null) { - return data1 == data2; - } - - return data1.getSpiritTree().equals(data2.getSpiritTree()) && - java.util.Objects.equals(data1.getCropState(), data2.getCropState()) && - data1.isAvailableForTravel() == data2.isAvailableForTravel(); - } - - /** - * Creates a summary string for spirit tree data for logging purposes. - */ - private static String getDataSummary(SpiritTreeData data) { - if (data == null) { - return "null"; - } - - return String.format("[%s|%s|travel=%s]", - data.getSpiritTree().name(), - data.getCropState() != null ? data.getCropState().name() : "N/A", - data.isAvailableForTravel()); - } /** - * Forces a refresh of farmable spirit tree states only. - */ - public static void refreshFarmableStates() { - log.debug("Refreshing farmable spirit tree states"); - Rs2SpiritTreeCache.getInstance().update(); - } - - // ============================================ - // Cache Statistics and Monitoring - // ============================================ - - /** - * Gets statistics about the spirit tree cache for debugging. - * - * @return Formatted statistics string - */ - public static String getCacheStatistics() { - Rs2SpiritTreeCache cache = getInstance(); - - long totalEntries = cache.size(); - long availableForTravel = cache.stream().filter(SpiritTreeData::isAvailableForTravel).count(); - long farmableEntries = cache.stream() - .filter(data -> data.getSpiritTree().getType() == SpiritTree.SpiritTreeType.FARMABLE) - .count(); - long staleEntries = cache.stream() - .filter(data -> data.isStale(STALE_DATA_THRESHOLD)) - .count(); - - return String.format( - "SpiritTreeCache Stats: Total=%d, Available=%d, Farmable=%d, Stale=%d", - totalEntries, availableForTravel, farmableEntries, staleEntries - ); - } - - /** - * Logs the current state of all cached spirit trees for debugging. - */ - public static void logState(LogOutputMode mode) { - StringBuilder logContent = new StringBuilder(); - logContent.append("=== Spirit Tree Cache States ===\n"); - logContent.append(String.format("%-20s %-12s %-12s %-10s %-10s %-8s\n", - "Name", "Type", "CropState", "Available", "Updated", "Via")); - - getInstance().stream() - .sorted(Comparator.comparing(data -> data.getSpiritTree().name())) - .forEach(data -> { - String spiritTreeType = data.getSpiritTree().getType().name(); - String cropState = data.getCropState() != null ? data.getCropState().name() : "N/A"; - String lastUpdated = Instant.ofEpochMilli(data.getLastUpdated()) - .atZone(ZoneId.systemDefault()) - .format(DateTimeFormatter.ofPattern("HH:mm:ss")); - String detection = data.isDetectedViaWidget() ? "WIDGET" : - (data.isDetectedViaNearBy() ? "NEARBY" : "INIT"); - - logContent.append(String.format("%-20s %-12s %-12s %-10s %-10s %-8s\n", - data.getSpiritTree().name(), - spiritTreeType, - cropState, - data.isAvailableForTravel(), - lastUpdated, - detection - )); - }); - - logContent.append("=== End Spirit Tree Cache States ===\n"); - - Rs2CacheLoggingUtils.outputCacheLog( - "spirit_tree", - logContent.toString(), - mode - ); - } - - // ============================================ - // CacheSerializable Implementation - // ============================================ - - @Override - public String getConfigKey() { - return "spiritTrees"; - } - - @Override - public boolean shouldPersist() { - return true; - } - - // ============================================ - // Event Handling - // ============================================ - - - // ============================================ - // Event Handling Delegation to Update Strategy - // ============================================ - - - - /** - * Handle WidgetLoaded event and delegate to update strategy. - */ - @Subscribe - public void onWidgetLoaded(net.runelite.api.events.WidgetLoaded event) { - getInstance().handleEvent(event); - } - - /** - * Handle VarbitChanged event and delegate to update strategy. - */ - @Subscribe - public void onVarbitChanged(net.runelite.api.events.VarbitChanged event) { - getInstance().handleEvent(event); - } - - /** - * Handle GameObjectSpawned event and delegate to update strategy. - */ - @Subscribe - public void onGameObjectSpawned(net.runelite.api.events.GameObjectSpawned event) { - getInstance().handleEvent(event); - } - - /** - * Handle GameObjectSpawned event and delegate to update strategy. - */ - @Subscribe - public void onGameObjectDespawned(net.runelite.api.events.GameObjectDespawned event) { - getInstance().handleEvent(event); - } - - /** - * Handle game state changes for cache lifecycle management (unchanged). - */ - @Subscribe - public void onGameStateChanged(GameStateChanged event) { - getInstance().handleEvent(event); - } - - - // ============================================ - // Utility Methods - // ============================================ - - /** - * Validates that the spirit tree cache is properly initialized and functional. - * - * @return true if the cache is ready for use - */ - public static boolean isInitialized() { - try { - return instance != null; - } catch (Exception e) { - log.error("Error checking spirit tree cache initialization: {}", e.getMessage()); - return false; - } - } - - /** - * Gets a summary of spirit tree farming status for user display. - * - * @return Formatted farming status summary - */ - public static String getFarmingStatusSummary() { - List farmableStates = getFarmableTreeStates(); - - if (farmableStates.isEmpty()) { - return "No farmable spirit tree data available"; - } - - long planted = farmableStates.stream() - .filter(data -> data.getCropState() != CropState.EMPTY) - .count(); - long grown = farmableStates.stream() - .filter(data -> data.getCropState() == CropState.HARVESTABLE || - data.getCropState() == CropState.UNCHECKED) - .count(); - long readyForHarvest = farmableStates.stream() - .filter(data -> data.getCropState() == CropState.HARVESTABLE) - .count(); - long needsAttention = farmableStates.stream() - .filter(data -> data.getCropState() == CropState.DISEASED || - data.getCropState() == CropState.DEAD) - .count(); - - return String.format("Spirit Trees: %d/%d planted, %d grown (%d harvest ready), %d need attention", - planted, farmableStates.size(), grown, readyForHarvest, needsAttention); - } - - // ============================================ - // Private Utility Methods - // ============================================ - - /** - * Get current player location safely - */ - private static WorldPoint getPlayerLocationSafely() { - try { - if (Microbot.getClient() != null && - Microbot.getClient().getGameState() == GameState.LOGGED_IN && - Microbot.getClient().getLocalPlayer() != null) { - return Rs2Player.getWorldLocation(); - } - } catch (Exception e) { - log.trace("Could not get player location: {}", e.getMessage()); - } - return null; - } - - /** - * Get current farming level safely - */ - private static Integer getFarmingLevelSafely() { - try { - if (Microbot.getClient() != null && - Microbot.getClient().getGameState() == GameState.LOGGED_IN) { - return Rs2Player.getRealSkillLevel(Skill.FARMING); - } - } catch (Exception e) { - log.trace("Could not get farming level: {}", e.getMessage()); - } - return null; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2VarPlayerCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2VarPlayerCache.java deleted file mode 100644 index 8a63c5d0351..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2VarPlayerCache.java +++ /dev/null @@ -1,423 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.VarbitChanged; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.model.VarbitData; -import net.runelite.client.plugins.microbot.util.cache.serialization.CacheSerializable; -import net.runelite.client.plugins.microbot.util.cache.strategy.simple.VarPlayerUpdateStrategy; -import net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2CacheLoggingUtils; - -import java.util.Map; -import java.util.Optional; -import java.util.stream.Stream; - -/** - * Cache for varplayer (varp) values with enhanced tracking and contextual information. - * Provides thread-safe access to varplayer data with temporal tracking, change history, - * and contextual information about when and where values changed. - * - * Reuses VarbitData model since varplayer and varbit data structures are identical. - */ -@Slf4j -public class Rs2VarPlayerCache extends Rs2Cache implements CacheSerializable { - - private static volatile Rs2VarPlayerCache instance; - - /** - * Private constructor for singleton pattern. - */ - private Rs2VarPlayerCache() { - super("VarPlayerCache", CacheMode.EVENT_DRIVEN_ONLY); - - // Set up update strategy to handle VarbitChanged events for varplayer values - this.withUpdateStrategy(new VarPlayerUpdateStrategy()) - .withPersistence("varplayers"); - - log.debug("Rs2VarPlayerCache initialized with EVENT_DRIVEN_ONLY mode"); - } - - /** - * Gets the singleton instance of Rs2VarPlayerCache. - * - * @return The singleton instance - */ - public static Rs2VarPlayerCache getInstance() { - if (instance == null) { - synchronized (Rs2VarPlayerCache.class) { - if (instance == null) { - instance = new Rs2VarPlayerCache(); - } - } - } - return instance; - } - - /** - * Gets the cache instance for backward compatibility. - * - * @return The singleton cache instance - */ - public static Rs2Cache getCache() { - return getInstance(); - } - - /** - * Loads varplayer data from the client for a specific varp ID. - * - * @param varpId The varp ID to load data for - * @return The VarbitData containing the current value - */ - private static VarbitData loadVarPlayerDataFromClient(int varpId) { - try { - if (Microbot.getClient() == null) { - log.warn("Client is null when loading varp {}", varpId); - return new VarbitData(0); - } - - int value = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getClient().getVarpValue(varpId)).orElse(0); - - log.debug("Loaded varp from client: {} = {}", varpId, value); - return new VarbitData(value); - } catch (Exception e) { - log.error("Error loading varp {}: {}", varpId, e.getMessage(), e); - return new VarbitData(0); - } - } - - /** - * Gets the current value of a varplayer. - * If not cached, retrieves from client and caches the result. - * - * @param varpId The varplayer ID - * @return The current varplayer value, or 0 if not found - */ - public static int getVarPlayerValue(int varpId) { - try { - VarbitData cached = getInstance().get(varpId); - if (cached != null) { - return cached.getValue(); - } - - // Not cached, get from client and cache it - VarbitData freshData = loadVarPlayerDataFromClient(varpId); - getInstance().put(varpId, freshData); - return freshData.getValue(); - - } catch (Exception e) { - log.warn("Failed to get varplayer value for {}: {}", varpId, e.getMessage()); - return 0; - } - } - - /** - * Gets varplayer data with full contextual information. - * - * @param varpId The varplayer ID - * @return Optional containing the VarbitData, or empty if not found - */ - public static Optional getVarPlayerData(int varpId) { - return Optional.ofNullable(getInstance().get(varpId)); - } - - /** - * Gets varplayers that have changed within the specified number of ticks. - * - * @param ticks Number of ticks to look back - * @return Stream of varplayer data that changed recently - */ - public static Stream> getRecentlyChangedVarPlayers(int ticks) { - long cutoffTime = System.currentTimeMillis() - (ticks * 600); // 600ms per tick - return getInstance().entryStream() - .filter(entry -> entry.getValue().getLastUpdated() >= cutoffTime) - .filter(entry -> entry.getValue().hasValueChanged()); - } - - /** - * Gets varplayers with a specific value. - * - * @param value The value to search for - * @return Stream of varplayer IDs that have the specified value - */ - public static Stream getVarPlayersWithValue(int value) { - return getInstance().entryStream() - .filter(entry -> entry.getValue().getValue() == value) - .map(Map.Entry::getKey); - } - - /** - * Gets varplayers that changed at a specific location. - * - * @param x World X coordinate - * @param y World Y coordinate - * @param plane The plane - * @return Stream of varplayer data that changed at the specified location - */ - public static Stream> getVarPlayersChangedAt(int x, int y, int plane) { - return getInstance().entryStream() - .filter(entry -> { - VarbitData data = entry.getValue(); - return data.getPlayerLocation() != null && - data.getPlayerLocation().getX() == x && - data.getPlayerLocation().getY() == y && - data.getPlayerLocation().getPlane() == plane; - }); - } - - /** - * Checks if a varplayer has a specific value. - * - * @param varpId The varplayer ID - * @param value The value to check - * @return true if the varplayer has the specified value - */ - public static boolean hasValue(int varpId, int value) { - return getVarPlayerValue(varpId) == value; - } - - /** - * Checks if a varplayer has changed recently. - * - * @param varpId The varplayer ID - * @param ticks Number of ticks to look back - * @return true if the varplayer changed within the specified time - */ - public static boolean hasChangedRecently(int varpId, int ticks) { - VarbitData data = getInstance().get(varpId); - if (data == null) return false; - - long cutoffTime = System.currentTimeMillis() - (ticks * 600); - return data.getLastUpdated() >= cutoffTime && data.hasValueChanged(); - } - - /** - * Gets the previous value of a varplayer if available. - * - * @param varpId The varplayer ID - * @return The previous value, or null if not available - */ - public static Integer getPreviousValue(int varpId) { - VarbitData data = getInstance().get(varpId); - return data != null ? data.getPreviousValue() : null; - } - - /** - * Updates all cached varplayers by retrieving fresh data from the game client. - * This method iterates over currently cached varplayer IDs and refreshes their values. - */ - public static void updateAllFromClient() { - getInstance().update(); - } - - /** - * Updates all cached data by retrieving fresh values from the game client. - * Implements the abstract method from Rs2Cache. - * - * Iterates over all currently cached varplayer keys and refreshes their values from the client. - */ - @Override - public void update() { - log.debug("Updating all cached varplayers from client..."); - - if (Microbot.getClient() == null) { - log.warn("Cannot update varplayers - client is null"); - return; - } - - int beforeSize = size(); - int updatedCount = 0; - - // Get all currently cached varplayer IDs (keys) and update them - java.util.Set cachedVarpIds = entryStream() - .map(java.util.Map.Entry::getKey) - .collect(java.util.stream.Collectors.toSet()); - - for (Integer varpId : cachedVarpIds) { - try { - // Refresh the data from client using the private method - VarbitData freshData = loadVarPlayerDataFromClient(varpId); - if (freshData != null) { - put(varpId, freshData); - updatedCount++; - log.debug("Updated varp {} with fresh value: {}", varpId, freshData.getValue()); - } - } catch (Exception e) { - log.warn("Failed to update varp {}: {}", varpId, e.getMessage()); - } - } - - log.info("Updated {} varplayers from client (cache had {} entries total)", - updatedCount, beforeSize); - } - - // CacheSerializable implementation - @Override - public String getConfigKey() { - return "varPlayerCache"; - } - - @Override - public boolean shouldPersist() { - // Varplayer values can be persisted as they represent player state - return true; - } - - /** - * Clears all cached varplayer data. - * Useful for testing or when switching profiles. - */ - public static void clearCache() { - getInstance().invalidateAll(); - log.debug("VarPlayer cache cleared"); - } - - /** - * Gets cache statistics. - * - * @return String containing cache statistics - */ - public static String getCacheStats() { - Rs2VarPlayerCache cache = getInstance(); - return String.format("VarPlayerCache - Size: %d", cache.size()); - } - - // ============================================ - // EventBus Integration - // ============================================ - - /** - * Handles VarbitChanged events specifically for varplayer (varp) changes. - * Filters out varbit events and only processes varplayer events. - */ - @Subscribe - public void onVarbitChanged(VarbitChanged event) { - try { - // This is a varplayer event, handle it - getInstance().handleEvent(event); - // Ignore varbit events (handled by Rs2VarbitCache) - } catch (Exception e) { - log.error("Error handling VarbitChanged event for varplayer: {}", e.getMessage(), e); - } - } - - /** - * Handles GameStateChanged events for cache management. - */ - @Subscribe - public void onGameStateChanged(GameStateChanged event) { - try { - switch (event.getGameState()) { - case LOGGED_IN: - case HOPPING: - case LOGIN_SCREEN: - case CONNECTION_LOST: - // Let the strategy handle cache invalidation if needed - break; - default: - break; - } - } catch (Exception e) { - log.error("Error handling GameStateChanged event: {}", e.getMessage(), e); - } - } - - /** - * Logs the current state of all cached varplayers for debugging. - * - * @param dumpToFile Whether to also dump the information to a file - */ - public static void logState(LogOutputMode mode) { - var cache = getInstance(); - var stats = cache.getStatistics(); - - // Create the log content - StringBuilder logContent = new StringBuilder(); - - String header = String.format("=== VarPlayer Cache State (%d entries) ===", cache.size()); - logContent.append(header).append("\n"); - - String statsInfo = Rs2CacheLoggingUtils.formatCacheStatistics( - stats.getHitRate(), stats.cacheHits, stats.cacheMisses, stats.cacheMode.toString()); - logContent.append(statsInfo).append("\n\n"); - - if (cache.size() == 0) { - String emptyMsg = "Cache is empty"; - logContent.append(emptyMsg).append("\n"); - } else { - final int MAXNAME_LENGTH = 45; // Maximum length for names - // Table format for varplayers with VarPlayerID names where available - String[] headers = {"VarPlayer ID", "Name", "Value", "Previous", "Changed", "Last Updated", "Cache Timestamp"}; - int[] columnWidths = {12, MAXNAME_LENGTH, 8, 8, 8, 30, 22}; - - String tableHeader = Rs2CacheLoggingUtils.formatTableHeader(headers, columnWidths); - - logContent.append("\n").append(tableHeader); - - // Sort varplayers by recent changes (most recent first) - cache.entryStream() - .sorted((a, b) -> { - try { - // Handle null entries - if (a == null && b == null) return 0; - if (a == null) return 1; - if (b == null) return -1; - if (a.getValue() == null && b.getValue() == null) return 0; - if (a.getValue() == null) return 1; - if (b.getValue() == null) return -1; - - return Long.compare(b.getValue().getLastUpdated(), a.getValue().getLastUpdated()); - } catch (Exception e) { - // Fallback to key comparison if anything goes wrong - try { - return Integer.compare( - a != null && a.getKey() != null ? a.getKey() : Integer.MAX_VALUE, - b != null && b.getKey() != null ? b.getKey() : Integer.MAX_VALUE - ); - } catch (Exception e2) { - return 0; // Last resort - consider equal - } - } - }) - .forEach(entry -> { - Integer varPlayerId = entry.getKey(); - VarbitData varPlayerData = entry.getValue(); - - // Get cache timestamp for this varPlayer - Long cacheTimestamp = cache.getCacheTimestamp(varPlayerId); - String cacheTimestampStr = cacheTimestamp != null ? - Rs2Cache.formatUtcTimestamp(cacheTimestamp) : "N/A"; - - String varPlayerName = Rs2CacheLoggingUtils.getVarPlayerFieldName(varPlayerId); - String[] values = { - String.valueOf(varPlayerId), - Rs2CacheLoggingUtils.truncate(varPlayerName, MAXNAME_LENGTH), - String.valueOf(varPlayerData.getValue()), - varPlayerData.getPreviousValue() != null ? String.valueOf(varPlayerData.getPreviousValue()) : "null", - varPlayerData.hasValueChanged() ? "Yes" : "No", - Rs2CacheLoggingUtils.formatTimestamp(varPlayerData.getLastUpdated()), - Rs2CacheLoggingUtils.truncate(cacheTimestampStr, 21) - }; - - String row = Rs2CacheLoggingUtils.formatTableRow(values, columnWidths); - logContent.append(row); - }); - - String tableFooter = Rs2CacheLoggingUtils.formatTableFooter(columnWidths); - logContent.append(tableFooter); - - String limitMsg = Rs2CacheLoggingUtils.formatLimitMessage(cache.size(), 50); - if (!limitMsg.isEmpty()) { - logContent.append(limitMsg).append("\n"); - } - } - - String footer = "=== End VarPlayer Cache State ==="; - logContent.append(footer).append("\n"); - Rs2CacheLoggingUtils.outputCacheLog(getInstance().getCacheName(), logContent.toString(), mode); - - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2VarbitCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2VarbitCache.java deleted file mode 100644 index 80da75e25de..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2VarbitCache.java +++ /dev/null @@ -1,507 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.VarbitChanged; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.strategy.simple.VarbitUpdateStrategy; -import net.runelite.client.plugins.microbot.util.cache.serialization.CacheSerializable; -import net.runelite.client.plugins.microbot.util.cache.model.VarbitData; -import net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2CacheLoggingUtils; - -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; - -/** - * Thread-safe cache for varbit values using the unified cache architecture. - * Automatically updates when VarbitChanged events are received and supports persistence. - * Stores VarbitData with contextual information about when and where changes occurred. - * - * This class extends Rs2UnifiedCache and provides specific varbit caching functionality - * with proper EventBus integration for @Subscribe methods. - */ -@Slf4j -public class Rs2VarbitCache extends Rs2Cache implements CacheSerializable { - - private static Rs2VarbitCache instance; - - /** - * Private constructor for singleton pattern. - */ - private Rs2VarbitCache() { - super("VarbitCache", CacheMode.EVENT_DRIVEN_ONLY); - this.withUpdateStrategy(new VarbitUpdateStrategy()) - .withPersistence("varbits"); - } - - /** - * Gets the singleton instance of Rs2VarbitCache. - * - * @return The singleton varbit cache instance - */ - public static synchronized Rs2VarbitCache getInstance() { - if (instance == null) { - instance = new Rs2VarbitCache(); - } - return instance; - } - - /** - * Gets the cache instance for backward compatibility. - * - * @return The singleton unified cache instance - */ - public static Rs2Cache getCache() { - return getInstance(); - } - - public synchronized void close() { - if (instance != null) { - instance = null; - } - } - - /** - * Gets a varbit value from the cache or loads it from the client. - * - * @param varbitId The varbit ID to retrieve - * @return The varbit value - */ - public static int getVarbitValue(int varbitId) { - VarbitData data = getVarbitData(varbitId); - return data != null ? data.getValue() : 0; - } - - /** - * Loads varbit data from the client for a specific varbit ID. - * - * @param varbitId The varbit ID to load data for - * @return The VarbitData containing the current value - */ - private static VarbitData loadVarbitDataFromClient(int varbitId) { - try { - // Additional safety check - if (Microbot.getClient() == null) { - log.warn("Client is null when loading varbit {}", varbitId); - return new VarbitData(0); - } - - int value = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getClient().getVarbitValue(varbitId)).orElse(-1); - log.debug("Loaded varbit from client: {} = {}", varbitId, value); - return new VarbitData(value); - } catch (Exception e) { - log.error("Error loading varbit {}: {}", varbitId, e.getMessage(), e); - return new VarbitData(-1); - } - } - - /** - * Gets varbit data from the cache or loads it from the client. - * - * @param varbitId The varbit ID to retrieve - * @return The VarbitData containing value and contextual information - */ - public static VarbitData getVarbitData(int varbitId) { - // Validate input - if (varbitId < 0) { - log.warn("Invalid varbit ID: {}", varbitId); - return new VarbitData(0); - } - VarbitData cachedValue = getInstance().get(varbitId, () -> loadVarbitDataFromClient(varbitId)); - if (cachedValue == null) { - log.warn("Varbit {} not found in cache, returning default value", varbitId); - return new VarbitData(-1); - } - return cachedValue; - } - - - - - /** - * Manually updates a varbit value in the cache. - * - * @param varbitId The varbit ID to update - * @param value The new value - */ - public static void updateVarbitValue(int varbitId, int value) { - VarbitData oldData = getInstance().get(Integer.valueOf(varbitId)); - VarbitData newData = oldData != null ? - oldData.withUpdate(value, null, null, null) : - new VarbitData(value); - - getInstance().put(varbitId, newData); - log.debug("Updated varbit cache: {} = {}", varbitId, value); - } - - /** - * Manually updates varbit data in the cache. - * - * @param varbitId The varbit ID to update - * @param varbitData The new varbit data - */ - public static void updateVarbitData(int varbitId, VarbitData varbitData) { - getInstance().put(varbitId, varbitData); - log.debug("Updated varbit cache data: {} = {}", varbitId, varbitData.getValue()); - } - - /** - * Invalidates all varbit cache entries. - */ - public static void invalidateAllVarbits() { - getInstance().invalidateAll(); - log.debug("Invalidated all varbit cache entries"); - } - - /** - * Invalidates a specific varbit cache entry. - * - * @param varbitId The varbit ID to invalidate - */ - public static void invalidateVarbit(int varbitId) { - getInstance().remove(varbitId); - log.debug("Invalidated varbit cache entry: {}", varbitId); - } - - /** - * Updates all cached varbits by retrieving fresh data from the game client. - * This method iterates over all currently cached varbits and refreshes their data. - * Since varbits can have any ID, this only updates currently cached entries. - */ - public static void updateAllFromClient() { - getInstance().update(); - } - - /** - * Updates all cached data by retrieving fresh values from the game client. - * Implements the abstract method from Rs2Cache. - * - * Iterates over all currently cached varbit keys and refreshes their values from the client. - */ - @Override - public void update() { - log.debug("Updating all cached varbits from client..."); - - if (Microbot.getClient() == null) { - log.warn("Cannot update varbits - client is null"); - return; - } - - int beforeSize = size(); - int updatedCount = 0; - - // Get all currently cached varbit IDs (keys) and update them - java.util.Set cachedVarbitIds = entryStream() - .map(java.util.Map.Entry::getKey) - .collect(java.util.stream.Collectors.toSet()); - - for (Integer varbitId : cachedVarbitIds) { - try { - // Refresh the data from client using the private method - VarbitData freshData = loadVarbitDataFromClient(varbitId); - if (freshData != null) { - put(varbitId, freshData); - updatedCount++; - log.debug("Updated varbit {} with fresh value: {}", varbitId, freshData.getValue()); - } - } catch (Exception e) { - log.warn("Failed to update varbit {}: {}", varbitId, e.getMessage()); - } - } - - log.info("Updated {} varbits from client (cache had {} entries total)", - updatedCount, beforeSize); - } - - // ============================================ - // Legacy API Compatibility Methods - // ============================================ - - /** - * Gets varbit value - Legacy compatibility method (already available as getVarbitValue). - * - * @param varbitId The varbit ID - * @return The varbit value - */ - public static int get(int varbitId) { - return getVarbitValue(varbitId); - } - - - - /** - * Event handler registration for the unified cache. - * The unified cache handles events through its strategy automatically. - */ - - - @Subscribe - public void onVarbitChanged(VarbitChanged event) { - try { - getInstance().handleEvent(event); - } catch (Exception e) { - log.error("Error handling VarbitChanged event: {}", e.getMessage(), e); - } - } - @Subscribe - public void onGameStateChanged(GameStateChanged event) { - try { - switch (event.getGameState()) { - case LOGGED_IN: - case HOPPING: - case LOGIN_SCREEN: - case CONNECTION_LOST: - // Let the strategy handle cache invalidation - break; - default: - break; - } - } catch (Exception e) { - log.error("Error handling GameStateChanged event: {}", e.getMessage(), e); - } - } - - - /** - * Resets the singleton instance. Used for testing. - */ - public static synchronized void resetInstance() { - if (instance != null) { - invalidateAllVarbits(); - instance = null; - } - } - - // ============================================ - // CacheSerializable Implementation - // ============================================ - - @Override - public String getConfigKey() { - return "varbits"; - } - - @Override - public String getConfigGroup() { - return "microbot"; - } - - @Override - public boolean shouldPersist() { - return true; // Varbits should be persisted for game state tracking - } - - // ============================================ - // Print Functions for Cache Information - // ============================================ - - /** - * Returns a detailed formatted string containing all varbit cache information. - * Includes complete varbit data with contextual tracking and change information. - * - * @return Detailed multi-line string representation of all cached varbits - */ - public static String printDetailedVarbitInfo() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") - .withZone(ZoneId.systemDefault()); - - sb.append("=".repeat(80)).append("\n"); - sb.append(" DETAILED VARBIT CACHE INFORMATION\n"); - sb.append("=".repeat(80)).append("\n"); - - // Cache metadata - Rs2Cache cache = getInstance(); - sb.append(String.format("Cache Name: %s\n", cache.getCacheName())); - sb.append(String.format("Cache Mode: %s\n", cache.getCacheMode())); - sb.append(String.format("Total Cached Varbits: %d\n", cache.size())); - - // Cache statistics - var stats = cache.getStatistics(); - sb.append(String.format("Cache Hits: %d\n", stats.cacheHits)); - sb.append(String.format("Cache Misses: %d\n", stats.cacheMisses)); - sb.append(String.format("Hit Ratio: %.2f%%\n", stats.getHitRate() * 100)); - sb.append(String.format("Total Invalidations: %d\n", stats.totalInvalidations)); - sb.append(String.format("Uptime: %d ms\n", stats.uptime)); - sb.append(String.format("TTL: %d ms\n", stats.ttlMillis)); - sb.append("\n"); - - sb.append("-".repeat(80)).append("\n"); - sb.append(" VARBIT DETAILS\n"); - sb.append("-".repeat(80)).append("\n"); - - // Headers - sb.append(String.format("%-10s %-8s %-13s %-15s %-25s %-19s\n", - "VARBIT ID", "VALUE", "PREV VALUE", "CHANGED", "PLAYER LOCATION", "LAST UPDATED")); - sb.append("-".repeat(80)).append("\n"); - - // Get all varbit entries and sort by ID - cache.values().stream() - .filter(data -> data != null) - .sorted((data1, data2) -> { - // We need to find the keys for sorting, but we'll sort by last updated for now - return Long.compare(data2.getLastUpdated(), data1.getLastUpdated()); - }) - .forEach(data -> { - String lastUpdated = formatter.format(Instant.ofEpochMilli(data.getLastUpdated())); - String prevValue = data.getPreviousValue() != null ? - data.getPreviousValue().toString() : "-"; - String changed = data.hasValueChanged() ? "YES" : "-"; - String location = data.getPlayerLocation() != null ? - String.format("(%d,%d,%d)", - data.getPlayerLocation().getX(), - data.getPlayerLocation().getY(), - data.getPlayerLocation().getPlane()) : "-"; - - sb.append(String.format("%-10s %-8d %-13s %-15s %-25s %-19s\n", - "Unknown", // We don't have access to the varbit ID in this context - data.getValue(), - prevValue, - changed, - location, - lastUpdated)); - - // Additional contextual information - if (!data.getNearbyNpcIds().isEmpty() || !data.getNearbyObjectIds().isEmpty()) { - if (!data.getNearbyNpcIds().isEmpty()) { - sb.append(String.format(" └─ Nearby NPCs: %s\n", - data.getNearbyNpcIds().toString())); - } - if (!data.getNearbyObjectIds().isEmpty()) { - sb.append(String.format(" └─ Nearby Objects: %s\n", - data.getNearbyObjectIds().toString())); - } - } - }); - - sb.append("-".repeat(80)).append("\n"); - sb.append(String.format("Generated at: %s\n", formatter.format(Instant.now()))); - sb.append("=".repeat(80)); - - return sb.toString(); - } - - /** - * Returns a summary formatted string containing essential varbit cache information. - * Compact view showing key metrics and recent changes. - * - * @return Summary multi-line string representation of varbit cache - */ - public static String printVarbitSummary() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss"); - - sb.append("┌─ VARBIT CACHE SUMMARY ").append("─".repeat(44)).append("┐\n"); - - Rs2Cache cache = getInstance(); - var stats = cache.getStatistics(); - - // Summary statistics - sb.append(String.format("│ Varbits Cached: %-3d │ Hits: %-6d │ Hit Rate: %5.1f%% │\n", - cache.size(), stats.cacheHits, stats.getHitRate() * 100)); - - // Count changed varbits - long changedCount = cache.values().stream() - .filter(data -> data != null && data.hasValueChanged()) - .count(); - - sb.append(String.format("│ Changed Varbits: %-3d │ Total Changes: %-8d │\n", - changedCount, stats.totalInvalidations)); - - sb.append("├─ RECENT CHANGES ").append("─".repeat(46)).append("â”Ī\n"); - - // Show recent varbit changes - cache.values().stream() - .filter(data -> data != null && data.hasValueChanged()) - .sorted((data1, data2) -> Long.compare(data2.getLastUpdated(), data1.getLastUpdated())) - .forEach(data -> { - String timeStr = formatter.format(Instant.ofEpochMilli(data.getLastUpdated())); - sb.append(String.format("│ Varbit changed: %d → %d (%s) %-18s │\n", - data.getPreviousValue() != null ? data.getPreviousValue() : 0, - data.getValue(), - timeStr, "")); - }); - - if (changedCount == 0) { - sb.append("│ No recent varbit changes detected ").append(" ".repeat(28)).append("│\n"); - } - - sb.append("└").append("─".repeat(63)).append("┘"); - - return sb.toString(); - } - - /** - * Logs the current state of all cached varbits for debugging. - * - * @param dumpToFile Whether to also dump the information to a file - */ - public static void logState(LogOutputMode mode) { - var cache = getInstance(); - var stats = cache.getStatistics(); - - // Create the log content - StringBuilder logContent = new StringBuilder(); - - String header = String.format("=== Varbit Cache State (%d entries) ===", cache.size()); - logContent.append(header).append("\n"); - - String statsInfo = Rs2CacheLoggingUtils.formatCacheStatistics( - stats.getHitRate(), stats.cacheHits, stats.cacheMisses, stats.cacheMode.toString()); - logContent.append(statsInfo).append("\n\n"); - int maxRows = mode == LogOutputMode.CONSOLE_ONLY ? 50 : cache.size(); - if (cache.size() == 0) { - String emptyMsg = "Cache is empty"; - logContent.append(emptyMsg).append("\n"); - } else { - final int MAXNAME_LENGTH = 45; // Maximum length for names - // Table format for varbits with VarbitID names where available - String[] headers = {"Varbit ID", "Name", "Value", "Previous", "Changed", "Last Updated"}; - int[] columnWidths = {10, MAXNAME_LENGTH, 8, 8, 8, 30}; - - String tableHeader = Rs2CacheLoggingUtils.formatTableHeader(headers, columnWidths); - logContent.append("\n").append(tableHeader); - - // Sort varbits by recent changes (most recent first) - cache.entryStream() - .sorted((a, b) -> Long.compare(b.getValue().getLastUpdated(), a.getValue().getLastUpdated())) - .forEach(entry -> { - Integer varbitId = entry.getKey(); - VarbitData varbitData = entry.getValue(); - - String varbitName = Rs2CacheLoggingUtils.getVarbitFieldName(varbitId); - String[] values = { - String.valueOf(varbitId), - Rs2CacheLoggingUtils.truncate(varbitName, MAXNAME_LENGTH), - String.valueOf(varbitData.getValue()), - varbitData.getPreviousValue() != null ? String.valueOf(varbitData.getPreviousValue()) : "null", - varbitData.hasValueChanged() ? "Yes" : "No", - Rs2CacheLoggingUtils.formatTimestamp(varbitData.getLastUpdated()) - }; - - String row = Rs2CacheLoggingUtils.formatTableRow(values, columnWidths); - logContent.append(row); - }); - - String tableFooter = Rs2CacheLoggingUtils.formatTableFooter(columnWidths); - logContent.append(tableFooter); - - String limitMsg = Rs2CacheLoggingUtils.formatLimitMessage(cache.size(), maxRows); - if (!limitMsg.isEmpty()) { - logContent.append(limitMsg).append("\n"); - } - } - - String footer = "=== End Varbit Cache State ==="; - logContent.append(footer).append("\n"); - Rs2CacheLoggingUtils.outputCacheLog("varbit", logContent.toString(), mode); - } - - - - // ============================================ -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/SkillData.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/SkillData.java deleted file mode 100644 index ce86da985dd..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/SkillData.java +++ /dev/null @@ -1,90 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.model; - -import lombok.Data; - -/** - * Data structure to hold skill information with temporal tracking. - */ -@Data -public class SkillData { - - private final int level; - private final int boostedLevel; - private final int experience; - private final long lastUpdated; // Timestamp when this skill data was last updated - private final Integer previousLevel; // Previous level before the update (null if unknown) - private final Integer previousExperience; // Previous experience before the update (null if unknown) - - /** - * Creates a new SkillData instance with current timestamp. - * - * @param level The real (unboosted) skill level - * @param boostedLevel The current boosted skill level - * @param experience The skill experience - */ - public SkillData(int level, int boostedLevel, int experience) { - this(level, boostedLevel, experience, System.currentTimeMillis(), null, null); - } - - /** - * Creates a new SkillData instance with previous values for change tracking. - * - * @param level The real (unboosted) skill level - * @param boostedLevel The current boosted skill level - * @param experience The skill experience - * @param previousLevel The previous level (null if unknown) - * @param previousExperience The previous experience (null if unknown) - */ - public SkillData(int level, int boostedLevel, int experience, Integer previousLevel, Integer previousExperience) { - this(level, boostedLevel, experience, System.currentTimeMillis(), previousLevel, previousExperience); - } - - /** - * Creates a new SkillData instance with full temporal tracking. - * - * @param level The real (unboosted) skill level - * @param boostedLevel The current boosted skill level - * @param experience The skill experience - * @param lastUpdated Timestamp when this data was created/updated - * @param previousLevel The previous level (null if unknown) - * @param previousExperience The previous experience (null if unknown) - */ - public SkillData(int level, int boostedLevel, int experience, long lastUpdated, Integer previousLevel, Integer previousExperience) { - this.level = level; - this.boostedLevel = boostedLevel; - this.experience = experience; - this.lastUpdated = lastUpdated; - this.previousLevel = previousLevel; - this.previousExperience = previousExperience; - } - - /** - * Creates a new SkillData with updated values while preserving previous state. - * - * @param newLevel The new skill level - * @param newBoostedLevel The new boosted level - * @param newExperience The new experience - * @return A new SkillData instance with the current values as previous values - */ - public SkillData withUpdate(int newLevel, int newBoostedLevel, int newExperience) { - return new SkillData(newLevel, newBoostedLevel, newExperience, this.level, this.experience); - } - - /** - * Checks if this skill data represents a level increase. - * - * @return true if the level increased from the previous value - */ - public boolean isLevelUp() { - return previousLevel != null && level > previousLevel; - } - - /** - * Gets the experience gained since the last update. - * - * @return the experience difference, or 0 if no previous experience - */ - public int getExperienceGained() { - return previousExperience != null ? experience - previousExperience : 0; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/SpiritTreeData.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/SpiritTreeData.java deleted file mode 100644 index 2025be7765e..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/SpiritTreeData.java +++ /dev/null @@ -1,226 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.model; - -import lombok.Data; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.questhelper.helpers.mischelpers.farmruns.CropState; -import net.runelite.client.plugins.microbot.util.farming.SpiritTree; - -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; - -/** - * Data structure to hold spirit tree farming patch information with contextual and temporal tracking. - * Includes information about patch state, travel availability, when last detected, - * and detection method tracking. - */ -@Data -public class SpiritTreeData { - - private final SpiritTree spiritTree; - private final CropState cropState; - private final boolean availableForTravel; - private final long lastUpdated; // UTC timestamp when this state was last detected - private final WorldPoint playerLocation; // Player location when the state was detected - private final boolean detectedViaWidget; // Whether this state was detected via spirit tree widget - private final boolean detectedViaNearBy; // Whether this state was detected via varbit when near by - - /** - * Creates a new SpiritTreeData instance with current timestamp and minimal context. - * - * @param spiritTree The spirit tree patch - * @param cropState The current crop state (null for built-in trees) - * @param availableForTravel Whether the tree is available for travel - */ - public SpiritTreeData(SpiritTree spiritTree, CropState cropState, boolean availableForTravel) { - this(spiritTree, cropState, availableForTravel, System.currentTimeMillis(), null, - false, false); - } - - /** - * Creates a new SpiritTreeData instance with detection method tracking. - * - * @param spiritTree The spirit tree patch - * @param cropState The current crop state (null for built-in trees) - * @param availableForTravel Whether the tree is available for travel - * @param detectedViaWidget Whether detected via spirit tree widget - * @param detectedViaNearBy Whether detected via varbit when near by - */ - public SpiritTreeData(SpiritTree spiritTree, CropState cropState, boolean availableForTravel, - boolean detectedViaWidget, boolean detectedViaNearBy) { - this(spiritTree, cropState, availableForTravel, System.currentTimeMillis(), null, - detectedViaWidget, detectedViaNearBy); - } - - /** - * Creates a new SpiritTreeData instance with contextual information. - * - * @param spiritTree The spirit tree patch - * @param cropState The current crop state (null for built-in trees) - * @param availableForTravel Whether the tree is available for travel - * @param playerLocation The player's world location when detected - * @param detectedViaWidget Whether detected via spirit tree widget - * @param detectedViaNearBy Whether detected via varbit when near by - */ - public SpiritTreeData(SpiritTree spiritTree, CropState cropState, boolean availableForTravel, - WorldPoint playerLocation, boolean detectedViaWidget, boolean detectedViaNearBy) { - this(spiritTree, cropState, availableForTravel, System.currentTimeMillis(), playerLocation, - detectedViaWidget, detectedViaNearBy); - } - - /** - * Creates a new SpiritTreeData instance with full temporal and contextual tracking. - * - * @param spiritTree The spirit tree patch - * @param cropState The current crop state (null for built-in trees) - * @param availableForTravel Whether the tree is available for travel - * @param lastUpdated UTC timestamp when this data was created/updated - * @param playerLocation The player's world location when detected - * @param detectedViaWidget Whether detected via spirit tree widget - * @param detectedViaNearBy Whether detected via varbit when near by - */ - public SpiritTreeData(SpiritTree spiritTree, CropState cropState, boolean availableForTravel, long lastUpdated, - WorldPoint playerLocation, boolean detectedViaWidget, boolean detectedViaNearBy) { - this.spiritTree = spiritTree; - this.cropState = cropState; - this.availableForTravel = availableForTravel; - this.lastUpdated = lastUpdated; - this.playerLocation = playerLocation; - this.detectedViaWidget = detectedViaWidget; - this.detectedViaNearBy = detectedViaNearBy; - } - - /** - * Creates a new SpiritTreeData with updated availability while preserving other data. - * - * @param newAvailability The new travel availability status - * @param detectedViaWidget Whether detected via widget - * @param detectedViaNearBy Whether detected via varbit when near by - * @param playerLocation Current player location - * @return A new SpiritTreeData instance with updated availability - */ - public SpiritTreeData withUpdatedAvailability(boolean newAvailability, boolean detectedViaWidget, - boolean detectedViaNearBy, WorldPoint playerLocation) { - return new SpiritTreeData(this.spiritTree, this.cropState, newAvailability, playerLocation, - detectedViaWidget, detectedViaNearBy); - } - - /** - * Creates a new SpiritTreeData with updated crop state. - * - * @param newCropState The new crop state - * @param playerLocation Current player location - * @return A new SpiritTreeData instance with updated crop state - */ - public SpiritTreeData withUpdatedCropState(CropState newCropState, WorldPoint playerLocation) { - // Update availability based on new crop state - boolean newAvailability = isAvailableBasedOnCropState(newCropState); - - return new SpiritTreeData(this.spiritTree, newCropState, newAvailability, playerLocation, - false, true); - } - - /** - * Determines travel availability based on crop state. - * - * @param cropState The crop state to evaluate - * @return true if the tree should be available for travel - */ - private boolean isAvailableBasedOnCropState(CropState cropState) { - if (cropState == null) { - return true; // Built-in trees are always available if quest requirements are met - } - - // Farmable trees are available when healthy and grown - return cropState == CropState.HARVESTABLE || cropState == CropState.UNCHECKED; - } - - /** - * Checks if this spirit tree data represents a travel availability change. - * - * @param previousData The previous data to compare against - * @return true if travel availability changed - */ - public boolean hasAvailabilityChanged(SpiritTreeData previousData) { - if (previousData == null) { - return true; // First detection is considered a change - } - return this.availableForTravel != previousData.availableForTravel; - } - - /** - * Checks if this spirit tree data represents a crop state change. - * - * @param previousData The previous data to compare against - * @return true if crop state changed - */ - public boolean hasCropStateChanged(SpiritTreeData previousData) { - if (previousData == null) { - return this.cropState != null; // First detection of farmable tree - } - - if (this.cropState == null && previousData.cropState == null) { - return false; // Both are built-in trees - } - - if (this.cropState == null || previousData.cropState == null) { - return true; // One is built-in, other is farmable - } - - return !this.cropState.equals(previousData.cropState); - } - - /** - * Checks if this detection occurred at a specific location. - * - * @param location The location to check - * @return true if the detection occurred at the specified location - */ - public boolean occurredAt(WorldPoint location) { - return playerLocation != null && playerLocation.distanceTo(location)<10; // Allow some tolerance - } - - /** - * Gets a human-readable timestamp of when this was last updated. - * - * @return Formatted timestamp string - */ - public String getFormattedLastUpdated() { - return Instant.ofEpochMilli(lastUpdated) - .atZone(ZoneId.systemDefault()) - .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); - } - - /** - * Gets the age of this data in milliseconds. - * - * @return Age in milliseconds since last update - */ - public long getAgeMillis() { - return System.currentTimeMillis() - lastUpdated; - } - - /** - * Checks if this data is considered stale based on age. - * - * @param maxAgeMillis Maximum acceptable age in milliseconds - * @return true if the data is older than the specified age - */ - public boolean isStale(long maxAgeMillis) { - return getAgeMillis() > maxAgeMillis; - } - - /** - * Gets a summary string for debugging purposes. - * - * @return Summary string containing key information - */ - public String getSummary() { - return String.format("SpiritTreeData{spiritTree=%s, state=%s, available=%s, age=%dms, via=%s}", - spiritTree.name(), - cropState != null ? cropState.name() : "BUILT_IN", - availableForTravel, - getAgeMillis(), - detectedViaWidget ? "WIDGET" : (detectedViaNearBy ? "NEAR_BY" : "UNKNOWN")); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/VarbitData.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/VarbitData.java deleted file mode 100644 index a5deded438e..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/VarbitData.java +++ /dev/null @@ -1,138 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.model; - -import lombok.Data; -import net.runelite.api.coords.WorldPoint; - -import java.util.Collections; -import java.util.List; - -/** - * Data structure to hold varbit information with contextual and temporal tracking. - * Includes information about when the varbit changed, where the player was, - * and what entities were nearby when the change occurred. - */ -@Data -public class VarbitData { - - private final int value; - private final long lastUpdated; // Timestamp when this varbit was last updated - private final Integer previousValue; // Previous value before the update (null if unknown) - private final WorldPoint playerLocation; // Player location when the varbit changed - private final List nearbyNpcIds; // NPC IDs that were nearby when the change occurred - private final List nearbyObjectIds; // Object IDs that were nearby when the change occurred - - /** - * Creates a new VarbitData instance with current timestamp and no context. - * - * @param value The varbit value - */ - public VarbitData(int value) { - this(value, System.currentTimeMillis(), null, null, Collections.emptyList(), Collections.emptyList()); - } - - /** - * Creates a new VarbitData instance with previous value tracking. - * - * @param value The varbit value - * @param previousValue The previous value (null if unknown) - */ - public VarbitData(int value, Integer previousValue) { - this(value, System.currentTimeMillis(), previousValue, null, Collections.emptyList(), Collections.emptyList()); - } - - /** - * Creates a new VarbitData instance with full contextual information. - * - * @param value The varbit value - * @param previousValue The previous value (null if unknown) - * @param playerLocation The player's world location when the change occurred - * @param nearbyNpcIds List of nearby NPC IDs - * @param nearbyObjectIds List of nearby object IDs - */ - public VarbitData(int value, Integer previousValue, WorldPoint playerLocation, - List nearbyNpcIds, List nearbyObjectIds) { - this(value, System.currentTimeMillis(), previousValue, playerLocation, nearbyNpcIds, nearbyObjectIds); - } - - /** - * Creates a new VarbitData instance with full temporal and contextual tracking. - * - * @param value The varbit value - * @param lastUpdated Timestamp when this data was created/updated - * @param previousValue The previous value (null if unknown) - * @param playerLocation The player's world location when the change occurred - * @param nearbyNpcIds List of nearby NPC IDs - * @param nearbyObjectIds List of nearby object IDs - */ - public VarbitData(int value, long lastUpdated, Integer previousValue, WorldPoint playerLocation, - List nearbyNpcIds, List nearbyObjectIds) { - this.value = value; - this.lastUpdated = lastUpdated; - this.previousValue = previousValue; - this.playerLocation = playerLocation; - this.nearbyNpcIds = nearbyNpcIds != null ? Collections.unmodifiableList(nearbyNpcIds) : Collections.emptyList(); - this.nearbyObjectIds = nearbyObjectIds != null ? Collections.unmodifiableList(nearbyObjectIds) : Collections.emptyList(); - } - - /** - * Creates a new VarbitData with updated value while preserving previous state. - * - * @param newValue The new varbit value - * @param playerLocation The player's current location - * @param nearbyNpcIds List of nearby NPC IDs - * @param nearbyObjectIds List of nearby object IDs - * @return A new VarbitData instance with the current value as previous value - */ - public VarbitData withUpdate(int newValue, WorldPoint playerLocation, - List nearbyNpcIds, List nearbyObjectIds) { - return new VarbitData(newValue, this.value, playerLocation, nearbyNpcIds, nearbyObjectIds); - } - - /** - * Checks if this varbit data represents a value change. - * - * @return true if the value changed from the previous value - */ - public boolean hasValueChanged() { - return previousValue != null && value != previousValue; - } - - /** - * Gets the change in value since the last update. - * - * @return the value difference, or 0 if no previous value - */ - public int getValueChange() { - return previousValue != null ? value - previousValue : 0; - } - - /** - * Checks if this varbit change occurred at a specific location. - * - * @param location The location to check - * @return true if the change occurred at the specified location - */ - public boolean occurredAt(WorldPoint location) { - return playerLocation != null && playerLocation.distanceTo(location)<10; // Allow some tolerance - } - - /** - * Checks if a specific NPC was nearby when this varbit changed. - * - * @param npcId The NPC ID to check for - * @return true if the NPC was nearby during the change - */ - public boolean hadNearbyNpc(int npcId) { - return nearbyNpcIds.contains(npcId); - } - - /** - * Checks if a specific object was nearby when this varbit changed. - * - * @param objectId The object ID to check for - * @return true if the object was nearby during the change - */ - public boolean hadNearbyObject(int objectId) { - return nearbyObjectIds.contains(objectId); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/HoverInfoContainer.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/HoverInfoContainer.java deleted file mode 100644 index 80fa781ffce..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/HoverInfoContainer.java +++ /dev/null @@ -1,142 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.overlay; - -import net.runelite.api.Point; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.ui.overlay.OverlayManager; - -import java.awt.Color; -import java.util.List; - -/** - * Container for hover information shared between cache overlays and the top-level info box overlay. - * This allows info boxes to appear on top of all other overlays. - * - * @author Vox - * @version 1.0 - */ -public class HoverInfoContainer { - private static volatile HoverInfo currentHoverInfo = null; - - /** - * Sets the current hover information. - * - * @param info The hover information to display, or null to clear - */ - public static void setHoverInfo(HoverInfo info) { - currentHoverInfo = info; - } - - /** - * Gets the current hover information. - * - * @return The current hover info or null if none - */ - public static HoverInfo getCurrentHoverInfo() { - return currentHoverInfo; - } - - /** - * Clears the current hover information. - */ - public static void clearHoverInfo() { - currentHoverInfo = null; - } - - /** - * Checks if there is currently hover information available. - * - * @return true if hover info is available - */ - public static boolean hasHoverInfo() { - return currentHoverInfo != null; - } - - /** - * Container for hover information data. - */ - public static class HoverInfo { - private final List infoLines; - private final Point location; - private final Color borderColor; - private final String entityType; - private final long creationTime; - - public HoverInfo(List infoLines, Point location, Color borderColor, String entityType) { - this.infoLines = infoLines; - this.location = location; - this.borderColor = borderColor; - this.entityType = entityType; - this.creationTime = System.currentTimeMillis(); - } - - public List getInfoLines() { - return infoLines; - } - - public Point getLocation() { - return location; - } - - public Color getBorderColor() { - return borderColor; - } - - public String getEntityType() { - return entityType; - } - - public long getCreationTime() { - return creationTime; - } - - /** - * Checks if this hover info is still fresh (not too old). - * - * @param maxAgeMs Maximum age in milliseconds - * @return true if the info is still fresh - */ - public boolean isFresh(long maxAgeMs) { - return (System.currentTimeMillis() - creationTime) <= maxAgeMs; - } - } - - // Static overlay management - private static Rs2CacheInfoBoxOverlay infoBoxOverlay; - private static boolean overlayRegistered = false; - - /** - * Manually registers the info box overlay with an overlay manager. - * This should be called by overlay managers or plugins that use the hover system. - * - * @param overlayManager The overlay manager to register with - */ - public static void registerInfoBoxOverlay(OverlayManager overlayManager) { - if (infoBoxOverlay == null && Microbot.getClient() != null) { - infoBoxOverlay = new Rs2CacheInfoBoxOverlay(Microbot.getClient()); - overlayManager.add(infoBoxOverlay); - overlayRegistered = true; - } - } - - /** - * Manually unregisters the info box overlay from an overlay manager. - * - * @param overlayManager The overlay manager to unregister from - */ - public static void unregisterInfoBoxOverlay(OverlayManager overlayManager) { - if (infoBoxOverlay != null && overlayRegistered) { - overlayManager.remove(infoBoxOverlay); - infoBoxOverlay = null; - overlayRegistered = false; - } - } - - /** - * Checks if the info box overlay is currently registered. - * - * @return true if registered - */ - public static boolean isInfoBoxOverlayRegistered() { - return overlayRegistered && infoBoxOverlay != null; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2BaseCacheOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2BaseCacheOverlay.java deleted file mode 100644 index acede59870c..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2BaseCacheOverlay.java +++ /dev/null @@ -1,215 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.overlay; - -import net.runelite.api.Client; -import net.runelite.client.plugins.microbot.Microbot; -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 net.runelite.client.ui.overlay.outline.ModelOutlineRenderer; - -import java.awt.*; - -/** - * Base overlay class for cache-based entity rendering. - * Provides common rendering utilities and setup for cache overlays. - * - * @author Vox - * @version 1.0 - */ -public abstract class Rs2BaseCacheOverlay extends Overlay { - - protected final Client client; - protected final ModelOutlineRenderer modelOutlineRenderer; - - // Default rendering settings (now abstract - overridden in subclasses) - protected static final float DEFAULT_BORDER_WIDTH = 2.0f; - - /** - * Gets the default border color for this entity type. - * @return The default border color - */ - protected abstract Color getDefaultBorderColor(); - - /** - * Gets the default fill color for this entity type. - * @return The default fill color - */ - protected abstract Color getDefaultFillColor(); - - public Rs2BaseCacheOverlay(Client client, ModelOutlineRenderer modelOutlineRenderer) { - this.client = client; - this.modelOutlineRenderer = modelOutlineRenderer; - setPosition(OverlayPosition.DYNAMIC); - setLayer(OverlayLayer.ABOVE_SCENE); - } - - /** - * Renders a polygon with border and fill colors. - * - * @param graphics The graphics context - * @param shape The shape to render - * @param borderColor The border color - * @param fillColor The fill color - * @param borderWidth The border width - */ - protected void renderShape(Graphics2D graphics, Shape shape, Color borderColor, Color fillColor, float borderWidth) { - if (shape == null) { - return; - } - - // Set stroke - graphics.setStroke(new BasicStroke(borderWidth)); - - // Draw border - graphics.setColor(borderColor); - graphics.draw(shape); - - // Fill shape - if (fillColor != null) { - graphics.setColor(fillColor); - graphics.fill(shape); - } - } - - /** - * Renders a polygon using OverlayUtil for consistency with RuneLite. - * - * @param graphics The graphics context - * @param shape The shape to render - * @param borderColor The border color - * @param fillColor The fill color - * @param stroke The stroke to use - */ - protected void renderPolygon(Graphics2D graphics, Shape shape, Color borderColor, Color fillColor, Stroke stroke) { - if (shape != null) { - OverlayUtil.renderPolygon(graphics, shape, borderColor, fillColor, stroke); - } - } - - /** - * Renders text at a specific location. - * - * @param graphics The graphics context - * @param text The text to render - * @param point The location to render at - * @param color The text color - */ - protected void renderText(Graphics2D graphics, String text, net.runelite.api.Point point, Color color) { - if (point != null && text != null && !text.isEmpty()) { - OverlayUtil.renderTextLocation(graphics, point, text, color); - } - } - - /** - * Checks if the client is available and ready for rendering. - * - * @return true if client is ready - */ - protected boolean isClientReady() { - return Microbot.isLoggedIn() && client != null; - } - - /** - * Gets a color with modified alpha for fill colors. - * - * @param baseColor The base color - * @param alpha The alpha value (0-255) - * @return Color with modified alpha - */ - protected Color withAlpha(Color baseColor, int alpha) { - return new Color(baseColor.getRed(), baseColor.getGreen(), baseColor.getBlue(), alpha); - } - - // ============================================ - // Enhanced Utility Methods - // ============================================ - - /** - * Checks if an entity is within the viewport bounds. - * Uses a simple canvas bounds check as a fast pre-filter. - * - * @param canvasPoint The canvas point to check - * @return true if the point is within viewport bounds - */ - protected boolean isWithinViewportBounds(net.runelite.api.Point canvasPoint) { - if (canvasPoint == null || client == null) { - return false; - } - - // Get canvas dimensions (viewport size) - int canvasWidth = client.getCanvasWidth(); - int canvasHeight = client.getCanvasHeight(); - - // Check if point is within bounds with some margin - return canvasPoint.getX() >= -50 && canvasPoint.getX() <= canvasWidth + 50 && - canvasPoint.getY() >= -50 && canvasPoint.getY() <= canvasHeight + 50; - } - - /** - * Renders a shape with enhanced visual effects. - * - * @param graphics The graphics context - * @param shape The shape to render - * @param borderColor The border color - * @param fillColor The fill color - * @param borderWidth The border width - * @param dashedBorder Whether to use a dashed border - */ - protected void renderShapeEnhanced(Graphics2D graphics, Shape shape, Color borderColor, - Color fillColor, float borderWidth, boolean dashedBorder) { - if (shape == null) { - return; - } - - // Set stroke (dashed or solid) - if (dashedBorder) { - float[] dash = {5.0f, 5.0f}; - graphics.setStroke(new BasicStroke(borderWidth, BasicStroke.CAP_ROUND, - BasicStroke.JOIN_ROUND, 1.0f, dash, 0.0f)); - } else { - graphics.setStroke(new BasicStroke(borderWidth)); - } - - // Fill shape first - if (fillColor != null) { - graphics.setColor(fillColor); - graphics.fill(shape); - } - - // Draw border - graphics.setColor(borderColor); - graphics.draw(shape); - } - - /** - * Renders text with a background for better visibility. - * - * @param graphics The graphics context - * @param text The text to render - * @param point The location to render at - * @param textColor The text color - * @param backgroundColor The background color (null for no background) - */ - protected void renderTextWithBackground(Graphics2D graphics, String text, net.runelite.api.Point point, - Color textColor, Color backgroundColor) { - if (point == null || text == null || text.isEmpty()) { - return; - } - - if (backgroundColor != null) { - // Calculate text bounds for background - FontMetrics metrics = graphics.getFontMetrics(); - int textWidth = metrics.stringWidth(text); - int textHeight = metrics.getHeight(); - - // Draw background rectangle - graphics.setColor(backgroundColor); - graphics.fillRect(point.getX() - 2, point.getY() - textHeight + 2, - textWidth + 4, textHeight); - } - - // Render the text - renderText(graphics, text, point, textColor); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2CacheInfoBoxOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2CacheInfoBoxOverlay.java deleted file mode 100644 index 81746f48b81..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2CacheInfoBoxOverlay.java +++ /dev/null @@ -1,154 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.overlay; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Client; -import net.runelite.client.ui.overlay.Overlay; -import net.runelite.client.ui.overlay.OverlayLayer; -import net.runelite.client.ui.overlay.OverlayPosition; - -import java.awt.*; -import java.util.List; - -/** - * Top-level overlay for displaying hover information boxes. - * This overlay uses ALWAYS_ON_TOP layer to ensure info boxes appear above all other overlays. - * - * @author Vox - * @version 1.0 - */ -@Slf4j -public class Rs2CacheInfoBoxOverlay extends Overlay { - - private final Client client; - - // Info box styling - private static final Color INFO_BOX_BACKGROUND = new Color(0, 0, 0, 180); // Semi-transparent black - private static final Color INFO_BOX_BORDER = new Color(255, 255, 255, 200); // Semi-transparent white - private static final Color INFO_TEXT_COLOR = Color.WHITE; - private static final int INFO_BOX_PADDING = 6; - private static final int INFO_BOX_LINE_SPACING = 2; - private static final long MAX_HOVER_INFO_AGE_MS = 100; // 100ms max age for hover info - - public Rs2CacheInfoBoxOverlay(Client client) { - this.client = client; - setPosition(OverlayPosition.DYNAMIC); - setLayer(OverlayLayer.ALWAYS_ON_TOP); // Ensure this appears on top of everything - } - - @Override - public Dimension render(Graphics2D graphics) { - if (!isClientReady()) { - return null; - } - - // Get current hover information - HoverInfoContainer.HoverInfo hoverInfo = HoverInfoContainer.getCurrentHoverInfo(); - if (hoverInfo == null || !hoverInfo.isFresh(MAX_HOVER_INFO_AGE_MS)) { - // Clear stale hover info - if (hoverInfo != null && !hoverInfo.isFresh(MAX_HOVER_INFO_AGE_MS)) { - HoverInfoContainer.clearHoverInfo(); - } - return null; - } - - try { - renderInfoBox(graphics, hoverInfo); - } catch (Exception e) { - log.warn("Error rendering cache info box overlay: {}", e.getMessage()); - } - - return null; - } - - /** - * Renders the info box with hover information. - * - * @param graphics The graphics context - * @param hoverInfo The hover information to display - */ - private void renderInfoBox(Graphics2D graphics, HoverInfoContainer.HoverInfo hoverInfo) { - List infoLines = hoverInfo.getInfoLines(); - if (infoLines.isEmpty()) { - return; - } - - // Set font for measurements - Font originalFont = graphics.getFont(); - Font infoFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12); - graphics.setFont(infoFont); - - FontMetrics fontMetrics = graphics.getFontMetrics(); - int lineHeight = fontMetrics.getHeight(); - - // Calculate info box dimensions - int maxTextWidth = infoLines.stream() - .mapToInt(fontMetrics::stringWidth) - .max() - .orElse(0); - - int infoBoxWidth = maxTextWidth + (INFO_BOX_PADDING * 2); - int infoBoxHeight = (infoLines.size() * lineHeight) + - ((infoLines.size() - 1) * INFO_BOX_LINE_SPACING) + - (INFO_BOX_PADDING * 2); - - // Calculate position (offset from hover location to avoid overlapping with cursor) - net.runelite.api.Point hoverLocation = hoverInfo.getLocation(); - int infoBoxX = hoverLocation.getX() + 15; // Offset right - int infoBoxY = hoverLocation.getY() - infoBoxHeight - 10; // Offset up - - // Ensure info box stays within viewport bounds - int viewportWidth = client.getCanvasWidth(); - int viewportHeight = client.getCanvasHeight(); - - // Adjust X position if too far right - if (infoBoxX + infoBoxWidth > viewportWidth) { - infoBoxX = hoverLocation.getX() - infoBoxWidth - 15; // Move to left side - } - - // Adjust Y position if too high - if (infoBoxY < 0) { - infoBoxY = hoverLocation.getY() + 25; // Move below cursor - } - - // Ensure final position is still within bounds - infoBoxX = Math.max(0, Math.min(infoBoxX, viewportWidth - infoBoxWidth)); - infoBoxY = Math.max(0, Math.min(infoBoxY, viewportHeight - infoBoxHeight)); - - // Draw info box background - graphics.setColor(INFO_BOX_BACKGROUND); - graphics.fillRoundRect(infoBoxX, infoBoxY, infoBoxWidth, infoBoxHeight, 6, 6); - - // Draw info box border using entity color - Color borderColor = hoverInfo.getBorderColor(); - if (borderColor != null) { - graphics.setColor(borderColor); - } else { - graphics.setColor(INFO_BOX_BORDER); - } - graphics.setStroke(new BasicStroke(2.0f)); - graphics.drawRoundRect(infoBoxX, infoBoxY, infoBoxWidth, infoBoxHeight, 6, 6); - - // Draw info lines - graphics.setColor(INFO_TEXT_COLOR); - int currentY = infoBoxY + INFO_BOX_PADDING + fontMetrics.getAscent(); - - for (String line : infoLines) { - graphics.drawString(line, infoBoxX + INFO_BOX_PADDING, currentY); - currentY += lineHeight + INFO_BOX_LINE_SPACING; - } - - // Restore original font - graphics.setFont(originalFont); - } - - /** - * Checks if the client is ready for rendering. - * - * @return true if ready - */ - private boolean isClientReady() { - return client != null && - client.getGameState() != null && - client.getLocalPlayer() != null; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2CacheOverlayManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2CacheOverlayManager.java deleted file mode 100644 index 5813ff3ab9f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2CacheOverlayManager.java +++ /dev/null @@ -1,229 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.overlay; - -import net.runelite.api.Client; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2NpcCacheUtils; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2ObjectCacheUtils; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2GroundItemCacheUtils; -import net.runelite.client.ui.overlay.OverlayManager; -import net.runelite.client.ui.overlay.outline.ModelOutlineRenderer; - -import javax.inject.Inject; - -/** - * Manager class for cache-based overlays. - * Provides easy setup and configuration of cache overlays for development and debugging. - * - * @author Vox - * @version 1.0 - */ -public class Rs2CacheOverlayManager { - - private final Client client; - private final OverlayManager overlayManager; - private final ModelOutlineRenderer modelOutlineRenderer; - - private Rs2NpcCacheOverlay npcOverlay; - private Rs2ObjectCacheOverlay objectOverlay; - private Rs2GroundItemCacheOverlay groundItemOverlay; - private Rs2CacheInfoBoxOverlay hoverInfoOverlay; - - public Rs2CacheOverlayManager(Client client, OverlayManager overlayManager, ModelOutlineRenderer modelOutlineRenderer) { - this.client = client; - this.overlayManager = overlayManager; - this.modelOutlineRenderer = modelOutlineRenderer; - initializeOverlays(); - } - - /** - * Initializes all cache overlays with default settings. - */ - private void initializeOverlays() { - // Create overlays - npcOverlay = new Rs2NpcCacheOverlay(client, modelOutlineRenderer); - objectOverlay = new Rs2ObjectCacheOverlay(client, modelOutlineRenderer); - groundItemOverlay = new Rs2GroundItemCacheOverlay(client, modelOutlineRenderer); - hoverInfoOverlay = new Rs2CacheInfoBoxOverlay(client); - - // Register the hover info overlay with the container for automatic management - HoverInfoContainer.registerInfoBoxOverlay(overlayManager); - - // Configure default filters and settings - configureDefaultSettings(); - } - - /** - * Configures default settings for all overlays. - */ - private void configureDefaultSettings() { - // NPC overlay - show only NPCs within 10 tiles and visible - npcOverlay.setRenderFilter(npc -> npc.getDistanceFromPlayer() <= 10); - - // Object overlay - show only interactable objects within 15 tiles - objectOverlay.setRenderFilter(obj -> obj.getDistanceFromPlayer() <= 15); - - // Ground item overlay - show only items within 5 tiles - groundItemOverlay.setRenderFilter(item -> item.getDistanceFromPlayer() <= 5); - } - - // ============================================ - // Overlay Management Methods - // ============================================ - - /** - * Enables NPC cache overlay. - */ - public void enableNpcOverlay() { - overlayManager.add(npcOverlay); - } - - /** - * Disables NPC cache overlay. - */ - public void disableNpcOverlay() { - overlayManager.remove(npcOverlay); - } - - /** - * Enables object cache overlay. - */ - public void enableObjectOverlay() { - overlayManager.add(objectOverlay); - } - - /** - * Disables object cache overlay. - */ - public void disableObjectOverlay() { - overlayManager.remove(objectOverlay); - } - - /** - * Enables ground item cache overlay. - */ - public void enableGroundItemOverlay() { - overlayManager.add(groundItemOverlay); - } - - /** - * Disables ground item cache overlay. - */ - public void disableGroundItemOverlay() { - overlayManager.remove(groundItemOverlay); - } - - /** - * Enables hover info overlay (always on top). - */ - public void enableHoverInfoOverlay() { - overlayManager.add(hoverInfoOverlay); - } - - /** - * Disables hover info overlay. - */ - public void disableHoverInfoOverlay() { - overlayManager.remove(hoverInfoOverlay); - } - - /** - * Enables all cache overlays including hover info. - */ - public void enableAllOverlays() { - enableNpcOverlay(); - enableObjectOverlay(); - enableGroundItemOverlay(); - enableHoverInfoOverlay(); - } - - /** - * Disables all cache overlays including hover info. - */ - public void disableAllOverlays() { - disableNpcOverlay(); - disableObjectOverlay(); - disableGroundItemOverlay(); - disableHoverInfoOverlay(); - } - - // ============================================ - // Overlay Configuration Getters - // ============================================ - - public Rs2NpcCacheOverlay getNpcOverlay() { - return npcOverlay; - } - - public Rs2ObjectCacheOverlay getObjectOverlay() { - return objectOverlay; - } - - public Rs2GroundItemCacheOverlay getGroundItemOverlay() { - return groundItemOverlay; - } - - public Rs2CacheInfoBoxOverlay getHoverInfoOverlay() { - return hoverInfoOverlay; - } - - // ============================================ - // Quick Configuration Methods - // ============================================ - - /** - * Quick setup for debugging NPCs by ID. - * - * @param npcId The NPC ID to highlight - */ - public void highlightNpcById(int npcId) { - npcOverlay.setRenderFilter(npc -> npc.getId() == npcId); - enableNpcOverlay(); - } - - /** - * Quick setup for debugging objects by ID. - * - * @param objectId The object ID to highlight - */ - public void highlightObjectById(int objectId) { - objectOverlay.setRenderFilter(obj -> obj.getId() == objectId); - enableObjectOverlay(); - } - - /** - * Quick setup for debugging ground items by ID. - * - * @param itemId The item ID to highlight - */ - public void highlightGroundItemById(int itemId) { - groundItemOverlay.setRenderFilter(item -> item.getId() == itemId); - enableGroundItemOverlay(); - } - - /** - * Quick setup for debugging entities by name (case-insensitive). - * - * @param entityName The entity name to highlight - */ - public void highlightEntitiesByName(String entityName) { - String lowerName = entityName.toLowerCase(); - - npcOverlay.setRenderFilter(npc -> npc.getName() != null && - npc.getName().toLowerCase().contains(lowerName)); - objectOverlay.setRenderFilter(obj -> obj.getName() != null && - obj.getName().toLowerCase().contains(lowerName)); - groundItemOverlay.setRenderFilter(item -> item.getName() != null && - item.getName().toLowerCase().contains(lowerName)); - - enableAllOverlays(); - } - - /** - * Cleanup method to properly unregister all overlays including the hover info overlay. - * Should be called when the overlay manager is no longer needed. - */ - public void cleanup() { - disableAllOverlays(); - HoverInfoContainer.unregisterInfoBoxOverlay(overlayManager); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2GroundItemCacheOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2GroundItemCacheOverlay.java deleted file mode 100644 index ebdb47a7726..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2GroundItemCacheOverlay.java +++ /dev/null @@ -1,645 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.overlay; - -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.util.cache.Rs2GroundItemCache; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2GroundItemCacheUtils; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; -import net.runelite.client.ui.overlay.outline.ModelOutlineRenderer; -import net.runelite.client.util.QuantityFormatter; - -import java.awt.*; -import java.util.List; -import java.util.Map; -import java.util.HashMap; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -/** - * Overlay for rendering cached ground items with various highlight options. - * Based on RuneLite's GroundItemsOverlay patterns but using the cache system. - * - * @author Vox - * @version 1.0 - */ -public class Rs2GroundItemCacheOverlay extends Rs2BaseCacheOverlay { - - /** - * Price calculation modes for value display. - */ - public enum PriceMode { - OFF("Off"), - GE("Grand Exchange"), - HA("High Alchemy"), - STORE("Store Price"), - BOTH("Both GE & HA"); - - private final String displayName; - - PriceMode(String displayName) { - this.displayName = displayName; - } - - @Override - public String toString() { - return displayName; - } - } - - // Ground item-specific colors (Green theme) - private static final Color GROUND_ITEM_BORDER_COLOR = Color.GREEN; - private static final Color GROUND_ITEM_FILL_COLOR = new Color(0, 255, 0, 50); // Green with alpha - - // Text rendering constants - private static final int TEXT_OFFSET_Z = 20; - private static final int STRING_GAP = 15; // Gap between multiple items on same tile - - // Rendering options - private boolean renderTile = true; - private boolean renderText = true; - private boolean renderItemInfo = true; // Show item ID - private boolean renderWorldCoordinates = false; // Show world coordinates - private boolean onlyShowTextOnHover = true; // Only show text when mouse is hovering - private Predicate renderFilter = groundItem -> true; // Default to no filter - - // Advanced rendering options - private boolean renderQuantity = true; // Show quantity for stackable items - private PriceMode priceMode = PriceMode.OFF; // Price display mode - private boolean renderDespawnTimer = false; // Show despawn countdown - private boolean renderOwnershipIndicator = false; // Show ownership status - - // Value thresholds for color coding - private int lowValueThreshold = 1000; - private int mediumValueThreshold = 10000; - private int highValueThreshold = 100000; - - // Map to track text offset for multiple items on same tile - private final Map offsetMap = new HashMap<>(); - - public Rs2GroundItemCacheOverlay(Client client, ModelOutlineRenderer modelOutlineRenderer) { - super(client, modelOutlineRenderer); - } - - @Override - protected Color getDefaultBorderColor() { - return GROUND_ITEM_BORDER_COLOR; - } - - @Override - protected Color getDefaultFillColor() { - return GROUND_ITEM_FILL_COLOR; - } - - @Override - public Dimension render(Graphics2D graphics) { - if (!isClientReady()) { - return null; - } - - // Clear offset map for new frame - offsetMap.clear(); - - // Group ground items by tile location to handle multiple items on same tile - Map> itemsByLocation = Rs2GroundItemCache.getInstance().stream() - .filter(item -> renderFilter == null || renderFilter.test(item)) - .filter(Rs2GroundItemCacheUtils::isVisibleInViewport) - .collect(Collectors.groupingBy(Rs2GroundItemModel::getLocation)); - - // Render each tile - for (Map.Entry> entry : itemsByLocation.entrySet()) { - WorldPoint location = entry.getKey(); - List itemsAtLocation = entry.getValue(); - - renderItemsAtTile(graphics, location, itemsAtLocation); - } - - return null; - } - - /** - * Renders all ground items at a specific tile location. - * Handles multiple items by spacing them vertically. - * - * @param graphics The graphics context - * @param location The tile location - * @param itemsAtLocation List of items at this location - */ - private void renderItemsAtTile(Graphics2D graphics, WorldPoint location, List itemsAtLocation) { - if (itemsAtLocation.isEmpty()) { - return; - } - - // Check if we should only show text on hover for this tile - LocalPoint localPoint = LocalPoint.fromWorld(client.getTopLevelWorldView(), location); - if (localPoint == null) { - return; - } - - boolean shouldShowText = !onlyShowTextOnHover || isMouseHoveringOver(localPoint); - - // Sort items by value (highest first) for better display order - itemsAtLocation.sort((a, b) -> Integer.compare(getItemDisplayValue(b), getItemDisplayValue(a))); - - // Render tile highlight if any item should be highlighted - if (renderTile) { - for (Rs2GroundItemModel item : itemsAtLocation) { - Color borderColor = getBorderColorForItem(item); - Color fillColor = getFillColorForItem(item); - renderItemTile(graphics, item, borderColor, fillColor, DEFAULT_BORDER_WIDTH); - break; // Only render tile once per location - } - } - - // Render text for each item with vertical offset - if (shouldShowText && (renderText || renderItemInfo || renderWorldCoordinates || - renderQuantity || priceMode != PriceMode.OFF || renderDespawnTimer || renderOwnershipIndicator)) { - - for (int i = 0; i < itemsAtLocation.size(); i++) { - Rs2GroundItemModel item = itemsAtLocation.get(i); - renderItemTextWithOffset(graphics, item, i); - } - } - } - - /** - * Gets the display value for an item based on the current price mode. - * - * @param item The ground item model - * @return The display value for sorting and comparison - */ - private int getItemDisplayValue(Rs2GroundItemModel item) { - switch (priceMode) { - case GE: - return item.getTotalGeValue(); - case HA: - return item.getTotalHaValue(); - case STORE: - return item.getTotalValue(); - case BOTH: - return Math.max(item.getTotalGeValue(), item.getTotalHaValue()); - case OFF: - default: - return item.getTotalValue(); // Default to store value - } - } - - /** - * Renders text for a ground item with vertical offset for multiple items. - * - * @param graphics The graphics context - * @param itemModel The ground item model - * @param offset The vertical offset index (0-based) - */ - private void renderItemTextWithOffset(Graphics2D graphics, Rs2GroundItemModel itemModel, int offset) { - LocalPoint localPoint = LocalPoint.fromWorld(client.getTopLevelWorldView(), itemModel.getLocation()); - if (localPoint == null) { - return; - } - - // Build text information for this item - String itemText = buildItemText(itemModel); - if (itemText.isEmpty()) { - return; - } - - // Get canvas point with Z offset for text - net.runelite.api.Point canvasPoint = Perspective.localToCanvas(client, localPoint, - client.getTopLevelWorldView().getPlane(), TEXT_OFFSET_Z); - - if (canvasPoint != null) { - // Apply vertical offset for multiple items on same tile - int adjustedY = canvasPoint.getY() - (STRING_GAP * offset); - net.runelite.api.Point adjustedPoint = new net.runelite.api.Point(canvasPoint.getX(), adjustedY); - - Color textColor = getBorderColorForItem(itemModel); - renderTextWithBackground(graphics, itemText, adjustedPoint, textColor); - } - } - - /** - * Renders a single ground item with the configured options (Legacy method for compatibility). - * - * @param graphics The graphics context - * @param itemModel The ground item model to render - */ - private void renderGroundItemOverlay(Graphics2D graphics, Rs2GroundItemModel itemModel) { - try { - Color borderColor = getBorderColorForItem(itemModel); - Color fillColor = getFillColorForItem(itemModel); - float borderWidth = DEFAULT_BORDER_WIDTH; - - // Render tile highlight - if (renderTile) { - renderItemTile(graphics, itemModel, borderColor, fillColor, borderWidth); - } - - // Render text information if enabled - if (renderText || renderItemInfo || renderWorldCoordinates || - renderQuantity || priceMode != PriceMode.OFF || renderDespawnTimer || renderOwnershipIndicator) { - renderItemText(graphics, itemModel, borderColor); - } - - } catch (Exception e) { - // Silent fail to avoid spam - } - } - - /** - * Renders a tile highlight for the ground item. - * - * @param graphics The graphics context - * @param itemModel The ground item model - * @param borderColor The border color - * @param fillColor The fill color - * @param borderWidth The border width - */ - private void renderItemTile(Graphics2D graphics, Rs2GroundItemModel itemModel, - Color borderColor, Color fillColor, float borderWidth) { - LocalPoint localPoint = LocalPoint.fromWorld(client.getTopLevelWorldView(), itemModel.getLocation()); - if (localPoint == null) { - return; - } - - Polygon tilePoly = Perspective.getCanvasTilePoly(client, localPoint); - if (tilePoly != null) { - Stroke stroke = new BasicStroke(borderWidth); - renderPolygon(graphics, tilePoly, borderColor, fillColor, stroke); - } - } - - /** - * Renders text information for the ground item with hover detection and background. - * - * @param graphics The graphics context - * @param itemModel The ground item model - * @param color The text color - */ - private void renderItemText(Graphics2D graphics, Rs2GroundItemModel itemModel, Color color) { - LocalPoint localPoint = LocalPoint.fromWorld(client.getTopLevelWorldView(), itemModel.getLocation()); - if (localPoint == null) { - return; - } - - // Check if we should only show text on hover - if (onlyShowTextOnHover && !isMouseHoveringOver(localPoint)) { - return; - } - - // Build single line of information - StringBuilder infoText = new StringBuilder(); - - // Main item name and quantity - if (renderText) { - infoText.append(buildItemText(itemModel)); - } - - // Add item ID and coordinates if enabled - if (renderItemInfo || renderWorldCoordinates) { - if (infoText.length() > 0) { - infoText.append(" | "); - } - - if (renderItemInfo) { - infoText.append("ID:").append(itemModel.getId()); - } - - if (renderWorldCoordinates) { - if (renderItemInfo) { - infoText.append(" "); - } - WorldPoint wp = itemModel.getLocation(); - infoText.append("(").append(wp.getX()).append(",").append(wp.getY()).append(")"); - } - } - - // Add additional information based on settings - if (renderQuantity && itemModel.getQuantity() > 1) { - if (infoText.length() > 0) { - infoText.append(" "); - } - infoText.append("x").append(itemModel.getQuantity()); - } - - if (priceMode != PriceMode.OFF) { - if (infoText.length() > 0) { - infoText.append(" "); - } - - switch (priceMode) { - case GE: - infoText.append("(GE: ").append(QuantityFormatter.quantityToStackSize(itemModel.getTotalGeValue())).append(" gp)"); - break; - case HA: - infoText.append("(HA: ").append(QuantityFormatter.quantityToStackSize(itemModel.getTotalHaValue())).append(" gp)"); - break; - case STORE: - infoText.append("(Store: ").append(QuantityFormatter.quantityToStackSize(itemModel.getTotalValue())).append(" gp)"); - break; - case BOTH: - if (itemModel.getTotalGeValue() > 0) { - infoText.append("(GE: ").append(QuantityFormatter.quantityToStackSize(itemModel.getTotalGeValue())).append(" gp)"); - } - if (itemModel.getTotalHaValue() > 0) { - infoText.append(" (HA: ").append(QuantityFormatter.quantityToStackSize(itemModel.getTotalHaValue())).append(" gp)"); - } - break; - case OFF: - default: - // No price display - break; - } - } - - if (renderDespawnTimer && !itemModel.isDespawned()) { - if (infoText.length() > 0) { - infoText.append(" "); - } - infoText.append("⏰").append(formatDespawnTime(itemModel.getSecondsUntilDespawn())); - } - - if (renderOwnershipIndicator) { - if (infoText.length() > 0) { - infoText.append(" "); - } - infoText.append(itemModel.isOwned() ? "ðŸ‘Ī" : "🌐"); - } - - // Get canvas point with Z offset for text - net.runelite.api.Point canvasPoint = Perspective.localToCanvas(client, localPoint, - client.getTopLevelWorldView().getPlane(), TEXT_OFFSET_Z); - - if (canvasPoint != null && infoText.length() > 0) { - renderTextWithBackground(graphics, infoText.toString(), canvasPoint, color); - } - } - - /** - * Checks if the mouse is hovering over a ground item location. - * - * @param localPoint The ground item location - * @return true if mouse is hovering over the location - */ - private boolean isMouseHoveringOver(LocalPoint localPoint) { - net.runelite.api.Point mousePos = client.getMouseCanvasPosition(); - if (mousePos == null) { - return false; - } - - // Check if mouse is over the tile - Polygon tilePoly = Perspective.getCanvasTilePoly(client, localPoint); - return tilePoly != null && tilePoly.contains(mousePos.getX(), mousePos.getY()); - } - - /** - * Renders text with a semi-transparent background for better readability. - * - * @param graphics The graphics context - * @param text The text to render - * @param location The location to render at - * @param color The text color - */ - private void renderTextWithBackground(Graphics2D graphics, String text, - net.runelite.api.Point location, Color color) { - FontMetrics fm = graphics.getFontMetrics(); - int textWidth = fm.stringWidth(text); - int textHeight = fm.getHeight(); - - // Create background rectangle - int padding = 4; - int backgroundX = location.getX() - padding; - int backgroundY = location.getY() - textHeight - padding; - int backgroundWidth = textWidth + (padding * 2); - int backgroundHeight = textHeight + (padding * 2); - - // Draw semi-transparent background - Color backgroundColor = new Color(0, 0, 0, 128); // Semi-transparent black - graphics.setColor(backgroundColor); - graphics.fillRect(backgroundX, backgroundY, backgroundWidth, backgroundHeight); - - // Draw text - renderText(graphics, text, location, color); - } - - /** - * Builds the text to display for a ground item. - * - * @param itemModel The ground item model - * @return The text to display - */ - private String buildItemText(Rs2GroundItemModel itemModel) { - StringBuilder text = new StringBuilder(); - - // Main item name - if (renderText) { - text.append(itemModel.getName()); - } - - // Add quantity if more than 1 - if (renderQuantity && itemModel.getQuantity() > 1) { - if (text.length() > 0) { - text.append(" "); - } - text.append("(").append(QuantityFormatter.quantityToStackSize(itemModel.getQuantity())).append(")"); - } - - // Add item ID and coordinates if enabled - if (renderItemInfo || renderWorldCoordinates) { - if (text.length() > 0) { - text.append(" | "); - } - - if (renderItemInfo) { - text.append("ID:").append(itemModel.getId()); - } - - if (renderWorldCoordinates) { - if (renderItemInfo) { - text.append(" "); - } - WorldPoint wp = itemModel.getLocation(); - text.append("(").append(wp.getX()).append(",").append(wp.getY()).append(")"); - } - } - - // Add price information based on price mode - if (priceMode != PriceMode.OFF) { - if (text.length() > 0) { - text.append(" "); - } - - switch (priceMode) { - case GE: - if (itemModel.getTotalGeValue() > 0) { - text.append("[GE: ").append(QuantityFormatter.quantityToStackSize(itemModel.getTotalGeValue())).append(" gp]"); - } - break; - case HA: - if (itemModel.getTotalHaValue() > 0) { - text.append("[HA: ").append(QuantityFormatter.quantityToStackSize(itemModel.getTotalHaValue())).append(" gp]"); - } - break; - case STORE: - if (itemModel.getTotalValue() > 0) { - text.append("[Store: ").append(QuantityFormatter.quantityToStackSize(itemModel.getTotalValue())).append(" gp]"); - } - break; - case BOTH: - boolean hasGe = itemModel.getTotalGeValue() > 0; - boolean hasHa = itemModel.getTotalHaValue() > 0; - if (hasGe || hasHa) { - text.append("["); - if (hasGe) { - text.append("GE: ").append(QuantityFormatter.quantityToStackSize(itemModel.getTotalGeValue())); - } - if (hasGe && hasHa) { - text.append(" | "); - } - if (hasHa) { - text.append("HA: ").append(QuantityFormatter.quantityToStackSize(itemModel.getTotalHaValue())); - } - text.append(" gp]"); - } - break; - case OFF: - default: - // No price display - break; - } - } - - // Add despawn timer if enabled - if (renderDespawnTimer && !itemModel.isDespawned()) { - if (text.length() > 0) { - text.append(" "); - } - text.append("⏰").append(formatDespawnTime(itemModel.getSecondsUntilDespawn())); - } - - // Add ownership indicator if enabled - if (renderOwnershipIndicator) { - if (text.length() > 0) { - text.append(" "); - } - text.append(itemModel.isOwned() ? "ðŸ‘Ī" : "🌐"); - } - - return text.toString(); - } - - /** - * Formats the item value for display. - * - * @param value The item value - * @return The formatted value string - */ - private String formatValue(int value) { - // Simple color formatting without ColorUtils dependency - if (value < lowValueThreshold) { - return String.format("%d gp", value); - } else if (value < mediumValueThreshold) { - return String.format("%d gp", value); - } else if (value < highValueThreshold) { - return String.format("%d gp", value); - } else { - return String.format("%d gp", value); - } - } - - /** - * Formats the despawn time for display. - * - * @param despawnSeconds The despawn time in seconds - * @return The formatted time string - */ - private String formatDespawnTime(long despawnSeconds) { - if (despawnSeconds <= 0) { - return ""; - } - - long minutes = despawnSeconds / 60; - long seconds = despawnSeconds % 60; - - return String.format("%02d:%02d", minutes, seconds); - } - - // ============================================ - // Configuration Methods - // ============================================ - - public Rs2GroundItemCacheOverlay setRenderTile(boolean renderTile) { - this.renderTile = renderTile; - return this; - } - - public Rs2GroundItemCacheOverlay setRenderText(boolean renderText) { - this.renderText = renderText; - return this; - } - - public Rs2GroundItemCacheOverlay setRenderItemInfo(boolean renderItemInfo) { - this.renderItemInfo = renderItemInfo; - return this; - } - - public Rs2GroundItemCacheOverlay setRenderWorldCoordinates(boolean renderWorldCoordinates) { - this.renderWorldCoordinates = renderWorldCoordinates; - return this; - } - - public Rs2GroundItemCacheOverlay setRenderFilter(Predicate renderFilter) { - this.renderFilter = renderFilter; - return this; - } - - // Configuration methods for advanced rendering options - public void setRenderQuantity(boolean renderQuantity) { - this.renderQuantity = renderQuantity; - } - - public void setPriceMode(PriceMode priceMode) { - this.priceMode = priceMode; - } - - public void setRenderDespawnTimer(boolean renderDespawnTimer) { - this.renderDespawnTimer = renderDespawnTimer; - } - - public void setRenderOwnershipIndicator(boolean renderOwnershipIndicator) { - this.renderOwnershipIndicator = renderOwnershipIndicator; - } - - public void setValueThresholds(int low, int medium, int high) { - this.lowValueThreshold = low; - this.mediumValueThreshold = medium; - this.highValueThreshold = high; - } - - /** - * Gets the border color for a specific ground item. - * Can be overridden by subclasses to provide per-item coloring. - * - * @param itemModel The ground item model - * @return The border color for this item - */ - protected Color getBorderColorForItem(Rs2GroundItemModel itemModel) { - return getDefaultBorderColor(); - } - - /** - * Gets the fill color for a specific ground item. - * Can be overridden by subclasses to provide per-item coloring. - * - * @param itemModel The ground item model - * @return The fill color for this item - */ - protected Color getFillColorForItem(Rs2GroundItemModel itemModel) { - return getDefaultFillColor(); - } - - public Rs2GroundItemCacheOverlay setOnlyShowTextOnHover(boolean onlyShowTextOnHover) { - this.onlyShowTextOnHover = onlyShowTextOnHover; - return this; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2NpcCacheOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2NpcCacheOverlay.java deleted file mode 100644 index cc7b26b98d4..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2NpcCacheOverlay.java +++ /dev/null @@ -1,451 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.overlay; - -import net.runelite.api.*; -import net.runelite.api.coords.LocalPoint; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.util.cache.Rs2NpcCache; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2NpcCacheUtils; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; -import net.runelite.client.ui.overlay.outline.ModelOutlineRenderer; -import net.runelite.client.util.Text; -import java.awt.*; -import java.util.function.Predicate; - -/** - * Overlay for rendering cached NPCs with various highlight options. - * Based on RuneLite's NpcOverlay patterns but using the cache system. - * - * @author Vox - * @version 1.0 - */ -public class Rs2NpcCacheOverlay extends Rs2BaseCacheOverlay { - - // NPC-specific colors - private static final Color NPC_BORDER_COLOR = Color.ORANGE; - private static final Color NPC_FILL_COLOR = new Color(255, 165, 0, 50); // Orange with alpha - private static final Color NPC_INTERACTING_COLOR = Color.RED; // Color for NPCs interacting with player - private static final Color NPC_INTERACTING_FILL_COLOR = new Color(255, 0, 0, 50); // Red with alpha - - // Rendering options - private boolean renderHull = true; - private boolean renderTile = false; - private boolean renderTrueTile = false; - private boolean renderOutline = false; - private boolean renderName = false; - private boolean renderNpcInfo = true; // Show NPC ID - private boolean renderWorldCoordinates = false; // Show world coordinates - private boolean renderCombatLevel = false; // Show combat level - private boolean renderDistance = false; // Show distance from player - private boolean onlyShowTextOnHover = true; // Only show text when mouse is hovering - private Predicate renderFilter = npc -> true; - - public Rs2NpcCacheOverlay(Client client, ModelOutlineRenderer modelOutlineRenderer) { - super(client, modelOutlineRenderer); - } - - @Override - protected Color getDefaultBorderColor() { - return NPC_BORDER_COLOR; - } - - @Override - protected Color getDefaultFillColor() { - return NPC_FILL_COLOR; - } - - @Override - public Dimension render(Graphics2D graphics) { - if (!isClientReady()) { - return null; - } - if (Rs2NpcCache.getInstance() == null || Rs2NpcCache.getInstance().size() == 0) { - return null; // No NPCs to render - } - renderFilter = renderFilter != null ? renderFilter : npc -> true; // Default to no filter - // Render all visible NPCs from cache - Rs2NpcCache.getAllNpcs() - .filter(npc -> renderFilter == null || renderFilter.test(npc)) - .filter(Rs2NpcCacheUtils::isVisibleInViewport) - .forEach(npc -> renderNpcOverlay(graphics, npc)); - - return null; - } - - /** - * Renders a single NPC with the configured options. - * - * @param graphics The graphics context - * @param npcModel The NPC model to render - */ - private void renderNpcOverlay(Graphics2D graphics, Rs2NpcModel npcModel) { - try { - // Get the underlying NPC from the model - NPC npc = npcModel.getRuneliteNpc(); - - NPCComposition npcComposition = npc.getTransformedComposition(); - if (npcComposition == null || !npcComposition.isInteractible()) { - return; - } - - Color borderColor = getBorderColorForNpc(npcModel); - Color fillColor = getFillColorForNpc(npcModel); - - // Check if NPC is interacting with the player to override colors - if (isNpcInteractingWithPlayer(npc)) { - borderColor = NPC_INTERACTING_COLOR; - fillColor = NPC_INTERACTING_FILL_COLOR; - } - - float borderWidth = DEFAULT_BORDER_WIDTH; - Stroke stroke = new BasicStroke(borderWidth); - - // Render convex hull - if (renderHull) { - Shape hull = npc.getConvexHull(); - renderPolygon(graphics, hull, borderColor, fillColor, stroke); - } - - // Render tile - if (renderTile) { - Polygon tilePoly = npc.getCanvasTilePoly(); - renderPolygon(graphics, tilePoly, borderColor, fillColor, stroke); - } - - // Render true tile (centered) - if (renderTrueTile) { - renderTrueTile(graphics, npc, npcComposition, borderColor, fillColor, stroke); - } - - // Render outline - if (renderOutline) { - modelOutlineRenderer.drawOutline(npc, (int) borderWidth, borderColor, 0); - } - - // Render NPC info (including name if enabled and hovering) - if (renderNpcInfo || renderWorldCoordinates || renderCombatLevel || renderDistance || renderName) { - renderNpcInfo(graphics, npc); - } - - } catch (Exception e) { - // Silent fail to avoid spam - } - } - - /** - * Renders the true tile (centered on NPC). - */ - private void renderTrueTile(Graphics2D graphics, NPC npc, NPCComposition composition, - Color borderColor, Color fillColor, Stroke stroke) { - LocalPoint lp = LocalPoint.fromWorld(client.getTopLevelWorldView(), npc.getWorldLocation()); - if (lp != null) { - final int size = composition.getSize(); - final LocalPoint centerLp = lp.plus( - Perspective.LOCAL_TILE_SIZE * (size - 1) / 2, - Perspective.LOCAL_TILE_SIZE * (size - 1) / 2); - Polygon tilePoly = Perspective.getCanvasTileAreaPoly(client, centerLp, size); - renderPolygon(graphics, tilePoly, borderColor, fillColor, stroke); - } - } - - /** - * Checks if the mouse is hovering over an NPC. - - /** - * Renders NPC information (name, ID, coordinates, combat level, distance) above the NPC. - * All information is displayed in a single line with hover detection and background. - */ - private void renderNpcInfo(Graphics2D graphics, NPC npc) { - // Check if we should only show text on hover - boolean isHovering = isMouseHoveringOver(npc); - if (onlyShowTextOnHover && !isHovering) { - return; - } - - // Build detailed information lines (always build them when renderNpcInfo is called) - java.util.List infoLines = new java.util.ArrayList<>(); - - // Add NPC name if enabled - if (renderName && npc.getName() != null) { - infoLines.add("Name: " + Text.removeTags(npc.getName())); - } - - // Add NPC ID if enabled - if (renderNpcInfo) { - infoLines.add("ID: " + npc.getId()); - } - - // Add combat level if enabled - if (renderCombatLevel) { - infoLines.add("Combat Level: " + npc.getCombatLevel()); - } - - // Add world coordinates if enabled - if (renderWorldCoordinates) { - WorldPoint wp = npc.getWorldLocation(); - infoLines.add("Coords: " + wp.getX() + ", " + wp.getY() + " (Plane " + wp.getPlane() + ")"); - } - - // Add distance from player if enabled - if (renderDistance) { - Player player = client.getLocalPlayer(); - if (player != null) { - int distance = (int) player.getWorldLocation().distanceTo(npc.getWorldLocation()); - infoLines.add("Distance: " + distance + " tiles"); - } - } - - // Add NPC composition details - NPCComposition npcComposition = npc.getComposition(); - if (npcComposition != null) { - if (npcComposition.getSize() != 1) { - infoLines.add("Size: " + npcComposition.getSize() + "x" + npcComposition.getSize()); - } - - // Add actions if available - String[] actions = npcComposition.getActions(); - if (actions != null && actions.length > 0) { - java.util.List validActions = new java.util.ArrayList<>(); - for (String action : actions) { - if (action != null && !action.trim().isEmpty()) { - validActions.add(action); - } - } - if (!validActions.isEmpty()) { - infoLines.add("Actions: " + String.join(", ", validActions)); - } - } - } - - // Add interaction status - if (isNpcInteractingWithPlayer(npc)) { - infoLines.add("Status: Interacting"); - } - - // Only set hover info if we have information to display - if (!infoLines.isEmpty()) { - Color borderColor = isNpcInteractingWithPlayer(npc) ? NPC_INTERACTING_COLOR : getDefaultBorderColor(); - String entityType = "NPC"; - - // Use NPC's canvas location for positioning, or mouse position if hovering - net.runelite.api.Point displayLocation; - if (isHovering) { - displayLocation = client.getMouseCanvasPosition(); - } else { - // Use NPC's text location for non-hover display - displayLocation = npc.getCanvasTextLocation(graphics, "", npc.getLogicalHeight() + 60); - } - - if (displayLocation != null) { - HoverInfoContainer.HoverInfo hoverInfo = new HoverInfoContainer.HoverInfo( - infoLines, displayLocation, borderColor, entityType); - //HoverInfoContainer.setHoverInfo(hoverInfo); - renderDetailedInfoBox(graphics, infoLines, - displayLocation, borderColor); - } - - } - } - /** - * Renders a detailed info box with multiple lines of information. - * Each line is rendered separately with a colored border indicating the object type. - * - * @param graphics The graphics context - * @param infoLines List of information lines to display - * @param location The location to render the info box - * @param borderColor The border color (indicates object type) - */ - private void renderDetailedInfoBox(Graphics2D graphics, java.util.List infoLines, - net.runelite.api.Point location, Color borderColor) { - if (infoLines.isEmpty()) return; - - FontMetrics fm = graphics.getFontMetrics(); - int lineHeight = fm.getHeight(); - int maxWidth = 0; - - // Calculate the maximum width needed - for (String line : infoLines) { - int lineWidth = fm.stringWidth(line); - if (lineWidth > maxWidth) { - maxWidth = lineWidth; - } - } - - // Calculate box dimensions - int padding = 6; - int boxWidth = maxWidth + (padding * 2); - int boxHeight = (infoLines.size() * lineHeight) + (padding * 2); - - // Calculate box position (centered above the location) - int boxX = location.getX() - (boxWidth / 2); - int boxY = location.getY() - boxHeight - 10; // 10 pixels above the object - - // Draw the info box background - Color backgroundColor = new Color(0, 0, 0, 180); // Semi-transparent black - graphics.setColor(backgroundColor); - graphics.fillRect(boxX, boxY, boxWidth, boxHeight); - - // Draw the border in object type color - graphics.setColor(borderColor); - graphics.setStroke(new BasicStroke(2.0f)); - graphics.drawRect(boxX, boxY, boxWidth, boxHeight); - - // Draw the text lines - graphics.setColor(Color.WHITE); - for (int i = 0; i < infoLines.size(); i++) { - String line = infoLines.get(i); - int textX = boxX + padding; - int textY = boxY + padding + fm.getAscent() + (i * lineHeight); - graphics.drawString(line, textX, textY); - } - } - - /** - * Checks if the mouse is hovering over an NPC. - * - * @param npc The NPC to check - * @return true if mouse is hovering over the NPC - */ - private boolean isMouseHoveringOver(NPC npc) { - net.runelite.api.Point mousePos = client.getMouseCanvasPosition(); - if (mousePos == null) { - return false; - } - - // Check if mouse is over the NPC's convex hull - Shape hull = npc.getConvexHull(); - if (hull != null) { - return hull.contains(mousePos.getX(), mousePos.getY()); - } - - // Fallback to tile poly - Polygon tilePoly = npc.getCanvasTilePoly(); - return tilePoly != null && tilePoly.contains(mousePos.getX(), mousePos.getY()); - } - - /** - * Renders text with a semi-transparent background for better readability. - * - * @param graphics The graphics context - * @param text The text to render - * @param location The location to render at - * @param color The text color - */ - private void renderTextWithBackground(Graphics2D graphics, String text, - net.runelite.api.Point location, Color color) { - FontMetrics fm = graphics.getFontMetrics(); - int textWidth = fm.stringWidth(text); - int textHeight = fm.getHeight(); - - // Create background rectangle - int padding = 4; - int backgroundX = location.getX() - padding; - int backgroundY = location.getY() - textHeight - padding; - int backgroundWidth = textWidth + (padding * 2); - int backgroundHeight = textHeight + (padding * 2); - - // Draw semi-transparent background - Color backgroundColor = new Color(0, 0, 0, 128); // Semi-transparent black - graphics.setColor(backgroundColor); - graphics.fillRect(backgroundX, backgroundY, backgroundWidth, backgroundHeight); - - // Draw text - renderText(graphics, text, location, color); - } - - /** - * Renders multiple lines of text above an NPC with proper spacing. - * - * @param graphics The graphics context - * @param npc The NPC to render text above - * @param lines The lines of text to render - * @param color The color to use - /** - * Checks if an NPC is interacting with the player. - * An NPC is considered interacting if it's targeting/attacking the player. - */ - private boolean isNpcInteractingWithPlayer(NPC npc) { - Actor interacting = npc.getInteracting(); - return interacting != null && interacting.equals(client.getLocalPlayer()); - } - - /** - * Gets the border color for a specific NPC. - * Can be overridden by subclasses to provide per-NPC coloring. - * - * @param npcModel The NPC model - * @return The border color for this NPC - */ - protected Color getBorderColorForNpc(Rs2NpcModel npcModel) { - return getDefaultBorderColor(); - } - - /** - * Gets the fill color for a specific NPC. - * Can be overridden by subclasses to provide per-NPC coloring. - * - * @param npcModel The NPC model - * @return The fill color for this NPC - */ - protected Color getFillColorForNpc(Rs2NpcModel npcModel) { - return getDefaultFillColor(); - } - - // ============================================ - // Configuration Methods - // ============================================ - - public Rs2NpcCacheOverlay setRenderHull(boolean renderHull) { - this.renderHull = renderHull; - return this; - } - - public Rs2NpcCacheOverlay setRenderTile(boolean renderTile) { - this.renderTile = renderTile; - return this; - } - - public Rs2NpcCacheOverlay setRenderTrueTile(boolean renderTrueTile) { - this.renderTrueTile = renderTrueTile; - return this; - } - - public Rs2NpcCacheOverlay setRenderOutline(boolean renderOutline) { - this.renderOutline = renderOutline; - return this; - } - - public Rs2NpcCacheOverlay setRenderName(boolean renderName) { - this.renderName = renderName; - return this; - } - - public Rs2NpcCacheOverlay setRenderNpcInfo(boolean renderNpcInfo) { - this.renderNpcInfo = renderNpcInfo; - return this; - } - - public Rs2NpcCacheOverlay setRenderWorldCoordinates(boolean renderWorldCoordinates) { - this.renderWorldCoordinates = renderWorldCoordinates; - return this; - } - - public Rs2NpcCacheOverlay setRenderCombatLevel(boolean renderCombatLevel) { - this.renderCombatLevel = renderCombatLevel; - return this; - } - - public Rs2NpcCacheOverlay setRenderDistance(boolean renderDistance) { - this.renderDistance = renderDistance; - return this; - } - - public Rs2NpcCacheOverlay setOnlyShowTextOnHover(boolean onlyShowTextOnHover) { - this.onlyShowTextOnHover = onlyShowTextOnHover; - return this; - } - - public Rs2NpcCacheOverlay setRenderFilter(Predicate renderFilter) { - this.renderFilter = renderFilter; - return this; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2ObjectCacheOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2ObjectCacheOverlay.java deleted file mode 100644 index 0abdfa60a66..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2ObjectCacheOverlay.java +++ /dev/null @@ -1,841 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.overlay; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.*; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.util.cache.Rs2ObjectCache; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2ObjectCacheUtils; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel; -import net.runelite.client.ui.overlay.outline.ModelOutlineRenderer; -import java.awt.*; -import java.util.EnumMap; -import java.util.List; -import java.util.Map; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -/** - * Overlay for rendering cached objects with various highlight options. - * Based on RuneLite's ObjectIndicatorsOverlay patterns but using the cache system. - * - * @author Vox - * @version 1.0 - */ -@Slf4j -public class Rs2ObjectCacheOverlay extends Rs2BaseCacheOverlay { - - // Object-specific colors (Blue theme) - Default fallback colors - private static final Color OBJECT_BORDER_COLOR = Color.BLUE; - private static final Color OBJECT_FILL_COLOR = new Color(0, 0, 255, 50); // Blue with alpha - - // Default object type colors - private static final Color GAME_OBJECT_COLOR = new Color(255, 0, 0); // Red - private static final Color WALL_OBJECT_COLOR = new Color(0, 255, 0); // Green - private static final Color DECORATIVE_OBJECT_COLOR = new Color(255, 255, 0); // Yellow - private static final Color GROUND_OBJECT_COLOR = new Color(255, 0, 255); // Magenta - private static final Color TILE_OBJECT_COLOR = new Color(255, 165, 0); // Orange - - // Rendering options - private boolean renderHull = true; - private boolean renderClickbox = false; - private boolean renderTile = false; - private boolean renderOutline = false; - private boolean renderObjectInfo = true; // Show object type and ID - private boolean renderObjectName = true; // Show object names - private boolean renderWorldCoordinates = false; // Show world coordinates - private boolean onlyShowTextOnHover = true; // Only show text when mouse is hovering - - // Object type enable/disable flags - private boolean enableGameObjects = true; - private boolean enableWallObjects = true; - private boolean enableDecorativeObjects = true; - private boolean enableGroundObjects = true; - - // Statistics tracking - private static final long STATISTICS_LOG_INTERVAL_MS = 10_000; // 10 seconds - private long lastStatisticsLogTime = 0; - - private Predicate renderFilter; - - public Rs2ObjectCacheOverlay(Client client, ModelOutlineRenderer modelOutlineRenderer) { - super(client, modelOutlineRenderer); - } - - @Override - protected Color getDefaultBorderColor() { - return OBJECT_BORDER_COLOR; - } - - @Override - protected Color getDefaultFillColor() { - return OBJECT_FILL_COLOR; - } - - @Override - public Dimension render(Graphics2D graphics) { - if (!isClientReady()) { - return null; - } - if (Rs2ObjectCache.getInstance() == null || Rs2ObjectCache.getInstance().size() == 0) { - return null; // No objects to render - } - - // Check if we should log statistics (every 10 seconds) - long currentTime = System.currentTimeMillis(); - boolean shouldLogStatistics = (currentTime - lastStatisticsLogTime) >= STATISTICS_LOG_INTERVAL_MS; - - // Get all objects from cache for statistics - List allObjects = Rs2ObjectCache.getInstance().stream() - .collect(Collectors.toList()); - - // Track statistics by object type for each filtering stage - Map totalByType = new EnumMap<>(Rs2ObjectModel.ObjectType.class); - Map afterRenderFilterByType = new EnumMap<>(Rs2ObjectModel.ObjectType.class); - Map afterTypeEnabledByType = new EnumMap<>(Rs2ObjectModel.ObjectType.class); - Map afterViewportByType = new EnumMap<>(Rs2ObjectModel.ObjectType.class); - - // Initialize counters - for (Rs2ObjectModel.ObjectType type : Rs2ObjectModel.ObjectType.values()) { - totalByType.put(type, 0); - afterRenderFilterByType.put(type, 0); - afterTypeEnabledByType.put(type, 0); - afterViewportByType.put(type, 0); - } - - // Count total objects by type - for (Rs2ObjectModel obj : allObjects) { - Rs2ObjectModel.ObjectType type = obj.getObjectType(); - totalByType.put(type, totalByType.get(type) + 1); - } - // Apply filters and count at each stage - List afterRenderFilter = allObjects.stream() - .filter(obj -> renderFilter == null || renderFilter.test(obj)) - .collect(Collectors.toList()); - - for (Rs2ObjectModel obj : afterRenderFilter) { - Rs2ObjectModel.ObjectType type = obj.getObjectType(); - afterRenderFilterByType.put(type, afterRenderFilterByType.get(type) + 1); - } - - List afterTypeEnabled = afterRenderFilter.stream() - .filter(obj -> isObjectTypeEnabled(obj.getObjectType())) - .collect(Collectors.toList()); - - for (Rs2ObjectModel obj : afterTypeEnabled) { - Rs2ObjectModel.ObjectType type = obj.getObjectType(); - afterTypeEnabledByType.put(type, afterTypeEnabledByType.get(type) + 1); - } - - List afterViewport = afterTypeEnabled.stream() - .filter(Rs2ObjectCacheUtils::isVisibleInViewport) - .collect(Collectors.toList()); - - for (Rs2ObjectModel obj : afterViewport) { - Rs2ObjectModel.ObjectType type = obj.getObjectType(); - afterViewportByType.put(type, afterViewportByType.get(type) + 1); - } - - // Log detailed statistics every 10 seconds - if (shouldLogStatistics) { - lastStatisticsLogTime = currentTime; - logRenderingStatistics(totalByType, afterRenderFilterByType, afterTypeEnabledByType, afterViewportByType); - } - - // Render the visible objects - afterViewport.forEach(obj -> renderObjectOverlay(graphics, obj)); - - return null; - } - - /** - * Logs detailed rendering statistics showing object type distribution at each filtering stage. - * This helps identify which object types are available, filtered out, and why. - * - * @param totalByType Total count of each object type in cache - * @param afterRenderFilterByType Count after custom render filter applied - * @param afterTypeEnabledByType Count after object type enable/disable filter - * @param afterViewportByType Count after viewport visibility filter (final rendered count) - */ - private void logRenderingStatistics(Map totalByType, - Map afterRenderFilterByType, - Map afterTypeEnabledByType, - Map afterViewportByType) { - - // Calculate totals across all types - int totalObjects = totalByType.values().stream().mapToInt(Integer::intValue).sum(); - int afterRenderFilterTotal = afterRenderFilterByType.values().stream().mapToInt(Integer::intValue).sum(); - int afterTypeEnabledTotal = afterTypeEnabledByType.values().stream().mapToInt(Integer::intValue).sum(); - int finalRenderedTotal = afterViewportByType.values().stream().mapToInt(Integer::intValue).sum(); - - log.info("=== Rs2ObjectCacheOverlay Rendering Statistics ==="); - log.info("Cache Total: {} | After RenderFilter: {} | After TypeEnabled: {} | Final Rendered: {}", - totalObjects, afterRenderFilterTotal, afterTypeEnabledTotal, finalRenderedTotal); - - // Log statistics for each object type - for (Rs2ObjectModel.ObjectType type : Rs2ObjectModel.ObjectType.values()) { - int total = totalByType.get(type); - int afterRenderFilter = afterRenderFilterByType.get(type); - int afterTypeEnabled = afterTypeEnabledByType.get(type); - int finalRendered = afterViewportByType.get(type); - - // Skip types with no objects - if (total == 0) { - continue; - } - - // Calculate filtering reasons - int filteredByRenderFilter = total - afterRenderFilter; - int filteredByTypeEnabled = afterRenderFilter - afterTypeEnabled; - int filteredByViewport = afterTypeEnabled - finalRendered; - - boolean typeEnabled = isObjectTypeEnabled(type); - - log.info(" {}: Total={} | RenderFilter={} | TypeEnabled={} ({}) | Viewport={} | Final={}", - type.getTypeName(), - total, - afterRenderFilter, - afterTypeEnabled, - typeEnabled ? "ENABLED" : "DISABLED", - finalRendered, - finalRendered); - - // Log filtering details if objects were filtered - if (filteredByRenderFilter > 0) { - log.info(" -> {} filtered by RenderFilter", filteredByRenderFilter); - } - if (filteredByTypeEnabled > 0) { - log.info(" -> {} filtered by TypeEnabled ({})", - filteredByTypeEnabled, typeEnabled ? "ERROR" : "disabled"); - } - if (filteredByViewport > 0) { - log.info(" -> {} filtered by Viewport (not visible)", filteredByViewport); - } - } - - // Log filtering summary - int totalFilteredByRender = totalObjects - afterRenderFilterTotal; - int totalFilteredByType = afterRenderFilterTotal - afterTypeEnabledTotal; - int totalFilteredByViewport = afterTypeEnabledTotal - finalRenderedTotal; - - if (totalFilteredByRender > 0 || totalFilteredByType > 0 || totalFilteredByViewport > 0) { - log.info("Filtering Summary: RenderFilter removed {} | TypeEnabled removed {} | Viewport removed {}", - totalFilteredByRender, totalFilteredByType, totalFilteredByViewport); - } - - // Log current filter settings - log.info("Current Settings: GameObj={} | WallObj={} | DecorObj={} | GroundObj={} | RenderFilter={}", - enableGameObjects, enableWallObjects, enableDecorativeObjects, enableGroundObjects, - renderFilter != null ? "ACTIVE" : "NONE"); - - log.info("=== End Rs2ObjectCacheOverlay Statistics ==="); - } - - /** - * Renders a single object with the configured options. - * - * @param graphics The graphics context - * @param objectModel The object model to render - */ - private void renderObjectOverlay(Graphics2D graphics, Rs2ObjectModel objectModel) { - try { - // Validate inputs - if (objectModel == null) { - return; - } - if (objectModel.getTileObject() == null) { - return; // No tile object to render - } - - TileObject tileObject = objectModel.getTileObject(); - - // Check if object is on current plane - try { - if (tileObject.getPlane() != client.getTopLevelWorldView().getPlane()) { - return; - } - } catch (Exception e) { - // Skip plane check if there's an issue accessing it - log.debug("Failed to check object plane for object {}: {}", objectModel.getId(), e.getMessage()); - } - - // Get colors for this specific object (supports per-object coloring) - Color borderColor = getBorderColorForObject(objectModel); - Color fillColor = getFillColorForObject(objectModel); - float borderWidth = DEFAULT_BORDER_WIDTH; - Stroke stroke = new BasicStroke(borderWidth); - - // Render convex hull - if (renderHull) { - renderConvexHull(graphics, tileObject, borderColor, fillColor, stroke); - } - - // Render clickbox - if (renderClickbox) { - try { - Shape clickbox = tileObject.getClickbox(); - if (clickbox != null) { - renderPolygon(graphics, clickbox, borderColor, fillColor, stroke); - } - } catch (Exception e) { - log.debug("Failed to render clickbox for object {}: {}", objectModel.getId(), e.getMessage()); - } - } - - // Render tile - if (renderTile) { - try { - Polygon tilePoly = tileObject.getCanvasTilePoly(); - if (tilePoly != null) { - renderPolygon(graphics, tilePoly, borderColor, fillColor, stroke); - } - } catch (Exception e) { - log.debug("Failed to render tile poly for object {}: {}", objectModel.getId(), e.getMessage()); - } - } - - // Render outline - if (renderOutline) { - try { - modelOutlineRenderer.drawOutline(tileObject, (int) borderWidth, borderColor, 0); - } catch (Exception e) { - log.debug("Failed to render outline for object {}: {}", objectModel.getId(), e.getMessage()); - } - } - - // Render object information (type, ID, and name) - if (renderObjectInfo) { - renderObjectInfo(graphics, objectModel, tileObject); - } - - } catch (Exception e) { - log.warn("Failed to render object overlay for object {}: {}", - objectModel != null ? objectModel.getId() : "unknown", e.getMessage(), e); - } - } - - /** - * Renders the convex hull for different object types. - * Based on ObjectIndicatorsOverlay pattern. - * - * @param graphics The graphics context - * @param object The tile object - * @param color The border color - * @param fillColor The fill color - * @param stroke The stroke - */ - private void renderConvexHull(Graphics2D graphics, TileObject object, Color color, Color fillColor, Stroke stroke) { - try { - Shape polygon = null; - Shape polygon2 = null; - - try { - if (object instanceof GameObject) { - polygon = ((GameObject) object).getConvexHull(); - } else if (object instanceof WallObject) { - WallObject wallObject = (WallObject) object; - polygon = wallObject.getConvexHull(); - polygon2 = wallObject.getConvexHull2(); - } else if (object instanceof DecorativeObject) { - DecorativeObject decorativeObject = (DecorativeObject) object; - polygon = decorativeObject.getConvexHull(); - polygon2 = decorativeObject.getConvexHull2(); - } else if (object instanceof GroundObject) { - polygon = ((GroundObject) object).getConvexHull(); - } else { - polygon = object.getCanvasTilePoly(); - } - } catch (Exception e) { - log.debug("Failed to get convex hull shapes: {}", e.getMessage()); - // Fallback to tile poly - try { - polygon = object.getCanvasTilePoly(); - } catch (Exception e2) { - log.debug("Failed to get tile poly as fallback: {}", e2.getMessage()); - } - } - - if (polygon != null) { - renderPolygon(graphics, polygon, color, fillColor, stroke); - } - - if (polygon2 != null) { - renderPolygon(graphics, polygon2, color, fillColor, stroke); - } - } catch (Exception e) { - log.debug("Error rendering convex hull: {}", e.getMessage()); - } - } - - /** - * Renders object information as a detailed info box with each piece of information on a separate line. - * The info box border uses the object type color for visual identification. - * - * @param graphics The graphics context - * @param objectModel The object model - * @param tileObject The tile object - */ - private void renderObjectInfo(Graphics2D graphics, Rs2ObjectModel objectModel, TileObject tileObject) { - try { - // Check if we should only show text on hover - boolean isHovering = isMouseHoveringOver(tileObject); - if (onlyShowTextOnHover && !isHovering) { - return; - } - // Build information lines (always build them when renderObjectInfo is called) - java.util.List infoLines = new java.util.ArrayList<>(); - - // Add object name if enabled and available - String objectName = objectModel.getName(); - if (renderObjectName && objectName != null && !objectName.equals("Unknown Object") && !objectName.trim().isEmpty()) { - infoLines.add("Name: " + objectName); - } - - // Add object type and ID - if (renderObjectInfo) { - infoLines.add("Type: " + objectModel.getObjectType().getTypeName()); - infoLines.add("ID: " + objectModel.getId()); - - // Object size information - infoLines.add("Size: " + objectModel.getSizeX() + "x" + objectModel.getSizeY()); - - // Object composition details - ObjectComposition comp = objectModel.getObjectComposition(); - if (comp != null) { - // Map scene and icon IDs - int mapSceneId = comp.getMapSceneId(); - int mapIconId = comp.getMapIconId(); - if (mapSceneId != -1) { - infoLines.add("MapScene: " + mapSceneId); - } - if (mapIconId != -1) { - infoLines.add("MapIcon: " + mapIconId); - } - - // Varbit/VarPlayer information for multiloc objects - int varbitId = comp.getVarbitId(); - int varPlayerId = comp.getVarPlayerId(); - if (varbitId != -1) { - infoLines.add("VarbitID: " + varbitId); - } - if (varPlayerId != -1) { - infoLines.add("VarPlayerID: " + varPlayerId); - } - - // Impostor information for multiloc objects - int[] impostorIds = comp.getImpostorIds(); - if (impostorIds != null && impostorIds.length > 0) { - infoLines.add("Impostors: " + impostorIds.length + " variants"); - } - } - - // Object properties - if (objectModel.isSolid()) { - infoLines.add("Property: Solid"); - } - if (objectModel.blocksLineOfSight()) { - infoLines.add("Property: Blocks LoS"); - } - - // Cache timing information - infoLines.add("Age: " + objectModel.getTicksSinceCreation() + " ticks"); - } - - // Add world coordinates if enabled - if (renderWorldCoordinates) { - WorldPoint wp = objectModel.getLocation(); - if (wp != null) { - infoLines.add("Coords: " + wp.getX() + ", " + wp.getY() + " (Plane " + wp.getPlane() + ")"); - - // Canonical location for multi-tile objects - WorldPoint canonical = objectModel.getCanonicalLocation(); - if (canonical != null && !canonical.equals(wp)) { - infoLines.add("Canonical: " + canonical.getX() + ", " + canonical.getY()); - } - } - } - - // Add additional object info - infoLines.add("Distance: " + objectModel.getDistanceFromPlayer() + " tiles"); - - // Add object actions if available - prefer ObjectComposition actions for completeness - String[] actions = null; - ObjectComposition comp = objectModel.getObjectComposition(); - if (comp != null) { - actions = comp.getActions(); - } - if (actions == null || actions.length == 0) { - actions = objectModel.getActions(); // Fallback to model actions - } - - if (actions != null && actions.length > 0) { - java.util.List validActions = new java.util.ArrayList<>(); - for (String action : actions) { - if (action != null && !action.trim().isEmpty()) { - validActions.add(action); - } - } - if (!validActions.isEmpty()) { - // Limit display to first 3 actions to avoid clutter - int maxActions = Math.min(validActions.size(), 3); - infoLines.add("Actions: " + String.join(", ", validActions.subList(0, maxActions))); - if (validActions.size() > 3) { - infoLines.add(" ... +" + (validActions.size() - 3) + " more"); - } - } - } - - // Only set hover info if we have information to display - if (!infoLines.isEmpty()) { - Color borderColor = getBorderColorForObject(objectModel); - String entityType = "Object (" + objectModel.getObjectType().getTypeName() + ")"; - - // Use object's canvas location for positioning, or mouse position if hovering - net.runelite.api.Point displayLocation; - if (isHovering) { - displayLocation = client.getMouseCanvasPosition(); - } else { - // Use object's text location for non-hover display - displayLocation = tileObject.getCanvasTextLocation(graphics, "", 0); - } - - if (displayLocation != null) { - HoverInfoContainer.HoverInfo hoverInfo = new HoverInfoContainer.HoverInfo( - infoLines, displayLocation, borderColor, entityType); - ///HoverInfoContainer.setHoverInfo(hoverInfo); - renderDetailedInfoBox(graphics, infoLines, - displayLocation, - borderColor); - } - - } - - } catch (Exception e) { - log.debug("Failed to render object info for object {}: {}", objectModel.getId(), e.getMessage()); - } - } - - /** - * Renders a detailed info box with multiple lines of information. - * Each line is rendered separately with a colored border indicating the object type. - * - * @param graphics The graphics context - * @param infoLines List of information lines to display - * @param location The location to render the info box - * @param borderColor The border color (indicates object type) - */ - private void renderDetailedInfoBox(Graphics2D graphics, java.util.List infoLines, - net.runelite.api.Point location, Color borderColor) { - if (infoLines.isEmpty()) return; - - FontMetrics fm = graphics.getFontMetrics(); - int lineHeight = fm.getHeight(); - int maxWidth = 0; - - // Calculate the maximum width needed - for (String line : infoLines) { - int lineWidth = fm.stringWidth(line); - if (lineWidth > maxWidth) { - maxWidth = lineWidth; - } - } - - // Calculate box dimensions - int padding = 6; - int boxWidth = maxWidth + (padding * 2); - int boxHeight = (infoLines.size() * lineHeight) + (padding * 2); - - // Calculate box position (centered above the location) - int boxX = location.getX() - (boxWidth / 2); - int boxY = location.getY() - boxHeight - 10; // 10 pixels above the object - - // Draw the info box background - Color backgroundColor = new Color(0, 0, 0, 180); // Semi-transparent black - graphics.setColor(backgroundColor); - graphics.fillRect(boxX, boxY, boxWidth, boxHeight); - - // Draw the border in object type color - graphics.setColor(borderColor); - graphics.setStroke(new BasicStroke(2.0f)); - graphics.drawRect(boxX, boxY, boxWidth, boxHeight); - - // Draw the text lines - graphics.setColor(Color.WHITE); - for (int i = 0; i < infoLines.size(); i++) { - String line = infoLines.get(i); - int textX = boxX + padding; - int textY = boxY + padding + fm.getAscent() + (i * lineHeight); - graphics.drawString(line, textX, textY); - } - } - - /** - * Checks if a specific object type should be rendered. - * - * @param objectType The object type to check - * @return true if the object type should be rendered - */ - protected boolean isObjectTypeEnabled(Rs2ObjectModel.ObjectType objectType) { - switch (objectType) { - case GAME_OBJECT: - return enableGameObjects; - case WALL_OBJECT: - return enableWallObjects; - case DECORATIVE_OBJECT: - return enableDecorativeObjects; - case GROUND_OBJECT: - return enableGroundObjects; - default: - return true; - } - } - - /** - * Checks if the mouse is hovering over a tile object. - * - * @param tileObject The tile object to check - * @return true if mouse is hovering over the object - */ - private boolean isMouseHoveringOver(TileObject tileObject) { - try { - net.runelite.api.Point mousePos = client.getMouseCanvasPosition(); - if (mousePos == null) { - return false; - } - - // Check if mouse is over the object's convex hull - Shape shape = null; - try { - if (tileObject instanceof GameObject) { - shape = ((GameObject) tileObject).getConvexHull(); - } else if (tileObject instanceof WallObject) { - shape = ((WallObject) tileObject).getConvexHull(); - } else if (tileObject instanceof DecorativeObject) { - shape = ((DecorativeObject) tileObject).getConvexHull(); - } else if (tileObject instanceof GroundObject) { - shape = ((GroundObject) tileObject).getConvexHull(); - } - } catch (Exception e) { - log.debug("Failed to get convex hull for hover detection: {}", e.getMessage()); - } - - // Fallback to tile poly if no hull available - if (shape == null) { - try { - shape = tileObject.getCanvasTilePoly(); - } catch (Exception e) { - log.debug("Failed to get tile poly for hover detection: {}", e.getMessage()); - return false; - } - } - - return shape != null && shape.contains(mousePos.getX(), mousePos.getY()); - } catch (Exception e) { - log.debug("Error in mouse hover detection: {}", e.getMessage()); - return false; - } - } - - /** - * Renders text with a semi-transparent background for better readability. - * - * @param graphics The graphics context - * @param text The text to render - * @param location The location to render at - * @param color The text color - */ - private void renderTextWithBackground(Graphics2D graphics, String text, - net.runelite.api.Point location, Color color) { - try { - if (text == null || text.trim().isEmpty() || location == null) { - return; - } - - FontMetrics fm = graphics.getFontMetrics(); - if (fm == null) { - return; - } - - int textWidth = fm.stringWidth(text); - int textHeight = fm.getHeight(); - - // Create background rectangle - int padding = 4; - int backgroundX = location.getX() - padding; - int backgroundY = location.getY() - textHeight - padding; - int backgroundWidth = textWidth + (padding * 2); - int backgroundHeight = textHeight + (padding * 2); - - // Draw semi-transparent background - Color backgroundColor = new Color(0, 0, 0, 128); // Semi-transparent black - graphics.setColor(backgroundColor); - graphics.fillRect(backgroundX, backgroundY, backgroundWidth, backgroundHeight); - - // Draw text - renderText(graphics, text, location, color); - } catch (Exception e) { - log.debug("Error rendering text with background: {}", e.getMessage()); - } - } - - /** - * Gets the abbreviated object type string. - * - * @param objectType The object type - * @return Abbreviated type string - */ - private String getObjectTypeAbbreviation(Rs2ObjectModel.ObjectType objectType) { - switch (objectType) { - case GAME_OBJECT: - return "GO"; - case WALL_OBJECT: - return "WO"; - case DECORATIVE_OBJECT: - return "DO"; - case GROUND_OBJECT: - return "Gnd"; - case TILE_OBJECT: - return "TO"; - default: - return "?"; - } - } - - /** - * Gets the border color for a specific object type. - * Can be overridden by subclasses to provide type-specific coloring. - * - * @param objectType The object type - * @return The border color for this object type, or default colors if not overridden - */ - protected Color getBorderColorForObjectType(Rs2ObjectModel.ObjectType objectType) { - switch (objectType) { - case GAME_OBJECT: - return GAME_OBJECT_COLOR; - case WALL_OBJECT: - return WALL_OBJECT_COLOR; - case DECORATIVE_OBJECT: - return DECORATIVE_OBJECT_COLOR; - case GROUND_OBJECT: - return GROUND_OBJECT_COLOR; - default: - return getDefaultBorderColor(); - } - } - - /** - * Gets the fill color for a specific object type. - * Can be overridden by subclasses to provide type-specific coloring. - * - * @param objectType The object type - * @return The fill color for this object type, or null for default - */ - protected Color getFillColorForObjectType(Rs2ObjectModel.ObjectType objectType) { - Color borderColor = getBorderColorForObjectType(objectType); - return new Color(borderColor.getRed(), borderColor.getGreen(), borderColor.getBlue(), 50); - } - - /** - * Gets the border color for a specific object. - * Can be overridden by subclasses to provide per-object coloring. - * - * @param objectModel The object model - * @return The border color for this object - */ - protected Color getBorderColorForObject(Rs2ObjectModel objectModel) { - // Check for object type-specific coloring first - Color typeColor = getBorderColorForObjectType(objectModel.getObjectType()); - if (typeColor != null) { - return typeColor; - } - - return getDefaultBorderColor(); - } - - /** - * Gets the fill color for a specific object. - * Can be overridden by subclasses to provide per-object coloring. - * - * @param objectModel The object model - * @return The fill color for this object - */ - protected Color getFillColorForObject(Rs2ObjectModel objectModel) { - // Check for object type-specific coloring first - Color typeColor = getFillColorForObjectType(objectModel.getObjectType()); - if (typeColor != null) { - return typeColor; - } - - // Create a fill color based on the border color - Color borderColor = getBorderColorForObject(objectModel); - return new Color(borderColor.getRed(), borderColor.getGreen(), borderColor.getBlue(), 50); - } - - // ============================================ - // Configuration Methods - // ============================================ - - public Rs2ObjectCacheOverlay setRenderHull(boolean renderHull) { - this.renderHull = renderHull; - return this; - } - - public Rs2ObjectCacheOverlay setRenderClickbox(boolean renderClickbox) { - this.renderClickbox = renderClickbox; - return this; - } - - public Rs2ObjectCacheOverlay setRenderTile(boolean renderTile) { - this.renderTile = renderTile; - return this; - } - - public Rs2ObjectCacheOverlay setRenderOutline(boolean renderOutline) { - this.renderOutline = renderOutline; - return this; - } - - public Rs2ObjectCacheOverlay setRenderFilter(Predicate renderFilter) { - this.renderFilter = renderFilter; - return this; - } - - public Rs2ObjectCacheOverlay setRenderObjectInfo(boolean renderObjectInfo) { - this.renderObjectInfo = renderObjectInfo; - return this; - } - - public Rs2ObjectCacheOverlay setRenderWorldCoordinates(boolean renderWorldCoordinates) { - this.renderWorldCoordinates = renderWorldCoordinates; - return this; - } - - public Rs2ObjectCacheOverlay setOnlyShowTextOnHover(boolean onlyShowTextOnHover) { - this.onlyShowTextOnHover = onlyShowTextOnHover; - return this; - } - - public Rs2ObjectCacheOverlay setRenderObjectName(boolean renderObjectName) { - this.renderObjectName = renderObjectName; - return this; - } - - public Rs2ObjectCacheOverlay setEnableGameObjects(boolean enableGameObjects) { - this.enableGameObjects = enableGameObjects; - return this; - } - - public Rs2ObjectCacheOverlay setEnableWallObjects(boolean enableWallObjects) { - this.enableWallObjects = enableWallObjects; - return this; - } - - public Rs2ObjectCacheOverlay setEnableDecorativeObjects(boolean enableDecorativeObjects) { - this.enableDecorativeObjects = enableDecorativeObjects; - return this; - } - - public Rs2ObjectCacheOverlay setEnableGroundObjects(boolean enableGroundObjects) { - this.enableGroundObjects = enableGroundObjects; - return this; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/CacheSerializable.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/CacheSerializable.java deleted file mode 100644 index eca420eb3c6..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/CacheSerializable.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.serialization; - -/** - * Interface for cache values that can be serialized to RuneLite profile config. - * Implementing this interface indicates that the cache supports persistence across sessions. - */ -public interface CacheSerializable { - - /** - * Gets the config key for this cache type. - * This should be unique for each cache type. - * - * @return The config key for storing this cache data - */ - String getConfigKey(); - - /** - * Gets the config group for this cache type. - * Typically "microbot" for all microbot caches. - * - * @return The config group for storing this cache data - */ - default String getConfigGroup() { - return "microbot"; - } - - /** - * Determines if this cache should be persisted. - * Some cache types like NPCs, Objects, and Ground Items should not be persisted - * as they are dynamically loaded and change frequently. - * - * @return true if this cache should be saved/loaded from config - */ - boolean shouldPersist(); -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/CacheSerializationManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/CacheSerializationManager.java deleted file mode 100644 index ca429ea449e..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/CacheSerializationManager.java +++ /dev/null @@ -1,757 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.serialization; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonSyntaxException; -import com.google.gson.reflect.TypeToken; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Quest; -import net.runelite.api.QuestState; -import net.runelite.api.Skill; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.Rs2Cache; -import net.runelite.client.plugins.microbot.util.cache.model.SkillData; -import net.runelite.client.plugins.microbot.util.cache.model.SpiritTreeData; -import net.runelite.client.plugins.microbot.util.cache.model.VarbitData; -import net.runelite.client.plugins.microbot.util.farming.SpiritTree; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -import java.lang.reflect.Type; -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; -import java.io.IOException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; -import java.util.Map; -import java.util.UUID; - - -/** - * Serialization manager for Rs2UnifiedCache instances. - * Handles automatic save/load to file-based storage under .runelite/microbot-profiles - * to prevent RuneLite profile bloat and improve performance. - * - * Includes cache freshness tracking to prevent loading stale cache data - * that wasn't properly saved due to ungraceful client shutdowns. - * - * Cache freshness is determined by whether data was saved after being loaded, - * not by session ID or time limits (unless explicitly specified). - * This ensures we only load cache data that was properly persisted after modifications. - */ -@Slf4j -public class CacheSerializationManager { - private static final String VERSION = "1.0.0"; // Version for cache serialization format compatibility - private static final String BASE_DIRECTORY = ".runelite/microbot-profiles"; - private static final String CACHE_SUBDIRECTORY = "caches"; - private static final String METADATA_SUFFIX = ".metadata"; - private static final String JSON_EXTENSION = ".json"; - - private static final Gson gson; - - // Session identifier to track cache freshness across client restarts - private static final String SESSION_ID = UUID.randomUUID().toString(); - - /** - * Enhanced metadata class to track cache freshness, validity, and integrity. - * Inspired by GLite's PersistenceMetadata with data size tracking and cache naming. - */ - private static class CacheMetadata { - private final String version; - private final String sessionId; - private final String saveTimestampUtc; // UTC timestamp in ISO 8601 format - private final boolean stale; - private final int dataSize; // Size of serialized data for integrity checks - private final String cacheName; // Name of cache for debugging - - // UTC formatter for consistent timestamp handling - private static final DateTimeFormatter UTC_FORMATTER = DateTimeFormatter.ISO_INSTANT; - - public CacheMetadata(String version, String sessionId, String saveTimestampUtc, boolean stale, int dataSize, String cacheName) { - this.version = version; - this.sessionId = sessionId; - this.saveTimestampUtc = saveTimestampUtc; - this.stale = stale; - this.dataSize = dataSize; - this.cacheName = cacheName; - } - - /** - * Create CacheMetadata with current UTC timestamp - */ - public static CacheMetadata createWithCurrentUtcTime(String version, String sessionId, boolean stale, int dataSize, String cacheName) { - String utcTimestamp = Instant.now().atOffset(ZoneOffset.UTC).format(UTC_FORMATTER); - return new CacheMetadata(version, sessionId, utcTimestamp, stale, dataSize, cacheName); - } - - /** - * Create CacheMetadata with current UTC timestamp and convenience method for common use - */ - public static CacheMetadata createWithCurrentUtcTime(String version, String sessionId, boolean stale) { - String utcTimestamp = Instant.now().atOffset(ZoneOffset.UTC).format(UTC_FORMATTER); - return new CacheMetadata(version, sessionId, utcTimestamp, stale, 0, "unknown"); - } - - public boolean isNewVersion(String currentVersion){ - // Check if the current version is different from the saved version - return !this.version.equals(currentVersion); - } - - /** - * Checks if this metadata indicates fresh cache data that was properly saved after loading. - * - * @param maxAgeMs Maximum age in milliseconds (0 = ignore time completely) - * @return true if cache data is fresh and was saved after loading - */ - public boolean isFresh(long maxAgeMs) { - // Data is fresh if it was saved after being loaded (indicating proper persistence) - if (stale) { - return false; - } - - // If maxAgeMs is 0, we don't care about time - only that it was saved after load - if (maxAgeMs == 0) { - return true; - } - - // Otherwise check if it's within the time limit - long age = getAgeMs(); - return age <= maxAgeMs; - } - - public boolean isFromCurrentSession() { - return SESSION_ID.equals(sessionId); - } - - /** - * Get age in milliseconds from the UTC timestamp - */ - public long getAgeMs() { - try { - Instant saveTime = Instant.parse(saveTimestampUtc); - return Instant.now().toEpochMilli() - saveTime.toEpochMilli(); - } catch (Exception e) { - log.warn("Failed to parse UTC timestamp '{}', treating as very old", saveTimestampUtc); - return Long.MAX_VALUE; // Treat as very old if parsing fails - } - } - - /** - * Get the save timestamp as human-readable string - */ - public String getSaveTimeFormatted() { - try { - Instant saveTime = Instant.parse(saveTimestampUtc); - return saveTime.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + " UTC"; - } catch (Exception e) { - return saveTimestampUtc; // Return raw if parsing fails - } - } - - public boolean isStale() { - return stale; - } - - /** - * Get the data size for integrity checking - */ - public int getDataSize() { - return dataSize; - } - - /** - * Get the cache name for debugging - */ - public String getCacheName() { - return cacheName; - } - - /** - * Validates that this metadata has reasonable values - */ - public void validate() throws IllegalStateException { - if (version == null || version.trim().isEmpty()) { - throw new IllegalStateException("Version cannot be null or empty"); - } - if (dataSize < 0) { - throw new IllegalStateException("Data size cannot be negative"); - } - if (saveTimestampUtc == null || saveTimestampUtc.trim().isEmpty()) { - throw new IllegalStateException("Save timestamp cannot be null or empty"); - } - } - - /** - * Gets a human-readable age description - */ - public String getFormattedAge() { - long ageMs = getAgeMs(); - - if (ageMs < 1000) { - return ageMs + "ms ago"; - } else if (ageMs < 60_000) { - return (ageMs / 1000) + "s ago"; - } else if (ageMs < 3_600_000) { - return (ageMs / 60_000) + "m ago"; - } else if (ageMs < 86_400_000) { - return (ageMs / 3_600_000) + "h ago"; - } else { - return (ageMs / 86_400_000) + "d ago"; - } - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("CacheMetadata{"); - sb.append("version='").append(version).append("'"); - sb.append(", cacheName='").append(cacheName).append("'"); - sb.append(", timestamp=").append(getSaveTimeFormatted()); - sb.append(" (").append(getFormattedAge()).append(")"); - sb.append(", dataSize=").append(dataSize); - sb.append(", stale=").append(stale); - sb.append(", sessionId='").append(sessionId).append("'"); - sb.append("}"); - return sb.toString(); - } - } - - // Initialize Gson with custom adapters - static { - gson = new GsonBuilder() - .registerTypeAdapter(Skill.class, new SkillAdapter()) - .registerTypeAdapter(Quest.class, new QuestAdapter()) - .registerTypeAdapter(QuestState.class, new QuestStateAdapter()) - .registerTypeAdapter(SkillData.class, new SkillDataAdapter()) - .registerTypeAdapter(VarbitData.class, new VarbitDataAdapter()) - .registerTypeAdapter(SpiritTree.class, new SpiritTreePatchAdapter()) - .registerTypeAdapter(SpiritTreeData.class, new SpiritTreeDataAdapter()) - .create(); - } - - /** - * Saves a cache to file-based storage with character-specific directory structure. - * Also stores metadata to track cache freshness and prevent loading stale data. - * - * @param cache The cache to save - * @param configKey The cache type identifier (skills, quests, etc.) - * @param rsProfileKey The RuneLite profile key - * @param playerName The player name for character-specific caching - * @param The key type - * @param The value type - */ - public static void saveCache(Rs2Cache cache, String configKey, String rsProfileKey, String playerName) { - try { - if (rsProfileKey == null) { - log.warn("Cannot save cache {}: profile key not available", configKey); - return; - } - - if (playerName == null || playerName.trim().isEmpty()) { - log.warn("Cannot save cache {}: player name not available", configKey); - return; - } - - // create directory structure - Path cacheDir = getCacheDirectory(rsProfileKey, playerName); - Files.createDirectories(cacheDir); - - // serialize cache data - String json = serializeCacheData(cache, configKey); - if (json == null || json.trim().isEmpty()) { - log.warn("No data to save for cache {} for player {}", configKey, playerName); - return; - } - - // save cache data file - Path cacheFile = cacheDir.resolve(configKey + JSON_EXTENSION); - Files.write(cacheFile, json.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - - // create and save metadata with data size and cache name - CacheMetadata metadata = CacheMetadata.createWithCurrentUtcTime(VERSION, SESSION_ID, false, json.length(), configKey); - String metadataJson = gson.toJson(metadata, CacheMetadata.class); - Path metadataFile = cacheDir.resolve(configKey + METADATA_SUFFIX); - Files.write(metadataFile, metadataJson.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - - log.info("Saved cache \"{}\" for player \"{}\" to file ({} chars) at {}", - configKey, playerName, json.length(), metadata.getSaveTimeFormatted()); - - } catch (IOException e) { - log.error("Failed to save cache {} to file for player {}", configKey, playerName, e); - } catch (Exception e) { - log.error("Failed to save cache {} for player {}", configKey, playerName, e); - } - } - - - - /** - * Gets the current player name using Rs2Player utility. - * - * @return Player name or null if not available - */ - private static String getCurrentPlayerName() { - try { - if (!Microbot.isLoggedIn()) { - return null; - } - // use Rs2Player to get local player and extract name - var localPlayer = Rs2Player.getLocalPlayer(); - return localPlayer != null ? localPlayer.getName() : null; - } catch (Exception e) { - log.debug("Error getting current player name: {}", e.getMessage()); - return null; - } - } - - - /** - * Loads a cache from file-based storage with character-specific directory structure. - * Checks cache freshness metadata before loading to prevent loading stale data. - * - * @param cache The cache to load into - * @param configKey The cache type identifier (skills, quests, etc.) - * @param rsProfileKey The profile key to load from - * @param playerName The player name for character-specific caching - * @param forceInvalidate Whether to force cache invalidation - * @param The key type - * @param The value type - */ - public static void loadCache(Rs2Cache cache, String configKey, String rsProfileKey, String playerName, boolean forceInvalidate) { - loadCache(cache, configKey, rsProfileKey, playerName, 0, forceInvalidate); // Default: ignore time, only check if saved after load - } - - - /** - * Loads a cache from file-based storage with age limit. - * Checks cache freshness metadata before loading to prevent loading stale data. - * - * @param cache The cache to load into - * @param configKey The cache type identifier - * @param rsProfileKey The profile key to load from - * @param playerName The player name for character-specific caching - * @param maxAgeMs Maximum age in milliseconds (0 = ignore time completely) - * @param forceInvalidate Whether to force cache invalidation - * @param The key type - * @param The value type - */ - public static void loadCache(Rs2Cache cache, String configKey, String rsProfileKey, String playerName, long maxAgeMs, boolean forceInvalidate) { - try { - if (rsProfileKey == null) { - log.warn("Cannot load cache {}: profile key not available", configKey); - return; - } - - if (playerName == null || playerName.trim().isEmpty()) { - log.warn("Cannot load cache {}: player name not available", configKey); - return; - } - - Path cacheDir = getCacheDirectory(rsProfileKey, playerName); - Path metadataFile = cacheDir.resolve(configKey + METADATA_SUFFIX); - Path cacheFile = cacheDir.resolve(configKey + JSON_EXTENSION); - - // check if files exist - if (!Files.exists(metadataFile) || !Files.exists(cacheFile)) { - log.debug("No cache files found for {} player {}, starting fresh", configKey, playerName); - if (forceInvalidate) cache.invalidateAll(); - - // create initial stale metadata to track first load - CacheMetadata loadedMetadata = CacheMetadata.createWithCurrentUtcTime(VERSION, SESSION_ID, true, 0, configKey); - String metadataJson = gson.toJson(loadedMetadata, CacheMetadata.class); - Files.createDirectories(cacheDir); - Files.write(metadataFile, metadataJson.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - return; - } - - // read and validate metadata - String metadataJson = Files.readString(metadataFile); - CacheMetadata metadata = gson.fromJson(metadataJson, CacheMetadata.class); - - if (metadata == null || !metadata.isFresh(maxAgeMs)) { - log.warn("Cache \"{}\" for player \"{}\" metadata indicates stale data (age: {}ms, fresh: {})", - configKey, playerName, metadata != null ? metadata.getAgeMs() : "unknown", - metadata != null ? metadata.isFresh(maxAgeMs) : false); - - if (forceInvalidate) cache.invalidateAll(); - return; - } - - // load cache data - String json = Files.readString(cacheFile); - if (json != null && !json.trim().isEmpty()) { - deserializeCacheData(cache, configKey, json); - log.debug("Loaded cache {} for player {} from file, entries loaded: {}", configKey, playerName, cache.size()); - } else { - log.warn("Cache file exists but contains no data for {} player {}", configKey, playerName); - } - - // mark as loaded but stale until next save - CacheMetadata loadedMetadata = CacheMetadata.createWithCurrentUtcTime(VERSION, SESSION_ID, true, json.length(), configKey); - String updatedMetadataJson = gson.toJson(loadedMetadata, CacheMetadata.class); - Files.write(metadataFile, updatedMetadataJson.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - - } catch (JsonSyntaxException e) { - log.warn("Failed to parse cache data for {} player {}, clearing corrupted cache", configKey, playerName, e); - clearCacheFiles(configKey, rsProfileKey, playerName); - } catch (IOException e) { - log.error("Failed to load cache {} for player {} from file", configKey, playerName, e); - } catch (Exception e) { - log.error("Failed to load cache {} for player {}", configKey, playerName, e); - } - } - - /** - * Clears cache files for current player and profile. - * - * @param configKey The cache type to clear - */ - public static void clearCache(String configKey) { - try { - String rsProfileKey = Microbot.getConfigManager() != null ? Microbot.getConfigManager().getRSProfileKey() : null; - String playerName = getCurrentPlayerName(); - clearCacheFiles(configKey, rsProfileKey, playerName); - } catch (Exception e) { - log.error("Failed to clear cache {} files", configKey, e); - } - } - - /** - * Clears cache files for a specific profile and player. - * - * @param configKey The cache type to clear - * @param rsProfileKey The profile key - */ - public static void clearCache(String configKey, String rsProfileKey) { - String playerName = getCurrentPlayerName(); - clearCacheFiles(configKey, rsProfileKey, playerName); - } - - /** - * Clears cache files for specific cache type, profile, and player. - * - * @param configKey The cache type to clear - * @param rsProfileKey The profile key - * @param playerName The player name - */ - public static void clearCacheFiles(String configKey, String rsProfileKey, String playerName) { - try { - if (rsProfileKey == null || playerName == null || playerName.trim().isEmpty()) { - log.warn("Cannot clear cache files: profile key or player name not available"); - return; - } - - Path cacheDir = getCacheDirectory(rsProfileKey, playerName); - Path cacheFile = cacheDir.resolve(configKey + JSON_EXTENSION); - Path metadataFile = cacheDir.resolve(configKey + METADATA_SUFFIX); - - Files.deleteIfExists(cacheFile); - Files.deleteIfExists(metadataFile); - - log.debug("Cleared cache files for {} player {}", configKey, playerName); - } catch (IOException e) { - log.error("Failed to clear cache files for {} player {}", configKey, playerName, e); - } - } - - /** - * Gets the cache directory path for a profile and player using URL encoding for safety. - * This ensures that different profiles/players don't collide into the same directory. - */ - private static Path getCacheDirectory(String profileKey, String playerName) { - try { - // use URL encoding to safely handle special characters while preserving uniqueness - String encodedPlayerName = URLEncoder.encode(playerName, StandardCharsets.UTF_8); - String encodedProfileKey = URLEncoder.encode(profileKey, StandardCharsets.UTF_8); - - Path userHome = Paths.get(System.getProperty("user.home")); - return userHome.resolve(BASE_DIRECTORY) - .resolve(encodedProfileKey) - .resolve(encodedPlayerName) - .resolve(CACHE_SUBDIRECTORY); - } catch (Exception e) { - log.error("Failed to encode path components, falling back to basic sanitization", e); - // fallback to basic sanitization if URL encoding fails - String sanitizedPlayerName = playerName.replaceAll("[^a-zA-Z0-9_-]", "_"); - String sanitizedProfileKey = profileKey.replaceAll("[^a-zA-Z0-9_-]", "_"); - - Path userHome = Paths.get(System.getProperty("user.home")); - return userHome.resolve(BASE_DIRECTORY) - .resolve(sanitizedProfileKey) - .resolve(sanitizedPlayerName) - .resolve(CACHE_SUBDIRECTORY); - } - } - - /** - * Serializes cache data to JSON based on cache type. - * This method handles different cache types with specific serialization strategies. - * Only persistent caches are serialized (Skills, Quests, Varbits). - * NPC cache is excluded as it's dynamically loaded based on game scene. - */ - @SuppressWarnings("unchecked") - public static String serializeCacheData(Rs2Cache cache, String configKey) { - try { - log.debug("Starting serialization for cache type: {}", configKey); - - // Handle different cache types - using actual config keys from caches - switch (configKey) { - case "skills": - String skillJson = serializeSkillCache((Rs2Cache) cache); - log.debug("Skills serialization completed, JSON length: {}", skillJson != null ? skillJson.length() : 0); - return skillJson; - case "quests": - String questJson = serializeQuestCache((Rs2Cache) cache); - log.debug("Quests serialization completed, JSON length: {}", questJson != null ? questJson.length() : 0); - return questJson; - case "varbits": - String varbitJson = serializeVarbitCache((Rs2Cache) cache); - log.debug("Varbits serialization completed, JSON length: {}", varbitJson != null ? varbitJson.length() : 0); - return varbitJson; - case "varPlayerCache": - String varPlayerJson = serializeVarPlayerCache((Rs2Cache) cache); - log.debug("VarPlayer serialization completed, JSON length: {}", varPlayerJson != null ? varPlayerJson.length() : 0); - return varPlayerJson; - case "spiritTrees": - String spiritTreeJson = serializeSpiritTreeCache((Rs2Cache) cache); - log.debug("SpiritTrees serialization completed, JSON length: {}", spiritTreeJson != null ? spiritTreeJson.length() : 0); - return spiritTreeJson; - default: - log.warn("Unknown cache type for serialization: {}", configKey); - return null; - } - } catch (Exception e) { - log.error("Failed to serialize cache data for {}", configKey, e); - return null; - } - } - - /** - * Deserializes cache data from JSON based on cache type. - * Only persistent caches are deserialized (Skills, Quests, Varbits). - * NPC cache is excluded as it's dynamically loaded based on game scene. - */ - @SuppressWarnings("unchecked") - public static void deserializeCacheData(Rs2Cache cache, String configKey, String json) { - try { - log.debug("Starting deserialization for cache type: {}, JSON length: {}", configKey, json != null ? json.length() : 0); - - switch (configKey) { - case "skills": - deserializeSkillCache((Rs2Cache) cache, json); - break; - case "quests": - deserializeQuestCache((Rs2Cache) cache, json); - break; - case "varbits": - deserializeVarbitCache((Rs2Cache) cache, json); - break; - case "varPlayerCache": - deserializeVarPlayerCache((Rs2Cache) cache, json); - break; - case "spiritTrees": - deserializeSpiritTreeCache((Rs2Cache) cache, json); - break; - default: - log.warn("Unknown cache type for deserialization: {}", configKey); - } - - log.debug("Deserialization completed for cache type: {}, final cache size: {}", configKey, cache.size()); - } catch (Exception e) { - log.error("Failed to deserialize cache data for {}", configKey, e); - } - } - - // Skill cache serialization - private static String serializeSkillCache(Rs2Cache cache) { - // Use the new method to get all entries for serialization - Map data = cache.getEntriesForSerialization(); - log.debug("Serializing {} skill entries", data.size()); - if (data.isEmpty()) { - log.warn("Skills cache is empty during serialization"); - return "{}"; - } - String json = gson.toJson(data); - log.debug("Skills JSON preview: {}", json.length() > 200 ? json.substring(0, 200) + "..." : json); - return json; - } - - private static void deserializeSkillCache(Rs2Cache cache, String json) { - Type type = new TypeToken>(){}.getType(); - Map data = gson.fromJson(json, type); - if (data != null) { - int entriesLoaded = 0; - int entriesSkipped = 0; - for (Map.Entry entry : data.entrySet()) { - // Only load entries that are not already present in cache (cache entries are newer) - if (!cache.containsKey(entry.getKey())) { - cache.put(entry.getKey(), entry.getValue()); - entriesLoaded++; - } else { - entriesSkipped++; - log.debug("Skipped loading skill {} - already present in cache with newer data", entry.getKey()); - } - } - log.debug("Deserialized {} skill entries into cache, skipped {} existing entries", entriesLoaded, entriesSkipped); - } else { - log.warn("Skill cache data was null after JSON parsing"); - } - } - - // Quest cache serialization - private static String serializeQuestCache(Rs2Cache cache) { - // Use the new method to get all entries for serialization - Map data = cache.getEntriesForSerialization(); - log.debug("Serializing {} quest entries", data.size()); - if (data.isEmpty()) { - log.warn("Quest cache is empty during serialization"); - return "{}"; - } - String json = gson.toJson(data); - log.debug("Quest JSON preview: {}", json.length() > 200 ? json.substring(0, 200) + "..." : json); - return json; - } - - private static void deserializeQuestCache(Rs2Cache cache, String json) { - Type type = new TypeToken>(){}.getType(); - Map data = gson.fromJson(json, type); - if (data != null) { - int entriesLoaded = 0; - int entriesSkipped = 0; - for (Map.Entry entry : data.entrySet()) { - // Only load entries that are not already present in cache (cache entries are newer) - if (!cache.containsKey(entry.getKey())) { - cache.put(entry.getKey(), entry.getValue()); - entriesLoaded++; - } else { - entriesSkipped++; - log.debug("Skipped loading quest {} - already present in cache with newer data", entry.getKey()); - } - } - log.debug("Deserialized {} quest entries into cache, skipped {} existing entries", entriesLoaded, entriesSkipped); - } else { - log.warn("Quest cache data was null after JSON parsing"); - } - } - - // Varbit cache serialization - private static String serializeVarbitCache(Rs2Cache cache) { - // Use the new method to get all entries for serialization - Map data = cache.getEntriesForSerialization(); - log.debug("Serializing {} varbit entries", data.size()); - if (data.isEmpty()) { - log.warn("Varbit cache is empty during serialization"); - return "{}"; - } - String json = gson.toJson(data); - log.debug("Varbit JSON preview: {}", json.length() > 200 ? json.substring(0, 200) + "..." : json); - return json; - } - - private static void deserializeVarbitCache(Rs2Cache cache, String json) { - Type type = new TypeToken>(){}.getType(); - Map data = gson.fromJson(json, type); - if (data != null) { - int entriesLoaded = 0; - int entriesSkipped = 0; - for (Map.Entry entry : data.entrySet()) { - // Only load entries that are not already present in cache (cache entries are newer) - if (!cache.containsKey(entry.getKey())) { - cache.put(entry.getKey(), entry.getValue()); - entriesLoaded++; - } else { - entriesSkipped++; - log.debug("Skipped loading varbit {} - already present in cache with newer data", entry.getKey()); - } - } - - log.debug("Deserialized {} varbit entries into cache, skipped {} existing entries", entriesLoaded, entriesSkipped); - } else { - log.warn("Varbit cache data was null after JSON parsing"); - } - } - - // VarPlayer cache serialization - reuses VarbitData structure - private static String serializeVarPlayerCache(Rs2Cache cache) { - // Use the new method to get all entries for serialization - Map data = cache.getEntriesForSerialization(); - log.debug("Serializing {} varplayer entries", data.size()); - if (data.isEmpty()) { - log.warn("VarPlayer cache is empty during serialization"); - return "{}"; - } - String json = gson.toJson(data); - log.debug("VarPlayer JSON preview: {}", json.length() > 200 ? json.substring(0, 200) + "..." : json); - return json; - } - - private static void deserializeVarPlayerCache(Rs2Cache cache, String json) { - Type type = new TypeToken>(){}.getType(); - Map data = gson.fromJson(json, type); - if (data != null) { - int entriesLoaded = 0; - int entriesSkipped = 0; - for (Map.Entry entry : data.entrySet()) { - // Only load entries that are not already present in cache (cache entries are newer) - if (!cache.containsKey(entry.getKey())) { - cache.put(entry.getKey(), entry.getValue()); - entriesLoaded++; - } else { - entriesSkipped++; - log.debug("Skipped loading varplayer {} - already present in cache with newer data", entry.getKey()); - } - } - - log.debug("Deserialized {} varplayer entries into cache, skipped {} existing entries", entriesLoaded, entriesSkipped); - } else { - log.warn("VarPlayer cache data was null after JSON parsing"); - } - } - - // Spirit tree cache serialization - private static String serializeSpiritTreeCache(Rs2Cache cache) { - // Use the new method to get all entries for serialization - Map data = cache.getEntriesForSerialization(); - return gson.toJson(data); - } - - private static void deserializeSpiritTreeCache(Rs2Cache cache, String json) { - Type type = new TypeToken>(){}.getType(); - Map data = gson.fromJson(json, type); - if (data != null) { - int entriesLoaded = 0; - int entriesSkipped = 0; - for (Map.Entry entry : data.entrySet()) { - // Only load entries that are not already present in cache (cache entries are newer) - if (!cache.containsKey(entry.getKey())) { - cache.put(entry.getKey(), entry.getValue()); - entriesLoaded++; - } else { - entriesSkipped++; - log.debug("Skipped loading spirit tree {} - already present in cache with newer data", entry.getKey()); - } - } - log.debug("Deserialized {} spirit tree entries into cache, skipped {} existing entries", entriesLoaded, entriesSkipped); - } else { - log.warn("Spirit tree cache data was null after JSON parsing"); - } - } - - - /** - * Creates a character-specific config key by appending player name. - * - * @param baseKey The base config key - * @param playerName The player name - * @return Character-specific config key - */ - public static String createCharacterSpecificKey(String baseKey, String playerName) { - // sanitize player name for config key usage - String sanitizedPlayerName = playerName.replaceAll("[^a-zA-Z0-9_-]", "_"); - return baseKey + "_" + sanitizedPlayerName; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/QuestAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/QuestAdapter.java deleted file mode 100644 index 3283af1a322..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/QuestAdapter.java +++ /dev/null @@ -1,54 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.serialization; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; -import net.runelite.api.Quest; - -import java.io.IOException; - -/** - * Gson TypeAdapter for Quest enum serialization/deserialization. - * Stores quests as their ordinal values for compact representation. - */ -public class QuestAdapter extends TypeAdapter { - - @Override - public void write(JsonWriter out, Quest value) throws IOException { - if (value == null) { - out.nullValue(); - } else { - out.value(value.ordinal()); - } - } - - @Override - public Quest read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - - // Handle both string names (legacy) and ordinal values (new format) - if (in.peek() == JsonToken.STRING) { - // Legacy format: enum name - String questName = in.nextString(); - try { - return Quest.valueOf(questName); - } catch (IllegalArgumentException e) { - throw new IOException("Invalid quest name: " + questName, e); - } - } else { - // New format: ordinal value - int ordinal = in.nextInt(); - Quest[] quests = Quest.values(); - - if (ordinal >= 0 && ordinal < quests.length) { - return quests[ordinal]; - } else { - throw new IOException("Invalid quest ordinal: " + ordinal); - } - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/QuestStateAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/QuestStateAdapter.java deleted file mode 100644 index cd15eaeaee2..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/QuestStateAdapter.java +++ /dev/null @@ -1,54 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.serialization; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; -import net.runelite.api.QuestState; - -import java.io.IOException; - -/** - * Gson TypeAdapter for QuestState enum serialization/deserialization. - * Stores quest states as their ordinal values for compact representation. - */ -public class QuestStateAdapter extends TypeAdapter { - - @Override - public void write(JsonWriter out, QuestState value) throws IOException { - if (value == null) { - out.nullValue(); - } else { - out.value(value.ordinal()); - } - } - - @Override - public QuestState read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - - // Handle both string names (legacy) and ordinal values (new format) - if (in.peek() == JsonToken.STRING) { - // Legacy format: enum name - String stateName = in.nextString(); - try { - return QuestState.valueOf(stateName); - } catch (IllegalArgumentException e) { - throw new IOException("Invalid quest state name: " + stateName, e); - } - } else { - // New format: ordinal value - int ordinal = in.nextInt(); - QuestState[] states = QuestState.values(); - - if (ordinal >= 0 && ordinal < states.length) { - return states[ordinal]; - } else { - throw new IOException("Invalid quest state ordinal: " + ordinal); - } - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SkillAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SkillAdapter.java deleted file mode 100644 index 85d6d36f708..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SkillAdapter.java +++ /dev/null @@ -1,54 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.serialization; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; -import net.runelite.api.Skill; - -import java.io.IOException; - -/** - * Gson TypeAdapter for Skill enum serialization/deserialization. - * Stores skills as their ordinal values for compact representation. - */ -public class SkillAdapter extends TypeAdapter { - - @Override - public void write(JsonWriter out, Skill value) throws IOException { - if (value == null) { - out.nullValue(); - } else { - out.value(value.ordinal()); - } - } - - @Override - public Skill read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - - // Handle both string names (legacy) and ordinal values (new format) - if (in.peek() == JsonToken.STRING) { - // Legacy format: enum name - String skillName = in.nextString(); - try { - return Skill.valueOf(skillName); - } catch (IllegalArgumentException e) { - throw new IOException("Invalid skill name: " + skillName, e); - } - } else { - // New format: ordinal value - int ordinal = in.nextInt(); - Skill[] skills = Skill.values(); - - if (ordinal >= 0 && ordinal < skills.length) { - return skills[ordinal]; - } else { - throw new IOException("Invalid skill ordinal: " + ordinal); - } - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SkillDataAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SkillDataAdapter.java deleted file mode 100644 index 71817024790..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SkillDataAdapter.java +++ /dev/null @@ -1,88 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.serialization; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; -import net.runelite.client.plugins.microbot.util.cache.model.SkillData; - -import java.io.IOException; - -/** - * Gson TypeAdapter for SkillData serialization/deserialization. - * Stores skill data as a compact array: [level, boostedLevel, experience, lastUpdated, previousLevel, previousExperience]. - * Previous values are optional and stored as null if not available. - */ -public class SkillDataAdapter extends TypeAdapter { - - @Override - public void write(JsonWriter out, SkillData value) throws IOException { - if (value == null) { - out.nullValue(); - } else { - out.beginArray(); - out.value(value.getLevel()); - out.value(value.getBoostedLevel()); - out.value(value.getExperience()); - out.value(value.getLastUpdated()); - - // Write previous level (nullable) - if (value.getPreviousLevel() != null) { - out.value(value.getPreviousLevel()); - } else { - out.nullValue(); - } - - // Write previous experience (nullable) - if (value.getPreviousExperience() != null) { - out.value(value.getPreviousExperience()); - } else { - out.nullValue(); - } - - out.endArray(); - } - } - - @Override - public SkillData read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - - in.beginArray(); - int level = in.nextInt(); - int boostedLevel = in.nextInt(); - int experience = in.nextInt(); - - // Handle backwards compatibility - check if more elements exist - long lastUpdated = System.currentTimeMillis(); - Integer previousLevel = null; - Integer previousExperience = null; - - if (in.hasNext()) { - lastUpdated = in.nextLong(); - - if (in.hasNext()) { - if (in.peek() != JsonToken.NULL) { - previousLevel = in.nextInt(); - } else { - in.nextNull(); - } - - if (in.hasNext()) { - if (in.peek() != JsonToken.NULL) { - previousExperience = in.nextInt(); - } else { - in.nextNull(); - } - } - } - } - - in.endArray(); - - return new SkillData(level, boostedLevel, experience, lastUpdated, previousLevel, previousExperience); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SpiritTreeDataAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SpiritTreeDataAdapter.java deleted file mode 100644 index b79eed59038..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SpiritTreeDataAdapter.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (c) 2024 Microbot - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package net.runelite.client.plugins.microbot.util.cache.serialization; - -import com.google.gson.*; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.questhelper.helpers.mischelpers.farmruns.CropState; -import net.runelite.client.plugins.microbot.util.cache.model.SpiritTreeData; -import net.runelite.client.plugins.microbot.util.farming.SpiritTree; - -import java.lang.reflect.Type; - -/** - * Gson adapter for SpiritTreeData serialization/deserialization. - * Handles safe serialization of spirit tree cache data for persistent storage. - */ -public class SpiritTreeDataAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(SpiritTreeData src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - try { - // Store patch as enum name for safe serialization - json.addProperty("patch", src.getSpiritTree().name()); - - // Store crop state as enum name (nullable) - if (src.getCropState() != null) { - json.addProperty("cropState", src.getCropState().name()); - } - - json.addProperty("availableForTravel", src.isAvailableForTravel()); - json.addProperty("lastUpdated", src.getLastUpdated()); - - // Store player location - if (src.getPlayerLocation() != null) { - JsonObject location = new JsonObject(); - location.addProperty("x", src.getPlayerLocation().getX()); - location.addProperty("y", src.getPlayerLocation().getY()); - location.addProperty("plane", src.getPlayerLocation().getPlane()); - json.add("playerLocation", location); - } - - // Store detection method flags - json.addProperty("detectedViaWidget", src.isDetectedViaWidget()); - json.addProperty("detectedViaNearBy", src.isDetectedViaNearBy()); - - // Remove farming level storage as it's no longer used - - // Store nearby entity IDs (optional, for debugging purposes only) - // Note: We don't serialize these as they're not persistent across sessions - - } catch (Exception e) { - // Create minimal fallback serialization - json.addProperty("patch", src.getSpiritTree().name()); - json.addProperty("availableForTravel", src.isAvailableForTravel()); - json.addProperty("lastUpdated", src.getLastUpdated()); - } - - return json; - } - - @Override - public SpiritTreeData deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - try { - // Required fields - String patchName = jsonObject.get("patch").getAsString(); - SpiritTree patch = SpiritTree.valueOf(patchName); - - boolean availableForTravel = jsonObject.get("availableForTravel").getAsBoolean(); - long lastUpdated = jsonObject.get("lastUpdated").getAsLong(); - - // Optional fields - CropState cropState = null; - if (jsonObject.has("cropState") && !jsonObject.get("cropState").isJsonNull()) { - cropState = CropState.valueOf(jsonObject.get("cropState").getAsString()); - } - - WorldPoint playerLocation = null; - if (jsonObject.has("playerLocation") && !jsonObject.get("playerLocation").isJsonNull()) { - JsonObject location = jsonObject.getAsJsonObject("playerLocation"); - playerLocation = new WorldPoint( - location.get("x").getAsInt(), - location.get("y").getAsInt(), - location.get("plane").getAsInt() - ); - } - - boolean detectedViaWidget = jsonObject.has("detectedViaWidget") ? - jsonObject.get("detectedViaWidget").getAsBoolean() : false; - - // Handle backward compatibility: check for old field names first, then new field name - boolean detectedViaNearBy = false; - if (jsonObject.has("detectedViaNearBy")) { - detectedViaNearBy = jsonObject.get("detectedViaNearBy").getAsBoolean(); - } else if (jsonObject.has("detectedViaNearPatch")) { - // Backward compatibility: migrate old field to new field - detectedViaNearBy = jsonObject.get("detectedViaNearPatch").getAsBoolean(); - } else if (jsonObject.has("detectedViaGameObject")) { - // Backward compatibility: migrate old field to new field - detectedViaNearBy = jsonObject.get("detectedViaGameObject").getAsBoolean(); - } - - // Ignore farmingLevel as it's no longer used - - // Create SpiritTreeData with preserved timestamp and available fields - return new SpiritTreeData( - patch, - cropState, - availableForTravel, - lastUpdated, - playerLocation, - detectedViaWidget, - detectedViaNearBy - ); - - } catch (Exception e) { - throw new JsonParseException("Failed to deserialize SpiritTreeData: " + e.getMessage(), e); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SpiritTreePatchAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SpiritTreePatchAdapter.java deleted file mode 100644 index c971bd8dc2f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SpiritTreePatchAdapter.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) 2024 Microbot - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package net.runelite.client.plugins.microbot.util.cache.serialization; - -import com.google.gson.*; -import net.runelite.client.plugins.microbot.util.farming.SpiritTree; - -import java.lang.reflect.Type; - -/** - * Gson adapter for SpiritTreePatch enum serialization/deserialization. - * Handles safe serialization of spirit tree patch enums for cache storage. - */ -public class SpiritTreePatchAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(SpiritTree src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive(src.name()); - } - - @Override - public SpiritTree deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - try { - String patchName = json.getAsString(); - return SpiritTree.valueOf(patchName); - } catch (IllegalArgumentException e) { - throw new JsonParseException("Unknown SpiritTreePatch: " + json.getAsString(), e); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/VarbitDataAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/VarbitDataAdapter.java deleted file mode 100644 index 96716d2d95f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/VarbitDataAdapter.java +++ /dev/null @@ -1,147 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.serialization; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.util.cache.model.VarbitData; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Gson TypeAdapter for VarbitData serialization/deserialization. - * Stores varbit data as a compact object with optional contextual information. - */ -public class VarbitDataAdapter extends TypeAdapter { - - @Override - public void write(JsonWriter out, VarbitData value) throws IOException { - if (value == null) { - out.nullValue(); - } else { - out.beginObject(); - - // Core data - out.name("value").value(value.getValue()); - out.name("lastUpdated").value(value.getLastUpdated()); - - // Previous value (nullable) - if (value.getPreviousValue() != null) { - out.name("previousValue").value(value.getPreviousValue()); - } - - // Player location (nullable) - if (value.getPlayerLocation() != null) { - out.name("location"); - out.beginArray(); - out.value(value.getPlayerLocation().getX()); - out.value(value.getPlayerLocation().getY()); - out.value(value.getPlayerLocation().getPlane()); - out.endArray(); - } - - // Nearby NPCs (optional) - if (!value.getNearbyNpcIds().isEmpty()) { - out.name("nearbyNpcs"); - out.beginArray(); - for (Integer npcId : value.getNearbyNpcIds()) { - out.value(npcId); - } - out.endArray(); - } - - // Nearby objects (optional) - if (!value.getNearbyObjectIds().isEmpty()) { - out.name("nearbyObjects"); - out.beginArray(); - for (Integer objectId : value.getNearbyObjectIds()) { - out.value(objectId); - } - out.endArray(); - } - - out.endObject(); - } - } - - @Override - public VarbitData read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - - int value = 0; - long lastUpdated = System.currentTimeMillis(); - Integer previousValue = null; - WorldPoint playerLocation = null; - List nearbyNpcIds = new ArrayList<>(); - List nearbyObjectIds = new ArrayList<>(); - - in.beginObject(); - while (in.hasNext()) { - String name = in.nextName(); - switch (name) { - case "value": - value = in.nextInt(); - break; - case "lastUpdated": - lastUpdated = in.nextLong(); - break; - case "previousValue": - if (in.peek() != JsonToken.NULL) { - previousValue = in.nextInt(); - } else { - in.nextNull(); - } - break; - case "location": - if (in.peek() != JsonToken.NULL) { - in.beginArray(); - int x = in.nextInt(); - int y = in.nextInt(); - int plane = in.nextInt(); - playerLocation = new WorldPoint(x, y, plane); - in.endArray(); - } else { - in.nextNull(); - } - break; - case "nearbyNpcs": - if (in.peek() != JsonToken.NULL) { - in.beginArray(); - while (in.hasNext()) { - nearbyNpcIds.add(in.nextInt()); - } - in.endArray(); - } else { - in.nextNull(); - } - break; - case "nearbyObjects": - if (in.peek() != JsonToken.NULL) { - in.beginArray(); - while (in.hasNext()) { - nearbyObjectIds.add(in.nextInt()); - } - in.endArray(); - } else { - in.nextNull(); - } - break; - default: - in.skipValue(); // Skip unknown fields for forwards compatibility - break; - } - } - in.endObject(); - - return new VarbitData(value, lastUpdated, previousValue, playerLocation, - nearbyNpcIds.isEmpty() ? Collections.emptyList() : nearbyNpcIds, - nearbyObjectIds.isEmpty() ? Collections.emptyList() : nearbyObjectIds); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/CacheOperations.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/CacheOperations.java deleted file mode 100644 index 45e35411e62..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/CacheOperations.java +++ /dev/null @@ -1,95 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy; - -import java.util.Map; -import java.util.stream.Stream; - -/** - * Interface providing controlled access to cache operations for strategies. - * This follows the framework guideline of providing limited, safe access to cache internals. - */ -public interface CacheOperations { - - /** - * Gets a value from the cache. - * - * @param key The key to retrieve - * @return The cached value or null if not found - */ - V get(K key); - - /** - * Gets a raw cached value without triggering additional cache operations like scene scans. - * This method bypasses any cache miss handling and returns only what's currently cached. - * Used by update strategies during scene synchronization to avoid recursive scanning. - * - * @param key The key to retrieve - * @return The raw cached value or null if not present in cache - */ - V getRawValue(K key); - - /** - * Puts a value into the cache. - * - * @param key The key to store - * @param value The value to store - */ - void put(K key, V value); - - /** - * Removes a specific key from the cache. - * - * @param key The key to remove - */ - void remove(K key); - - /** - * Invalidates all cached data. - */ - void invalidateAll(); - - /** - * Checks if the cache contains a specific key. - * - * @param key The key to check - * @return True if the key exists and is not expired - */ - boolean containsKey(K key); - - /** - * Gets the current size of the cache. - * - * @return The number of entries in the cache - */ - int size(); - - /** - * Provides a stream of all cache entries. - * This allows for efficient filtering and processing of cache contents. - * Note: The stream should be used within the same thread context and not cached. - * - * @return A stream of Map.Entry representing all cache entries - */ - Stream> entryStream(); - - /** - * Provides a stream of all cache keys. - * - * @return A stream of keys in the cache - */ - Stream keyStream(); - - /** - * Provides a stream of all cache values. - * - * @return A stream of values in the cache - */ - Stream valueStream(); - - /** - * Gets the name of this cache for logging and debugging. - * - * @return The cache name - */ - String getCacheName(); - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/CacheUpdateStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/CacheUpdateStrategy.java deleted file mode 100644 index 7316ac8c422..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/CacheUpdateStrategy.java +++ /dev/null @@ -1,64 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy; - -/** - * Strategy interface for handling cache updates based on events. - * Follows the game cache framework guidelines for pluggable cache enhancement. - * - * These strategies handle event-driven cache updates, enriching cache data - * with temporal information, contextual data, and change tracking rather - * than just invalidating entries. - * - * Implements AutoCloseable to ensure proper resource cleanup including - * shutdown of any background tasks, executor services, or other resources. - * - * @param Cache key type - * @param Cache value type - */ -public interface CacheUpdateStrategy extends AutoCloseable { - - /** - * Handles an event and potentially updates cache entries with enhanced data. - * - * @param event The event that occurred - * @param cache The cache to potentially update - */ - void handleEvent(Object event, CacheOperations cache); - - /** - * Gets the event types this strategy handles. - * - * @return Array of event classes this strategy processes - */ - Class[] getHandledEventTypes(); - - /** - * Called when the strategy is attached to a cache. - * - * @param cache The cache this strategy is attached to - */ - default void onAttach(CacheOperations cache) { - // Default: no action - } - - /** - * Called when the strategy is detached from a cache. - * - * @param cache The cache this strategy was attached to - */ - default void onDetach(CacheOperations cache) { - // Default: no action - } - - /** - * Closes this strategy and releases any resources such as scheduled tasks, - * executor services, or other background processing. - * - * Default implementation does nothing - strategies that use resources - * should override this method to ensure proper cleanup. - */ - @Override - default void close() { - // Default: no action - } -} - diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/PredicateQuery.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/PredicateQuery.java deleted file mode 100644 index 2f0c8359368..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/PredicateQuery.java +++ /dev/null @@ -1,48 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy; - -import java.util.function.Predicate; - -/** - * Predicate-based query criteria for simple filtering. - * - * @param The value type to filter - */ -public class PredicateQuery implements QueryCriteria { - - private final Predicate predicate; - private final String description; - - /** - * Creates a new predicate query. - * - * @param predicate The predicate to apply - * @param description A description of what this query does - */ - public PredicateQuery(Predicate predicate, String description) { - this.predicate = predicate; - this.description = description; - } - - /** - * Gets the predicate for this query. - * - * @return The predicate function - */ - public Predicate getPredicate() { - return predicate; - } - - /** - * Gets the description of this query. - * - * @return The query description - */ - public String getDescription() { - return description; - } - - @Override - public String getQueryType() { - return "PREDICATE:" + description; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/QueryCriteria.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/QueryCriteria.java deleted file mode 100644 index 6429159c07a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/QueryCriteria.java +++ /dev/null @@ -1,15 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy; - -/** - * Base interface for query criteria. - * Specific strategies can define their own criteria types. - */ -public interface QueryCriteria { - - /** - * Gets the type identifier for this query criteria. - * - * @return The query type string - */ - String getQueryType(); -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/QueryStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/QueryStrategy.java deleted file mode 100644 index 3a72070939d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/QueryStrategy.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy; - -import java.util.stream.Stream; - -/** - * Strategy interface for specialized cache queries. - * Allows caches to support complex queries without inheritance. - * - * @param Cache key type - * @param Cache value type - */ -public interface QueryStrategy { - - /** - * Executes a query against the cache. - * - * @param cache The cache to query - * @param criteria The query criteria - * @return Stream of matching values - */ - Stream executeQuery(CacheOperations cache, QueryCriteria criteria); - - /** - * Gets the query types this strategy supports. - * - * @return Array of supported query criteria classes - */ - Class[] getSupportedQueryTypes(); -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/ValueWrapper.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/ValueWrapper.java deleted file mode 100644 index f6100019498..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/ValueWrapper.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy; - -/** - * Strategy interface for wrapping values before storing them in cache. - * Allows adding metadata, spawn tracking, etc. without inheritance. - * - * @param The original value type - * @param The wrapped value type - */ -public interface ValueWrapper { - - /** - * Wraps a value before storing in cache. - * - * @param value The original value - * @param key The cache key for context - * @return The wrapped value - */ - W wrap(V value, Object key); - - /** - * Unwraps a value when retrieving from cache. - * - * @param wrappedValue The wrapped value from cache - * @return The original value - */ - V unwrap(W wrappedValue); - - /** - * Gets the wrapped value type for type safety. - * - * @return The wrapped type class - */ - Class getWrappedType(); -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/GroundItemUpdateStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/GroundItemUpdateStrategy.java deleted file mode 100644 index aa3116ac49a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/GroundItemUpdateStrategy.java +++ /dev/null @@ -1,536 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy.entity; - -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; -import java.util.concurrent.ScheduledExecutorService; - -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.*; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.ItemDespawned; -import net.runelite.api.events.ItemSpawned; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheOperations; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheUpdateStrategy; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; -import net.runelite.client.plugins.microbot.util.cache.Rs2GroundItemCache; - -/** - * Enhanced cache update strategy for ground item data. - * Handles automatic cache updates based on ground item spawn/despawn events and provides scene scanning. - * Follows the same pattern as ObjectUpdateStrategy for consistency. - */ -@Slf4j -public class GroundItemUpdateStrategy implements CacheUpdateStrategy { - - GameState lastGameState = null; - - // ScheduledExecutorService for non-blocking operations - private final ScheduledExecutorService executorService; - private ScheduledFuture periodicSceneScanTask; - private ScheduledFuture sceneScanTask; - - // Scene scan tracking - private final AtomicBoolean scanActive = new AtomicBoolean(false); - private final AtomicBoolean scanRequest = new AtomicBoolean(false); - private static final long MIN_SCAN_INTERVAL_MS = Constants.GAME_TICK_LENGTH; // Minimum interval between scans - private volatile long lastSceneScan = 0; // Last time a scene scan was performed - - /** - * Constructor initializes the executor service for background tasks. - */ - public GroundItemUpdateStrategy() { - this.executorService = Executors.newScheduledThreadPool(2, r -> { - Thread thread = new Thread(r, "GroundItemUpdateStrategy-" + System.currentTimeMillis()); - thread.setDaemon(true); - return thread; - }); - log.debug("GroundItemUpdateStrategy initialized with ScheduledExecutorService"); - } - - @Override - public void handleEvent(Object event, CacheOperations cache) { - if (executorService == null || executorService.isShutdown() || !Microbot.isLoggedIn() || Microbot.getClient() == null || Microbot.getClient().getLocalPlayer() == null) { - log.warn("GroundItemUpdateStrategy is shut down or not logged in, ignoring event: {}", event.getClass().getSimpleName()); - return; // Don't process events if shut down - } - if (scanActive.get()){ - log.debug("Skipping event processing - scan already active: {}", event.getClass().getSimpleName()); - return; // Don't process events if a scan is already active - } - // Submit event handling to executor service for non-blocking processing - //executorService.submit(() -> { - // try { - processEventInternal(event, cache); - //} catch (Exception e) { - // log.error("Error processing event: {}", event.getClass().getSimpleName(), e); - //} - //}); - } - - /** - * Internal method to process events - runs on background thread. - */ - private void processEventInternal(Object event, CacheOperations cache) { - if (event instanceof ItemSpawned) { - handleItemSpawned((ItemSpawned) event, cache); - } else if (event instanceof ItemDespawned) { - handleItemDespawned((ItemDespawned) event, cache); - } else if (event instanceof GameStateChanged) { - handleGameStateChanged((GameStateChanged) event, cache); - } - } - - /** - * Performs an scene scan to populate the cache. - * Only scans if certain conditions are met to avoid unnecessary processing. - * This method runs asynchronously on a background thread to avoid blocking. - * - * @param cache The cache to populate - */ - public void performSceneScan(CacheOperations cache,long delayMs) { - if (executorService == null || executorService.isShutdown()) { - log.debug("Skipping ground item scene scan - strategy is shut down or not logged in"); - return; - } - - // Respect minimum scan interval unless forced - if ((System.currentTimeMillis() - lastSceneScan) < MIN_SCAN_INTERVAL_MS) { - log.debug("Skipping ground item scene scan due to minimum interval not reached"); - scanActive.set(false); - return; - } - - if (sceneScanTask != null && !sceneScanTask.isDone()) { - log.debug("Skipping ground item scene scan - already scheduled or running"); - return; // Don't perform scan if already scheduled or running - } - - if (scanActive.compareAndSet(false, true)) { - // Submit scene scan to executor service for non-blocking execution - sceneScanTask = executorService.schedule(() -> { - try { - performSceneScanInternal(cache); - } catch (Exception e) { - log.error("Error during ground item scene scan", e); - } finally { - scanActive.set(false); - } - }, delayMs, TimeUnit.MILLISECONDS); // delay before scan - } else { - log.debug("Skipping ground item scene scan - already active"); - return; // Don't perform scan if already active - } - } - - /** - * Schedules a periodic scene scan task. - * This is useful for ensuring the cache stays up-to-date even if events are missed. - * - * @param cache The cache to scan - * @param intervalSeconds How often to scan in seconds - */ - public void schedulePeriodicSceneScan(CacheOperations cache, long intervalSeconds) { - if (executorService == null || executorService.isShutdown()) { - log.warn("Cannot schedule periodic scan - strategy is shut down"); - return; - } - - // Cancel existing task if any - stopPeriodicSceneScan(); - - periodicSceneScanTask = executorService.scheduleWithFixedDelay(() -> { - try { - if (scanActive.compareAndSet(false, true)) { - if (scanRequest.get() && Microbot.isLoggedIn()) { - log.debug("Periodic ground item scene scan triggered"); - performSceneScanInternal(cache); - } - } else { - log.debug("Skipping scheduled ground item scene scan - already active"); - } - } catch (Exception e) { - log.error("Error in periodic ground item scene scan", e); - } finally { - scanActive.set(false); - } - }, intervalSeconds, intervalSeconds, TimeUnit.SECONDS); - - log.debug("Scheduled periodic ground item scene scan every {} seconds", intervalSeconds); - } - - /** - * Stops periodic scene scanning. - */ - public void stopPeriodicSceneScan() { - if (periodicSceneScanTask != null && !periodicSceneScanTask.isDone()) { - periodicSceneScanTask.cancel(false); - periodicSceneScanTask = null; - log.debug("Stopped periodic ground item scene scanning"); - } - } - - /** - * Checks if a scene scan is needed based on cache state. - * - * @param cache The cache to check - * @return true if a scan would be beneficial - */ - public boolean requestSceneScan(CacheOperations cache) { - if (scanActive.get()) { - log.debug("Skipping scene scan request - already active"); - return false; // Don't request scan if already active - } - if (scanRequest.compareAndSet(false, true)) { - log.debug("Ground item scene scan requested"); - performSceneScan(cache, 5); // Perform with 5ms delay for stability - } - if (!Microbot.getClient().isClientThread()){ - sleepUntil(()->!scanRequest.get(), 1000); // Wait until scan is requested and reset - } - return !scanRequest.get(); // Return true if scan was requested,reseted - } - - /** - * Performs the actual scene scan to synchronize ground items with the current scene. - * This method both adds new items found in the scene AND removes cached items no longer present. - * Provides complete scene synchronization in a single operation. - */ - private void performSceneScanInternal(CacheOperations cache) { - try { - long currentTime = System.currentTimeMillis(); - - // Check minimum interval to prevent excessive scanning - if (currentTime - lastSceneScan < MIN_SCAN_INTERVAL_MS) { - log.debug("Skipping Ground scene scan - too soon since last scan"); - scanActive.set(false); - return; - } - Player player = Microbot.getClient().getLocalPlayer(); - if (player == null) { - log.debug("Cannot perform ground item scene scan - no player"); - scanActive.set(false); - return; - } - - Scene scene = player.getWorldView().getScene(); - if (scene == null) { - log.debug("Cannot perform ground item scene scan - no scene"); - scanActive.set(false); - return; - } - - Tile[][][] tiles = scene.getTiles(); - if (tiles == null) { - log.debug("Cannot perform ground item scene scan - no tiles"); - scanActive.set(false); - return; - } - - // Build a set of all currently existing ground item keys from the scene - java.util.Set currentSceneKeys = new java.util.HashSet<>(); - int addedItems = 0; - int z = player.getWorldView().getPlane(); - - log.debug("Starting ground item scene synchronization (cache size: {})", cache.size()); - - // Phase 1: Scan scene and add new items - for (int x = 0; x < Constants.SCENE_SIZE; x++) { - for (int y = 0; y < Constants.SCENE_SIZE; y++) { - Tile tile = tiles[z][x][y]; - if (tile == null) continue; - if(tile.getGroundItems() == null) continue; // Ensure ground items are loaded - - // Get ground items on this tile - for (TileItem tileItem : tile.getGroundItems()) { - if (tileItem != null) { - String key = generateKey(tileItem, tile.getWorldLocation()); - currentSceneKeys.add(key); // Track all scene items - - // Only add if not already in cache to avoid recursive calls - if (!cache.containsKey(key)) { - Rs2GroundItemModel groundItemModel = new Rs2GroundItemModel(tileItem, tile); - cache.put(key, groundItemModel); - addedItems++; - } - } - } - } - } - - // Phase 2: Remove cached items no longer in scene - int removedItems = 0; - if (!currentSceneKeys.isEmpty()) { - // Find cached items that are no longer in the scene using CacheOperations streaming - List keysToRemove = cache.entryStream() - .map(java.util.Map.Entry::getKey) - .filter(key -> !currentSceneKeys.contains(key)) - .collect(Collectors.toList()); - - // Remove the items that are no longer in scene - for (String key : keysToRemove) { - Rs2GroundItemModel item = cache.getRawValue(key); // Use raw value to avoid triggering recursive scene scans - cache.remove(key); - if (item != null) { - removedItems++; - log.trace("Removed ground item not in scene: ID {} ({})", item.getId(), key); - } - } - } - - // Log comprehensive results - if (addedItems > 0 || removedItems > 0) { - log.debug("Ground item scene synchronization completed - added {} items, removed {} items (total cache size: {})", - addedItems, removedItems, cache.size()); - } else { - log.debug("Ground item scene synchronization completed - no changes made"); - } - - scanRequest.set(false); //NOT in finally block to allow for rescan if there are an error - }catch (Exception e) { - log.error("Error during ground item scene synchronization", e); - }finally { - scanActive.set(false); - lastSceneScan = System.currentTimeMillis(); // Update last scan time - } - } - - private void handleItemSpawned(ItemSpawned event, CacheOperations cache) { - TileItem item = event.getItem(); - if (item != null) { - String key = generateKey(item, event.getTile().getWorldLocation()); - Rs2GroundItemModel groundItem = new Rs2GroundItemModel(item, event.getTile()); - cache.put(key, groundItem); - log.trace("Added ground item {} at {} to cache via spawn event", item.getId(), event.getTile().getWorldLocation()); - } - } - - private void handleItemDespawned(ItemDespawned event, CacheOperations cache) { - TileItem item = event.getItem(); - //Rs2GroundItemModel groundItem = new Rs2GroundItemModel(item, event.getTile()); - if (item != null) { - String key = generateKey(item, event.getTile().getWorldLocation()); - cache.remove(key); - log.trace("Removed ground item {} at {} from cache via despawn event", item.getId(), event.getTile().getWorldLocation()); - } - } - /** - * Cleanup persistent items that don't naturally despawn. - * This method follows the same pattern as performSceneScan with proper async execution. - * - * @param cache The cache operations interface - * @param delayMs Delay before performing the cleanup - */ - public void cleanupPersistentItems(CacheOperations cache, long delayMs) { - if (executorService == null || executorService.isShutdown()) { - log.debug("Skipping persistent item cleanup - strategy is shut down"); - return; - } - - // Submit cleanup to executor service for non-blocking execution - executorService.schedule(() -> { - try { - cleanupPersistentItemsInternal(cache); - } catch (Exception e) { - log.error("Error during persistent item cleanup", e); - } - }, delayMs, TimeUnit.MILLISECONDS); - } - - /** - * Internal method that performs the actual persistent item cleanup. - * Directly removes persistent ground items that don't naturally despawn without scene scanning. - * - * @param cache The cache operations interface - * @return The number of persistent items cleaned up - */ - private int cleanupPersistentItemsInternal(CacheOperations cache) { - // Use CacheOperations streaming to find and remove persistent items - List keysToRemove = cache.entryStream() - .filter(entry -> { - Rs2GroundItemModel item = entry.getValue(); - // Check if this is a persistent item (using isPersistened method) - return item.isPersistened(); - }) - .map(java.util.Map.Entry::getKey) - .collect(Collectors.toList()); - - if (keysToRemove.isEmpty()) { - log.debug("No persistent ground items found to cleanup"); - return 0; - } - - // Remove all persistent items directly - int removedCount = 0; - for (String key : keysToRemove) { - Rs2GroundItemModel item = cache.getRawValue(key); // Use raw value to avoid triggering recursive scene scans - cache.remove(key); - if (item != null) { - removedCount++; - log.trace("Removed persistent ground item: ID {} ({})", item.getId(), key); - } - } - - log.debug("Cleaned up {} persistent ground items", removedCount); - return removedCount; - } - - /** - * Handles game state changes for ground item cache management. - * - *

Ground Item Despawn Handling Strategy:

- *
    - *
  • Unlike NPCs, ground items have complex despawn timing that isn't always captured by {@link net.runelite.api.events.ItemDespawned}
  • - *
  • Items can despawn based on game ticks elapsed since spawn time, which may not trigger ItemDespawned events
  • - *
  • We rely on {@link Rs2GroundItemCache#performPeriodicCleanup()} to check {@link Rs2GroundItemModel#isDespawned()}
  • - *
  • The {@link Rs2GroundItemCache#isExpired(String)} method integrates despawn timing directly into cache operations
  • - *
  • This dual approach ensures expired ground items are removed even when events are missed
  • - *
- * - *

Why scene scanning is essential for ground items:

- *
    - *
  • Ground items have complex despawn mechanics not always captured by ItemDespawned events
  • - *
  • Persistent items (player drops, quest items) may have indefinite lifespans requiring special detection
  • - *
  • Events can be missed during region changes, network issues, or client restarts
  • - *
  • Scene scanning ensures cache synchronization with actual game state after login/loading
  • - *
  • The {@link #performSceneScan(CacheOperations, long)} method provides complete scene-to-cache synchronization
  • - *
- * - *

Persistent Item Handling:

- *
    - *
  • Persistent items are detected using {@link Rs2GroundItemModel#isPersistened()}
  • - *
  • Scene scanning includes cleanup of persistent items that should no longer exist
  • - *
  • Combines natural despawn cleanup with scene validation for comprehensive item management
  • - *
- * - *

Game State Specific Actions:

- *
    - *
  • LOGGED_IN/LOADING: Perform scene scan with 2-tick delay for stability and cleanup persistent items
  • - *
  • LOGOUT States: Cancel ongoing scan operations and invalidate entire cache
  • - *
  • Other States: Update state tracking without additional actions
  • - *
- * - * @param event The game state change event - * @param cache The ground item cache operations interface - */ - private void handleGameStateChanged(GameStateChanged event, CacheOperations cache) { - switch (event.getGameState()) { - case LOGGED_IN: - // Ground item despawn handling is managed by Rs2GroundItemCache.performPeriodicCleanup() - // and Rs2GroundItemCache.isExpired() using Rs2GroundItemModel.isDespawned() - lastGameState = GameState.LOGGED_IN; - log.debug("Player logged in - ground item despawn handled by periodic cleanup"); - - // Perform scene scan to synchronize cache with current scene and cleanup persistent items - performSceneScan(cache, Constants.GAME_TICK_LENGTH*2); // 2 ticks delay for stability - break; - case LOADING: - // Ground item despawn handling is managed by Rs2GroundItemCache.performPeriodicCleanup() - // and Rs2GroundItemCache.isExpired() using Rs2GroundItemModel.isDespawned() - lastGameState = GameState.LOADING; - log.debug("Game loading - ground item despawn handled by periodic cleanup"); - - // Perform scene scan after loading completes and cleanup persistent items - performSceneScan(cache, Constants.GAME_TICK_LENGTH*2); // 2 ticks delay for stability - break; - case LOGIN_SCREEN: - case LOGGING_IN: - case CONNECTION_LOST: - if (sceneScanTask != null && !sceneScanTask.isDone()) { - sceneScanTask.cancel(true); - sceneScanTask = null; - } - // Clear scan request when logging out and stop periodic scanning - scanRequest.set(false); // Reset scan request - cache.invalidateAll(); - lastGameState = event.getGameState(); - log.debug("Player logged out - cleared ground item cache and stopped operations"); - break; - default: - lastGameState = event.getGameState(); - break; - } - } - - /** - * Generates a unique key for ground items based on item ID, quantity, and location. - * - * @param item The tile item - * @param location The world location - * @return Unique key string - */ - private String generateKey(TileItem item, net.runelite.api.coords.WorldPoint location) { - return String.format("%d_%d_%d_%d_%d", - item.getId(), - item.getQuantity(), - location.getX(), - location.getY(), - location.getPlane()); - } - - @Override - public Class[] getHandledEventTypes() { - return new Class[]{ItemSpawned.class, ItemDespawned.class, GameStateChanged.class}; - } - - @Override - public void onAttach(CacheOperations cache) { - log.debug("GroundItemUpdateStrategy attached to cache"); - } - - @Override - public void onDetach(CacheOperations cache) { - log.debug("GroundItemUpdateStrategy detached from cache"); - // Cancel periodic scanning when detaching - if (periodicSceneScanTask != null && !periodicSceneScanTask.isDone()) { - periodicSceneScanTask.cancel(false); - periodicSceneScanTask = null; - } - } - - @Override - public void close() { - log.debug("Shutting down GroundItemUpdateStrategy"); - stopPeriodicSceneScan(); - if (sceneScanTask != null && !sceneScanTask.isDone()) { - sceneScanTask.cancel(false); - sceneScanTask = null; - log.debug("Cancelled active ground item scene scan task"); - } - shutdownExecutorService(); - } - /** - * Shuts down the executor service gracefully, waiting for currently executing tasks to complete. - * If the executor does not terminate within the initial timeout, it attempts a forced shutdown. - * Logs warnings or errors if the shutdown process does not complete as expected. - * If interrupted during shutdown, the method forces shutdown and re-interrupts the current thread. - */ - private void shutdownExecutorService() { - if (executorService != null && !executorService.isShutdown()) { - // Shutdown executor service - executorService.shutdown(); - try { - if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { - log.warn("Executor service did not terminate gracefully, forcing shutdown"); - executorService.shutdownNow(); - - // Wait a bit more for tasks to respond to being cancelled - if (!executorService.awaitTermination(2, TimeUnit.SECONDS)) { - log.error("Executor service did not terminate after forced shutdown"); - } - } - } catch (InterruptedException e) { - log.warn("Interrupted during executor shutdown", e); - executorService.shutdownNow(); - Thread.currentThread().interrupt(); - } - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/NpcUpdateStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/NpcUpdateStrategy.java deleted file mode 100644 index d1561dd703f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/NpcUpdateStrategy.java +++ /dev/null @@ -1,390 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy.entity; - -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.ScheduledExecutorService; - -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.*; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.NpcDespawned; -import net.runelite.api.events.NpcSpawned; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheUpdateStrategy; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheOperations; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; - -/** - * Enhanced cache update strategy for NPC data. - * Handles NPC spawn/despawn events and provides scene scanning. - * Follows the same pattern as ObjectUpdateStrategy for consistency. - */ -@Slf4j -public class NpcUpdateStrategy implements CacheUpdateStrategy { - - GameState lastGameState = null; - - // ScheduledExecutorService for non-blocking operations - private final ScheduledExecutorService executorService; - private ScheduledFuture periodicSceneScanTask; - private ScheduledFuture sceneScanTask; - - // Scene scan tracking - private final AtomicBoolean scanActive = new AtomicBoolean(false); - private final AtomicBoolean scanRequest = new AtomicBoolean(false); - private volatile long lastSceneScan = 0; - private static final long MIN_SCAN_INTERVAL_MS = Constants.GAME_TICK_LENGTH; - - /** - * Constructor initializes the executor service for background tasks. - */ - public NpcUpdateStrategy() { - this.executorService = Executors.newScheduledThreadPool(2, r -> { - Thread thread = new Thread(r, "NpcUpdateStrategy-" + System.currentTimeMillis()); - thread.setDaemon(true); - return thread; - }); - log.debug("NpcUpdateStrategy initialized with ScheduledExecutorService"); - } - - @Override - public void handleEvent(final Object event, CacheOperations cache) { - if (executorService == null || executorService.isShutdown()) { - return; // Don't process events if shut down - } - processEventInternal(event, cache); - } - - /** - * Internal method to process events - runs on background thread. - */ - private void processEventInternal(Object event, CacheOperations cache) { - if (event instanceof NpcSpawned) { - handleNpcSpawned((NpcSpawned) event, cache); - } else if (event instanceof NpcDespawned) { - handleNpcDespawned((NpcDespawned) event, cache); - } else if (event instanceof GameStateChanged) { - handleGameStateChanged((GameStateChanged) event, cache); - } - } - - /** - * Performs an scene scan to populate the cache. - * Only scans if certain conditions are met to avoid unnecessary processing. - * This method runs asynchronously on a background thread to avoid blocking. - * - * @param cache The cache to populate - */ - public void performSceneScan(CacheOperations cache, long delayMs) { - if (executorService == null || executorService.isShutdown() ) { - log.debug("Skipping NPC scene scan - strategy is shut down or not logged in"); - return; - } - - // Respect minimum scan interval unless forced - if ((System.currentTimeMillis() - lastSceneScan) < MIN_SCAN_INTERVAL_MS) { - log.debug("Skipping NPC scene scan due to minimum interval not reached"); - scanActive.set(false); - return; - } - - if (sceneScanTask != null && !sceneScanTask.isDone()) { - log.debug("Skipping NPC scene scan - already scheduled or running"); - return; // Don't perform scan if already scheduled or running - } - - if (scanActive.compareAndSet(false, true)) { - // Submit scene scan to executor service for non-blocking execution - sceneScanTask = executorService.schedule(() -> { - try { - performSceneScanInternal(cache); - } catch (Exception e) { - log.error("Error during NPC scene scan", e); - } finally { - scanActive.set(false); - scanRequest.set(false); // Reset request after scan - } - }, delayMs, TimeUnit.MILLISECONDS); // delay before scan - } else { - log.debug("Skipping NPC scene scan - already active"); - return; // Don't perform scan if already active - } - } - - /** - * Schedules a periodic scene scan task. - * This is useful for ensuring the cache stays up-to-date even if events are missed. - * - * @param cache The cache to scan - * @param intervalSeconds How often to scan in seconds - */ - public void schedulePeriodicSceneScan(CacheOperations cache, long intervalSeconds) { - if (executorService == null || executorService.isShutdown()) { - log.warn("Cannot schedule periodic scan - strategy is shut down"); - return; - } - - // Cancel existing task if any - stopPeriodicSceneScan(); - - periodicSceneScanTask = executorService.scheduleWithFixedDelay(() -> { - try { - if (scanActive.compareAndSet(false, true)) { - if (scanRequest.get() && Microbot.isLoggedIn()) { - log.debug("Periodic NPC scene scan triggered"); - performSceneScanInternal(cache); - } - } else { - log.debug("Skipping scheduled NPC scene scan - already active"); - } - } catch (Exception e) { - log.error("Error in periodic NPC scene scan", e); - } finally { - scanActive.set(false); - } - }, intervalSeconds, intervalSeconds, TimeUnit.SECONDS); - - log.debug("Scheduled periodic NPC scene scan every {} seconds", intervalSeconds); - } - - /** - * Stops periodic scene scanning. - */ - public void stopPeriodicSceneScan() { - if (periodicSceneScanTask != null && !periodicSceneScanTask.isDone()) { - periodicSceneScanTask.cancel(false); - periodicSceneScanTask = null; - log.debug("Stopped periodic NPC scene scanning"); - } - } - - - /** - * Checks if a scene scan is needed based on cache state. - * - * @param cache The cache to check - * @return true if a scan would be beneficial - */ - public boolean requestSceneScan(CacheOperations cache) { - if (scanActive.get()) { - log.debug("Skipping scene scan request - already active"); - return false; // Don't request scan if already active - } - if (scanRequest.compareAndSet(false, true)) { - log.debug("NPC scene scan requested"); - performSceneScan(cache, 5); // Perform with 5ms delay for stability - } - if (!Microbot.getClient().isClientThread()){ - sleepUntil(()->!scanRequest.get(), 1000); // Wait until scan is requested and reset - } - return !scanRequest.get(); // Return true if scan was requested,reseted - } - - /** - * Performs the actual scene scan to synchronize NPCs with the current scene. - * This method both adds new NPCs found in the scene AND removes cached NPCs no longer present. - * Provides complete scene synchronization in a single operation. - */ - private void performSceneScanInternal(CacheOperations cache) { - try { - long currentTime = System.currentTimeMillis(); - - // Check minimum interval to prevent excessive scanning - if (currentTime - lastSceneScan < MIN_SCAN_INTERVAL_MS) { - log.debug("Skipping NPC scene scan - too soon since last scan"); - scanActive.set(false); - return; - } - - Player player = Microbot.getClient().getLocalPlayer(); - if (player == null) { - log.debug("Cannot perform NPC scene scan - no player"); - scanActive.set(false); - return; - } - - // Build a set of all currently existing NPC indices from the scene - java.util.Set currentSceneIndices = new java.util.HashSet<>(); - int addedNpcs = 0; - log.debug("Starting NPC scene synchronization (cache size: {})", cache.size()); - - // Phase 1: Scan scene and add new NPCs - for (NPC npc : Microbot.getClient().getTopLevelWorldView().npcs()) { - if (npc != null && npc.getId() != -1) { // Use ID instead of getName() to avoid client thread requirement - currentSceneIndices.add(npc.getIndex()); // Track all scene NPCs - - // Only add if not already in cache to avoid recursive calls - if (!cache.containsKey(npc.getIndex())) { - Rs2NpcModel npcModel = new Rs2NpcModel(npc); - cache.put(npc.getIndex(), npcModel); - addedNpcs++; - } - } - } - - // Phase 2: Remove cached NPCs no longer in scene - int removedNpcs = 0; - if (!currentSceneIndices.isEmpty()) { - // Find cached NPCs that are no longer in the scene using CacheOperations streaming - java.util.List keysToRemove = cache.entryStream() - .map(java.util.Map.Entry::getKey) - .filter(key -> !currentSceneIndices.contains(key)) - .collect(java.util.stream.Collectors.toList()); - - // Remove the NPCs that are no longer in scene - for (Integer key : keysToRemove) { - // Use raw cached value to avoid triggering recursive scene scans - Rs2NpcModel npc = cache.getRawValue(key); - cache.remove(key); - if (npc != null) { - removedNpcs++; - log.trace("Removed NPC not in scene: ID {} (index: {})", npc.getId(), key); - } - } - } - - // Log comprehensive results - if (addedNpcs > 0 || removedNpcs > 0) { - log.debug("NPC scene synchronization completed - added {} NPCs, removed {} NPCs (total cache size: {})", - addedNpcs, removedNpcs, cache.size()); - } else { - log.debug("NPC scene synchronization completed - no changes made"); - } - scanRequest.set(false); // Reset request after scan - } catch (Exception e) { - log.error("Error during NPC scene synchronization", e); - } finally { - lastSceneScan = System.currentTimeMillis(); // Update last scan time; - scanActive.set(false); - } - } - - private void handleNpcSpawned(NpcSpawned event, CacheOperations cache) { - NPC npc = event.getNpc(); - if (npc != null) { - Rs2NpcModel npcModel = new Rs2NpcModel(npc); - cache.put(npc.getIndex(), npcModel); - log.trace("Added NPC ID {} (index: {}) to cache via spawn event", npc.getId(), npc.getIndex()); - } - } - - private void handleNpcDespawned(NpcDespawned event, CacheOperations cache) { - NPC npc = event.getNpc(); - if (npc != null) { - cache.remove(npc.getIndex()); - log.trace("Removed NPC ID {} (index: {}) from cache via despawn event", npc.getId(), npc.getIndex()); - } - } - - /** - * Handles game state changes for NPC cache management. - * - *

Why NPCs don't require scene scanning on LOGGED_IN/LOADING:

- *
    - *
  • NPCs are automatically managed by the RuneLite client's event system
  • - *
  • {@link net.runelite.api.events.NpcSpawned} events are reliably triggered when NPCs appear
  • - *
  • {@link net.runelite.api.events.NpcDespawned} events are reliably triggered when NPCs disappear
  • - *
  • Region changes automatically trigger proper spawn/despawn events for all NPCs
  • - *
  • This makes manual scene scanning unnecessary and potentially wasteful
  • - *
- * - *

This is different from ground items, which have timing-based despawn mechanics - * that require additional handling through periodic cleanup.

- * - * @param event The game state change event - * @param cache The NPC cache operations interface - */ - private void handleGameStateChanged(GameStateChanged event, CacheOperations cache) { - switch (event.getGameState()) { - case LOGGED_IN: - // NPCs are handled entirely by spawn/despawn events - no manual scanning needed - lastGameState = GameState.LOGGED_IN; - log.debug("Player logged in - NPC events will handle population automatically"); - break; - case LOADING: - // Region changes during loading automatically trigger NPC despawn/spawn events - lastGameState = GameState.LOADING; - log.debug("Game loading - NPC events will handle region change automatically"); - break; - case LOGIN_SCREEN: - case LOGGING_IN: - case CONNECTION_LOST: - // Clear any ongoing operations and invalidate cache on logout - if (sceneScanTask != null && !sceneScanTask.isDone()) { - sceneScanTask.cancel(true); - sceneScanTask = null; - } - cache.invalidateAll(); - lastGameState = event.getGameState(); - log.debug("Player logged out - cleared NPC cache and stopped operations"); - break; - default: - lastGameState = event.getGameState(); - break; - } - } - - @Override - public Class[] getHandledEventTypes() { - return new Class[]{NpcSpawned.class, NpcDespawned.class, GameStateChanged.class}; - } - - @Override - public void onAttach(CacheOperations cache) { - log.debug("NpcUpdateStrategy attached to cache"); - } - - @Override - public void onDetach(CacheOperations cache) { - log.debug("NpcUpdateStrategy detached from cache"); - // Cancel periodic scanning when detaching - if (periodicSceneScanTask != null && !periodicSceneScanTask.isDone()) { - periodicSceneScanTask.cancel(false); - periodicSceneScanTask = null; - } - } - - @Override - public void close() { - log.debug("Shutting down NpcUpdateStrategy"); - stopPeriodicSceneScan(); - if (sceneScanTask != null && !sceneScanTask.isDone()) { - sceneScanTask.cancel(false); - sceneScanTask = null; - log.debug("Cancelled active NPC scene scan task"); - } - shutdownExecutorService(); - } - /** - * Shuts down the executor service gracefully, waiting for currently executing tasks to complete. - * If the executor does not terminate within the initial timeout, it attempts a forced shutdown. - * Logs warnings or errors if the shutdown process does not complete as expected. - * If interrupted during shutdown, the method forces shutdown and re-interrupts the current thread. - */ - private void shutdownExecutorService() { - if (executorService != null && !executorService.isShutdown()) { - // Shutdown executor service - executorService.shutdown(); - try { - if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { - log.warn("Executor service did not terminate gracefully, forcing shutdown"); - executorService.shutdownNow(); - - // Wait a bit more for tasks to respond to being cancelled - if (!executorService.awaitTermination(2, TimeUnit.SECONDS)) { - log.error("Executor service did not terminate after forced shutdown"); - } - } - } catch (InterruptedException e) { - log.warn("Interrupted during executor shutdown", e); - executorService.shutdownNow(); - Thread.currentThread().interrupt(); - } - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/ObjectUpdateStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/ObjectUpdateStrategy.java deleted file mode 100644 index 375385c1157..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/ObjectUpdateStrategy.java +++ /dev/null @@ -1,720 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy.entity; - -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.ScheduledExecutorService; - -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.*; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.*; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.questhelper.steps.tools.QuestPerspective; -import net.runelite.client.plugins.microbot.util.cache.Rs2Cache; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheOperations; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheUpdateStrategy; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -/** - * Enhanced cache update strategy for game object data. - * Handles all types of object spawn/despawn events and provides scene scanning. - * Uses String-based cache keys for better tracking and region change handling. - */ -@Slf4j -public class ObjectUpdateStrategy implements CacheUpdateStrategy { - GameState lastGameState = null; - - // ScheduledExecutorService for non-blocking operations - private final ScheduledExecutorService executorService; - private ScheduledFuture periodicSceneScanTask; - private ScheduledFuture sceneScanTask; - AtomicBoolean scanActive = new AtomicBoolean(false); - AtomicBoolean scanRequest = new AtomicBoolean(false); - private volatile long lastSceneScan = 0; - private static final long MIN_SCAN_INTERVAL_MS = Constants.GAME_TICK_LENGTH; - - /** - * Constructor initializes the executor service for background tasks. - */ - public ObjectUpdateStrategy() { - this.executorService = Executors.newScheduledThreadPool(2, r -> { - Thread thread = new Thread(r, "ObjectUpdateStrategy-" + System.currentTimeMillis()); - thread.setDaemon(true); - return thread; - }); - } - /** - * Generates a unique cache key for any TileObject (GameObject, WallObject, GroundObject, DecorativeObject). - * Uses canonical location logic for GameObjects, and world location for others. - * - * @param object The TileObject (GameObject, WallObject, etc.) - * @param tile The tile containing the object - * @return The cache key string - */ - public static String generateCacheIdForObject(TileObject object, Tile tile) { - if (object instanceof GameObject) { - // Use canonical location logic for GameObjects - return ObjectUpdateStrategy.generateCacheIdForGameObject((GameObject) object, tile); - } else if (object instanceof WallObject) { - WallObject wallObject = (WallObject) object; - return ObjectUpdateStrategy.generateCacheId("WallObject", wallObject.getId(), wallObject.getWorldLocation()); - } else if (object instanceof GroundObject) { - GroundObject groundObject = (GroundObject) object; - return ObjectUpdateStrategy.generateCacheId("GroundObject", groundObject.getId(), groundObject.getWorldLocation()); - } else if (object instanceof DecorativeObject) { - DecorativeObject decorativeObject = (DecorativeObject) object; - return ObjectUpdateStrategy.generateCacheId("DecorativeObject", decorativeObject.getId(), decorativeObject.getWorldLocation()); - } - // Fallback: use type name and world location if available - String type = object != null ? object.getClass().getSimpleName() : "Unknown"; - WorldPoint location = object != null ? object.getWorldLocation() : null; - int objectId = object != null ? object.getId() : -1; - return location != null ? String.format("%s_%d_%d_%d_%d", type, objectId, location.getX(), location.getY(), location.getPlane()) : type + "_null"; - } - - - @Override - public void handleEvent(final Object event, final CacheOperations cache) { - - if (executorService == null || executorService.isShutdown() || !Microbot.isLoggedIn() || Microbot.getClient() == null || Microbot.getClient().getLocalPlayer() == null) { - log.warn("ObjectUpdateStrategy is shut down, ignoring event: {}", event.getClass().getSimpleName()); - return; // Don't process events if shut down - } - if(scanActive.get()){ - log.debug("Skipping event processing - scan already active: {}", event.getClass().getSimpleName()); - return; // Don't process events if a scan is already active - } - // Submit event handling to executor service for non-blocking processing - //executorService.submit(() -> { - //try { - processEventInternal(event, cache); - //} catch (Exception e) { - //log.error("Error processing event: {}", event.getClass().getSimpleName(), e); - //} - //}); - } - - /** - * Internal method to process events - runs on client thread after coalescing. - * This is the renamed version of the original processEvent method. - */ - private void processEventInternal(final Object event, final CacheOperations cache) { - try { - if (event instanceof GameObjectSpawned) { - if(lastGameState == GameState.LOGGED_IN) handleGameObjectSpawned((GameObjectSpawned) event, cache); - } else if (event instanceof GameObjectDespawned) { - if(lastGameState == GameState.LOGGED_IN) handleGameObjectDespawned((GameObjectDespawned) event, cache); - } else if (event instanceof GroundObjectSpawned) { - if(lastGameState == GameState.LOGGED_IN) handleGroundObjectSpawned((GroundObjectSpawned) event, cache); - } else if (event instanceof GroundObjectDespawned) { - if(lastGameState == GameState.LOGGED_IN) handleGroundObjectDespawned((GroundObjectDespawned) event, cache); - } else if (event instanceof WallObjectSpawned) { - if(lastGameState == GameState.LOGGED_IN) handleWallObjectSpawned((WallObjectSpawned) event, cache); - } else if (event instanceof WallObjectDespawned) { - if(lastGameState == GameState.LOGGED_IN) handleWallObjectDespawned((WallObjectDespawned) event, cache); - } else if (event instanceof DecorativeObjectSpawned) { - if(lastGameState == GameState.LOGGED_IN) handleDecorativeObjectSpawned((DecorativeObjectSpawned) event, cache); - } else if (event instanceof DecorativeObjectDespawned) { - if(lastGameState == GameState.LOGGED_IN) handleDecorativeObjectDespawned((DecorativeObjectDespawned) event, cache); - } else if (event instanceof GameStateChanged) { - handleGameStateChanged((GameStateChanged) event, cache); - } else if (event instanceof GameTick) { - //handleGameTick((GameTick) event, cache); - } - } catch (Exception e) { - log.error("Error handling event: {}", event.getClass().getSimpleName(), e); - } - } - - /** - * Performs an scene scan to populate the cache. - * Only scans if certain conditions are met to avoid unnecessary processing. - * This method now runs asynchronously on a background thread to avoid blocking. - * - * @param cache The cache to populate - * @param force Whether to force a scan regardless of conditions - */ - public void performSceneScan(CacheOperations cache, long delayMs) { - if (executorService == null || executorService.isShutdown()) { - log.debug("Skipping scene scan - is executor service shutdown: {} or null {}, scan active: {}", - executorService.isShutdown() ,executorService == null , scanActive.get()); - return; - } - - // Respect minimum scan interval unless forced - if ((System.currentTimeMillis() - lastSceneScan) < MIN_SCAN_INTERVAL_MS) { - log.debug("Skipping scene scan due to minimum interval not reached"); - scanActive.set(false); - return; - } - if (sceneScanTask != null && !sceneScanTask.isDone()) { - log.debug("Skipping scene scan - already scheduled or running"); - return; // Don't perform scan if already scheduled or running - } - if (scanActive.compareAndSet(false,true)){ - // Submit scene scan to executor service for non-blocking execution - sceneScanTask = executorService.schedule(() -> { - try { - performSceneScanInternal(cache); - } catch (Exception e) { - log.error("Error during scene scan", e); - }finally { - - } - }, delayMs, TimeUnit.MILLISECONDS); // 100ms delay before scan - }else{ - log.debug("Skipping scene scan - already active"); - return; // Don't perform scan if already active - } - } - - /** - * Schedules a periodic scene scan task. - * This is useful for ensuring the cache stays up-to-date even if events are missed. - * - * @param cache The cache to scan - * @param intervalSeconds The interval between scans in seconds - */ - public void schedulePeriodicSceneScan(CacheOperations cache, long intervalSeconds) { - if (executorService == null || executorService.isShutdown()) { - log.debug("Cannot schedule periodic scan - strategy is shut down"); - return; - } - stopPeriodicSceneScan(); - - periodicSceneScanTask = executorService.scheduleWithFixedDelay(() -> { - try { - if (scanActive.compareAndSet(false,true)){ - if (scanRequest.get() && Microbot.isLoggedIn()) { // Only perform scan if request is set and not already active - log.debug("Performing scheduled scene scan"); - performSceneScanInternal(cache); - } - } else { - log.debug("Skipping scheduled scene scan - already active"); - } - } catch (Exception e) { - log.error("Error during scheduled scene scan", e); - }finally { - - } - - }, intervalSeconds, intervalSeconds, TimeUnit.SECONDS); - - log.debug("Scheduled periodic scene scan every {} seconds", intervalSeconds); - } - - /** - * Internal implementation of scene scanning that runs on background thread. - */ - - private void performSceneScanInternal(CacheOperations cache) { - - try { - long currentTime = System.currentTimeMillis(); - log.debug("Performing scene scan (last scan: {}, current time: {}) , loggedin: {}", lastSceneScan, currentTime, Microbot.isLoggedIn()); - if (!Microbot.isLoggedIn() || Microbot.getClient() == null || Microbot.getClient().getLocalPlayer() == null) { - log.warn("Cannot perform scene scan - not logged in"); - scanActive.set(false); - return; - } - - Player player = Rs2Player.getLocalPlayer(); - if (player == null) { - log.warn("Cannot perform scene scan - no player"); - scanActive.set(false); - return; - } - WorldPoint playerPoint = QuestPerspective.getRealWorldPointFromLocal( Microbot.getClient(), player.getWorldLocation()); - if (playerPoint == null) { - log.warn("Cannot perform scene scan - player location is null"); - scanActive.set(false); - return ; - } - - WorldView worldView = player.getWorldView(); - if (worldView == null) { - log.warn("Cannot perform scene scan - no world view"); - scanActive.set(false); - return; - } - WorldView topLevelWorldView = Microbot.getClient().getTopLevelWorldView(); - if (topLevelWorldView == null) { - log.warn("Cannot perform scene scan - no top-level world view"); - scanActive.set(false); - return; - } - Scene scene = worldView.getScene(); - if (scene == null) { - log.warn("Cannot perform scene scan - no scene"); - scanActive.set(false); - return; - } - - - Tile[][][] tiles = scene.getTiles(); - if (tiles == null) { - log.warn("Cannot perform scene scan - no tiles"); - scanActive.set(false); - return; - } - - // Build a set of all currently existing object keys from the scene - java.util.Set currentSceneKeys = new java.util.HashSet<>(); - java.util.Map objectsToAdd = new java.util.HashMap<>(); - int z = worldView.getPlane(); - - log.debug("Starting object scene synchronization (cache size: {})", cache.size()); - - // Phase 1: Scan scene and collect all objects - for (int x = 0; x < Constants.SCENE_SIZE; x++) { - for (int y = 0; y < Constants.SCENE_SIZE; y++) { - Tile tile = tiles[z][x][y]; - if (tile == null) continue; - - // Check GameObjects - GameObject[] gameObjects = tile.getGameObjects(); - if (gameObjects != null) { - for (GameObject gameObject : gameObjects) { - if (gameObject == null) continue; - - // Only add if it's the primary location for multi-tile objects - if (gameObject.getSceneMinLocation().equals(tile.getSceneLocation())) { - String cacheId = generateCacheIdForGameObject(gameObject, tile); - currentSceneKeys.add(cacheId); // Track all scene objects - if (!objectsToAdd.containsKey(cacheId)) { - Rs2ObjectModel objectModel = new Rs2ObjectModel(gameObject, tile); - objectsToAdd.put(cacheId, objectModel); - } - } - } - } - - // Check GroundObject - GroundObject groundObject = tile.getGroundObject(); - if (groundObject != null) { - String cacheId = generateCacheId("GroundObject", groundObject.getId(), groundObject.getWorldLocation()); - currentSceneKeys.add(cacheId); // Track all scene objects - if (!objectsToAdd.containsKey(cacheId)) { - Rs2ObjectModel objectModel = new Rs2ObjectModel(groundObject, tile); - objectsToAdd.put(cacheId, objectModel); - } - } - - // Check WallObject - WallObject wallObject = tile.getWallObject(); - if (wallObject != null) { - String cacheId = generateCacheId("WallObject", wallObject.getId(), wallObject.getWorldLocation()); - currentSceneKeys.add(cacheId); // Track all scene objects - if (!objectsToAdd.containsKey(cacheId)) { - Rs2ObjectModel objectModel = new Rs2ObjectModel(wallObject, tile); - objectsToAdd.put(cacheId, objectModel); - } - } - - // Check DecorativeObject - DecorativeObject decorativeObject = tile.getDecorativeObject(); - if (decorativeObject != null) { - String cacheId = generateCacheId("DecorativeObject", decorativeObject.getId(), decorativeObject.getWorldLocation()); - currentSceneKeys.add(cacheId); // Track all scene objects - if (!objectsToAdd.containsKey(cacheId)) { - Rs2ObjectModel objectModel = new Rs2ObjectModel(decorativeObject, tile); - objectsToAdd.put(cacheId, objectModel); - } - } - } - } - - // Phase 2: Add new objects to cache - int addedObjects = 0; - for (java.util.Map.Entry entry : objectsToAdd.entrySet()) { - String cacheId = entry.getKey(); - Rs2ObjectModel objectModel = entry.getValue(); - - // Only add if not already in cache (avoid recursive get calls by checking internally) - if (!cache.containsKey(cacheId)) { - cache.put(cacheId, objectModel); - addedObjects++; - } - } - - // Phase 3: Remove cached objects no longer in scene - int removedObjects = 0; - if (!currentSceneKeys.isEmpty()) { - // Find cached objects that are no longer in the scene using CacheOperations streaming - java.util.List keysToRemove = cache.entryStream() - .map(java.util.Map.Entry::getKey) - .filter(key -> !currentSceneKeys.contains(key)) - .collect(java.util.stream.Collectors.toList()); - - // Remove the objects that are no longer in scene - for (String key : keysToRemove) { - Rs2ObjectModel object = cache.getRawValue(key); // Use raw value to avoid triggering recursive scene scans - cache.remove(key); - if (object != null) { - removedObjects++; - log.trace("Removed object not in scene: ID {} ({})", object.getId(), key); - } - } - } - - // Log comprehensive results - if (addedObjects > 0 || removedObjects > 0) { - log.debug("Object scene synchronization completed - added {} objects, removed {} objects (total cache size: {}), time taken: {} ms", - addedObjects, removedObjects, cache.size(), System.currentTimeMillis() - currentTime); - } else { - log.debug("Object scene synchronization completed - no changes made"); - } - scanRequest.set(false); //NOT in finally block to allow for rescan if there are an error - } catch (Exception e) { - log.error("Error during scene scan", e); - } finally { - // Reset scan state - lastSceneScan = System.currentTimeMillis(); // Update last scan time; - scanActive.set(false); - } - - - } - - - - /** - * Stops periodic scene scanning if currently active. - */ - public void stopPeriodicSceneScan() { - if (isPeriodicSceneScanActive()) { - periodicSceneScanTask.cancel(false); - periodicSceneScanTask = null; - log.debug("Stopped periodic scene scanning"); - } - } - - /** - * Checks if periodic scene scanning is currently active. - * - * @return true if periodic scanning is running - */ - private boolean isPeriodicSceneScanActive() { - return periodicSceneScanTask != null && !periodicSceneScanTask.isDone(); - } - - /** - * Checks if a scene scan is needed based on cache state. - * - * @param cache The cache to check - * @return true if a scan would be beneficial - */ - public boolean requestSceneScan(CacheOperations cache) { - if (scanActive.get()) { - log.debug("Skipping scene scan request - already active"); - return false; // Don't request scan if already active - } - if (scanRequest.compareAndSet(false, true)) { - log.debug("Object scene scan requested"); - performSceneScan(cache, 5); // Perform immediately - } - if (!Microbot.getClient().isClientThread()){ - sleepUntil(()->!scanRequest.get(), 1000); // Wait until scan is requested and reset - } - return !scanRequest.get(); // Return true if scan was requested,reseted - } - private void handleGameObjectSpawned(GameObjectSpawned event, CacheOperations cache) { - GameObject gameObject = event.getGameObject(); - Tile tile = event.getTile(); - if (gameObject != null && tile != null) { - // Only add multi-tile objects from their primary (southwest) tile to prevent duplicates - String cacheId = generateCacheIdForGameObject(gameObject, tile); - if (cache.containsKey(cacheId)) { - log.warn("GameObject {} already in cache, skipping spawn event", gameObject.getId()); - return; // Already cached, skip - } - if (isPrimaryTile(gameObject, tile) || !cache.containsKey(cacheId)) { - Rs2ObjectModel objectModel = new Rs2ObjectModel(gameObject, tile); - cache.put(cacheId, objectModel); - log.debug("Added GameObject {} (id: {}) to cache via spawn event from primary tile", - gameObject.getId(), cacheId); - } else { - log.warn("Skipped GameObject {} spawn event from non-primary tile", gameObject.getId()); - } - } - } - - private void handleGameObjectDespawned(GameObjectDespawned event, CacheOperations cache) { - GameObject gameObject = event.getGameObject(); - Tile tile = event.getTile(); - if (gameObject != null && tile != null) { - // Only process despawn events from the primary tile to prevent multiple removal attempts - String cacheId = generateCacheIdForGameObject(gameObject, tile); - if (!cache.containsKey(cacheId)) { - log.warn("GameObject {} not in cache, skipping despawn event", gameObject.getId()); - return; // Not in cache, skip - } - if (isPrimaryTile(gameObject, tile)|| cache.containsKey(cacheId)) { - cache.remove(cacheId); - log.debug("Removed GameObject {} (id: {}) from cache via despawn event from primary tile", - gameObject.getId(), cacheId); - } else { - log.warn("Skipped GameObject {} despawn event from non-primary tile", gameObject.getId()); - } - } - } - - private void handleGroundObjectSpawned(GroundObjectSpawned event, CacheOperations cache) { - GroundObject groundObject = event.getGroundObject(); - Tile tile = event.getTile(); - String cacheId = generateCacheId("GroundObject", groundObject.getId(), groundObject.getWorldLocation()); - if (cache.containsKey(cacheId)) { - log.warn("GroundObject {} already in cache, skipping spawn event", groundObject.getId()); - return; // Already cached, skip - } - if (groundObject != null && tile != null) { - Rs2ObjectModel objectModel = new Rs2ObjectModel(groundObject, tile); - cache.put(cacheId, objectModel); - log.debug("Added GroundObject {} (id: {}) to cache via spawn event", groundObject.getId(), cacheId); - } - } - - private void handleGroundObjectDespawned(GroundObjectDespawned event, CacheOperations cache) { - GroundObject groundObject = event.getGroundObject(); - if (groundObject != null) { - String cacheId = generateCacheId("GroundObject", groundObject.getId(), groundObject.getWorldLocation()); - cache.remove(cacheId); - log.debug("Removed GroundObject {} (id: {}) from cache via despawn event", groundObject.getId(), cacheId); - } - } - - private void handleWallObjectSpawned(WallObjectSpawned event, CacheOperations cache) { - WallObject wallObject = event.getWallObject(); - Tile tile = event.getTile(); - - if (wallObject != null && tile != null) { - String cacheId = generateCacheId("WallObject", wallObject.getId(), wallObject.getWorldLocation()); - if (cache.containsKey(cacheId)) { - log.warn("WallObject {} already in cache, skipping spawn event", wallObject.getId()); - return; // Already cached, skip - } - Rs2ObjectModel objectModel = new Rs2ObjectModel(wallObject, tile); - - cache.put(cacheId, objectModel); - log.debug("Added WallObject {} (id: {}) to cache via spawn event", wallObject.getId(), cacheId); - } - } - - private void handleWallObjectDespawned(WallObjectDespawned event, CacheOperations cache) { - WallObject wallObject = event.getWallObject(); - if (wallObject != null) { - String cacheId = generateCacheId("WallObject", wallObject.getId(), wallObject.getWorldLocation()); - cache.remove(cacheId); - log.debug("Removed WallObject {} (id: {}) from cache via despawn event", wallObject.getId(), cacheId); - } - } - - private void handleDecorativeObjectSpawned(DecorativeObjectSpawned event, CacheOperations cache) { - DecorativeObject decorativeObject = event.getDecorativeObject(); - Tile tile = event.getTile(); - if (decorativeObject != null && tile != null) { - String cacheId = generateCacheId("DecorativeObject", decorativeObject.getId(), decorativeObject.getWorldLocation()); - if (cache.containsKey(cacheId)) { - log.warn("DecorativeObject {} already in cache, skipping spawn event", decorativeObject.getId()); - return; // Already cached, skip - } - Rs2ObjectModel objectModel = new Rs2ObjectModel(decorativeObject, tile); - cache.put(cacheId, objectModel); - log.debug("Added DecorativeObject {} (id: {}) to cache via spawn event", decorativeObject.getId(), cacheId); - } - } - - private void handleDecorativeObjectDespawned(DecorativeObjectDespawned event, CacheOperations cache) { - DecorativeObject decorativeObject = event.getDecorativeObject(); - if (decorativeObject != null) { - String cacheId = generateCacheId("DecorativeObject", decorativeObject.getId(), decorativeObject.getWorldLocation()); - cache.remove(cacheId); - log.debug("Removed DecorativeObject {} (id: {}) from cache via despawn event", decorativeObject.getId(), cacheId); - } - } - - private void handleGameStateChanged(GameStateChanged event, CacheOperations cache) { - switch (event.getGameState()) { - case LOGGED_IN: - // Check for region changes and perform scene scan to synchronize - if (Rs2Cache.checkAndHandleRegionChange(cache)) { - log.debug("Region change detected on login - performing scene synchronization"); - performSceneScan(cache, Constants.GAME_TICK_LENGTH *3); - } else if (lastGameState != null && lastGameState != GameState.LOGGED_IN) { - // Perform scene synchronization when logging in - might have missed spawn events - performSceneScan(cache, Constants.GAME_TICK_LENGTH *3); // Perform scan after 2 game ticks to allow scene to stabilize - } - - lastGameState = GameState.LOGGED_IN; - log.debug("Player logged in - checking regions and requesting scene synchronization"); - break; - case LOADING: - // Check for region changes during loading - if (Rs2Cache.checkAndHandleRegionChange(cache)) { - log.debug("Region change detected during loading - performing scene synchronization"); - performSceneScan(cache, Constants.GAME_TICK_LENGTH *1); - } else { - performSceneScan(cache, Constants.GAME_TICK_LENGTH*1); // Perform scan after 4 game ticks to allow scene to stabilize - } - lastGameState = GameState.LOADING; - log.debug("Game loading - checking regions and requesting scene synchronization"); - break; - case LOGIN_SCREEN: - case LOGGING_IN: - case CONNECTION_LOST: - // Clear scan request when logging out and stop periodic scanning - if (sceneScanTask != null && !sceneScanTask.isDone()) { - sceneScanTask.cancel(true); - sceneScanTask = null; - } - scanRequest.set(false); // Reset scan request - cache.invalidateAll(); - lastGameState = event.getGameState(); - log.debug("Player logged out - clearing scan request and stopping periodic scanning"); - break; - default: - lastGameState = event.getGameState(); - break; - } - } - - /** - * Gets the canonical world location for a GameObject. - * For multi-tile objects, this returns the southwest tile location. - * - * @param gameObject The GameObject to get the canonical location for - * @param tile The tile from the event - * @return The canonical world location - */ - private static WorldPoint getCanonicalLocation(GameObject gameObject, Tile tile) { - // For multi-tile objects, we need to ensure we use the southwest tile consistently - Point sceneMinLocation = gameObject.getSceneMinLocation(); - Point currentSceneLocation = tile.getSceneLocation(); - - // If this is the southwest tile, use this tile's location - if (sceneMinLocation != null && currentSceneLocation != null && - sceneMinLocation.getX() == currentSceneLocation.getX() && - sceneMinLocation.getY() == currentSceneLocation.getY()) { - return tile.getWorldLocation(); - } - - // Otherwise, we need to calculate the southwest tile's world location - // This is tricky without scene-to-world conversion, so we'll use a different approach - WorldPoint currentLocation = tile.getWorldLocation(); - if (sceneMinLocation != null && currentSceneLocation != null) { - int deltaX = currentSceneLocation.getX() - sceneMinLocation.getX(); - int deltaY = currentSceneLocation.getY() - sceneMinLocation.getY(); - return new WorldPoint(currentLocation.getX() - deltaX, currentLocation.getY() - deltaY, currentLocation.getPlane()); - } - - return currentLocation; - } - - /** - * Checks if the given tile is the primary (southwest) tile for a GameObject. - * - * @param gameObject The GameObject to check - * @param tile The tile to verify - * @return true if this is the primary tile, false otherwise - */ - private boolean isPrimaryTile(GameObject gameObject, Tile tile) { - Point sceneMinLocation = gameObject.getSceneMinLocation(); - Point currentSceneLocation = tile.getSceneLocation(); - - return sceneMinLocation != null && currentSceneLocation != null && - sceneMinLocation.getX() == currentSceneLocation.getX() && - sceneMinLocation.getY() == currentSceneLocation.getY(); - } - - /** - * Generates a unique object ID for tracking. - * For GameObjects, uses the canonical (southwest) location to ensure consistent caching. - */ - private static String generateCacheId(String type, int objectID, net.runelite.api.coords.WorldPoint location) { - return String.format("%s_%d_%d_%d_%d", type, objectID, location.getX(), location.getY(), location.getPlane()); - } - - /** - * Generates a unique object ID for tracking GameObjects using their canonical location. - * This ensures that multi-tile GameObjects have consistent cache keys. - */ - private static String generateCacheIdForGameObject(GameObject gameObject, Tile tile) { - WorldPoint canonicalLocation = getCanonicalLocation(gameObject, tile); - return generateCacheId("GameObject", gameObject.getId(), canonicalLocation); - } - - @Override - public Class[] getHandledEventTypes() { - return new Class[]{ - GameObjectSpawned.class, GameObjectDespawned.class, - GroundObjectSpawned.class, GroundObjectDespawned.class, - WallObjectSpawned.class, WallObjectDespawned.class, - DecorativeObjectSpawned.class, DecorativeObjectDespawned.class, - GameStateChanged.class, GameTick.class - }; - } - - @Override - public void onAttach(CacheOperations cache) { - log.debug("ObjectUpdateStrategy attached to cache"); - // Start periodic scene scanning if logged in - if (Microbot.isLoggedIn() && lastGameState == GameState.LOGGED_IN) { - //schedulePeriodicSceneScan(cache, 30); // Every 30 seconds - } - } - - @Override - public void onDetach(CacheOperations cache) { - log.debug("ObjectUpdateStrategy detached from cache"); - // Cancel periodic scanning when detaching - if (periodicSceneScanTask != null && !periodicSceneScanTask.isDone()) { - periodicSceneScanTask.cancel(false); - periodicSceneScanTask = null; - } - } - - @Override - public void close() { - log.debug("Shutting down ObjectUpdateStrategy"); - stopPeriodicSceneScan(); - if (sceneScanTask != null && !sceneScanTask.isDone()) { - sceneScanTask.cancel(false); - sceneScanTask = null; - log.debug("Cancelled active scene scan task"); - - } - shutdownExecutorService(); - } - /** - * Shuts down the executor service gracefully, waiting for currently executing tasks to complete. - * If the executor does not terminate within the initial timeout, it attempts a forced shutdown. - * Logs warnings or errors if the shutdown process does not complete as expected. - * If interrupted during shutdown, the method forces shutdown and re-interrupts the current thread. - */ - private void shutdownExecutorService() { - if (executorService != null && !executorService.isShutdown()) { - // Shutdown executor service - executorService.shutdown(); - try { - if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { - log.warn("Executor service did not terminate gracefully, forcing shutdown"); - executorService.shutdownNow(); - - // Wait a bit more for tasks to respond to being cancelled - if (!executorService.awaitTermination(2, TimeUnit.SECONDS)) { - log.error("Executor service did not terminate after forced shutdown"); - } - } - } catch (InterruptedException e) { - log.warn("Interrupted during executor shutdown", e); - executorService.shutdownNow(); - Thread.currentThread().interrupt(); - } - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/farming/SpiritTreeUpdateStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/farming/SpiritTreeUpdateStrategy.java deleted file mode 100644 index 0c47a9b9d82..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/farming/SpiritTreeUpdateStrategy.java +++ /dev/null @@ -1,385 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy.farming; - -import java.util.Objects; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.GameState; -import net.runelite.api.GameObject; -import net.runelite.api.Skill; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.*; -import net.runelite.api.gameval.ObjectID; -import net.runelite.api.gameval.VarbitID; -import net.runelite.api.widgets.Widget; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.questhelper.helpers.mischelpers.farmruns.CropState; -import net.runelite.client.plugins.microbot.util.cache.Rs2Cache; -import net.runelite.client.plugins.microbot.util.cache.model.SpiritTreeData; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheOperations; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheUpdateStrategy; -import net.runelite.client.plugins.microbot.util.farming.SpiritTree; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; -import net.runelite.client.plugins.microbot.util.poh.PohTeleports; -import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; - -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -/** - * Cache update strategy for spirit tree farming data. - * Handles WidgetLoaded, VarbitChanged, and GameObjectSpawned events to detect spirit tree states - * and travel availability with enhanced contextual information. - */ -@Slf4j -public class SpiritTreeUpdateStrategy implements CacheUpdateStrategy { - - // Widget constants for spirit tree detection - private static final int ADVENTURE_LOG_GROUP_ID = 187; - private static final int ADVENTURE_LOG_CONTAINER_CHILD = 0; - private static final String SPIRIT_TREE_WIDGET_TITLE = SpiritTree.SPIRIT_TREE_WIDGET_TITLE; - - // Spirit tree object IDs for game object detection - private static final List SPIRIT_TREE_OBJECT_IDS = Arrays.asList( - ObjectID.FARMING_SPIRIT_TREE_PATCH_5, // Object ID found in farming guild fully grown spirit tree patch - ObjectID.SPIRIT_TREE_FULLYGROWN, // Standard spirit spiritTree id when fully grown and available for travel -> healty ) - ObjectID.SPIRITTREE_PRIF, // Prifddinas spirit tree - ObjectID.POG_SPIRIT_TREE_ALIVE_STATIC, // Poison Waste spirit tree - ObjectID.SPIRITTREE_SMALL, // Small spirit tree in grand Exchange 1295 - ObjectID.ENT, // for the "great" trees in tree gnome village 1293 - ObjectID.STRONGHOLD_ENT, // nd tree gnome stronghold 1294 - ObjectID.POH_SPIRIT_TREE // Player-owned house spirit tree object ID - ); - - // Spirit tree specific farming transmit varbits - private static final List SPIRIT_TREE_VARBIT_IDS = Arrays.asList( - VarbitID.FARMING_TRANSMIT_A, // 4771 - Port Sarim and Farming Guild - VarbitID.FARMING_TRANSMIT_B, // 4772 - Etceteria and Brimhaven patches - VarbitID.FARMING_TRANSMIT_F // 7904 - Hosidius - ); - - /** - * Handle an event from the client and update the cache accordingly. - * @param event The event that occurred - * @param cache The cache to potentially update - */ - @Override - public void handleEvent(Object event, CacheOperations cache) { - try { - if (event instanceof WidgetLoaded) { - handleWidgetLoaded((WidgetLoaded) event, cache); - } else if (event instanceof VarbitChanged) { - handleVarbitChanged((VarbitChanged) event, cache); - } else if (event instanceof GameStateChanged) { - handleGameStateChanged((GameStateChanged) event, cache); - } else if (event instanceof GameObjectSpawned) { - GameObject go = ((GameObjectSpawned) event).getGameObject(); - handleGameObjectChange(go, true, cache); - } else if (event instanceof GameObjectDespawned) { - GameObject go = ((GameObjectDespawned) event).getGameObject(); - handleGameObjectChange(go, false, cache); - } - } catch (Exception e) { - log.error("Error handling event in SpiritTreeUpdateStrategy: {}", e.getMessage(), e); - } - } - - /** - * Get the event types that are handled by this strategy - */ - @Override - public Class[] getHandledEventTypes() { - return new Class[]{WidgetLoaded.class, VarbitChanged.class, GameStateChanged.class, GameObjectSpawned.class, GameObjectDespawned.class}; - } - - /** - * Handle widget loaded events to detect spirit tree widget opening - */ - private void handleWidgetLoaded(WidgetLoaded event, CacheOperations cache) { - // Check if this is the adventure log widget with spirit tree locations - if (event.getGroupId() == ADVENTURE_LOG_GROUP_ID) { - // Small delay to ensure widget is fully loaded - Microbot.getClientThread().invokeLater(() -> { - try { - if(Rs2Widget.isHidden(ADVENTURE_LOG_GROUP_ID, ADVENTURE_LOG_CONTAINER_CHILD)) { - log.debug("Adventure log widget not found, skipping spirit tree update"); - return; - } - Widget titleWidget = Rs2Widget.getWidget(ADVENTURE_LOG_GROUP_ID, ADVENTURE_LOG_CONTAINER_CHILD); - if (titleWidget == null || titleWidget.isHidden()) { - log.debug("Adventure log widget not found, skipping spirit tree update"); - return; - } - log.debug("Adventure log widget loaded (group {}), checking for spirit tree locations", event.getGroupId()); - boolean hasRightTitle = Rs2Widget.hasWidgetText(SPIRIT_TREE_WIDGET_TITLE, ADVENTURE_LOG_GROUP_ID, ADVENTURE_LOG_CONTAINER_CHILD, false); - - if (hasRightTitle) { - log.debug("Spirit tree locations widget detected, updating cache from widget data"); - updateCacheFromWidget(cache); - - } - } catch (Exception e) { - log.debug("Error checking spirit tree widget: {}", e.getMessage()); - } - }); - } - } - - /** - * Handle varbit changed events for spirit tree farming transmit varbits - */ - private void handleVarbitChanged(VarbitChanged event, CacheOperations cache) { - int varbitId = event.getVarbitId(); - // Check if this is a spirit tree farming transmit varbit - if (SPIRIT_TREE_VARBIT_IDS.contains(varbitId)) { - log.debug("Spirit tree farming varbit {} changed to value {}, updating spirit tree farming states", varbitId, event.getValue()); - // Update farming states for all farmable spirit tree patches - updateFarmingStatesFromVarbits(cache); - } - if(varbitId == VarbitID.POH_SPIRIT_TREE_UPROOTED){ - log.debug("TODO update cache for POH spirit tree uprooted varbit change,currently in POH? {} ",PohTeleports.isInHouse()); - } - } - - /** - * Handles changes to game objects related to spirit trees and updates the cache accordingly. - * If the provided game object matches known spirit tree objects, it logs the detection - * and updates the cache with the relevant state. - * - * @param gameObject The game object that has changed. May be null, in which case this method does nothing. - * @param spawned Indicates whether the game object was spawned (true) or despawned (false). - * @param cache The cache instance used for storing and updating {@link SpiritTreeData} for detected spirit trees. - */ - private void handleGameObjectChange(GameObject gameObject, boolean spawned, CacheOperations cache) { - if(gameObject == null){ - return; - } - Arrays.stream(SpiritTree.values()).filter(tree -> tree.getType() == SpiritTree.SpiritTreeType.POH).forEach(tree -> { - if (tree.getObjectId().contains(gameObject.getId())) { - log.info("Found spirit tree object {} for POH spirit tree {}, {} to cache", gameObject.getId(), tree.name(), spawned ? "added" : "removed"); - SpiritTreeData newData = new SpiritTreeData( - tree, - spawned ? CropState.HARVESTABLE: CropState.DEAD, - spawned, - gameObject.getWorldLocation(), - false, // Not detected via widget - true // Detected via nearby tree if present - ); - cache.put(tree, newData); - } - }); - - } - - /** - * Handle GameStateChanged events to detect POH region changes and validate spirit tree presence - */ - private void handleGameStateChanged(GameStateChanged event, CacheOperations cache) { - GameState gameState = event.getGameState(); - - // Only process when entering game or loading regions - if (gameState != GameState.LOGGED_IN && gameState != GameState.LOADING) { - return; - } - - try { - // Use unified region detection from Rs2Cache - Rs2Cache.checkAndHandleRegionChange(cache); - } catch (Exception e) { - log.error("Error handling GameStateChanged in SpiritTreeUpdateStrategy: {}", e.getMessage(), e); - } - } - - /** - * Update cache from spirit tree widget data - */ - private void updateCacheFromWidget(CacheOperations cache) { - try { - // Extract available destinations from the widget - List availableSpiritTrees = SpiritTree.extractAvailableFromWidget(); - WorldPoint playerLocation = getPlayerLocation(); - log.debug("Widget extraction found {} available spirit tree destinations", availableSpiritTrees.size()); - for (SpiritTree spiritTree : SpiritTree.values()) { - boolean isAvailable = availableSpiritTrees.contains(spiritTree); - boolean availableForTravel = spiritTree.hasQuestRequirements(); - - SpiritTreeData existingData = cache.get(spiritTree); - - // Create new data with widget detection - SpiritTreeData newData = new SpiritTreeData( - spiritTree, - existingData != null ? existingData.getCropState() : null, // Preserve existing crop state if available - isAvailable && availableForTravel, - playerLocation, - isAvailable, // Detected via widget - false // Not detected via near tree - ); - - cache.put(spiritTree, newData); - - log.debug("Updated spirit tree cache for via widget ({} for travel)\n\t{}", isAvailable ? "available" : "not available", spiritTree.name()); - } - - } catch (Exception e) { - log.error("Error updating cache from spirit tree widget: {}", e.getMessage(), e); - } - } - - /** - * Update farming states from varbit changes with object detection - only when near spirit tree patches - */ - private void updateFarmingStatesFromVarbits(CacheOperations cache) { - try { - WorldPoint playerLocation = getPlayerLocation(); - - if (playerLocation == null) { - return; // Can't determine player location - } - - // Only update if player is near a spirit tree (within region) - boolean nearSpiritTree = SpiritTree.getFarmableSpirtTrees().stream() - .anyMatch(spiritTree -> Arrays.stream(spiritTree.getRegionIds()) - .anyMatch(regionId -> regionId != -1 && regionId == playerLocation.getRegionID())); - - if (!nearSpiritTree) { - log.trace("Player not near any spirit tree patches, skipping varbit update"); - return; - } - - // Update all farmable spirit tree patches - for (SpiritTree spiritTree : SpiritTree.getFarmableSpirtTrees()) { - try { - CropState currentState = spiritTree.getPatchState(); - boolean availableForTravel = spiritTree.isAvailableForTravel(); - - // Enhanced: Check for nearby spirit tree objects to verify travel availability - boolean hasNearbyTravelObject = checkForNearbyTree(spiritTree, playerLocation); - - // Use object detection to override travel availability if object is found - if (hasNearbyTravelObject) { - availableForTravel = true; - log.debug("Found nearby travel object for {}, setting available=true", spiritTree.name()); - } - - SpiritTreeData existingData = cache.get(spiritTree); - - // Only update if state actually changed or this is new data - if (existingData == null || - existingData.getCropState() != currentState || - existingData.isAvailableForTravel() != availableForTravel) { - - SpiritTreeData newData = new SpiritTreeData( - spiritTree, - currentState, - availableForTravel, - playerLocation, - false, // Not detected via widget - true // Detected via varbit when near patch - ); - - cache.put(spiritTree, newData); - log.debug("Updated spirit tree cache for {} via varbit (state: {}, available: {}, hasObject: {})", - spiritTree.name(), currentState, availableForTravel, hasNearbyTravelObject); - } - } catch (Exception e) { - log.debug("Error updating farming state for spiritTree {}: {}", spiritTree.name(), e.getMessage()); - } - } - } catch (Exception e) { - log.error("Error updating farming states from varbits: {}", e.getMessage(), e); - } - } - - /** - * Check for nearby spirit tree objects that have travel actions. - * This integrates the object detection logic from the former GameObjectSpawned handler. - * - * @param spiritTree The spirit tree patch to check for - * @param playerLocation Current player location - * @return true if a nearby object with travel action is found - */ - private boolean checkForNearbyTree(SpiritTree spiritTree, WorldPoint playerLocation) { - try { - // Get all game objects near the spirit tree patch location - Optional nearbyObject = Rs2GameObject.getGameObjects() - .stream() - .filter(obj -> SPIRIT_TREE_OBJECT_IDS.contains(obj.getId())) - .filter(obj -> spiritTree.getLocation().distanceTo(obj.getWorldLocation()) <= 5) - .findFirst(); - - if (nearbyObject.isPresent()) { - GameObject gameObject = nearbyObject.get(); - - // Check if the game object has "Travel" action (indicates it's usable) - try { - String[] actions = Microbot.getClient().getObjectDefinition(gameObject.getId()).getActions(); - boolean hasTravel = Arrays.stream(actions) - .filter(Objects::nonNull) - .anyMatch(action -> action.equalsIgnoreCase("Travel")); - - if (hasTravel) { - log.debug("Found spirit tree object {} with Travel action at {} for patch {}", - gameObject.getId(), gameObject.getWorldLocation(), spiritTree.name()); - return true; - } - } catch (Exception e) { - log.debug("Could not get actions for spirit tree object {}: {}", gameObject.getId(), e.getMessage()); - } - } - - return false; - } catch (Exception e) { - log.debug("Error checking for nearby travel object for {}: {}", spiritTree.name(), e.getMessage()); - return false; - } - } - - /** - - * Find spirit tree spiritTree by object location - */ - private SpiritTree findSpiritTreeByLocation(WorldPoint objectLocation) { - if (objectLocation == null) { - return null; - } - - for (SpiritTree spiritTree : SpiritTree.values()) { - WorldPoint spiritTreeLocation = spiritTree.getLocation(); - if (spiritTreeLocation != null && spiritTreeLocation.distanceTo(objectLocation) <= 5) { - return spiritTree; - } - } - - return null; - } - - /** - * Get current player location safely - */ - private WorldPoint getPlayerLocation() { - try { - if (Microbot.getClient() != null && - Microbot.getClient().getGameState() == GameState.LOGGED_IN && - Microbot.getClient().getLocalPlayer() != null) { - return Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - } catch (Exception e) { - log.trace("Could not get player location: {}", e.getMessage()); - } - return null; - } - - /** - * Get current farming level safely - */ - private Integer getFarmingLevel() { - try { - if (Microbot.getClient() != null && - Microbot.getClient().getGameState() == GameState.LOGGED_IN) { - return Microbot.getClient().getRealSkillLevel(Skill.FARMING); - } - } catch (Exception e) { - log.trace("Could not get farming level: {}", e.getMessage()); - } - return null; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/QuestUpdateStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/QuestUpdateStrategy.java deleted file mode 100644 index 3f37b3cd20c..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/QuestUpdateStrategy.java +++ /dev/null @@ -1,387 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy.simple; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Quest; -import net.runelite.api.QuestState; -import net.runelite.api.events.ChatMessage; -import net.runelite.api.events.VarbitChanged; -import net.runelite.api.ChatMessageType; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.questhelper.QuestHelperPlugin; -import net.runelite.client.plugins.microbot.questhelper.questhelpers.QuestHelper; -import net.runelite.client.plugins.microbot.questhelper.questinfo.QuestHelperQuest; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheOperations; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheUpdateStrategy; - -import java.lang.reflect.Field; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Cache update strategy for quest data. - * Handles VarbitChanged and ChatMessage events to update quest state information. - * Uses QuestHelperQuest enum to efficiently detect quest-related changes. - */ -@Slf4j -public class QuestUpdateStrategy implements CacheUpdateStrategy { - - // Quest tracking variables moved from Rs2QuestCache - private static Quest trackedQuest = null; - - // Map from varbit/varplayer IDs to corresponding RuneLite Quest objects - private static final Map varbitToQuestMap = new ConcurrentHashMap<>(); - private static final Map varPlayerToQuestMap = new ConcurrentHashMap<>(); - - // Flag to track if quest maps have been initialized - private static volatile boolean mapsInitialized = false; - - /** - * Gets the currently tracked quest. - * - * @return The currently tracked quest, or null if none - */ - public static Quest getTrackedQuest() { - return trackedQuest; - } - - /** - * Sets the quest to track for changes. - * - * @param quest The quest to track, or null to stop tracking - */ - public static void setTrackedQuest(Quest quest) { - if (trackedQuest != quest) { - Quest oldQuest = trackedQuest; - trackedQuest = quest; - log.debug("Tracked quest changed from {} to {}", - oldQuest != null ? oldQuest.getName() : "none", - quest != null ? quest.getName() : "none"); - } - } - - /** - * Initializes the varbit/varPlayer to Quest mapping from QuestHelperQuest enum. - * This is done lazily on first access to avoid initialization order issues. - */ - private static void initializeQuestMaps() { - if (mapsInitialized) { - return; - } - - synchronized (QuestUpdateStrategy.class) { - if (mapsInitialized) { - return; - } - - try { - log.debug("Initializing quest variable maps from QuestHelperQuest enum..."); - - for (QuestHelperQuest questHelperQuest : QuestHelperQuest.values()) { - // Get RuneLite Quest by ID - Quest runeliteQuest = getQuestById(questHelperQuest.getId()); - if (runeliteQuest == null) { - continue; // Skip quests without RuneLite Quest mapping - } - - // Use reflection to access private varbit and varPlayer fields - try { - Field varbitField = QuestHelperQuest.class.getDeclaredField("varbit"); - varbitField.setAccessible(true); - Object varbitValue = varbitField.get(questHelperQuest); - - if (varbitValue != null) { - // Get the ID from the QuestVarbits enum - Field idField = varbitValue.getClass().getDeclaredField("id"); - idField.setAccessible(true); - int varbitId = (Integer) idField.get(varbitValue); - varbitToQuestMap.put(varbitId, runeliteQuest); - log.trace("Mapped varbit {} to quest {}", varbitId, runeliteQuest.getName()); - } - - Field varPlayerField = QuestHelperQuest.class.getDeclaredField("varPlayer"); - varPlayerField.setAccessible(true); - Object varPlayerValue = varPlayerField.get(questHelperQuest); - - if (varPlayerValue != null) { - // Get the ID from the QuestVarPlayer enum - Field idField = varPlayerValue.getClass().getDeclaredField("id"); - idField.setAccessible(true); - int varPlayerId = (Integer) idField.get(varPlayerValue); - varPlayerToQuestMap.put(varPlayerId, runeliteQuest); - log.trace("Mapped varPlayer {} to quest {}", varPlayerId, runeliteQuest.getName()); - } - - } catch (Exception reflectionException) { - log.trace("Reflection failed for quest {}: {}", questHelperQuest.getName(), reflectionException.getMessage()); - } - } - - mapsInitialized = true; - log.info("Initialized quest maps: {} varbits, {} varPlayers", - varbitToQuestMap.size(), varPlayerToQuestMap.size()); - - } catch (Exception e) { - log.error("Error initializing quest variable maps: {}", e.getMessage(), e); - } - } - } - - /** - * Gets a RuneLite Quest by its ID. - * - * @param questId The quest ID - * @return The Quest object, or null if not found - */ - private static Quest getQuestById(int questId) { - try { - for (Quest quest : Quest.values()) { - if (quest.getId() == questId) { - return quest; - } - } - } catch (Exception e) { - log.trace("Error finding quest by ID {}: {}", questId, e.getMessage()); - } - return null; - } - - /** - * Gets the Quest associated with a varbit ID. - * - * @param varbitId The varbit ID - * @return The associated Quest, or null if none found - */ - public static Quest getQuestByVarbit(int varbitId) { - initializeQuestMaps(); - return varbitToQuestMap.get(varbitId); - } - - /** - * Gets the Quest associated with a varPlayer ID. - * - * @param varPlayerId The varPlayer ID - * @return The associated Quest, or null if none found - */ - public static Quest getQuestByVarPlayer(int varPlayerId) { - initializeQuestMaps(); - return varPlayerToQuestMap.get(varPlayerId); - } - - /** - * Gets the varbit ID associated with a quest. - * - * @param quest The quest to look up - * @return The varbit ID, or null if none found - */ - public static Integer getVarbitIdByQuest(Quest quest) { - initializeQuestMaps(); - return varbitToQuestMap.entrySet().stream() - .filter(entry -> entry.getValue().equals(quest)) - .map(Map.Entry::getKey) - .findFirst() - .orElse(null); - } - - /** - * Gets the varPlayer ID associated with a quest. - * - * @param quest The quest to look up - * @return The varPlayer ID, or null if none found - */ - public static Integer getVarPlayerIdByQuest(Quest quest) { - initializeQuestMaps(); - return varPlayerToQuestMap.entrySet().stream() - .filter(entry -> entry.getValue().equals(quest)) - .map(Map.Entry::getKey) - .findFirst() - .orElse(null); - } - - /** - * Gets the QuestHelperPlugin instance. - * - * @return The QuestHelperPlugin instance, or null if not available - */ - private static QuestHelperPlugin getQuestHelperPlugin() { - try { - return (QuestHelperPlugin) Microbot.getPluginManager().getPlugins().stream() - .filter(plugin -> plugin instanceof QuestHelperPlugin && Microbot.getPluginManager().isPluginEnabled(plugin)) - .findFirst() - .orElse(null); - } catch (Exception e) { - log.trace("Error getting QuestHelper plugin: {}", e.getMessage()); - return null; - } - } - - /** - * Gets the currently selected quest from QuestHelperPlugin if available. - * - * @return The currently selected QuestHelper, or null if none selected or plugin unavailable - */ - private static QuestHelper getSelectedQuestHelper() { - QuestHelperPlugin plugin = getQuestHelperPlugin(); - if (plugin != null) { - return plugin.getSelectedQuest(); - } - return null; - } - - /** - * Gets the currently active RuneLite Quest from QuestHelperPlugin. - * Maps QuestHelper to RuneLite Quest objects using QuestHelperQuest info. - * - * @return The currently active Quest, or null if none selected or not mappable - */ - public static Quest getCurrentlyActiveQuest() { - try { - QuestHelper selectedHelper = getSelectedQuestHelper(); - if (selectedHelper == null) { - return null; - } - - // Find the corresponding QuestHelperQuest enum entry - for (QuestHelperQuest questHelperQuest : QuestHelperQuest.values()) { - if (questHelperQuest.getQuestHelper() != null && - questHelperQuest.getQuestHelper().getClass().equals(selectedHelper.getClass())) { - return getQuestById(questHelperQuest.getId()); - } - } - - log.trace("No RuneLite Quest found for selected QuestHelper: {}", selectedHelper.getClass().getSimpleName()); - return null; - } catch (Exception e) { - log.trace("Error getting currently active quest: {}", e.getMessage()); - return null; - } - } - - @Override - public void handleEvent(Object event, CacheOperations cache) { - if (event instanceof VarbitChanged) { - handleVarbitChanged((VarbitChanged) event, cache); - } else if (event instanceof ChatMessage) { - handleChatMessage((ChatMessage) event, cache); - } - } - - private void handleVarbitChanged(VarbitChanged event, CacheOperations cache) { - try { - initializeQuestMaps(); // Ensure maps are initialized - - // Check if the changed varbit/varPlayer corresponds to a known quest - Quest affectedQuest = null; - - // Check varbit mapping - if (event.getVarbitId() > 0) { - affectedQuest = varbitToQuestMap.get(event.getVarbitId()); - if (affectedQuest != null) { - log.debug("VarbitChanged - Detected quest {} affected by varbit {}", - affectedQuest.getName(), event.getVarbitId()); - } - } - - // Check varPlayer mapping (VarbitChanged can also affect varPlayers) - if (affectedQuest == null && event.getVarpId() > 0) { - affectedQuest = varPlayerToQuestMap.get(event.getVarpId()); - if (affectedQuest != null) { - log.debug("VarbitChanged - Detected quest {} affected by varPlayer {}", - affectedQuest.getName(), event.getVarpId()); - } - } - - // If a specific quest is affected, trigger its update - if (affectedQuest != null) { - updateQuestAsync(affectedQuest, cache); - } else { - // Fallback: check tracked quest or currently active quest - Quest questToCheck = trackedQuest != null ? trackedQuest : getCurrentlyActiveQuest(); - if (questToCheck != null) { - log.trace("VarbitChanged - Checking tracked/active quest: {}", questToCheck.getName()); - updateQuestAsync(questToCheck, cache); - } - } - - } catch (Exception e) { - log.error("Error handling VarbitChanged event for quests: {}", e.getMessage(), e); - } - } - - private void handleChatMessage(ChatMessage chatMessage, CacheOperations cache) { - try { - if (chatMessage.getType() != ChatMessageType.GAMEMESSAGE) { - return; - } - - String message = chatMessage.getMessage(); - if (message == null) { - return; - } - - // Check for quest-related messages - if (message.contains("You have completed") || - message.contains("Quest complete") || - message.contains("quest") || - message.contains("Quest")) { - //should not be need, the varbitChanged event should handle this already, only dont work for qeust not in the enum QuestHelperQuest - log.debug("ChatMessage - Quest-related message detected: {}", message); - // Update tracked quest if any - Quest questToUpdate = trackedQuest != null ? trackedQuest : getCurrentlyActiveQuest(); - if (questToUpdate != null) { - updateQuestAsync(questToUpdate, cache); - } - } - - } catch (Exception e) { - log.error("Error handling ChatMessage event for quests: {}", e.getMessage(), e); - } - } - - /** - * Asynchronously updates a quest's state in the cache. - * - * @param quest The quest to update - * @param cache The cache operations interface - */ - private void updateQuestAsync(Quest quest, CacheOperations cache) { - Microbot.getClientThread().invokeLater(() -> { - try { - if (Microbot.getClient() == null) { - return; - } - - QuestState oldState = cache.get(quest); - QuestState newState = quest.getState(Microbot.getClient()); - - if (oldState != newState) { - log.debug("\n\tdetection Quest state changed update cache\n\t\t {}: cached: {} -> client: {} ", - quest.getName(), oldState, newState); - cache.put(quest, newState); - - // If quest is now complete and was being tracked, clear tracking - if (newState == QuestState.FINISHED && trackedQuest == quest) { - log.debug("Quest completed, clearing tracking: {}", quest.getName()); - trackedQuest = null; - } - } - } catch (Exception e) { - log.error("Error updating quest state for {}: {}", quest.getName(), e.getMessage()); - } - }); - } - - @Override - public Class[] getHandledEventTypes() { - return new Class[]{VarbitChanged.class, ChatMessage.class}; - } - - @Override - public void onAttach(CacheOperations cache) { - log.debug("QuestUpdateStrategy attached to cache"); - } - - @Override - public void onDetach(CacheOperations cache) { - log.debug("QuestUpdateStrategy detached from cache"); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/SkillUpdateStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/SkillUpdateStrategy.java deleted file mode 100644 index caa3bc79bba..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/SkillUpdateStrategy.java +++ /dev/null @@ -1,82 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy.simple; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Skill; -import net.runelite.api.events.StatChanged; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.model.SkillData; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheOperations; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheUpdateStrategy; - -/** - * Cache update strategy for skill data. - * Handles StatChanged events to update skill information with enhanced data - * including temporal tracking and change detection. - */ -@Slf4j -public class SkillUpdateStrategy implements CacheUpdateStrategy { - - @Override - public void handleEvent(Object event, CacheOperations cache) { - if (event instanceof StatChanged) { - handleStatChanged((StatChanged) event, cache); - } - } - - private void handleStatChanged(StatChanged event, CacheOperations cache) { - try { - Skill skill = event.getSkill(); - if (skill != null && Microbot.getClient() != null) { - - // Get current skill data from client - int level = Microbot.getClient().getRealSkillLevel(skill); - int boostedLevel = Microbot.getClient().getBoostedSkillLevel(skill); - int experience = Microbot.getClient().getSkillExperience(skill); - - // Get existing data to preserve previous values - SkillData existingData = cache.get(skill); - - // Create new skill data with temporal and change tracking - SkillData skillData; - if (existingData != null) { - // Use withUpdate to preserve previous values - skillData = existingData.withUpdate(level, boostedLevel, experience); - } else { - // No previous data available - skillData = new SkillData(level, boostedLevel, experience); - } - - cache.put(skill, skillData); - - // Log level ups and significant experience gains - if (skillData.isLevelUp()) { - log.debug("Level up detected: {} leveled from {} to {}", skill, skillData.getPreviousLevel(), level); - } - if (skillData.getExperienceGained() > 0) { - log.debug("\n\tUpdated skill cache: {} (level: {}, boosted: {}, exp: {}, gained: {} exp)", - skill, level, boostedLevel, experience, skillData.getExperienceGained()); - } else { - log.trace("Updated skill cache: {} (level: {}, boosted: {}, exp: {})", - skill, level, boostedLevel, experience); - } - } - } catch (Exception e) { - log.error("Error handling StatChanged event: {}", e.getMessage(), e); - } - } - - @Override - public Class[] getHandledEventTypes() { - return new Class[]{StatChanged.class}; - } - - @Override - public void onAttach(CacheOperations cache) { - log.debug("SkillUpdateStrategy attached to cache"); - } - - @Override - public void onDetach(CacheOperations cache) { - log.debug("SkillUpdateStrategy detached from cache"); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/VarPlayerUpdateStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/VarPlayerUpdateStrategy.java deleted file mode 100644 index 60609d26b4d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/VarPlayerUpdateStrategy.java +++ /dev/null @@ -1,103 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy.simple; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.VarbitChanged; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.Rs2NpcCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2ObjectCache; -import net.runelite.client.plugins.microbot.util.cache.model.VarbitData; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheOperations; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheUpdateStrategy; - -import java.util.List; -import java.util.stream.Collectors; - -/** - * Cache update strategy for varplayer data. - * Handles VarbitChanged events to update varplayer information with enhanced data - * including temporal tracking, player location, and nearby entity context. - */ -@Slf4j -public class VarPlayerUpdateStrategy implements CacheUpdateStrategy { - - @Override - public void handleEvent(Object event, CacheOperations cache) { - if (event instanceof VarbitChanged) { - handleVarbitChanged((VarbitChanged) event, cache); - } - } - - private void handleVarbitChanged(VarbitChanged event, CacheOperations cache) { - try { - int varpId = event.getVarpId(); - if (varpId != -1) { - // Get current value from client - int newValue = Microbot.getClient().getVarpValue(varpId); - - // Get existing data to preserve previous value - VarbitData existingData = cache.get(varpId); - Integer previousValue = existingData != null ? existingData.getValue() : null; - - // Collect contextual information - WorldPoint finalPlayerLocation = null; - List nearbyNpcIds = null; - List nearbyObjectIds = null; - - try { - // Get player location - if (Microbot.getClient().getLocalPlayer() != null) { - finalPlayerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - - // Get nearby NPCs within 10 tiles - if (finalPlayerLocation != null) { - final WorldPoint playerLoc = finalPlayerLocation; - nearbyNpcIds = Rs2NpcCache.getInstance().stream() - .filter(npc -> npc.getWorldLocation() != null && - npc.getWorldLocation().distanceTo(playerLoc) <= 10) - .map(npc -> npc.getId()) - .distinct() - .collect(Collectors.toList()); - - // Get nearby objects within 10 tiles - nearbyObjectIds = Rs2ObjectCache.getInstance().stream() - .filter(obj -> obj.getWorldLocation() != null && - obj.getWorldLocation().distanceTo(playerLoc) <= 10) - .map(obj -> obj.getId()) - .distinct() - .collect(Collectors.toList()); - } - } catch (Exception e) { - log.debug("Could not collect contextual information for varplayer {}: {}", varpId, e.getMessage()); - // Continue with null values - the VarPlayerData constructor handles this gracefully - } - - // Create new VarbitData with contextual information - VarbitData newData = new VarbitData(newValue, previousValue, finalPlayerLocation, nearbyNpcIds, nearbyObjectIds); - - // Update the cache - cache.put(varpId, newData); - log.trace("Updated varplayer cache: {} = {} (previous: {}) at location: {}", - varpId, newValue, previousValue, finalPlayerLocation); - } - } catch (Exception e) { - log.error("Error handling VarbitChanged event for varplayer: {}", e.getMessage(), e); - } - } - - @Override - public Class[] getHandledEventTypes() { - return new Class[]{VarbitChanged.class}; - } - - @Override - public void onAttach(CacheOperations cache) { - log.debug("VarPlayerUpdateStrategy attached to cache"); - } - - @Override - public void onDetach(CacheOperations cache) { - log.debug("VarPlayerUpdateStrategy detached from cache"); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/VarbitUpdateStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/VarbitUpdateStrategy.java deleted file mode 100644 index 8d66924e38b..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/VarbitUpdateStrategy.java +++ /dev/null @@ -1,103 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy.simple; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.VarbitChanged; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.Rs2NpcCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2ObjectCache; -import net.runelite.client.plugins.microbot.util.cache.model.VarbitData; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheOperations; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheUpdateStrategy; - -import java.util.List; -import java.util.stream.Collectors; - -/** - * Cache update strategy for varbit data. - * Handles VarbitChanged events to update varbit information with enhanced data - * including temporal tracking, player location, and nearby entity context. - */ -@Slf4j -public class VarbitUpdateStrategy implements CacheUpdateStrategy { - - @Override - public void handleEvent(Object event, CacheOperations cache) { - if (event instanceof VarbitChanged) { - handleVarbitChanged((VarbitChanged) event, cache); - } - } - - private void handleVarbitChanged(VarbitChanged event, CacheOperations cache) { - try { - int varbitId = event.getVarbitId(); - if (varbitId != -1) { - // Get current value from client - int newValue = Microbot.getClient().getVarbitValue(varbitId); - - // Get existing data to preserve previous value - VarbitData existingData = cache.get(varbitId); - Integer previousValue = existingData != null ? existingData.getValue() : null; - - // Collect contextual information - WorldPoint finalPlayerLocation = null; - List nearbyNpcIds = null; - List nearbyObjectIds = null; - - try { - // Get player location - if (Microbot.getClient().getLocalPlayer() != null) { - finalPlayerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - - // Get nearby NPCs within 10 tiles - if (finalPlayerLocation != null) { - final WorldPoint playerLoc = finalPlayerLocation; - nearbyNpcIds = Rs2NpcCache.getInstance().stream() - .filter(npc -> npc.getWorldLocation() != null && - npc.getWorldLocation().distanceTo(playerLoc) <= 10) - .map(npc -> npc.getId()) - .distinct() - .collect(Collectors.toList()); - - // Get nearby objects within 10 tiles - nearbyObjectIds = Rs2ObjectCache.getInstance().stream() - .filter(obj -> obj.getWorldLocation() != null && - obj.getWorldLocation().distanceTo(playerLoc) <= 10) - .map(obj -> obj.getId()) - .distinct() - .collect(Collectors.toList()); - } - } catch (Exception e) { - log.debug("Could not collect contextual information for varbit {}: {}", varbitId, e.getMessage()); - // Continue with null values - the VarbitData constructor handles this gracefully - } - - // Create new VarbitData with contextual information - VarbitData newData = new VarbitData(newValue, previousValue, finalPlayerLocation, nearbyNpcIds, nearbyObjectIds); - - // Update the cache - cache.put(varbitId, newData); - log.trace("Updated varbit cache: {} = {} (previous: {}) at location: {}", - varbitId, newValue, previousValue, finalPlayerLocation); - } - } catch (Exception e) { - log.error("Error handling VarbitChanged event: {}", e.getMessage(), e); - } - } - - @Override - public Class[] getHandledEventTypes() { - return new Class[]{VarbitChanged.class}; - } - - @Override - public void onAttach(CacheOperations cache) { - log.debug("VarbitUpdateStrategy attached to cache"); - } - - @Override - public void onDetach(CacheOperations cache) { - log.debug("VarbitUpdateStrategy detached from cache"); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/LogOutputMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/LogOutputMode.java deleted file mode 100644 index aac6c316285..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/LogOutputMode.java +++ /dev/null @@ -1,24 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.util; - -/** - * Defines where cache logging output should be directed. - * - * @author Vox - * @version 1.0 - */ -public enum LogOutputMode { - /** - * Log only to console/logger - */ - CONSOLE_ONLY, - - /** - * Log only to file - */ - FILE_ONLY, - - /** - * Log to both console and file - */ - BOTH -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2CacheLoggingUtils.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2CacheLoggingUtils.java deleted file mode 100644 index d4585a2c689..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2CacheLoggingUtils.java +++ /dev/null @@ -1,492 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.util; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.gameval.VarbitID; -import net.runelite.api.gameval.VarPlayerID; -import net.runelite.client.RuneLite; -import net.runelite.client.plugins.microbot.util.cache.Rs2GroundItemCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2NpcCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2ObjectCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2QuestCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2SkillCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2VarPlayerCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2VarbitCache; - -import java.io.BufferedWriter; -import java.io.IOException; -import java.lang.reflect.Field; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Utility class for logging cache states and dumping to files. - * Provides reflection-based utilities for VarbitID and VarPlayerID name resolution. - * - * @author Vox - * @version 1.0 - */ -@Slf4j -public class Rs2CacheLoggingUtils { - - private static final String CACHE_LOG_FOLDER = "cache"; - private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"); - - // Cache for VarbitID field mappings to avoid repeated reflection - private static final Map varbitIdCache = new ConcurrentHashMap<>(); - private static final Map varPlayerIdCache = new ConcurrentHashMap<>(); - private static boolean varbitCacheInitialized = false; - private static boolean varPlayerCacheInitialized = false; - - /** - * Gets the RuneLite user directory for cache logs. - * - * @return Path to the cache log directory - */ - public static Path getCacheLogDirectory() { - Path runeliteDir = RuneLite.RUNELITE_DIR.toPath(); - Path microbotPluginsDir = runeliteDir.resolve("microbot-plugins"); - Path cacheDir = microbotPluginsDir.resolve(CACHE_LOG_FOLDER); - - try { - Files.createDirectories(cacheDir); - } catch (IOException e) { - log.warn("Failed to create cache log directory: {}", cacheDir, e); - // Fall back to a temp directory - return Paths.get(System.getProperty("java.io.tmpdir"), "microbot-cache"); - } - - return cacheDir; - } - - /** - * Gets a timestamp-based filename for cache dumps. - * - * @param cacheType The cache type (e.g., "npc", "object") - * @return Filename with timestamp - */ - public static String getCacheLogFilename(String cacheType) { - String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT); - return String.format("%s_cache_%s.log", cacheType, timestamp); - } - - /** - * Writes content to a cache log file. - * - * @param cacheType The cache type identifier - * @param content The content to write - * @param includeTimestamp Whether to include a timestamp in the filename - */ - public static void writeCacheLogFile(String cacheType, String content, boolean includeTimestamp) { - try { - Path logDir = getCacheLogDirectory().resolve(cacheType); - Files.createDirectories(logDir); - String filename = includeTimestamp ? getCacheLogFilename(cacheType) : cacheType + "_cache.log"; - Path logFile = logDir.resolve(filename); - - try (BufferedWriter writer = Files.newBufferedWriter(logFile)) { - writer.write(content); - writer.flush(); - } - - log.info("Cache log written to: {}", logFile.toAbsolutePath()); - - } catch (IOException e) { - log.error("Failed to write cache log file for {}: {}", cacheType, e.getMessage(), e); - } - } - - /** - * Gets the field name for a VarbitID value using reflection. - * - * @param varbitId The varbit ID value - * @return The field name if found, or the ID as string if not found - */ - public static String getVarbitFieldName(int varbitId) { - initializeVarbitCache(); - return varbitIdCache.getOrDefault(varbitId, String.valueOf(varbitId)); - } - - /** - * Gets the field name for a VarPlayerID value using reflection. - * - * @param varPlayerId The var player ID value - * @return The field name if found, or the ID as string if not found - */ - public static String getVarPlayerFieldName(int varPlayerId) { - initializeVarPlayerCache(); - return varPlayerIdCache.getOrDefault(varPlayerId, String.valueOf(varPlayerId)); - } - - /** - * Initializes the VarbitID cache using reflection. - */ - private static synchronized void initializeVarbitCache() { - if (varbitCacheInitialized) { - return; - } - - try { - Field[] fields = VarbitID.class.getDeclaredFields(); - for (Field field : fields) { - if (field.getType() == int.class && java.lang.reflect.Modifier.isStatic(field.getModifiers())) { - try { - int value = field.getInt(null); - varbitIdCache.put(value, field.getName()); - } catch (IllegalAccessException e) { - log.debug("Could not access VarbitID field: {}", field.getName()); - } - } - } - varbitCacheInitialized = true; - log.debug("Initialized VarbitID cache with {} entries", varbitIdCache.size()); - - } catch (Exception e) { - log.warn("Failed to initialize VarbitID cache via reflection", e); - varbitCacheInitialized = true; // Prevent retries - } - } - - /** - * Initializes the VarPlayerID cache using reflection. - */ - private static synchronized void initializeVarPlayerCache() { - if (varPlayerCacheInitialized) { - return; - } - - try { - Field[] fields = VarPlayerID.class.getDeclaredFields(); - for (Field field : fields) { - if (field.getType() == int.class && java.lang.reflect.Modifier.isStatic(field.getModifiers())) { - try { - int value = field.getInt(null); - varPlayerIdCache.put(value, field.getName()); - } catch (IllegalAccessException e) { - log.debug("Could not access VarPlayerID field: {}", field.getName()); - } - } - } - varPlayerCacheInitialized = true; - log.debug("Initialized VarPlayerID cache with {} entries", varPlayerIdCache.size()); - - } catch (Exception e) { - log.warn("Failed to initialize VarPlayerID cache via reflection", e); - varPlayerCacheInitialized = true; // Prevent retries - } - } - - /** - * Formats a table header with specified column headers and widths. - * - * @param headers Array of header strings - * @param columnWidths Array of column widths - * @return Formatted table header - */ - public static String formatTableHeader(String[] headers, int[] columnWidths) { - StringBuilder sb = new StringBuilder(); - sb.append("╔"); - for (int i = 0; i < columnWidths.length; i++) { - for (int j = 0; j < columnWidths[i]; j++) { - sb.append("═"); - } - if (i < columnWidths.length - 1) { - sb.append("â•Ķ"); - } - } - sb.append("╗\n"); - - sb.append("║"); - for (int i = 0; i < headers.length; i++) { - String header = truncate(headers[i], columnWidths[i] - 2); - sb.append(String.format(" %-" + (columnWidths[i] - 2) + "s ", header)); - if (i < headers.length - 1) { - sb.append("║"); - } - } - sb.append("║\n"); - - sb.append("╠"); - for (int i = 0; i < columnWidths.length; i++) { - for (int j = 0; j < columnWidths[i]; j++) { - sb.append("═"); - } - if (i < columnWidths.length - 1) { - sb.append("╮"); - } - } - sb.append("â•Ģ\n"); - - return sb.toString(); - } - - /** - * Formats a table row with specified values and column widths. - * - * @param values Array of cell values - * @param columnWidths Array of column widths - * @return Formatted table row - */ - public static String formatTableRow(String[] values, int[] columnWidths) { - StringBuilder sb = new StringBuilder(); - sb.append("║"); - for (int i = 0; i < values.length; i++) { - String value = truncate(values[i], columnWidths[i] - 2); - sb.append(String.format(" %-" + (columnWidths[i] - 2) + "s ", value)); - if (i < values.length - 1) { - sb.append("║"); - } - } - sb.append("║\n"); - return sb.toString(); - } - - /** - * Formats a table footer with specified column widths. - * - * @param columnWidths Array of column widths - * @return Formatted table footer - */ - public static String formatTableFooter(int[] columnWidths) { - StringBuilder sb = new StringBuilder(); - sb.append("╚"); - for (int i = 0; i < columnWidths.length; i++) { - for (int j = 0; j < columnWidths[i]; j++) { - sb.append("═"); - } - if (i < columnWidths.length - 1) { - sb.append("â•Đ"); - } - } - sb.append("╝\n"); - return sb.toString(); - } - - /** - * Truncates a string to the specified maximum length. - * - * @param str The string to truncate - * @param maxLength Maximum length - * @return Truncated string - */ - public static String truncate(String str, int maxLength) { - if (str == null) { - return ""; - } - if (str.length() <= maxLength) { - return str; - } - return str.substring(0, maxLength - 3) + "..."; - } - - /** - * Safely converts an object to string, handling null values. - * - * @param obj The object to convert - * @return String representation or "null" if object is null - */ - public static String safeToString(Object obj) { - return obj != null ? obj.toString() : "null"; - } - - /** - * Formats a cache header with type, size and mode information. - * - * @param cacheType The cache type name - * @param cacheSize The number of entries in the cache - * @param cacheMode The cache mode - * @return Formatted header string - */ - public static String formatCacheHeader(String cacheType, int cacheSize, String cacheMode) { - return String.format("=== %s Cache State (%d entries, %s mode) ===", - cacheType, cacheSize, cacheMode); - } - - /** - * Formats a WorldPoint location for display. - * - * @param location The WorldPoint to format - * @return Formatted location string - */ - public static String formatLocation(net.runelite.api.coords.WorldPoint location) { - if (location == null) { - return "N/A"; - } - return String.format("(%d,%d,%d)", location.getX(), location.getY(), location.getPlane()); - } - - /** - * Formats a timestamp in milliseconds to a readable date/time string. - * - * @param timestampMillis Timestamp in milliseconds - * @return Formatted timestamp string - */ - public static String formatTimestamp(long timestampMillis) { - if (timestampMillis <= 0) { - return "N/A"; - } - LocalDateTime dateTime = LocalDateTime.ofEpochSecond(timestampMillis / 1000, 0, java.time.ZoneOffset.UTC); - return dateTime.format(TIMESTAMP_FORMAT); - } - - /** - * Formats cache statistics for display. - * - * @param hitRate Cache hit rate as a percentage - * @param hits Number of cache hits - * @param misses Number of cache misses - * @param mode Cache mode - * @return Formatted statistics string - */ - public static String formatCacheStatistics(double hitRate, long hits, long misses, String mode) { - return String.format("Cache Statistics: %.1f%% hit rate (%d hits, %d misses) - Mode: %s", - hitRate, hits, misses, mode); - } - - /** - * Formats a limit message when cache content is truncated. - * - * @param totalSize The total number of items - * @param displayedSize The number of items displayed - * @return Formatted limit message or empty string if no truncation - */ - public static String formatLimitMessage(int totalSize, int displayedSize) { - if (totalSize > displayedSize) { - return String.format("... and %d more entries (showing first %d)", - totalSize - displayedSize, displayedSize); - } - return ""; - } - - /** - * Outputs cache log content based on the specified output mode. - * - * @param cacheType The cache type identifier - * @param content The content to output - * @param outputMode Where to direct the output - */ - public static void outputCacheLog(String cacheType, String content, LogOutputMode outputMode) { - switch (outputMode) { - case CONSOLE_ONLY: - log.info(content); - break; - case FILE_ONLY: - writeCacheLogFile(cacheType, content, false); - break; - case BOTH: - log.info(content); - writeCacheLogFile(cacheType, content, false); - break; - } - } - - /** - * Logs all cache states to files only. - * This is useful for generating comprehensive cache dumps for analysis. - * Uses the existing boolean-based logState methods with dumpToFile=true. - */ - public static void logAllCachesToFiles() { - log.info("Starting cache file dump for all Rs2Cache instances..."); - - try { - // NPC Cache - uses LogOutputMode.FILE_ONLY - if (Rs2NpcCache.getInstance() != null) { - Rs2NpcCache.logState(LogOutputMode.FILE_ONLY); - } - - // Object Cache - uses boolean method - if (Rs2ObjectCache.getInstance() != null) { - try { - Rs2ObjectCache.logState(LogOutputMode.FILE_ONLY); - } catch (Exception e) { - log.error("Failed to log Object Cache state: {}", e.getMessage(), e); - } - - } - - // Ground Item Cache - uses boolean method - if (Rs2GroundItemCache.getInstance() != null) { - Rs2GroundItemCache.logState(LogOutputMode.FILE_ONLY); - } - - // Skill Cache - uses boolean method - if (Rs2SkillCache.getInstance() != null) { - Rs2SkillCache.logState(LogOutputMode.FILE_ONLY); - } - - // Varbit Cache - uses boolean method - if (Rs2VarbitCache.getInstance() != null) { - Rs2VarbitCache.logState(LogOutputMode.FILE_ONLY); - } - - // VarPlayer Cache - uses boolean method - if (Rs2VarPlayerCache.getInstance() != null) { - Rs2VarPlayerCache.logState(LogOutputMode.FILE_ONLY); - } - - // Quest Cache - uses boolean method - if (Rs2QuestCache.getInstance() != null) { - Rs2QuestCache.logState(LogOutputMode.FILE_ONLY); - } - - log.info("Cache file dump completed successfully. Files written to cache log directory."); - - } catch (Exception e) { - log.error("Error during cache file dump: {}", e.getMessage(), e); - } - } - - /** - * Logs all cache states to both console and files. - * This provides comprehensive output for debugging sessions. - */ - public static void logAllCachesToConsoleAndFiles() { - log.info("Starting comprehensive cache dump (console + files)..."); - - try { - // NPC Cache - uses LogOutputMode.BOTH - if (Rs2NpcCache.getInstance() != null) { - Rs2NpcCache.logState(LogOutputMode.BOTH); - } - - // Object Cache - uses boolean method (console + file) - if (Rs2ObjectCache.getInstance() != null) { - Rs2ObjectCache.logState(LogOutputMode.BOTH); - } - - // Ground Item Cache - uses boolean method (console + file) - if (Rs2GroundItemCache.getInstance() != null) { - Rs2GroundItemCache.logState(LogOutputMode.BOTH); - } - - // Skill Cache - uses boolean method (console + file) - if (Rs2SkillCache.getInstance() != null) { - Rs2SkillCache.logState(LogOutputMode.BOTH); - } - - // Varbit Cache - uses boolean method (console + file) - if (Rs2VarbitCache.getInstance() != null) { - Rs2VarbitCache.logState(LogOutputMode.BOTH); - } - - // VarPlayer Cache - uses boolean method (console + file) - if (Rs2VarPlayerCache.getInstance() != null) { - Rs2VarPlayerCache.logState(LogOutputMode.BOTH); - } - - // Quest Cache - uses boolean method (console + file) - if (Rs2QuestCache.getInstance() != null) { - Rs2QuestCache.logState(LogOutputMode.BOTH); - } - - log.info("Comprehensive cache dump completed successfully."); - - } catch (Exception e) { - log.error("Error during comprehensive cache dump: {}", e.getMessage(), e); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2GroundItemCacheUtils.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2GroundItemCacheUtils.java deleted file mode 100644 index 8cba1de008f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2GroundItemCacheUtils.java +++ /dev/null @@ -1,1480 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.util; - -import net.runelite.api.Client; -import net.runelite.api.Perspective; -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.util.cache.Rs2GroundItemCache; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; - -import java.util.*; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Advanced cache-based utilities for ground items. - * Provides scene-independent methods for finding and filtering ground items. - * - * This class offers high-performance ground item operations using cached data, - * avoiding the need to iterate through scene tiles. - * - * @author Vox - * @version 1.0 - */ -public class Rs2GroundItemCacheUtils { - - // ============================================ - // Core Cache Access Methods - // ============================================ - - /** - * Gets ground items by their game ID. - * - * @param itemId The item ID - * @return Stream of matching ground items - */ - public static Stream getByGameId(int itemId) { - try { - return Rs2GroundItemCache.getItemsByGameId(itemId); - } catch (Exception e) { - return Stream.empty(); - } - } - - /** - * Gets the first ground item matching the criteria. - * - * @param itemId The item ID - * @return Optional containing the first matching ground item - */ - public static Optional getFirst(int itemId) { - try { - return Rs2GroundItemCache.getFirstItemByGameId(itemId); - } catch (Exception e) { - return Optional.empty(); - } - } - - /** - * Gets the closest ground item to the player. - * - * @param itemId The item ID - * @return Optional containing the closest ground item - */ - public static Optional getClosest(int itemId) { - try { - return Rs2GroundItemCache.getClosestItemByGameId(itemId); - } catch (Exception e) { - return Optional.empty(); - } - } - - /** - * Gets all cached ground items. - * - * @return Stream of all ground items - */ - public static Stream getAll() { - try { - return Rs2GroundItemCache.getAllItems(); - } catch (Exception e) { - return Stream.empty(); - } - } - - // ============================================ - // Advanced Finding Methods - // ============================================ - - /** - * Finds the first ground item matching a predicate. - * - * @param predicate The predicate to match - * @return Optional containing the first matching ground item - */ - public static Optional find(Predicate predicate) { - try { - return Rs2GroundItemCache.getAllItems() - .filter(predicate) - .findFirst(); - } catch (Exception e) { - return Optional.empty(); - } - } - - /** - * Finds all ground items matching a predicate. - * - * @param predicate The predicate to match - * @return Stream of matching ground items - */ - public static Stream findAll(Predicate predicate) { - try { - return Rs2GroundItemCache.getAllItems().filter(predicate); - } catch (Exception e) { - return Stream.empty(); - } - } - - /** - * Finds the closest ground item matching a predicate. - * - * @param predicate The predicate to match - * @return Optional containing the closest matching ground item - */ - public static Optional findClosest(Predicate predicate) { - try { - return Rs2GroundItemCache.getAllItems() - .filter(predicate) - .min(Comparator.comparingInt(Rs2GroundItemModel::getDistanceFromPlayer)); - } catch (Exception e) { - return Optional.empty(); - } - } - - // ============================================ - // Distance-Based Finding Methods - // ============================================ - - /** - * Finds the first ground item within distance from player by ID. - * - * @param itemId The item ID - * @param distance Maximum distance in tiles - * @return Optional containing the first matching ground item within distance - */ - public static Optional findWithinDistance(int itemId, int distance) { - return find(item -> item.getId() == itemId && item.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds all ground items within distance from player by ID. - * - * @param itemId The item ID - * @param distance Maximum distance in tiles - * @return Stream of matching ground items within distance - */ - public static Stream findAllWithinDistance(int itemId, int distance) { - return findAll(item -> item.getId() == itemId && item.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest ground item by ID within distance from player. - * - * @param itemId The item ID - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching ground item within distance - */ - public static Optional findClosestWithinDistance(int itemId, int distance) { - return findClosest(item -> item.getId() == itemId && item.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds ground items within distance from an anchor point. - * - * @param predicate The predicate to match - * @param anchor The anchor point - * @param distance Maximum distance in tiles - * @return Stream of matching ground items within distance from anchor - */ - public static Stream findWithinDistance(Predicate predicate, WorldPoint anchor, int distance) { - return findAll(item -> predicate.test(item) && item.getLocation().distanceTo(anchor) <= distance); - } - - /** - * Finds the closest ground item to an anchor point. - * - * @param predicate The predicate to match - * @param anchor The anchor point - * @return Optional containing the closest matching ground item to anchor - */ - public static Optional findClosest(Predicate predicate, WorldPoint anchor) { - return findAll(predicate) - .min(Comparator.comparingInt(item -> item.getLocation().distanceTo(anchor))); - } - - /** - * Finds the closest ground item to an anchor point within distance. - * - * @param predicate The predicate to match - * @param anchor The anchor point - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching ground item to anchor within distance - */ - public static Optional findClosest(Predicate predicate, WorldPoint anchor, int distance) { - return findWithinDistance(predicate, anchor, distance) - .min(Comparator.comparingInt(item -> item.getLocation().distanceTo(anchor))); - } - - // ============================================ - // Name-Based Finding Methods - // ============================================ - - /** - * Creates a predicate that matches ground items whose name contains the given string (case-insensitive). - * - * @param itemName The name to match (partial or full) - * @param exact Whether to match exactly or contain - * @return Predicate for name matching - */ - public static Predicate nameMatches(String itemName, boolean exact) { - String lower = itemName.toLowerCase(); - return item -> { - String name = item.getName(); - if (name == null) return false; - return exact ? name.equalsIgnoreCase(itemName) : name.toLowerCase().contains(lower); - }; - } - - /** - * Creates a predicate that matches ground items whose name contains the given string (case-insensitive). - * - * @param itemName The name to match (partial) - * @return Predicate for name matching - */ - public static Predicate nameMatches(String itemName) { - return nameMatches(itemName, false); - } - - /** - * Finds the first ground item by name. - * - * @param itemName The item name - * @param exact Whether to match exactly or contain - * @return Optional containing the first matching ground item - */ - public static Optional findByName(String itemName, boolean exact) { - return find(nameMatches(itemName, exact)); - } - - /** - * Finds the first ground item by name (partial match). - * - * @param itemName The item name - * @return Optional containing the first matching ground item - */ - public static Optional findByName(String itemName) { - return findByName(itemName, false); - } - - /** - * Finds the closest ground item by name. - * - * @param itemName The item name - * @param exact Whether to match exactly or contain - * @return Optional containing the closest matching ground item - */ - public static Optional findClosestByName(String itemName, boolean exact) { - return findClosest(nameMatches(itemName, exact)); - } - - /** - * Finds the closest ground item by name (partial match). - * - * @param itemName The item name - * @return Optional containing the closest matching ground item - */ - public static Optional findClosestByName(String itemName) { - return findClosestByName(itemName, false); - } - - /** - * Finds ground items by name within distance from player. - * - * @param itemName The item name - * @param exact Whether to match exactly or contain - * @param distance Maximum distance in tiles - * @return Stream of matching ground items within distance - */ - public static Stream findByNameWithinDistance(String itemName, boolean exact, int distance) { - return findAll(item -> nameMatches(itemName, exact).test(item) && item.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds ground items by name within distance from player (partial match). - * - * @param itemName The item name - * @param distance Maximum distance in tiles - * @return Stream of matching ground items within distance - */ - public static Stream findByNameWithinDistance(String itemName, int distance) { - return findByNameWithinDistance(itemName, false, distance); - } - - /** - * Finds the closest ground item by name within distance from player. - * - * @param itemName The item name - * @param exact Whether to match exactly or contain - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching ground item within distance - */ - public static Optional findClosestByNameWithinDistance(String itemName, boolean exact, int distance) { - return findClosest(item -> nameMatches(itemName, exact).test(item) && item.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest ground item by name within distance from player (partial match). - * - * @param itemName The item name - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching ground item within distance - */ - public static Optional findClosestByNameWithinDistance(String itemName, int distance) { - return findClosestByNameWithinDistance(itemName, false, distance); - } - - // ============================================ - // Array-Based ID Methods - // ============================================ - - /** - * Finds the first ground item matching any of the given IDs. - * - * @param itemIds Array of item IDs - * @return Optional containing the first matching ground item - */ - public static Optional findByIds(Integer[] itemIds) { - Set idSet = Set.of(itemIds); - return find(item -> idSet.contains(item.getId())); - } - - /** - * Finds the closest ground item matching any of the given IDs. - * - * @param itemIds Array of item IDs - * @return Optional containing the closest matching ground item - */ - public static Optional findClosestByIds(Integer[] itemIds) { - Set idSet = Set.of(itemIds); - return findClosest(item -> idSet.contains(item.getId())); - } - - /** - * Finds ground items matching any of the given IDs within distance. - * - * @param itemIds Array of item IDs - * @param distance Maximum distance in tiles - * @return Stream of matching ground items within distance - */ - public static Stream findByIdsWithinDistance(Integer[] itemIds, int distance) { - Set idSet = Set.of(itemIds); - return findAll(item -> idSet.contains(item.getId()) && item.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest ground item matching any of the given IDs within distance. - * - * @param itemIds Array of item IDs - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching ground item within distance - */ - public static Optional findClosestByIdsWithinDistance(Integer[] itemIds, int distance) { - Set idSet = Set.of(itemIds); - return findClosest(item -> idSet.contains(item.getId()) && item.isWithinDistanceFromPlayer(distance)); - } - - // ============================================ - // Value and Property-Based Methods - // ============================================ - - /** - * Finds ground items with value greater than or equal to the specified amount. - * - * @param minValue Minimum value - * @return Stream of ground items with value >= minValue - */ - public static Stream findByMinValue(int minValue) { - return findAll(item -> item.getValue() >= minValue); - } - - /** - * Finds the closest ground item with value greater than or equal to the specified amount. - * - * @param minValue Minimum value - * @return Optional containing the closest valuable ground item - */ - public static Optional findClosestByMinValue(int minValue) { - return findClosest(item -> item.getValue() >= minValue); - } - - /** - * Finds ground items with value in the specified range. - * - * @param minValue Minimum value (inclusive) - * @param maxValue Maximum value (inclusive) - * @return Stream of ground items with value in range - */ - public static Stream findByValueRange(int minValue, int maxValue) { - return findAll(item -> item.getValue() >= minValue && item.getValue() <= maxValue); - } - - /** - * Finds stackable ground items. - * - * @return Stream of stackable ground items - */ - public static Stream findStackable() { - return findAll(Rs2GroundItemModel::isStackable); - } - - /** - * Finds noted ground items. - * - * @return Stream of noted ground items - */ - public static Stream findNoted() { - return findAll(Rs2GroundItemModel::isNoted); - } - - /** - * Finds tradeable ground items. - * - * @return Stream of tradeable ground items - */ - public static Stream findTradeable() { - return findAll(Rs2GroundItemModel::isTradeable); - } - - /** - * Finds ground items owned by the player. - * - * @return Stream of owned ground items - */ - public static Stream findOwned() { - return findAll(Rs2GroundItemModel::isOwned); - } - - /** - * Finds ground items not owned by the player. - * - * @return Stream of unowned ground items - */ - public static Stream findUnowned() { - return findAll(item -> !item.isOwned()); - } - - /** - * Finds the most valuable ground item. - * - * @return Optional containing the most valuable ground item - */ - public static Optional findMostValuable() { - return getAll().max(Comparator.comparingInt(Rs2GroundItemModel::getValue)); - } - - /** - * Finds the most valuable ground item within distance. - * - * @param distance Maximum distance in tiles - * @return Optional containing the most valuable ground item within distance - */ - public static Optional findMostValuableWithinDistance(int distance) { - return findAll(item -> item.isWithinDistanceFromPlayer(distance)) - .max(Comparator.comparingInt(Rs2GroundItemModel::getValue)); - } - - // ============================================ - // Quantity-Based Methods - // ============================================ - - /** - * Finds ground items with quantity greater than or equal to the specified amount. - * - * @param minQuantity Minimum quantity - * @return Stream of ground items with quantity >= minQuantity - */ - public static Stream findByMinQuantity(int minQuantity) { - return findAll(item -> item.getQuantity() >= minQuantity); - } - - /** - * Finds ground items with quantity in the specified range. - * - * @param minQuantity Minimum quantity (inclusive) - * @param maxQuantity Maximum quantity (inclusive) - * @return Stream of ground items with quantity in range - */ - public static Stream findByQuantityRange(int minQuantity, int maxQuantity) { - return findAll(item -> item.getQuantity() >= minQuantity && item.getQuantity() <= maxQuantity); - } - - /** - * Finds the ground item with the highest quantity for a specific item ID. - * - * @param itemId The item ID - * @return Optional containing the ground item with highest quantity - */ - public static Optional findHighestQuantity(int itemId) { - return getByGameId(itemId).max(Comparator.comparingInt(Rs2GroundItemModel::getQuantity)); - } - - // ============================================ - // Age-Based Methods - // ============================================ - - /** - * Finds ground items that have been on the ground for at least the specified number of ticks. - * - * @param minTicks Minimum ticks since spawn - * @return Stream of ground items aged at least minTicks - */ - public static Stream findByMinAge(int minTicks) { - return findAll(item -> item.getTicksSinceSpawn() >= minTicks); - } - - /** - * Finds ground items that have been on the ground for less than the specified number of ticks. - * - * @param maxTicks Maximum ticks since spawn - * @return Stream of fresh ground items - */ - public static Stream findFresh(int maxTicks) { - return findAll(item -> item.getTicksSinceSpawn() <= maxTicks); - } - - /** - * Finds the oldest ground item. - * - * @return Optional containing the oldest ground item - */ - public static Optional findOldest() { - return getAll().max(Comparator.comparingInt(Rs2GroundItemModel::getTicksSinceSpawn)); - } - - /** - * Finds the newest ground item. - * - * @return Optional containing the newest ground item - */ - public static Optional findNewest() { - return getAll().min(Comparator.comparingInt(Rs2GroundItemModel::getTicksSinceSpawn)); - } - - // ============================================ - // Scene and Viewport Extraction Methods - // ============================================ - - /** - * Gets all ground items currently in the scene (all cached ground items). - * This includes ground items that may not be visible in the current viewport. - * - * @return Stream of all ground items in the scene - */ - public static Stream getAllInScene() { - return getAll(); - } - - /** - * Gets all ground items currently visible in the viewport (on screen). - * Only includes ground items whose location can be converted to screen coordinates. - * - * @return Stream of ground items visible in viewport - */ - public static Stream getAllInViewport() { - return filterVisibleInViewport(getAll()); - } - - /** - * Gets all ground items by ID that are currently visible in the viewport. - * - * @param itemId The item ID to filter by - * @return Stream of ground items with the specified ID that are visible in viewport - */ - public static Stream getAllInViewport(int itemId) { - return filterVisibleInViewport(getByGameId(itemId)); - } - - /** - * Gets the closest ground item in the viewport by ID. - * - * @param itemId The item ID - * @return Optional containing the closest ground item in viewport - */ - public static Optional getClosestInViewport(int itemId) { - return getAllInViewport(itemId) - .min(Comparator.comparingInt(Rs2GroundItemModel::getDistanceFromPlayer)); - } - - /** - * Gets all ground items in the viewport that are interactable (within reasonable distance). - * - * @param maxDistance Maximum distance for interaction - * @return Stream of interactable ground items in viewport - */ - public static Stream getAllInteractable(int maxDistance) { - return getAllInViewport() - .filter(item -> isInteractable(item, maxDistance)); - } - - /** - * Gets all ground items by ID in the viewport that are interactable. - * - * @param itemId The item ID - * @param maxDistance Maximum distance for interaction - * @return Stream of interactable ground items with the specified ID - */ - public static Stream getAllInteractable(int itemId, int maxDistance) { - return getAllInViewport(itemId) - .filter(item -> isInteractable(item, maxDistance)); - } - - /** - * Gets the closest interactable ground item by ID. - * - * @param itemId The item ID - * @param maxDistance Maximum distance for interaction - * @return Optional containing the closest interactable ground item - */ - public static Optional getClosestInteractable(int itemId, int maxDistance) { - return getAllInteractable(itemId, maxDistance) - .min(Comparator.comparingInt(Rs2GroundItemModel::getDistanceFromPlayer)); - } - - // ============================================ - // Line of Sight Utilities - // ============================================ - - /** - * Checks if there is a line of sight between the player and a ground item. - * Uses RuneLite's WorldArea.hasLineOfSightTo for accurate scene collision detection. - * - * @param groundItem The ground item to check - * @return True if line of sight exists, false otherwise - */ - public static boolean hasLineOfSight(Rs2GroundItemModel groundItem) { - if (groundItem == null) return false; - - try { - // Get player's current world location and create a small area (1x1) - WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - return hasLineOfSight(playerLocation, groundItem); - } catch (Exception e) { - return false; - } - } - - /** - * Checks if there is a line of sight between a specific point and a ground item. - * - * @param point The world point to check from - * @param groundItem The ground item to check against - * @return True if line of sight exists, false otherwise - */ - public static boolean hasLineOfSight(WorldPoint point, Rs2GroundItemModel groundItem) { - if (groundItem == null || point == null) return false; - - try { - WorldPoint itemLocation = groundItem.getLocation(); - - // Check same plane - if (point.getPlane() != itemLocation.getPlane()) { - return false; - } - - // Ground items are always 1x1 - return new WorldArea(itemLocation, 1, 1) - .hasLineOfSightTo( - Microbot.getClient().getTopLevelWorldView(), - new WorldArea(point, 1, 1)); - } catch (Exception e) { - return false; - } - } - - /** - * Gets all ground items that have line of sight to the player. - * Useful for identifying interactive items. - * - * @return Stream of ground items with line of sight to player - */ - public static Stream getGroundItemsWithLineOfSightToPlayer() { - return getAll().filter(Rs2GroundItemCacheUtils::hasLineOfSight); - } - - /** - * Gets all ground items that have line of sight to a specific world point. - * - * @param point The world point to check from - * @return Stream of ground items with line of sight to the point - */ - public static Stream getGroundItemsWithLineOfSightTo(WorldPoint point) { - return getAll().filter(item -> hasLineOfSight(point, item)); - } - - /** - * Gets all ground items at a location that have line of sight to the player. - * - * @param worldPoint The world point to check at - * @param maxDistance Maximum distance from the world point - * @return Stream of ground items at the location with line of sight - */ - public static Stream getGroundItemsAtLocationWithLineOfSight(WorldPoint worldPoint, int maxDistance) { - return getAll() - .filter(item -> item.getLocation().distanceTo(worldPoint) <= maxDistance) - .filter(Rs2GroundItemCacheUtils::hasLineOfSight); - } - - // ============================================ - // Looting-Specific Utility Methods - // ============================================ - - /** - * Gets all ground items that are lootable by the player. - * - * @return Stream of lootable ground items - */ - public static Stream getLootableItems() { - return findAll(Rs2GroundItemModel::isLootAble); - } - - /** - * Gets all lootable ground items within distance from player. - * - * @param distance Maximum distance from player - * @return Stream of lootable ground items within distance - */ - public static Stream getLootableItemsWithinDistance(int distance) { - return findAll(item -> item.isLootAble() && item.isWithinDistanceFromPlayer(distance)); - } - - /** - * Gets lootable ground items with value greater than or equal to the specified amount. - * - * @param minValue Minimum total value threshold - * @return Stream of valuable lootable ground items - */ - public static Stream getLootableItemsByValue(int minValue) { - return findAll(item -> item.isLootAble() && item.isWorthLooting(minValue)); - } - - /** - * Gets lootable ground items with Grand Exchange value greater than or equal to the specified amount. - * - * @param minGeValue Minimum GE value threshold - * @return Stream of valuable lootable ground items - */ - public static Stream getLootableItemsByGeValue(int minGeValue) { - return findAll(item -> item.isLootAble() && item.isWorthLootingGe(minGeValue)); - } - - /** - * Gets lootable ground items in value range. - * - * @param minValue Minimum total value (inclusive) - * @param maxValue Maximum total value (inclusive) - * @return Stream of lootable ground items in value range - */ - public static Stream getLootableItemsByValueRange(int minValue, int maxValue) { - return findAll(item -> item.isLootAble() && - item.getTotalValue() >= minValue && - item.getTotalValue() <= maxValue); - } - - /** - * Gets commonly desired loot items that are available for pickup. - * - * @return Stream of common loot ground items - */ - public static Stream getCommonLootItems() { - return findAll(item -> item.isLootAble() && item.isCommonLoot()); - } - - /** - * Gets high-priority items that should be looted urgently. - * - * @return Stream of priority loot ground items - */ - public static Stream getPriorityLootItems() { - return findAll(item -> item.isLootAble() && item.shouldPrioritize()); - } - - /** - * Gets items that are profitable to high alch. - * - * @param minProfit The minimum profit threshold - * @return Stream of profitable high alch ground items - */ - public static Stream getHighAlchProfitableItems(int minProfit) { - return findAll(item -> item.isLootAble() && item.isProfitableToHighAlch(minProfit)); - } - - /** - * Gets the most valuable lootable item within distance. - * - * @param maxDistance Maximum distance from player - * @return Optional containing the most valuable lootable item - */ - public static Optional getMostValuableLootableItem(int maxDistance) { - return getLootableItemsWithinDistance(maxDistance) - .max(Comparator.comparingInt(Rs2GroundItemModel::getTotalValue)); - } - - /** - * Gets the closest valuable item to the player. - * - * @param minValue Minimum value threshold - * @return Optional containing the closest valuable lootable item - */ - public static Optional getClosestValuableItem(int minValue) { - return findClosest(item -> item.isLootAble() && item.isWorthLooting(minValue)); - } - - /** - * Gets the closest lootable item to the player. - * - * @return Optional containing the closest lootable item - */ - public static Optional getClosestLootableItem() { - return findClosest(Rs2GroundItemModel::isLootAble); - } - - /** - * Gets the closest lootable item to the player within distance. - * - * @param maxDistance Maximum distance from player - * @return Optional containing the closest lootable item within distance - */ - public static Optional getClosestLootableItemWithinDistance(int maxDistance) { - return findClosest(item -> item.isLootAble() && item.isWithinDistanceFromPlayer(maxDistance)); - } - - /** - * Gets lootable items that match any of the provided item IDs. - * - * @param itemIds Array of item IDs to match - * @return Stream of matching lootable ground items - */ - public static Stream getLootableItemsByIds(Integer[] itemIds) { - Set idSet = Set.of(itemIds); - return findAll(item -> item.isLootAble() && idSet.contains(item.getId())); - } - - /** - * Gets lootable items by name pattern. - * - * @param namePattern The name pattern (supports partial matches) - * @param exact Whether to match exactly or use contains - * @return Stream of matching lootable ground items - */ - public static Stream getLootableItemsByName(String namePattern, boolean exact) { - return findAll(item -> item.isLootAble() && nameMatches(namePattern, exact).test(item)); - } - - /** - * Gets lootable items by name pattern (partial match). - * - * @param namePattern The name pattern - * @return Stream of matching lootable ground items - */ - public static Stream getLootableItemsByName(String namePattern) { - return getLootableItemsByName(namePattern, false); - } - - // ============================================ - // Despawn-Based Utility Methods - // ============================================ - - /** - * Gets ground items that will despawn within the specified number of seconds. - * - * @param seconds The time threshold in seconds - * @return Stream of ground items about to despawn - */ - public static Stream getItemsDespawningWithin(long seconds) { - return findAll(item -> item.willDespawnWithin(seconds)); - } - - /** - * Gets ground items that will despawn within the specified number of ticks. - * - * @param ticks The time threshold in ticks - * @return Stream of ground items about to despawn - */ - public static Stream getItemsDespawningWithinTicks(int ticks) { - return findAll(item -> item.willDespawnWithinTicks(ticks)); - } - - /** - * Gets lootable ground items that will despawn within the specified number of seconds. - * - * @param seconds The time threshold in seconds - * @return Stream of lootable ground items about to despawn - */ - public static Stream getLootableItemsDespawningWithin(long seconds) { - return findAll(item -> item.isLootAble() && item.willDespawnWithin(seconds)); - } - - /** - * Gets the ground item that will despawn next. - * - * @return Optional containing the next ground item to despawn - */ - public static Optional getNextItemToDespawn() { - return findAll(item -> !item.isDespawned()) - .min(Comparator.comparingLong(Rs2GroundItemModel::getSecondsUntilDespawn)); - } - - /** - * Gets the lootable ground item that will despawn next. - * - * @return Optional containing the next lootable ground item to despawn - */ - public static Optional getNextLootableItemToDespawn() { - return findAll(item -> item.isLootAble() && !item.isDespawned()) - .min(Comparator.comparingLong(Rs2GroundItemModel::getSecondsUntilDespawn)); - } - - /** - * Gets the time in seconds until the next item despawns. - * - * @return Seconds until next despawn, or -1 if no items - */ - public static long getSecondsUntilNextDespawn() { - return getNextItemToDespawn() - .map(Rs2GroundItemModel::getSecondsUntilDespawn) - .orElse(-1L); - } - - /** - * Gets the time in seconds until the next lootable item despawns. - * - * @return Seconds until next lootable item despawn, or -1 if no items - */ - public static long getSecondsUntilNextLootableDespawn() { - return getNextLootableItemToDespawn() - .map(Rs2GroundItemModel::getSecondsUntilDespawn) - .orElse(-1L); - } - - /** - * Gets ground items that have despawned and should be cleaned up. - * - * @return Stream of despawned ground items - */ - public static Stream getDespawnedItems() { - return findAll(Rs2GroundItemModel::isDespawned); - } - - // ============================================ - // Statistics and Analysis Methods - // ============================================ - - /** - * Gets the total value of all lootable items. - * - * @return Total value of all lootable ground items - */ - public static int getTotalLootableValue() { - return getLootableItems() - .mapToInt(Rs2GroundItemModel::getTotalValue) - .sum(); - } - - /** - * Gets the total Grand Exchange value of all lootable items. - * - * @return Total GE value of all lootable ground items - */ - public static int getTotalLootableGeValue() { - return getLootableItems() - .mapToInt(Rs2GroundItemModel::getTotalGeValue) - .sum(); - } - - /** - * Gets the count of lootable items. - * - * @return Number of lootable ground items - */ - public static int getLootableItemCount() { - return (int) getLootableItems().count(); - } - - /** - * Gets the count of lootable items within distance. - * - * @param distance Maximum distance from player - * @return Number of lootable ground items within distance - */ - public static int getLootableItemCountWithinDistance(int distance) { - return (int) getLootableItemsWithinDistance(distance).count(); - } - - - - /** - * Gets statistics about lootable items. - * - * @return Map containing lootable item statistics - */ - public static Map getLootableItemStatistics() { - Map stats = new HashMap<>(); - - List allItems = getAll().collect(Collectors.toList()); - List lootableItems = getLootableItems().collect(Collectors.toList()); - - stats.put("totalItems", allItems.size()); - stats.put("lootableItems", lootableItems.size()); - stats.put("lootablePercentage", allItems.isEmpty() ? 0 : (lootableItems.size() * 100.0 / allItems.size())); - stats.put("totalLootableValue", lootableItems.stream().mapToInt(Rs2GroundItemModel::getTotalValue).sum()); - stats.put("totalLootableGeValue", lootableItems.stream().mapToInt(Rs2GroundItemModel::getTotalGeValue).sum()); - stats.put("averageLootableValue", lootableItems.isEmpty() ? 0 : - lootableItems.stream().mapToInt(Rs2GroundItemModel::getTotalValue).average().orElse(0)); - stats.put("lootableItemsDespawningIn30s", lootableItems.stream().filter(item -> item.willDespawnWithin(30)).count()); - stats.put("secondsUntilNextLootableDespawn", getSecondsUntilNextLootableDespawn()); - stats.put("priorityLootItems", getPriorityLootItems().count()); - stats.put("commonLootItems", getCommonLootItems().count()); - - return stats; - } - - // ============================================ - // Advanced Filtering Methods - // ============================================ - - /** - * Gets ground items matching multiple criteria with custom predicates. - * - * @param isLootable Whether to filter for lootable items only - * @param minValue Minimum value filter (0 to ignore) - * @param maxDistance Maximum distance filter (0 to ignore) - * @param customPredicate Additional custom predicate (null to ignore) - * @return Stream of matching ground items - */ - public static Stream getItemsWithCriteria( - boolean isLootable, - int minValue, - int maxDistance, - Predicate customPredicate) { - - Stream stream = getAll(); - - if (isLootable) { - stream = stream.filter(Rs2GroundItemModel::isLootAble); - } - - if (minValue > 0) { - stream = stream.filter(item -> item.getTotalValue() >= minValue); - } - - if (maxDistance > 0) { - stream = stream.filter(item -> item.isWithinDistanceFromPlayer(maxDistance)); - } - - if (customPredicate != null) { - stream = stream.filter(customPredicate); - } - - return stream; - } - - /** - * Gets items that are both lootable and have line of sight to the player. - * Useful for identifying immediately interactable loot. - * - * @return Stream of lootable ground items with line of sight - */ - public static Stream getLootableItemsWithLineOfSight() { - return getLootableItems().filter(Rs2GroundItemCacheUtils::hasLineOfSight); - } - - /** - * Gets lootable items within distance that have line of sight to the player. - * - * @param maxDistance Maximum distance from player - * @return Stream of lootable ground items within distance with line of sight - */ - public static Stream getLootableItemsWithLineOfSightWithinDistance(int maxDistance) { - return getLootableItemsWithinDistance(maxDistance) - .filter(Rs2GroundItemCacheUtils::hasLineOfSight); - } - - /** - * Gets the closest lootable item with line of sight to the player. - * - * @return Optional containing the closest lootable item with line of sight - */ - public static Optional getClosestLootableItemWithLineOfSight() { - return getLootableItemsWithLineOfSight() - .min(Comparator.comparingInt(Rs2GroundItemModel::getDistanceFromPlayer)); - } - - /** - * Gets the closest valuable lootable item with line of sight to the player. - * - * @param minValue Minimum value threshold - * @return Optional containing the closest valuable lootable item with line of sight - */ - public static Optional getClosestValuableLootableItemWithLineOfSight(int minValue) { - return getLootableItemsWithLineOfSight() - .filter(item -> item.isWorthLooting(minValue)) - .min(Comparator.comparingInt(Rs2GroundItemModel::getDistanceFromPlayer)); - } - - // ============================================ - // Tile-Based Access Methods (Rs2GroundItem replacement support) - // ============================================ - - /** - * Gets all ground items at a specific tile location. - * Direct replacement for Rs2GroundItem.getAllAt(x, y). - * - * @param location The world point location of the tile - * @return Stream of ground items at the specified tile - */ - public static Stream getItemsAtTile(WorldPoint location) { - return getAll().filter(item -> item.getLocation().equals(location)); - } - - /** - * Gets all ground items at a specific tile coordinate. - * Direct replacement for Rs2GroundItem.getAllAt(x, y). - * - * @param x The x coordinate of the tile - * @param y The y coordinate of the tile - * @return Stream of ground items at the specified tile - */ - public static Stream getItemsAtTile(int x, int y) { - return getItemsAtTile(new WorldPoint(x, y, Microbot.getClient().getLocalPlayer().getWorldLocation().getPlane())); - } - - /** - * Gets all ground items within a range of a specific WorldPoint. - * Direct replacement for Rs2GroundItem.getAllFromWorldPoint(range, worldPoint). - * - * @param range The radius in tiles to search around the given world point - * @param worldPoint The center WorldPoint to search around - * @return Stream of ground items found within the specified range, sorted by proximity - */ - public static Stream getItemsFromWorldPoint(int range, WorldPoint worldPoint) { - return getAll() - .filter(item -> item.getLocation().distanceTo(worldPoint) <= range) - .sorted(Comparator.comparingInt(item -> item.getLocation().distanceTo(worldPoint))); - } - - /** - * Gets all ground items within a range of the player. - * Direct replacement for Rs2GroundItem.getAll(range). - * - * @param range The radius in tiles to search around the player - * @return Stream of ground items found within the specified range, sorted by proximity to player - */ - public static Stream getItemsAroundPlayer(int range) { - try { - WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - return getItemsFromWorldPoint(range, playerLocation); - } catch (Exception e) { - return Stream.empty(); - } - } - - // ============================================ - // Enhanced Interaction Utilities - // ============================================ - - /** - * Finds the best ground item to interact with based on criteria. - * - * @param itemId The item ID to search for - * @param action The action to perform (e.g., "Take") - * @param range Maximum search range - * @return Optional containing the best item to interact with - */ - public static Optional findBestInteractionTarget(int itemId, String action, int range) { - return getItemsAroundPlayer(range) - .filter(item -> item.getId() == itemId) - .filter(item -> item.isLootAble()) - .filter(Rs2GroundItemCacheUtils::hasLineOfSight) - .min(Comparator.comparingInt(Rs2GroundItemModel::getDistanceFromPlayer)); - } - - /** - * Finds the best ground item to interact with based on name. - * - * @param itemName The item name to search for - * @param action The action to perform (e.g., "Take") - * @param range Maximum search range - * @return Optional containing the best item to interact with - */ - public static Optional findBestInteractionTarget(String itemName, String action, int range) { - return getItemsAroundPlayer(range) - .filter(item -> nameMatches(itemName, false).test(item)) - .filter(item -> item.isLootAble()) - .filter(Rs2GroundItemCacheUtils::hasLineOfSight) - .min(Comparator.comparingInt(Rs2GroundItemModel::getDistanceFromPlayer)); - } - - /** - * Checks if any ground item exists with the specified ID within range. - * Direct replacement for Rs2GroundItem.exists(id, range). - * - * @param itemId The item ID to check for - * @param range Maximum search range - * @return true if the item exists within range - */ - public static boolean exists(int itemId, int range) { - return getItemsAroundPlayer(range) - .anyMatch(item -> item.getId() == itemId); - } - - /** - * Checks if any ground item exists with the specified name within range. - * Direct replacement for Rs2GroundItem.exists(itemName, range). - * - * @param itemName The item name to check for - * @param range Maximum search range - * @return true if the item exists within range - */ - public static boolean exists(String itemName, int range) { - return getItemsAroundPlayer(range) - .anyMatch(item -> nameMatches(itemName, false).test(item)); - } - - /** - * Checks if valuable items exist on the ground based on minimum value. - * Direct replacement for Rs2GroundItem.isItemBasedOnValueOnGround(value, range). - * - * @param minValue Minimum value threshold - * @param range Maximum search range - * @return true if valuable items exist within range - */ - public static boolean existsValueableItems(int minValue, int range) { - return getLootableItemsWithinDistance(range) - .anyMatch(item -> item.isWorthLooting(minValue)); - } - - // ============================================ - // Batch Processing Methods - // ============================================ - - /** - * Gets multiple ground items of different IDs within range. - * Useful for batch operations. - * - * @param itemIds Array of item IDs to search for - * @param range Maximum search range - * @return Map of item ID to list of ground items - */ - public static Map> getBatchItems(Integer[] itemIds, int range) { - Set idSet = Set.of(itemIds); - return getItemsAroundPlayer(range) - .filter(item -> idSet.contains(item.getId())) - .filter(item -> item.isLootAble()) - .collect(Collectors.groupingBy(Rs2GroundItemModel::getId)); - } - - /** - * Gets ground items sorted by value within range. - * Useful for priority-based looting. - * - * @param range Maximum search range - * @param minValue Minimum value threshold - * @return Stream of ground items sorted by value (highest first) - */ - public static Stream getItemsSortedByValue(int range, int minValue) { - return getLootableItemsWithinDistance(range) - .filter(item -> item.getTotalValue() >= minValue) - .sorted((a, b) -> Integer.compare(b.getTotalValue(), a.getTotalValue())); - } - - /** - * Gets ground items sorted by despawn urgency. - * Items closest to despawning are returned first. - * - * @param range Maximum search range - * @return Stream of ground items sorted by despawn urgency - */ - public static Stream getItemsSortedByDespawnUrgency(int range) { - return getLootableItemsWithinDistance(range) - .filter(item -> !item.isDespawned()) - .sorted(Comparator.comparingLong(Rs2GroundItemModel::getSecondsUntilDespawn)); - } - - // ============================================ - // Viewport Visibility and Interactability Utilities - // ============================================ - - /** - * Checks if a ground item is visible in the current viewport. - * Uses the tile location with client thread safety to determine visibility. - * - * @param groundItem The ground item to check - * @return true if the ground item's location is visible on screen - */ - public static boolean isVisibleInViewport(Rs2GroundItemModel groundItem) { - try { - if (groundItem == null || groundItem.getLocation() == null) { - return false; - } - - // Use client thread for safe access to client state - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - Client client = Microbot.getClient(); - if (client == null) { - return false; - } - - // Convert world point to local point - LocalPoint localPoint = LocalPoint.fromWorld(client.getTopLevelWorldView(), groundItem.getLocation()); - if (localPoint == null) { - return false; - } - - // Check if the local point can be converted to canvas coordinates - net.runelite.api.Point canvasPoint = Perspective.localToCanvas(client, localPoint, client.getTopLevelWorldView().getPlane()); - return canvasPoint != null; - }).orElse(false); - } catch (Exception e) { - return false; - } - } - - /** - * Checks if any entity with a location is within the viewport by checking canvas conversion. - * This is a generic method that can work with any entity that has a world location. - * Uses client thread for safe access to client state. - * - * @param worldPoint The world point to check - * @return true if the location is visible on screen - */ - public static boolean isLocationVisibleInViewport(net.runelite.api.coords.WorldPoint worldPoint) { - try { - if (worldPoint == null) { - return false; - } - - // Use client thread for safe access to client state - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - Client client = Microbot.getClient(); - if (client == null) { - return false; - } - - LocalPoint localPoint = LocalPoint.fromWorld(client.getTopLevelWorldView(), worldPoint); - if (localPoint == null) { - return false; - } - - net.runelite.api.Point canvasPoint = Perspective.localToCanvas(client, localPoint, client.getTopLevelWorldView().getPlane()); - return canvasPoint != null; - }).orElse(false); - } catch (Exception e) { - return false; - } - } - - /** - * Filters a stream of ground items to only include those visible in viewport. - * - * @param groundItemStream Stream of ground items to filter - * @return Stream of ground items visible in viewport - */ - public static Stream filterVisibleInViewport(Stream groundItemStream) { - return groundItemStream.filter(Rs2GroundItemCacheUtils::isVisibleInViewport); - } - - /** - * Checks if a ground item is interactable (visible and within reasonable distance). - * - * @param groundItem The ground item to check - * @param maxDistance Maximum distance in tiles for interaction - * @return true if the ground item is interactable - */ - public static boolean isInteractable(Rs2GroundItemModel groundItem, int maxDistance) { - try { - if (groundItem == null) { - return false; - } - - // Check if visible in viewport first - if (!isVisibleInViewport(groundItem)) { - return false; - } - - // Check distance from player - return groundItem.getDistanceFromPlayer() <= maxDistance; - } catch (Exception e) { - return false; - } - } - - /** - * Checks if an entity at a world point is interactable (within reasonable distance and visible). - * Uses client thread for safe access to player location. - * - * @param worldPoint The world point to check - * @param maxDistance Maximum distance in tiles for interaction - * @return true if the location is potentially interactable - */ - public static boolean isInteractable(net.runelite.api.coords.WorldPoint worldPoint, int maxDistance) { - try { - if (worldPoint == null) { - return false; - } - - // Use client thread for safe access to player location - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - net.runelite.api.coords.WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - if (playerLocation.distanceTo(worldPoint) > maxDistance) { - return false; - } - - // Check if visible in viewport (already uses client thread internally) - return isLocationVisibleInViewport(worldPoint); - }).orElse(false); - } catch (Exception e) { - return false; - } - } - - // ============================================ - // Updated Existing Methods to Use Local Functions - // ============================================ - - /** - * Gets all ground items visible in the viewport. - * - * @return Stream of ground items visible in viewport - */ - public static Stream getVisibleInViewport() { - return filterVisibleInViewport(getAll()); - } - - /** - * Gets ground items by ID that are visible in the viewport. - * - * @param itemId The item ID - * @return Stream of ground items with the specified ID visible in viewport - */ - public static Stream getVisibleInViewportById(int itemId) { - return filterVisibleInViewport(getByGameId(itemId)); - } - - /** - * Finds interactable ground items by ID within distance from player. - * - * @param itemId The item ID - * @param maxDistance Maximum distance in tiles - * @return Stream of interactable ground items with the specified ID - */ - public static Stream findInteractableById(int itemId, int maxDistance) { - return getByGameId(itemId) - .filter(item -> isInteractable(item, maxDistance)); - } - - /** - * Finds interactable ground items by name within distance from player. - * - * @param name The item name - * @param maxDistance Maximum distance in tiles - * @return Stream of interactable ground items with the specified name - */ - public static Stream findInteractableByName(String name, int maxDistance) { - return findAll(nameMatches(name, false)) - .filter(item -> isInteractable(item, maxDistance)); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2NpcCacheUtils.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2NpcCacheUtils.java deleted file mode 100644 index 57e7faaa1df..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2NpcCacheUtils.java +++ /dev/null @@ -1,1185 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.util; - -import net.runelite.api.Client; -import net.runelite.api.NPCComposition; -import net.runelite.api.Perspective; -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.util.cache.Rs2NpcCache; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -import java.util.*; -import java.util.function.Predicate; -import java.util.stream.Stream; - -/** - * Cache-based utility class for NPC operations. - * Provides comprehensive utilities for finding and filtering NPCs using the cache system. - * This is a cache-based alternative to Rs2Npc for persistent NPC tracking. - */ -public class Rs2NpcCacheUtils { - - // ============================================ - // Primary Player-Based Utility Methods - // ============================================ - - /** - * Gets all NPCs within a specified radius from the player's current location. - * This is the method called by toggleNearestNpcTracking() and similar functions. - * - * @param radius Maximum distance in tiles from player - * @return Stream of NPCs within the specified radius from player - */ - public static Stream getNearBy(int radius) { - WorldPoint playerLocation = Rs2Player.getWorldLocation(); - if (playerLocation == null) { - return Stream.empty(); - } - return findWithinDistance(npc -> true, playerLocation, radius); - } - - /** - * Gets all NPCs within a specified radius from the player's current location, - * sorted by distance from closest to furthest. - * - * @param radius Maximum distance in tiles from player - * @return Stream of NPCs within the specified radius, sorted by distance - */ - public static Stream getNearBySorted(int radius) { - return getNearBy(radius) - .sorted(Comparator.comparingInt(Rs2NpcModel::getDistanceFromPlayer)); - } - - /** - * Gets the nearest NPC to the player within the specified radius. - * - * @param radius Maximum distance in tiles from player - * @return Optional containing the nearest NPC, or empty if none found - */ - public static Optional getNearestWithinRadius(int radius) { - return getNearBy(radius) - .min(Comparator.comparingInt(Rs2NpcModel::getDistanceFromPlayer)); - } - - // ============================================ - // Action-Based Utility Methods (Rs2Npc compatibility) - // ============================================ - - /** - * Creates a predicate that matches NPCs with a specific action. - * - * @param action The action to check for (e.g., "Talk-to", "Bank", "Trade") - * @return Predicate for action matching - */ - public static Predicate hasAction(String action) { - return npc -> { - try { - NPCComposition baseComposition = npc.getComposition(); - NPCComposition transformedComposition = npc.getTransformedComposition(); - - List baseActions = baseComposition != null ? - Arrays.asList(baseComposition.getActions()) : Collections.emptyList(); - List transformedActions = transformedComposition != null ? - Arrays.asList(transformedComposition.getActions()) : Collections.emptyList(); - - return baseActions.contains(action) || transformedActions.contains(action); - } catch (Exception e) { - return false; - } - }; - } - - /** - * Finds NPCs with a specific action. - * - * @param action The action to search for - * @return Stream of NPCs with the specified action - */ - public static Stream findWithAction(String action) { - return findAll(hasAction(action)); - } - - /** - * Finds the nearest NPC with a specific action. - * Equivalent to Rs2Npc.getNearestNpcWithAction(). - * - * @param action The action to search for - * @return Optional containing the nearest NPC with the action - */ - public static Optional findNearestWithAction(String action) { - return findClosest(hasAction(action)); - } - - /** - * Finds NPCs with a specific action within distance from player. - * - * @param action The action to search for - * @param distance Maximum distance in tiles - * @return Stream of NPCs with the action within distance - */ - public static Stream findWithActionWithinDistance(String action, int distance) { - return findAll(npc -> hasAction(action).test(npc) && npc.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the nearest NPC with a specific action within distance from player. - * - * @param action The action to search for - * @param distance Maximum distance in tiles - * @return Optional containing the nearest NPC with the action within distance - */ - public static Optional findNearestWithActionWithinDistance(String action, int distance) { - return findClosest(npc -> hasAction(action).test(npc) && npc.isWithinDistanceFromPlayer(distance)); - } - - /** - * Gets the first available action from a list of possible actions for an NPC. - * Equivalent to Rs2Npc.getAvailableAction(). - * - * @param npc The NPC to check - * @param possibleActions List of actions to check for - * @return The first available action, or null if none found - */ - public static String getAvailableAction(Rs2NpcModel npc, List possibleActions) { - if (npc == null || possibleActions == null) return null; - - try { - NPCComposition baseComposition = npc.getComposition(); - NPCComposition transformedComposition = npc.getTransformedComposition(); - - List baseActions = baseComposition != null ? - Arrays.asList(baseComposition.getActions()) : Collections.emptyList(); - List transformedActions = transformedComposition != null ? - Arrays.asList(transformedComposition.getActions()) : Collections.emptyList(); - - for (String action : possibleActions) { - if (baseActions.contains(action) || transformedActions.contains(action)) { - return action; - } - } - return null; - } catch (Exception e) { - return null; - } - } - - // ============================================ - // Specialized NPC Type Utilities - // ============================================ - - /** - * Finds pet NPCs (NPCs with "Dismiss" action that are interacting with player). - * Equivalent to Rs2Npc pet detection logic. - * - * @return Stream of pet NPCs - */ - public static Stream findPets() { - return findAll(npc -> { - try { - NPCComposition npcComposition = npc.getComposition(); - if (npcComposition == null) return false; - - List npcActions = Arrays.asList(npcComposition.getActions()); - if (npcActions.isEmpty()) return false; - - return npcActions.contains("Dismiss") && - Objects.equals(npc.getInteracting(), Microbot.getClient().getLocalPlayer()); - } catch (Exception e) { - return false; - } - }); - } - - /** - * Finds the closest pet NPC. - * - * @return Optional containing the closest pet NPC - */ - public static Optional findClosestPet() { - return findClosest(npc -> { - try { - NPCComposition npcComposition = npc.getComposition(); - if (npcComposition == null) return false; - - List npcActions = Arrays.asList(npcComposition.getActions()); - if (npcActions.isEmpty()) return false; - - return npcActions.contains("Dismiss") && - Objects.equals(npc.getInteracting(), Microbot.getClient().getLocalPlayer()); - } catch (Exception e) { - return false; - } - }); - } - - /** - * Finds bank NPCs (NPCs with "Bank" action). - * Equivalent to Rs2Npc bank detection logic. - * - * @return Stream of bank NPCs - */ - public static Stream findBankNpcs() { - return findWithAction("Bank"); - } - - /** - * Finds the closest bank NPC. - * - * @return Optional containing the closest bank NPC - */ - public static Optional findClosestBankNpc() { - return findNearestWithAction("Bank"); - } - - /** - * Finds shop NPCs (NPCs with "Trade" action). - * - * @return Stream of shop NPCs - */ - public static Stream findShopNpcs() { - return findWithAction("Trade"); - } - - /** - * Finds the closest shop NPC. - * - * @return Optional containing the closest shop NPC - */ - public static Optional findClosestShopNpc() { - return findNearestWithAction("Trade"); - } - - // ============================================ - // Cache-Based NPC Retrieval Methods - // ============================================ - - /** - * Gets an NPC by its index. - * - * @param index The NPC index - * @return Optional containing the NPC model if found - */ - public static Optional getByIndex(int index) { - try { - return Rs2NpcCache.getNpcByIndex(index); - } catch (Exception e) { - return Optional.empty(); - } - } - - /** - * Gets NPCs by their game ID. - * - * @param npcId The NPC ID - * @return Stream of matching NPCs - */ - public static Stream getById(int npcId) { - try { - return Rs2NpcCache.getNpcsById(npcId); - } catch (Exception e) { - return Stream.empty(); - } - } - - /** - * Gets the first NPC matching the criteria. - * - * @param npcId The NPC ID - * @return Optional containing the first matching NPC - */ - public static Optional getFirst(int npcId) { - try { - return Rs2NpcCache.getFirstNpcById(npcId); - } catch (Exception e) { - return Optional.empty(); - } - } - - /** - * Gets all cached NPCs. - * - * @return Stream of all NPCs - */ - public static Stream getAll() { - try { - return Rs2NpcCache.getAllNpcs(); - } catch (Exception e) { - return Stream.empty(); - } - } - - // Advanced cache-based finding utilities - - /** - * Finds the first NPC matching a predicate. - * - * @param predicate The predicate to match - * @return Optional containing the first matching NPC - */ - public static Optional find(Predicate predicate) { - try { - return Rs2NpcCache.getAllNpcs() - .filter(predicate) - .findFirst(); - } catch (Exception e) { - return Optional.empty(); - } - } - - /** - * Finds all NPCs matching a predicate. - * - * @param predicate The predicate to match - * @return Stream of matching NPCs - */ - public static Stream findAll(Predicate predicate) { - try { - return Rs2NpcCache.getAllNpcs().filter(predicate); - } catch (Exception e) { - return Stream.empty(); - } - } - - /** - * Finds the closest NPC matching a predicate. - * - * @param predicate The predicate to match - * @return Optional containing the closest matching NPC - */ - public static Optional findClosest(Predicate predicate) { - try { - return Rs2NpcCache.getAllNpcs() - .filter(predicate) - .min(Comparator.comparingInt(Rs2NpcModel::getDistanceFromPlayer)); - } catch (Exception e) { - return Optional.empty(); - } - } - - /** - * Finds the first NPC within distance from player by ID. - * - * @param npcId The NPC ID - * @param distance Maximum distance in tiles - * @return Optional containing the first matching NPC within distance - */ - public static Optional findWithinDistance(int npcId, int distance) { - return find(npc -> npc.getId() == npcId && npc.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds all NPCs within distance from player by ID. - * - * @param npcId The NPC ID - * @param distance Maximum distance in tiles - * @return Stream of matching NPCs within distance - */ - public static Stream findAllWithinDistance(int npcId, int distance) { - return findAll(npc -> npc.getId() == npcId && npc.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest NPC by ID within distance from player. - * - * @param npcId The NPC ID - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching NPC within distance - */ - public static Optional findClosestWithinDistance(int npcId, int distance) { - return findClosest(npc -> npc.getId() == npcId && npc.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds NPCs within distance from an anchor point. - * - * @param predicate The predicate to match - * @param anchor The anchor point - * @param distance Maximum distance in tiles - * @return Stream of matching NPCs within distance from anchor - */ - public static Stream findWithinDistance(Predicate predicate, WorldPoint anchor, int distance) { - return findAll(npc -> predicate.test(npc) && npc.getWorldLocation().distanceTo(anchor) <= distance); - } - - /** - * Finds the closest NPC to an anchor point. - * - * @param predicate The predicate to match - * @param anchor The anchor point - * @return Optional containing the closest matching NPC to anchor - */ - public static Optional findClosest(Predicate predicate, WorldPoint anchor) { - return findAll(predicate) - .min(Comparator.comparingInt(npc -> npc.getWorldLocation().distanceTo(anchor))); - } - - /** - * Finds the closest NPC to an anchor point within distance. - * - * @param predicate The predicate to match - * @param anchor The anchor point - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching NPC to anchor within distance - */ - public static Optional findClosest(Predicate predicate, WorldPoint anchor, int distance) { - return findWithinDistance(predicate, anchor, distance) - .min(Comparator.comparingInt(npc -> npc.getWorldLocation().distanceTo(anchor))); - } - - // Name-based finding utilities - - /** - * Creates a predicate that matches NPCs whose name contains the given string (case-insensitive). - * - * @param npcName The name to match (partial or full) - * @param exact Whether to match exactly or contain - * @return Predicate for name matching - */ - public static Predicate nameMatches(String npcName, boolean exact) { - String lower = npcName.toLowerCase(); - return npc -> { - String name = npc.getName(); - if (name == null) return false; - return exact ? name.equalsIgnoreCase(npcName) : name.toLowerCase().contains(lower); - }; - } - - /** - * Creates a predicate that matches NPCs whose name contains the given string (case-insensitive). - * - * @param npcName The name to match (partial) - * @return Predicate for name matching - */ - public static Predicate nameMatches(String npcName) { - return nameMatches(npcName, false); - } - - /** - * Finds the first NPC by name. - * - * @param npcName The NPC name - * @param exact Whether to match exactly or contain - * @return Optional containing the first matching NPC - */ - public static Optional findByName(String npcName, boolean exact) { - return find(nameMatches(npcName, exact)); - } - - /** - * Finds the first NPC by name (partial match). - * - * @param npcName The NPC name - * @return Optional containing the first matching NPC - */ - public static Optional findByName(String npcName) { - return findByName(npcName, false); - } - - /** - * Finds the closest NPC by name. - * - * @param npcName The NPC name - * @param exact Whether to match exactly or contain - * @return Optional containing the closest matching NPC - */ - public static Optional findClosestByName(String npcName, boolean exact) { - return findClosest(nameMatches(npcName, exact)); - } - - /** - * Finds the closest NPC by name (partial match). - * - * @param npcName The NPC name - * @return Optional containing the closest matching NPC - */ - public static Optional findClosestByName(String npcName) { - return findClosestByName(npcName, false); - } - - /** - * Finds NPCs by name within distance from player. - * - * @param npcName The NPC name - * @param exact Whether to match exactly or contain - * @param distance Maximum distance in tiles - * @return Stream of matching NPCs within distance - */ - public static Stream findByNameWithinDistance(String npcName, boolean exact, int distance) { - return findAll(npc -> nameMatches(npcName, exact).test(npc) && npc.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds NPCs by name within distance from player (partial match). - * - * @param npcName The NPC name - * @param distance Maximum distance in tiles - * @return Stream of matching NPCs within distance - */ - public static Stream findByNameWithinDistance(String npcName, int distance) { - return findByNameWithinDistance(npcName, false, distance); - } - - /** - * Finds the closest NPC by name within distance from player. - * - * @param npcName The NPC name - * @param exact Whether to match exactly or contain - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching NPC within distance - */ - public static Optional findClosestByNameWithinDistance(String npcName, boolean exact, int distance) { - return findClosest(npc -> nameMatches(npcName, exact).test(npc) && npc.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest NPC by name within distance from player (partial match). - * - * @param npcName The NPC name - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching NPC within distance - */ - public static Optional findClosestByNameWithinDistance(String npcName, int distance) { - return findClosestByNameWithinDistance(npcName, false, distance); - } - - // Array-based ID utilities - - /** - * Finds the first NPC matching any of the given IDs. - * - * @param npcIds Array of NPC IDs - * @return Optional containing the first matching NPC - */ - public static Optional findByIds(Integer[] npcIds) { - Set idSet = Set.of(npcIds); - return find(npc -> idSet.contains(npc.getId())); - } - - /** - * Finds the closest NPC matching any of the given IDs. - * - * @param npcIds Array of NPC IDs - * @return Optional containing the closest matching NPC - */ - public static Optional findClosestByIds(Integer[] npcIds) { - Set idSet = Set.of(npcIds); - return findClosest(npc -> idSet.contains(npc.getId())); - } - - /** - * Finds NPCs matching any of the given IDs within distance. - * - * @param npcIds Array of NPC IDs - * @param distance Maximum distance in tiles - * @return Stream of matching NPCs within distance - */ - public static Stream findByIdsWithinDistance(Integer[] npcIds, int distance) { - Set idSet = Set.of(npcIds); - return findAll(npc -> idSet.contains(npc.getId()) && npc.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest NPC matching any of the given IDs within distance. - * - * @param npcIds Array of NPC IDs - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching NPC within distance - */ - public static Optional findClosestByIdsWithinDistance(Integer[] npcIds, int distance) { - Set idSet = Set.of(npcIds); - return findClosest(npc -> idSet.contains(npc.getId()) && npc.isWithinDistanceFromPlayer(distance)); - } - - // Combat-specific utilities - - /** - * Finds attackable NPCs (combat level > 0, not dead). - * - * @return Stream of attackable NPCs - */ - public static Stream findAttackable() { - return findAll(npc -> npc.getCombatLevel() > 0 && !npc.isDead()); - } - - /** - * Finds the closest attackable NPC. - * - * @return Optional containing the closest attackable NPC - */ - public static Optional findClosestAttackable() { - return findClosest(npc -> npc.getCombatLevel() > 0 && !npc.isDead()); - } - - /** - * Finds attackable NPCs by name. - * - * @param npcName The NPC name - * @param exact Whether to match exactly or contain - * @return Stream of attackable NPCs matching the name - */ - public static Stream findAttackableByName(String npcName, boolean exact) { - return findAll(npc -> - npc.getCombatLevel() > 0 && - !npc.isDead() && - nameMatches(npcName, exact).test(npc)); - } - - /** - * Finds attackable NPCs by name (partial match). - * - * @param npcName The NPC name - * @return Stream of attackable NPCs matching the name - */ - public static Stream findAttackableByName(String npcName) { - return findAttackableByName(npcName, false); - } - - /** - * Finds the closest attackable NPC by name. - * - * @param npcName The NPC name - * @param exact Whether to match exactly or contain - * @return Optional containing the closest attackable NPC matching the name - */ - public static Optional findClosestAttackableByName(String npcName, boolean exact) { - return findClosest(npc -> - npc.getCombatLevel() > 0 && - !npc.isDead() && - nameMatches(npcName, exact).test(npc)); - } - - /** - * Finds the closest attackable NPC by name (partial match). - * - * @param npcName The NPC name - * @return Optional containing the closest attackable NPC matching the name - */ - public static Optional findClosestAttackableByName(String npcName) { - return findClosestAttackableByName(npcName, false); - } - - /** - * Finds attackable NPCs within distance. - * - * @param distance Maximum distance in tiles - * @return Stream of attackable NPCs within distance - */ - public static Stream findAttackableWithinDistance(int distance) { - return findAll(npc -> - npc.getCombatLevel() > 0 && - !npc.isDead() && - npc.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest attackable NPC within distance. - * - * @param distance Maximum distance in tiles - * @return Optional containing the closest attackable NPC within distance - */ - public static Optional findClosestAttackableWithinDistance(int distance) { - return findClosest(npc -> - npc.getCombatLevel() > 0 && - !npc.isDead() && - npc.isWithinDistanceFromPlayer(distance)); - } - - // Player interaction utilities - - /** - * Finds NPCs that are interacting with the player. - * - * @return Stream of NPCs interacting with the player - */ - public static Stream findInteractingWithPlayer() { - return findAll(npc -> npc.isInteractingWithPlayer()); - } - - /** - * Finds the closest NPC that is interacting with the player. - * - * @return Optional containing the closest NPC interacting with the player - */ - public static Optional findClosestInteractingWithPlayer() { - return findClosest(npc -> npc.isInteractingWithPlayer()); - } - - /** - * Finds NPCs that are not interacting with anyone. - * - * @return Stream of NPCs not interacting - */ - public static Stream findNotInteracting() { - return findAll(npc -> !npc.isInteracting()); - } - - /** - * Finds the closest NPC that is not interacting with anyone. - * - * @return Optional containing the closest non-interacting NPC - */ - public static Optional findClosestNotInteracting() { - return findClosest(npc -> !npc.isInteracting()); - } - - // Health and status utilities - - /** - * Finds NPCs with health below a certain percentage. - * - * @param healthPercentage Maximum health percentage (0-100) - * @return Stream of NPCs with health below the threshold - */ - public static Stream findWithHealthBelow(double healthPercentage) { - return findAll(npc -> npc.getHealthPercentage() < healthPercentage); - } - - /** - * Finds the closest NPC with health below a certain percentage. - * - * @param healthPercentage Maximum health percentage (0-100) - * @return Optional containing the closest NPC with health below the threshold - */ - public static Optional findClosestWithHealthBelow(double healthPercentage) { - return findClosest(npc -> npc.getHealthPercentage() < healthPercentage); - } - - /** - * Finds NPCs that are moving. - * - * @return Stream of moving NPCs - */ - public static Stream findMoving() { - return findAll(npc -> npc.isMoving()); - } - - /** - * Finds NPCs that are not moving (idle). - * - * @return Stream of idle NPCs - */ - public static Stream findIdle() { - return findAll(npc -> !npc.isMoving()); - } - - /** - * Finds the closest moving NPC. - * - * @return Optional containing the closest moving NPC - */ - public static Optional findClosestMoving() { - return findClosest(npc -> npc.isMoving()); - } - - /** - * Finds the closest idle NPC. - * - * @return Optional containing the closest idle NPC - */ - public static Optional findClosestIdle() { - return findClosest(npc -> !npc.isMoving()); - } - - // ============================================ - // Scene and Viewport Extraction Methods - // ============================================ - - /** - * Gets all NPCs currently in the scene (all cached NPCs). - * This includes NPCs that may not be visible in the current viewport. - * - * @return Stream of all NPCs in the scene - */ - public static Stream getAllInScene() { - return getAll(); - } - - /** - * Gets all NPCs currently visible in the viewport (on screen). - * Only includes NPCs that have a convex hull and are rendered. - * - * @return Stream of NPCs visible in viewport - */ - public static Stream getAllInViewport() { - return filterVisibleInViewport(getAll()); - } - - /** - * Gets all NPCs by ID that are currently visible in the viewport. - * - * @param npcId The NPC ID to filter by - * @return Stream of NPCs with the specified ID that are visible in viewport - */ - public static Stream getAllInViewport(int npcId) { - return filterVisibleInViewport(getById(npcId)); - } - - /** - * Gets the closest NPC in the viewport by ID. - * - * @param npcId The NPC ID - * @return Optional containing the closest NPC in viewport - */ - public static Optional getClosestInViewport(int npcId) { - return getAllInViewport(npcId) - .min(Comparator.comparingInt(Rs2NpcModel::getDistanceFromPlayer)); - } - - /** - * Gets all NPCs in the viewport that are interactable (within reasonable distance). - * - * @param maxDistance Maximum distance for interaction - * @return Stream of interactable NPCs in viewport - */ - public static Stream getAllInteractable(int maxDistance) { - return getAllInViewport() - .filter(npc -> isInteractable(npc, maxDistance)); - } - - /** - * Gets all NPCs by ID in the viewport that are interactable. - * - * @param npcId The NPC ID - * @param maxDistance Maximum distance for interaction - * @return Stream of interactable NPCs with the specified ID - */ - public static Stream getAllInteractable(int npcId, int maxDistance) { - return getAllInViewport(npcId) - .filter(npc -> isInteractable(npc, maxDistance)); - } - - /** - * Gets the closest interactable NPC by ID. - * - * @param npcId The NPC ID - * @param maxDistance Maximum distance for interaction - * @return Optional containing the closest interactable NPC - */ - public static Optional getClosestInteractable(int npcId, int maxDistance) { - return getAllInteractable(npcId, maxDistance) - .min(Comparator.comparingInt(Rs2NpcModel::getDistanceFromPlayer)); - } - - // ============================================ - // Line of Sight Methods - // ============================================ - - - - /** - * Finds NPCs that are in line of sight from a specific point. - * This is useful for finding NPCs that the player can see. - * - * @param from Starting world point - * @param maxDistance Maximum distance to search (in tiles) - * @return Stream of NPCs that are in line of sight - */ - public static Stream getNpcsInLineOfSight(WorldPoint from, int maxDistance) { - if (from == null) { - return Stream.empty(); - } - - int plane = from.getPlane(); - - return getAll() - .filter(npc -> { - WorldPoint npcLocation = npc.getWorldLocation(); - if (npcLocation == null || npcLocation.getPlane() != plane) { - return false; - } - - // Check distance first as it's a cheaper operation - int distance = from.distanceTo(npcLocation); - if (distance > maxDistance) { - return false; - } - - // Then check line of sight - return hasLineOfSight(from, npc); - }); - } - - /** - * Finds the nearest NPC in line of sight from a specific point. - * - * @param from Starting world point - * @param maxDistance Maximum distance to search (in tiles) - * @return Optional containing the nearest NPC in line of sight - */ - public static Optional getNearestNpcInLineOfSight(WorldPoint from, int maxDistance) { - return getNpcsInLineOfSight(from, maxDistance) - .min(Comparator.comparingInt(npc -> from.distanceTo(npc.getWorldLocation()))); - } - - /** - * Finds NPCs that match a predicate and are in line of sight from a specific point. - * - * @param from Starting world point - * @param maxDistance Maximum distance to search (in tiles) - * @param predicate Filter to apply to NPCs - * @return Stream of NPCs that match the predicate and are in line of sight - */ - public static Stream getNpcsInLineOfSight(WorldPoint from, int maxDistance, Predicate predicate) { - return getNpcsInLineOfSight(from, maxDistance).filter(predicate); - } - - // ============================================ - // Line of Sight Utilities - // ============================================ - - /** - * Checks if there is a line of sight between the player and an NPC. - * Uses RuneLite's WorldArea.hasLineOfSightTo for accurate scene collision detection. - * - * @param npc The NPC to check - * @return True if line of sight exists, false otherwise - */ - public static boolean hasLineOfSight(Rs2NpcModel npc) { - if (npc == null) return false; - - try { - // Get player's current world location and create a small area (1x1) - WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - return hasLineOfSight(playerLocation, npc); - } catch (Exception e) { - return false; - } - } - - /** - * Checks if there is a line of sight between a specific point and an NPC. - * - * @param point The world point to check from - * @param npc The NPC to check against - * @return True if line of sight exists, false otherwise - */ - public static boolean hasLineOfSight(WorldPoint point, Rs2NpcModel npc) { - if (npc == null || point == null) return false; - - try { - WorldPoint npcLocation = npc.getWorldLocation(); - - // Check same plane - if (point.getPlane() != npcLocation.getPlane()) { - return false; - } - - // Create WorldAreas for the point and NPC - int npcSize = npc.getComposition() != null ? npc.getComposition().getSize() : 1; - - return new WorldArea(npcLocation, npcSize, npcSize) - .hasLineOfSightTo( - Microbot.getClient().getTopLevelWorldView(), - new WorldArea(point, 1, 1)); - } catch (Exception e) { - return false; - } - } - - /** - * Gets all NPCs that have line of sight to the player. - * Useful for identifying potential threats or interactive NPCs. - * - * @return Stream of NPCs with line of sight to player - */ - public static Stream getNpcsWithLineOfSightToPlayer() { - return getAll().filter(Rs2NpcCacheUtils::hasLineOfSight); - } - - /** - * Gets all NPCs that have line of sight to a specific world point. - * - * @param point The world point to check from - * @return Stream of NPCs with line of sight to the point - */ - public static Stream getNpcsWithLineOfSightTo(WorldPoint point) { - return getAll().filter(npc -> hasLineOfSight(point, npc)); - } - - /** - * Gets all NPCs at a location that have line of sight to the player. - * - * @param worldPoint The world point to check at - * @param maxDistance Maximum distance from the world point - * @return Stream of NPCs at the location with line of sight - */ - public static Stream getNpcsAtLocationWithLineOfSight(WorldPoint worldPoint, int maxDistance) { - return getAll() - .filter(npc -> npc.getWorldLocation().distanceTo(worldPoint) <= maxDistance) - .filter(Rs2NpcCacheUtils::hasLineOfSight); - } - - // ============================================ - // Viewport Visibility and Interactability Utilities - // ============================================ - - /** - * Checks if an NPC is visible in the current viewport using convex hull detection. - * Uses client thread for safe access to NPC state. - * - * @param npc The NPC to check - * @return true if the NPC is visible on screen - */ - public static boolean isVisibleInViewport(Rs2NpcModel npc) { - try { - if (npc == null) { - return false; - } - - // Use client thread for safe access to convex hull - return Microbot.getClientThread().runOnClientThreadOptional(() -> - npc.getConvexHull() != null - ).orElse(false); - } catch (Exception e) { - return false; - } - } - - /** - * Checks if any entity with a location is within the viewport by checking canvas conversion. - * This is a generic method that can work with any entity that has a world location. - * Uses client thread for safe access to client state. - * - * @param worldPoint The world point to check - * @return true if the location is visible on screen - */ - public static boolean isLocationVisibleInViewport(net.runelite.api.coords.WorldPoint worldPoint) { - try { - if (worldPoint == null) { - return false; - } - - // Use client thread for safe access to client state - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - Client client = Microbot.getClient(); - if (client == null) { - return false; - } - - LocalPoint localPoint = LocalPoint.fromWorld(client.getTopLevelWorldView(), worldPoint); - if (localPoint == null) { - return false; - } - - net.runelite.api.Point canvasPoint = Perspective.localToCanvas(client, localPoint, client.getTopLevelWorldView().getPlane()); - return canvasPoint != null; - }).orElse(false); - } catch (Exception e) { - return false; - } - } - - /** - * Filters a stream of NPCs to only include those visible in viewport. - * - * @param npcStream Stream of NPCs to filter - * @return Stream of NPCs visible in viewport - */ - public static Stream filterVisibleInViewport(Stream npcStream) { - return npcStream.filter(Rs2NpcCacheUtils::isVisibleInViewport); - } - - /** - * Checks if an NPC is interactable (visible and within reasonable distance). - * - * @param npc The NPC to check - * @param maxDistance Maximum distance in tiles for interaction - * @return true if the NPC is interactable - */ - public static boolean isInteractable(Rs2NpcModel npc, int maxDistance) { - try { - if (npc == null) { - return false; - } - - // Check if visible in viewport first - if (!isVisibleInViewport(npc)) { - return false; - } - - // Check distance from player - return npc.getDistanceFromPlayer() <= maxDistance; - } catch (Exception e) { - return false; - } - } - - /** - * Checks if an entity at a world point is interactable (within reasonable distance and visible). - * Uses client thread for safe access to player location. - * - * @param worldPoint The world point to check - * @param maxDistance Maximum distance in tiles for interaction - * @return true if the location is potentially interactable - */ - public static boolean isInteractable(net.runelite.api.coords.WorldPoint worldPoint, int maxDistance) { - try { - if (worldPoint == null) { - return false; - } - - // Use client thread for safe access to player location - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - net.runelite.api.coords.WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - if (playerLocation.distanceTo(worldPoint) > maxDistance) { - return false; - } - - // Check if visible in viewport (already uses client thread internally) - return isLocationVisibleInViewport(worldPoint); - }).orElse(false); - } catch (Exception e) { - return false; - } - } - - // ============================================ - // Updated Existing Methods to Use Local Functions - // ============================================ - - /** - * Gets all NPCs visible in the viewport. - * - * @return Stream of NPCs visible in viewport - */ - public static Stream getVisibleInViewport() { - return filterVisibleInViewport(getAll()); - } - - /** - * Gets NPCs by ID that are visible in the viewport. - * - * @param npcId The NPC ID - * @return Stream of NPCs with the specified ID visible in viewport - */ - public static Stream getVisibleInViewportById(int npcId) { - return filterVisibleInViewport(getById(npcId)); - } - - /** - * Finds interactable NPCs by ID within distance from player. - * - * @param npcId The NPC ID - * @param maxDistance Maximum distance in tiles - * @return Stream of interactable NPCs with the specified ID - */ - public static Stream findInteractableById(int npcId, int maxDistance) { - return getById(npcId) - .filter(npc -> isInteractable(npc, maxDistance)); - } - - /** - * Finds interactable NPCs by name within distance from player. - * - * @param name The NPC name - * @param maxDistance Maximum distance in tiles - * @return Stream of interactable NPCs with the specified name - */ - public static Stream findInteractableByName(String name, int maxDistance) { - return findAll(nameMatches(name, false)) - .filter(npc -> isInteractable(npc, maxDistance)); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2ObjectCacheUtils.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2ObjectCacheUtils.java deleted file mode 100644 index 65fb38650ff..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2ObjectCacheUtils.java +++ /dev/null @@ -1,1423 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.util; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Client; -import net.runelite.api.DecorativeObject; -import net.runelite.api.GameObject; -import net.runelite.api.GroundObject; -import net.runelite.api.Model; -import net.runelite.api.Perspective; -import net.runelite.api.Point; -import net.runelite.api.Renderable; -import net.runelite.api.TileObject; -import net.runelite.api.WallObject; -import net.runelite.api.coords.LocalPoint; -import net.runelite.api.coords.WorldArea; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.util.cache.Rs2ObjectCache; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel; -import net.runelite.client.plugins.microbot.Microbot; - -import java.awt.Rectangle; -import java.awt.Shape; -import java.util.Comparator; -import java.util.Optional; -import java.util.Set; -import java.util.function.Predicate; -import java.util.stream.Stream; - -/** - * Advanced cache-based utilities for game objects. - * Provides scene-independent methods for finding and filtering objects. - * - * This class offers high-performance object operations using cached data, - * avoiding the need to iterate through scene tiles. Supports all object types: - * GameObject, GroundObject, WallObject, and DecorativeObject. - * - * @author Vox - * @version 1.0 - */ -@Slf4j -public class Rs2ObjectCacheUtils { - - // ============================================ - // Core Cache Access Methods - // ============================================ - - /** - * Gets objects by their game ID. - * - * @param objectId The object ID - * @return Stream of matching objects - */ - public static Stream getByGameId(int objectId) { - try { - return Rs2ObjectCache.getInstance().stream() - .filter(obj -> obj.getId() == objectId); - } catch (Exception e) { - return Stream.empty(); - } - } - - /** - * Gets the first object matching the criteria. - * - * @param objectId The object ID - * @return Optional containing the first matching object - */ - public static Optional getFirst(int objectId) { - try { - return Rs2ObjectCache.getInstance().stream() - .filter(obj -> obj.getId() == objectId) - .findFirst(); - } catch (Exception e) { - return Optional.empty(); - } - } - - /** - * Gets the closest object to the player. - * - * @param objectId The object ID - * @return Optional containing the closest object - */ - public static Optional getClosest(int objectId) { - try { - return Rs2ObjectCache.getInstance().stream() - .filter(obj -> obj.getId() == objectId) - .min(Comparator.comparingInt(Rs2ObjectModel::getDistanceFromPlayer)); - } catch (Exception e) { - return Optional.empty(); - } - } - - /** - * Gets all cached objects. - * - * @return Stream of all objects - */ - public static Stream getAll() { - try { - return Rs2ObjectCache.getInstance().stream(); - } catch (Exception e) { - return Stream.empty(); - } - } - - // ============================================ - // Advanced Finding Methods - // ============================================ - - /** - * Finds the nearest object matching a predicate. - * - * @param predicate The predicate to match - * @return Optional containing the first matching object - */ - public static Optional findNearest(Predicate predicate) { - try { - return findClosest(predicate); - } catch (Exception e) { - return Optional.empty(); - } - } - - /** - * Finds all objects matching a predicate. - * - * @param predicate The predicate to match - * @return Stream of matching objects - */ - public static Stream findAll(Predicate predicate) { - try { - return Rs2ObjectCache.getInstance().stream().filter(predicate); - } catch (Exception e) { - return Stream.empty(); - } - } - - /** - * Finds the closest object matching a predicate. - * - * @param predicate The predicate to match - * @return Optional containing the closest matching object - */ - public static Optional findClosest(Predicate predicate) { - try { - return Rs2ObjectCache.getInstance().stream() - .filter(predicate) - .min(Comparator.comparingInt(Rs2ObjectModel::getDistanceFromPlayer)); - } catch (Exception e) { - return Optional.empty(); - } - } - - // ============================================ - // Distance-Based Finding Methods - // ============================================ - - /** - * Finds the first object within distance from player by ID. - * - * @param objectId The object ID - * @param distance Maximum distance in tiles - * @return Optional containing the first matching object within distance - */ - public static Optional findWithinDistance(int objectId, int distance) { - return findNearest(obj -> obj.getId() == objectId && obj.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds all objects within distance from player by ID. - * - * @param objectId The object ID - * @param distance Maximum distance in tiles - * @return Stream of matching objects within distance - */ - public static Stream findAllWithinDistance(int objectId, int distance) { - return findAll(obj -> obj.getId() == objectId && obj.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest object by ID within distance from player. - * - * @param objectId The object ID - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching object within distance - */ - public static Optional findClosestWithinDistance(int objectId, int distance) { - return findClosest(obj -> obj.getId() == objectId && obj.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds objects within distance from an anchor point. - * - * @param predicate The predicate to match - * @param anchor The anchor point - * @param distance Maximum distance in tiles - * @return Stream of matching objects within distance from anchor - */ - public static Stream findWithinDistance(Predicate predicate, WorldPoint anchor, int distance) { - return findAll(obj -> predicate.test(obj) && obj.getLocation().distanceTo(anchor) <= distance); - } - - /** - * Finds the closest object to an anchor point. - * - * @param predicate The predicate to match - * @param anchor The anchor point - * @return Optional containing the closest matching object to anchor - */ - public static Optional findClosest(Predicate predicate, WorldPoint anchor) { - return findAll(predicate) - .min(Comparator.comparingInt(obj -> obj.getLocation().distanceTo(anchor))); - } - - /** - * Finds the closest object to an anchor point within distance. - * - * @param predicate The predicate to match - * @param anchor The anchor point - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching object to anchor within distance - */ - public static Optional findClosest(Predicate predicate, WorldPoint anchor, int distance) { - return findWithinDistance(predicate, anchor, distance) - .min(Comparator.comparingInt(obj -> obj.getLocation().distanceTo(anchor))); - } - - // ============================================ - // Name-Based Finding Methods - // ============================================ - - /** - * Creates a predicate that matches objects whose name contains the given string (case-insensitive). - * - * @param objectName The name to match (partial or full) - * @param exact Whether to match exactly or contain - * @return Predicate for name matching - */ - public static Predicate nameMatches(String objectName, boolean exact) { - String lower = objectName.toLowerCase(); - return obj -> { - String objName = obj.getName(); - if (objName == null) return false; - return exact ? objName.equalsIgnoreCase(objectName) : objName.toLowerCase().contains(lower); - }; - } - - /** - * Creates a predicate that matches objects whose name contains the given string (case-insensitive). - * - * @param objectName The name to match (partial) - * @return Predicate for name matching - */ - public static Predicate nameMatches(String objectName) { - return nameMatches(objectName, false); - } - - /** - * Finds the first object by name. - * - * @param objectName The object name - * @param exact Whether to match exactly or contain - * @return Optional containing the first matching object - */ - public static Optional findNearestByName(String objectName, boolean exact) { - return findNearest(nameMatches(objectName, exact)); - } - - /** - * Finds the first object by name (partial match). - * - * @param objectName The object name - * @return Optional containing the first matching object - */ - public static Optional findNearestByName(String objectName) { - return findNearestByName(objectName, false); - } - - /** - * Finds the closest object by name. - * - * @param objectName The object name - * @param exact Whether to match exactly or contain - * @return Optional containing the closest matching object - */ - public static Optional findClosestByName(String objectName, boolean exact) { - return findClosest(nameMatches(objectName, exact)); - } - - /** - * Finds the closest object by name (partial match). - * - * @param objectName The object name - * @return Optional containing the closest matching object - */ - public static Optional findClosestByName(String objectName) { - return findClosestByName(objectName, false); - } - - /** - * Finds objects by name within distance from player. - * - * @param objectName The object name - * @param exact Whether to match exactly or contain - * @param distance Maximum distance in tiles - * @return Stream of matching objects within distance - */ - public static Stream findByNameWithinDistance(String objectName, boolean exact, int distance) { - return findAll(obj -> nameMatches(objectName, exact).test(obj) && obj.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds objects by name within distance from player (partial match). - * - * @param objectName The object name - * @param distance Maximum distance in tiles - * @return Stream of matching objects within distance - */ - public static Stream findByNameWithinDistance(String objectName, int distance) { - return findByNameWithinDistance(objectName, false, distance); - } - - /** - * Finds the closest object by name within distance from player. - * - * @param objectName The object name - * @param exact Whether to match exactly or contain - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching object within distance - */ - public static Optional findClosestByNameWithinDistance(String objectName, boolean exact, int distance) { - return findClosest(obj -> nameMatches(objectName, exact).test(obj) && obj.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest object by name within distance from player (partial match). - * - * @param objectName The object name - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching object within distance - */ - public static Optional findClosestByNameWithinDistance(String objectName, int distance) { - return findClosestByNameWithinDistance(objectName, false, distance); - } - - // ============================================ - // Array-Based ID Methods - // ============================================ - - /** - * Finds the first object matching any of the given IDs. - * - * @param objectIds Array of object IDs - * @return Optional containing the first matching object - */ - public static Optional findNearestByIds(Integer[] objectIds) { - Set idSet = Set.of(objectIds); - return findClosest(obj -> idSet.contains(obj.getId())); - } - - /** - * Finds the closest object matching any of the given IDs. - * - * @param objectIds Array of object IDs - * @return Optional containing the closest matching object - */ - public static Optional findClosestByIds(Integer[] objectIds) { - Set idSet = Set.of(objectIds); - return findClosest(obj -> idSet.contains(obj.getId())); - } - - /** - * Finds objects matching any of the given IDs within distance. - * - * @param objectIds Array of object IDs - * @param distance Maximum distance in tiles - * @return Stream of matching objects within distance - */ - public static Stream findByIdsWithinDistance(Integer[] objectIds, int distance) { - Set idSet = Set.of(objectIds); - return findAll(obj -> idSet.contains(obj.getId()) && obj.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest object matching any of the given IDs within distance. - * - * @param objectIds Array of object IDs - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching object within distance - */ - public static Optional findClosestByIdsWithinDistance(Integer[] objectIds, int distance) { - Set idSet = Set.of(objectIds); - return findClosest(obj -> idSet.contains(obj.getId()) && obj.isWithinDistanceFromPlayer(distance)); - } - - // ============================================ - // Type-Specific Finding Methods - // ============================================ - - /** - * Finds objects of a specific type. - * - * @param objectType The object type to find - * @return Stream of objects of the specified type - */ - public static Stream findByType(Rs2ObjectModel.ObjectType objectType) { - return findAll(obj -> obj.getObjectType() == objectType); - } - - /** - * Finds the closest object of a specific type. - * - * @param objectType The object type to find - * @return Optional containing the closest object of the specified type - */ - public static Optional findClosestByType(Rs2ObjectModel.ObjectType objectType) { - return findClosest(obj -> obj.getObjectType() == objectType); - } - - /** - * Finds objects of a specific type within distance. - * - * @param objectType The object type to find - * @param distance Maximum distance in tiles - * @return Stream of objects of the specified type within distance - */ - public static Stream findByTypeWithinDistance(Rs2ObjectModel.ObjectType objectType, int distance) { - return findAll(obj -> obj.getObjectType() == objectType && obj.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest object of a specific type within distance. - * - * @param objectType The object type to find - * @param distance Maximum distance in tiles - * @return Optional containing the closest object of the specified type within distance - */ - public static Optional findClosestByTypeWithinDistance(Rs2ObjectModel.ObjectType objectType, int distance) { - return findClosest(obj -> obj.getObjectType() == objectType && obj.isWithinDistanceFromPlayer(distance)); - } - - // ============================================ - // Convenience Methods for Specific Object Types - // ============================================ - - /** - * Finds GameObjects only. - * - * @return Stream of GameObjects - */ - public static Stream findGameObjects() { - return findByType(Rs2ObjectModel.ObjectType.GAME_OBJECT); - } - - /** - * Finds the closest GameObject. - * - * @return Optional containing the closest GameObject - */ - public static Optional findClosestGameObject() { - return findClosestByType(Rs2ObjectModel.ObjectType.GAME_OBJECT); - } - - /** - * Finds GameObjects within distance. - * - * @param distance Maximum distance in tiles - * @return Stream of GameObjects within distance - */ - public static Stream findGameObjectsWithinDistance(int distance) { - return findByTypeWithinDistance(Rs2ObjectModel.ObjectType.GAME_OBJECT, distance); - } - - /** - * Finds GroundObjects only. - * - * @return Stream of GroundObjects - */ - public static Stream findGroundObjects() { - return findByType(Rs2ObjectModel.ObjectType.GROUND_OBJECT); - } - - /** - * Finds the closest GroundObject. - * - * @return Optional containing the closest GroundObject - */ - public static Optional findClosestGroundObject() { - return findClosestByType(Rs2ObjectModel.ObjectType.GROUND_OBJECT); - } - - /** - * Finds GroundObjects within distance. - * - * @param distance Maximum distance in tiles - * @return Stream of GroundObjects within distance - */ - public static Stream findGroundObjectsWithinDistance(int distance) { - return findByTypeWithinDistance(Rs2ObjectModel.ObjectType.GROUND_OBJECT, distance); - } - - /** - * Finds WallObjects only. - * - * @return Stream of WallObjects - */ - public static Stream findWallObjects() { - return findByType(Rs2ObjectModel.ObjectType.WALL_OBJECT); - } - - /** - * Finds the closest WallObject. - * - * @return Optional containing the closest WallObject - */ - public static Optional findClosestWallObject() { - return findClosestByType(Rs2ObjectModel.ObjectType.WALL_OBJECT); - } - - /** - * Finds WallObjects within distance. - * - * @param distance Maximum distance in tiles - * @return Stream of WallObjects within distance - */ - public static Stream findWallObjectsWithinDistance(int distance) { - return findByTypeWithinDistance(Rs2ObjectModel.ObjectType.WALL_OBJECT, distance); - } - - /** - * Finds DecorativeObjects only. - * - * @return Stream of DecorativeObjects - */ - public static Stream findDecorativeObjects() { - return findByType(Rs2ObjectModel.ObjectType.DECORATIVE_OBJECT); - } - - /** - * Finds the closest DecorativeObject. - * - * @return Optional containing the closest DecorativeObject - */ - public static Optional findClosestDecorativeObject() { - return findClosestByType(Rs2ObjectModel.ObjectType.DECORATIVE_OBJECT); - } - - /** - * Finds DecorativeObjects within distance. - * - * @param distance Maximum distance in tiles - * @return Stream of DecorativeObjects within distance - */ - public static Stream findDecorativeObjectsWithinDistance(int distance) { - return findByTypeWithinDistance(Rs2ObjectModel.ObjectType.DECORATIVE_OBJECT, distance); - } - - // ============================================ - // Action-Based Finding Methods - // ============================================ - - /** - * Finds objects that have a specific action. - * - * @param action The action to look for - * @return Stream of objects with the specified action - */ - public static Stream findByAction(String action) { - return findAll(obj -> obj.hasAction(action)); - } - - /** - * Finds the closest object that has a specific action. - * - * @param action The action to look for - * @return Optional containing the closest object with the specified action - */ - public static Optional findClosestByAction(String action) { - return findClosest(obj -> obj.hasAction(action)); - } - - /** - * Finds objects with a specific action within distance. - * - * @param action The action to look for - * @param distance Maximum distance in tiles - * @return Stream of objects with the specified action within distance - */ - public static Stream findByActionWithinDistance(String action, int distance) { - return findAll(obj -> obj.hasAction(action) && obj.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest object with a specific action within distance. - * - * @param action The action to look for - * @param distance Maximum distance in tiles - * @return Optional containing the closest object with the specified action within distance - */ - public static Optional findClosestByActionWithinDistance(String action, int distance) { - return findClosest(obj -> obj.hasAction(action) && obj.isWithinDistanceFromPlayer(distance)); - } - - // ============================================ - // Size-Based Finding Methods - // ============================================ - - /** - * Finds objects with a specific width. - * - * @param width The width to match - * @return Stream of objects with the specified width - */ - public static Stream findByWidth(int width) { - return findAll(obj -> obj.getWidth() == width); - } - - /** - * Finds objects with a specific height. - * - * @param height The height to match - * @return Stream of objects with the specified height - */ - public static Stream findByHeight(int height) { - return findAll(obj -> obj.getHeight() == height); - } - - /** - * Finds objects with specific dimensions. - * - * @param width The width to match - * @param height The height to match - * @return Stream of objects with the specified dimensions - */ - public static Stream findBySize(int width, int height) { - return findAll(obj -> obj.getWidth() == width && obj.getHeight() == height); - } - - /** - * Finds large objects (width or height > 1). - * - * @return Stream of large objects - */ - public static Stream findLargeObjects() { - return findAll(obj -> obj.getWidth() > 1 || obj.getHeight() > 1); - } - - /** - * Finds single-tile objects (width and height = 1). - * - * @return Stream of single-tile objects - */ - public static Stream findSingleTileObjects() { - return findAll(obj -> obj.getWidth() == 1 && obj.getHeight() == 1); - } - - // ============================================ - // Property-Based Finding Methods - // ============================================ - - /** - * Finds solid objects (objects that block movement). - * - * @return Stream of solid objects - */ - public static Stream findSolidObjects() { - return findAll(Rs2ObjectModel::isSolid); - } - - /** - * Finds non-solid objects (objects that don't block movement). - * - * @return Stream of non-solid objects - */ - public static Stream findNonSolidObjects() { - return findAll(obj -> !obj.isSolid()); - } - - // ============================================ - // Age-Based Finding Methods - // ============================================ - - /** - * Finds objects that have been cached for at least the specified number of ticks. - * - * @param minTicks Minimum ticks since cache creation - * @return Stream of objects aged at least minTicks - */ - public static Stream findByMinAge(int minTicks) { - return findAll(obj -> obj.getTicksSinceCreation() >= minTicks); - } - - /** - * Finds objects that have been cached for less than the specified number of ticks. - * - * @param maxTicks Maximum ticks since cache creation - * @return Stream of fresh objects - */ - public static Stream findFresh(int maxTicks) { - return findAll(obj -> obj.getTicksSinceCreation() <= maxTicks); - } - - /** - * Finds the oldest cached object. - * - * @return Optional containing the oldest cached object - */ - public static Optional findOldest() { - return getAll().max(Comparator.comparingInt(Rs2ObjectModel::getTicksSinceCreation)); - } - - /** - * Finds the newest cached object. - * - * @return Optional containing the newest cached object - */ - public static Optional findNewest() { - return getAll().min(Comparator.comparingInt(Rs2ObjectModel::getTicksSinceCreation)); - } - - - - - // ============================================ - // Scene and Viewport Extraction Methods - // ============================================ - - /** - * Gets all objects currently in the scene (all cached objects). - * This includes objects that may not be visible in the current viewport. - * - * @return Stream of all objects in the scene - */ - public static Stream getAllInScene() { - return getAll(); - } - - /** - * Gets all objects currently visible in the viewport (on screen). - * Only includes objects that have a convex hull and are rendered. - * - * @return Stream of objects visible in viewport - */ - public static Stream getAllInViewport() { - return filterVisibleInViewport(getAll()); - } - - /** - * Gets all objects by ID that are currently visible in the viewport. - * - * @param objectId The object ID to filter by - * @return Stream of objects with the specified ID that are visible in viewport - */ - public static Stream getAllInViewport(int objectId) { - return filterVisibleInViewport(getByGameId(objectId)); - } - - /** - * Gets the closest object in the viewport by ID. - * - * @param objectId The object ID - * @return Optional containing the closest object in viewport - */ - public static Optional getClosestInViewport(int objectId) { - return getAllInViewport(objectId) - .min(Comparator.comparingInt(Rs2ObjectModel::getDistanceFromPlayer)); - } - - /** - * Gets all objects in the viewport that are interactable (within reasonable distance). - * - * @param maxDistance Maximum distance for interaction - * @return Stream of interactable objects in viewport - */ - public static Stream getAllInteractable(int maxDistance) { - return getAllInViewport() - .filter(obj -> isInteractable(obj, maxDistance)); - } - - /** - * Gets all objects by ID in the viewport that are interactable. - * - * @param objectId The object ID - * @param maxDistance Maximum distance for interaction - * @return Stream of interactable objects with the specified ID - */ - public static Stream getAllInteractable(int objectId, int maxDistance) { - return getAllInViewport(objectId) - .filter(obj -> isInteractable(obj, maxDistance)); - } - - /** - * Gets the closest interactable object by ID. - * - * @param objectId The object ID - * @param maxDistance Maximum distance for interaction - * @return Optional containing the closest interactable object - */ - public static Optional getClosestInteractable(int objectId, int maxDistance) { - return getAllInteractable(objectId, maxDistance) - .min(Comparator.comparingInt(Rs2ObjectModel::getDistanceFromPlayer)); - } - - // ============================================ - // Line of Sight Methods - // ============================================ - - - /** - * Finds objects that intersect the line of sight between two points. - * This is useful for determining what objects are blocking visibility. - * - * @param from Starting world point - * @param to Destination world point - * @return Stream of objects that might block line of sight - */ - public static Stream getObjectsInLineOfSight(WorldPoint from, WorldPoint to) { - if (from == null || to == null || from.getPlane() != to.getPlane()) { - return Stream.empty(); - } - - // Calculate the bounding box that contains both points - int minX = Math.min(from.getX(), to.getX()); - int maxX = Math.max(from.getX(), to.getX()); - int minY = Math.min(from.getY(), to.getY()); - int maxY = Math.max(from.getY(), to.getY()); - int plane = from.getPlane(); - - // Add a small buffer to ensure we catch all relevant objects - int buffer = 3; - - // Get all objects in the bounding box region - return getAll() - .filter(obj -> { - WorldPoint objPoint = obj.getWorldLocation(); - if (objPoint == null) return false; - - // Check if in bounding box with buffer - return objPoint.getPlane() == plane && - objPoint.getX() >= (minX - buffer) && objPoint.getX() <= (maxX + buffer) && - objPoint.getY() >= (minY - buffer) && objPoint.getY() <= (maxY + buffer); - }) - // Filter to objects that actually intersect the line - .filter(obj -> intersectsLine(obj, from, to)); - } - - /** - * Determines if an object intersects the line between two points. - * Uses the object's size and position to check for intersection. - * - * @param obj The object to check - * @param from Starting world point - * @param to Destination world point - * @return true if the object intersects the line - */ - private static boolean intersectsLine(Rs2ObjectModel obj, WorldPoint from, WorldPoint to) { - // For simplicity, consider the object as a rectangle/square - // and check if the line intersects this rectangle - WorldPoint objLocation = obj.getLocation(); - if (objLocation == null) return false; - - // Get object dimensions (default to 1x1) - int sizeX = 1; - int sizeY = 1; - - // Try to get actual dimensions if it's a GameObject - if (obj.getObjectType() == Rs2ObjectModel.ObjectType.GAME_OBJECT && obj.getTileObject() instanceof GameObject) { - GameObject gameObject = (GameObject) obj.getTileObject(); - sizeX = gameObject.getSceneMinLocation().distanceTo(gameObject.getSceneMaxLocation()) + 1; - sizeY = sizeX; // Assume square for simplicity - } - - // Create a WorldArea representing the object - net.runelite.api.coords.WorldArea objArea = new net.runelite.api.coords.WorldArea(objLocation, sizeX, sizeY); - - // Use the line-of-sight method to check if this area blocks the line - // First check if from point has line of sight to object - boolean fromToObjLOS = new net.runelite.api.coords.WorldArea(from, 1, 1) - .hasLineOfSightTo(net.runelite.client.plugins.microbot.Microbot.getClient().getTopLevelWorldView(), objArea); - - // Then check if object has line of sight to destination - boolean objToToLOS = objArea - .hasLineOfSightTo(net.runelite.client.plugins.microbot.Microbot.getClient().getTopLevelWorldView(), - new net.runelite.api.coords.WorldArea(to, 1, 1)); - - // Object intersects line if both checks pass - return fromToObjLOS && objToToLOS; - } - - // ============================================ - // Line of Sight Utilities - // ============================================ - - /** - * Checks if there is a line of sight between the player and a game object. - * Uses RuneLite's WorldArea.hasLineOfSightTo for accurate scene collision detection. - * - * @param object The object to check - * @return True if line of sight exists, false otherwise - */ - public static boolean hasLineOfSight(Rs2ObjectModel object) { - if (object == null) return false; - - try { - // Get player's current world location and create a small area (1x1) - WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - WorldPoint objectLocation = object.getLocation(); - - // Check same plane - if (playerLocation.getPlane() != objectLocation.getPlane()) { - return false; - } - - // For GameObjects, use the actual size of the object - if (object.getObjectType() == Rs2ObjectModel.ObjectType.GAME_OBJECT && - object.getTileObject() instanceof GameObject) { - GameObject gameObject = (GameObject) object.getTileObject(); - - return new WorldArea( - objectLocation, - gameObject.sizeX(), - gameObject.sizeY()) - .hasLineOfSightTo( - Microbot.getClient().getTopLevelWorldView(), - new WorldArea(playerLocation, 1, 1)); - } else { - // For other objects, use 1x1 area as default - return new WorldArea(objectLocation, 1, 1) - .hasLineOfSightTo( - Microbot.getClient().getTopLevelWorldView(), - new WorldArea(playerLocation, 1, 1)); - } - } catch (Exception e) { - return false; - } - } - - /** - * Checks if there is a line of sight between a specific point and a game object. - * - * @param point The world point to check from - * @param object The object to check against - * @return True if line of sight exists, false otherwise - */ - public static boolean hasLineOfSight(WorldPoint point, Rs2ObjectModel object) { - if (object == null || point == null) return false; - - try { - WorldPoint objectLocation = object.getLocation(); - - // Check same plane - if (point.getPlane() != objectLocation.getPlane()) { - return false; - } - - // For GameObjects, use the actual size of the object - if (object.getObjectType() == Rs2ObjectModel.ObjectType.GAME_OBJECT && - object.getTileObject() instanceof GameObject) { - GameObject gameObject = (GameObject) object.getTileObject(); - - return new WorldArea( - objectLocation, - gameObject.sizeX(), - gameObject.sizeY()) - .hasLineOfSightTo( - Microbot.getClient().getTopLevelWorldView(), - new WorldArea(point, 1, 1)); - } else { - // For other objects, use 1x1 area as default - return new WorldArea(objectLocation, 1, 1) - .hasLineOfSightTo( - Microbot.getClient().getTopLevelWorldView(), - new WorldArea(point, 1, 1)); - } - } catch (Exception e) { - return false; - } - } - - /** - * Gets all objects that have line of sight to the player. - * Useful for identifying interactive game objects. - * - * @return Stream of objects with line of sight to player - */ - public static Stream getObjectsWithLineOfSightToPlayer() { - return getAll().filter(Rs2ObjectCacheUtils::hasLineOfSight); - } - - /** - * Gets all objects that have line of sight to a specific world point. - * - * @param point The world point to check from - * @return Stream of objects with line of sight to the point - */ - public static Stream getObjectsWithLineOfSightTo(WorldPoint point) { - return getAll().filter(object -> hasLineOfSight(point, object)); - } - - /** - * Gets all objects at a location that have line of sight to the player. - * - * @param worldPoint The world point to check at - * @param maxDistance Maximum distance from the world point - * @return Stream of objects at the location with line of sight - */ - public static Stream getObjectsAtLocationWithLineOfSight(WorldPoint worldPoint, int maxDistance) { - return getAll() - .filter(object -> object.getLocation().distanceTo(worldPoint) <= maxDistance) - .filter(Rs2ObjectCacheUtils::hasLineOfSight); - } - - // ============================================ - // Viewport Visibility and Interactability Utilities - // ============================================ - - /** - * Checks if any type of game object is visible in the current viewport using enhanced detection. - * Supports all TileObject subtypes: GameObject, WallObject, GroundObject, DecorativeObject. - * Uses client thread for safe access to viewport bounds, canvas coordinates, and object rendering data. - * Includes staleness detection to prevent NPEs from invalid cached objects. - * - * @param objectModel The object model to check (supports all object types) - * @return true if the object is visible on screen - */ - public static boolean isVisibleInViewport(Rs2ObjectModel objectModel) { - try { - if (objectModel == null || objectModel.getTileObject() == null) { - return false; - } - - TileObject tileObject = objectModel.getTileObject(); - - - // Use client thread for safe access to viewport and canvas coordinates - Boolean result = Microbot.getClientThread().runOnClientThreadOptional(() -> { - Client client = Microbot.getClient(); - if (client == null) { - return false; - } - - try { - // Get canvas location (screen coordinates) for the object - Point canvasLocation = tileObject.getCanvasLocation(); - if (canvasLocation == null) { - //return false; // Object is not visible (behind camera, too far, etc.) - } - - // Check if canvas coordinates are within viewport bounds - if (!isPointInViewport(client, canvasLocation)) { - //return false; // Object is outside visible screen area - } - - // Enhanced visibility checks for better accuracy - // First check convex hull (fast geometric check) - Shape hull = getObjectConvexHull(tileObject); - if (hull == null) { - // If no hull available, the basic canvas location check is sufficient - return true; - } - - // Check if convex hull intersects with viewport - Rectangle viewportBounds = getViewportBounds(client); - if (!hull.intersects(viewportBounds)) { - //return false; // Hull doesn't intersect viewport - } - - // For maximum accuracy, check model visibility (optional enhanced check) - return isObjectModelVisible(tileObject); - } catch (NullPointerException | IllegalStateException e) { - // Object became stale during processing - return false; - } catch (Exception e) { - // Other errors should default to not visible - return false; - } - }).orElse(false); - - return result; - - } catch (Exception e) { - // Log error for debugging but don't spam logs - if (Math.random() < 0.001) { // Log ~0.1% of errors to avoid spam - log.info("Error checking viewport visibility for object {}: {}", - objectModel.getId(), e.getMessage()); - } - return false; - } - } - - /** - * Checks if a canvas point is within the current viewport bounds. - * - * @param client The game client - * @param point The canvas point to check - * @return true if the point is within viewport bounds - */ - private static boolean isPointInViewport(Client client, Point point) { - if (point == null) { - return false; - } - - int viewportX = client.getViewportXOffset(); - int viewportY = client.getViewportYOffset(); - int viewportWidth = client.getViewportWidth(); - int viewportHeight = client.getViewportHeight(); - - return point.getX() >= viewportX && - point.getX() <= (viewportX + viewportWidth) && - point.getY() >= viewportY && - point.getY() <= (viewportY + viewportHeight); - } - - /** - * Gets the viewport bounds as a Rectangle. - * - * @param client The game client - * @return Rectangle representing the viewport bounds - */ - private static Rectangle getViewportBounds(Client client) { - return new Rectangle( - client.getViewportXOffset(), - client.getViewportYOffset(), - client.getViewportWidth(), - client.getViewportHeight() - ); - } - - /** - * Gets the convex hull for any type of tile object. - * Based on ObjectIndicatorsOverlay and VisibilityHelper patterns. - * Includes null safety checks to prevent stale object references. - * - * @param tileObject The tile object - * @return The convex hull shape, or null if not visible or object is stale - */ - public static Shape getObjectConvexHull(Object tileObject) { - if(tileObject == null) { - return null; // No object to check - } - - try { - if (tileObject instanceof GameObject) { - GameObject gameObject = (GameObject) tileObject; - // Check if the object is still valid before accessing convex hull - - return gameObject.getConvexHull(); - } else if (tileObject instanceof GroundObject) { - GroundObject groundObject = (GroundObject) tileObject; - - return groundObject.getConvexHull(); - } else if (tileObject instanceof DecorativeObject) { - DecorativeObject decorativeObject = (DecorativeObject) tileObject; - - return decorativeObject.getConvexHull(); - } else if (tileObject instanceof WallObject) { - WallObject wallObject = (WallObject) tileObject; - - return wallObject.getConvexHull(); - } else if (tileObject instanceof TileObject) { - TileObject tileObj = (TileObject) tileObject; - - return tileObj.getCanvasTilePoly(); - } - } catch (NullPointerException | IllegalStateException e) { - // Object has become stale/invalid - this is expected behavior in a cache system - log.info("Stale object detected in convex hull calculation: {}", e.getMessage()); - return null; - } catch (Exception e) { - // Log unexpected errors for debugging - log.warn("Unexpected error getting convex hull: {}", e.getMessage()); - return null; - } - return null; - } - - /** - * Checks if an object's model has visible triangles (enhanced visibility check). - * Based on VisibilityHelper approach - checks model triangle transparency. - * Includes safety checks for stale objects. - * - * @param tileObject The tile object to check - * @return true if the object has visible model triangles - */ - public static boolean isObjectModelVisible(Object tileObject) { - try { - if (tileObject instanceof TileObject) { - return false; - } - return checkObjectModelVisibility(tileObject); - } catch (NullPointerException | IllegalStateException e) { - // Object is stale - return false; - } catch (Exception e) { - return true; // Default to visible on unexpected error - } - } - - /** - * Performs the actual model visibility check for different object types. - * Includes defensive checks for stale objects. - * - * @param tileObject The tile object to check - * @return true if visible triangles are found - */ - private static boolean checkObjectModelVisibility(Object tileObject) { - try { - if (tileObject instanceof GameObject) { - GameObject gameObject = (GameObject) tileObject; - Model model = extractModel(gameObject.getRenderable()); - return modelHasVisibleTriangles(model); - } else if (tileObject instanceof GroundObject) { - GroundObject groundObject = (GroundObject) tileObject; - Model model = extractModel(groundObject.getRenderable()); - return modelHasVisibleTriangles(model); - } else if (tileObject instanceof DecorativeObject) { - DecorativeObject decoObj = (DecorativeObject) tileObject; - Model model1 = extractModel(decoObj.getRenderable()); - Model model2 = extractModel(decoObj.getRenderable2()); - return modelHasVisibleTriangles(model1) || modelHasVisibleTriangles(model2); - } else if (tileObject instanceof WallObject) { - WallObject wallObj = (WallObject) tileObject; - Model model1 = extractModel(wallObj.getRenderable1()); - Model model2 = extractModel(wallObj.getRenderable2()); - return modelHasVisibleTriangles(model1) || modelHasVisibleTriangles(model2); - } - } catch (NullPointerException | IllegalStateException e) { - // Object is stale, not visible - return false; - } catch (Exception e) { - // For other exceptions, log and assume visible - log.debug("Error checking model visibility: {}", e.getMessage()); - } - return true; // Default to visible for unknown types - } - - /** - * Extracts a Model from a Renderable object. - * - * @param renderable The renderable object - * @return The model, or null if not available - */ - private static Model extractModel(Renderable renderable) { - if (renderable == null) { - return null; - } - return renderable instanceof Model ? (Model) renderable : renderable.getModel(); - } - - /** - * Checks if a model has visible triangles by examining transparency. - * - * @param model The model to check - * @return true if visible triangles are found - */ - private static boolean modelHasVisibleTriangles(Model model) { - if (model == null) { - return false; - } - - byte[] triangleTransparencies = model.getFaceTransparencies(); - int triangleCount = model.getFaceCount(); - - if (triangleTransparencies == null) { - return true; // No transparency data means visible - } - - // Check if any triangle is not fully transparent (255 = fully transparent) - for (int i = 0; i < triangleCount; i++) { - if ((triangleTransparencies[i] & 255) < 254) { - return true; - } - } - return false; - } - - /** - * Checks if any entity with a location is within the viewport by checking canvas conversion. - * This is a generic method that can work with any entity that has a world location. - * Uses client thread for safe access to client state. - * - * @param worldPoint The world point to check - * @return true if the location is visible on screen - */ - public static boolean isLocationVisibleInViewport(net.runelite.api.coords.WorldPoint worldPoint) { - try { - if (worldPoint == null) { - return false; - } - - // Use client thread for safe access to client state - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - Client client = Microbot.getClient(); - if (client == null) { - return false; - } - - LocalPoint localPoint = LocalPoint.fromWorld(client.getTopLevelWorldView(), worldPoint); - if (localPoint == null) { - return false; - } - - net.runelite.api.Point canvasPoint = Perspective.localToCanvas(client, localPoint, client.getTopLevelWorldView().getPlane()); - return canvasPoint != null; - }).orElse(false); - } catch (Exception e) { - return false; - } - } - - /** - * Filters a stream of objects to only include those visible in viewport. - * - * @param objectStream Stream of objects to filter - * @return Stream of objects visible in viewport - */ - public static Stream filterVisibleInViewport(Stream objectStream) { - return objectStream.filter(Rs2ObjectCacheUtils::isVisibleInViewport); - } - - /** - * Checks if an object is interactable (visible and within reasonable distance). - * - * @param objectModel The object to check - * @param maxDistance Maximum distance in tiles for interaction - * @return true if the object is interactable - */ - public static boolean isInteractable(Rs2ObjectModel objectModel, int maxDistance) { - try { - if (objectModel == null) { - return false; - } - - // Check if visible in viewport first - if (!isVisibleInViewport(objectModel)) { - return false; - } - - // Check distance from player - return objectModel.getDistanceFromPlayer() <= maxDistance; - } catch (Exception e) { - return false; - } - } - - /** - * Checks if an entity at a world point is interactable (within reasonable distance and visible). - * Uses client thread for safe access to player location. - * - * @param worldPoint The world point to check - * @param maxDistance Maximum distance in tiles for interaction - * @return true if the location is potentially interactable - */ - public static boolean isInteractable(net.runelite.api.coords.WorldPoint worldPoint, int maxDistance) { - try { - if (worldPoint == null) { - return false; - } - - // Use client thread for safe access to player location - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - net.runelite.api.coords.WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - if (playerLocation.distanceTo(worldPoint) > maxDistance) { - return false; - } - - // Check if visible in viewport (already uses client thread internally) - return isLocationVisibleInViewport(worldPoint); - }).orElse(false); - } catch (Exception e) { - return false; - } - } - - // ============================================ - // Updated Existing Methods to Use Local Functions - // ============================================ - - /** - * Gets all objects visible in the viewport. - * - * @return Stream of objects visible in viewport - */ - public static Stream getVisibleInViewport() { - return filterVisibleInViewport(getAll()); - } - - /** - * Gets objects by ID that are visible in the viewport. - * - * @param objectId The object ID - * @return Stream of objects with the specified ID visible in viewport - */ - public static Stream getVisibleInViewportById(int objectId) { - return filterVisibleInViewport(getByGameId(objectId)); - } - - /** - * Finds interactable objects by ID within distance from player. - * - * @param objectId The object ID - * @param maxDistance Maximum distance in tiles - * @return Stream of interactable objects with the specified ID - */ - public static Stream findInteractableById(int objectId, int maxDistance) { - return getByGameId(objectId) - .filter(obj -> isInteractable(obj, maxDistance)); - } - - /** - * Finds interactable objects by name within distance from player. - * - * @param name The object name - * @param maxDistance Maximum distance in tiles - * @return Stream of interactable objects with the specified name - */ - public static Stream findInteractableByName(String name, int maxDistance) { - - return findAll(nameMatches(name, false)) - .filter(obj -> isInteractable(obj, maxDistance)); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/farming/SpiritTree.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/farming/SpiritTree.java index 02cc212d506..e0c5a94c22a 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/farming/SpiritTree.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/farming/SpiritTree.java @@ -14,7 +14,6 @@ import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.questhelper.helpers.mischelpers.farmruns.CropState; import net.runelite.client.plugins.microbot.questhelper.helpers.mischelpers.farmruns.FarmingPatch; -import net.runelite.client.plugins.microbot.util.cache.Rs2SpiritTreeCache; import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; import net.runelite.client.plugins.microbot.util.misc.Rs2UiHelper; import net.runelite.client.plugins.microbot.util.player.Rs2Player; @@ -311,10 +310,6 @@ public boolean isPOHTreeAvailable() { // Check if we are in the player's house // Check if player is in their own house (POH) if (!PohTeleports.isInHouse()) { - // Check if this POH spirit tree is present in the spirit tree cache - if (Rs2SpiritTreeCache.getInstance().containsKey(this)) { - return Rs2SpiritTreeCache.getInstance().get(this).isAvailableForTravel(); - } return false; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/farming/SpiritTreeHelper.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/farming/SpiritTreeHelper.java deleted file mode 100644 index 7306d6b70b3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/farming/SpiritTreeHelper.java +++ /dev/null @@ -1,248 +0,0 @@ -package net.runelite.client.plugins.microbot.util.farming; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.questhelper.helpers.mischelpers.farmruns.CropState; -import net.runelite.client.plugins.microbot.shortestpath.Transport; -import net.runelite.client.plugins.microbot.util.cache.Rs2SpiritTreeCache; -import net.runelite.client.plugins.microbot.util.cache.model.SpiritTreeData; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Helper class for Spirit Tree operations and pathfinder integration. - * Provides a high-level convenience API that delegates to the Rs2SpiritTreeCache - * for consistent and performant access to spirit tree state information. - */ -@Slf4j -public class SpiritTreeHelper { - - /** - * Check if a spirit tree transport is available. - * This method integrates with the pathfinder to determine transport availability. - * - * @param transport The transport object to check (validates origin availability) - * @return true if the spirit tree at the origin is available for travel - */ - public static boolean isSpiritTreeTransportAvailable(Transport transport) { - return Rs2SpiritTreeCache.isSpiritTreeTransportAvailable(transport); - } - - /** - * Get all available spirit tree origins as world points. - * Used by pathfinder to determine valid transport starting points. - * - * @return Set of world points where spirit trees are available for use - */ - public static Set getAvailableOrigins() { - return Rs2SpiritTreeCache.getAvailableOrigins(); - } - - /** - * Get all available spirit tree destinations as world points. - * Used by pathfinder to determine valid transport destinations. - * Note: This is an alias for getAvailableOrigins() since available origins can serve as destinations. - * - * @return Set of world points where spirit trees are available for travel - */ - public static Set getAvailableDestinations() { - return Rs2SpiritTreeCache.getAvailableDestinations(); - } - - /** - * Check if a specific world point has an available spirit tree (origin check). - * - * @param origin The origin point to check (where a spirit tree should be standing) - * @return true if a spirit tree is available at this location - */ - public static boolean isOriginAvailable(WorldPoint origin) { - return Rs2SpiritTreeCache.isOriginAvailable(origin); - } - - /** - * Check if a specific world point has an available spirit tree (destination check). - * This is an alias for isOriginAvailable() for backward compatibility. - * - * @param destination The destination to check - * @return true if a spirit tree is available at this location - */ - public static boolean isDestinationAvailable(WorldPoint destination) { - return Rs2SpiritTreeCache.isDestinationAvailable(destination); - } - - /** - * Get the closest available spirit tree to the player. - * - * @return Optional containing the closest available spirit tree - */ - public static Optional getClosestAvailableTree() { - WorldPoint playerLocation = Rs2Player.getWorldLocation(); - if (playerLocation == null) { - return Optional.empty(); - } - - return Rs2SpiritTreeCache.getClosestAvailableTree(playerLocation) - .map(SpiritTreeData::getSpiritTree); - } - - /** - * Check spirit tree patches that need farming attention. - * Useful for farming scripts to prioritize patch management. - * - * @return List of patches requiring attention (diseased, dead, or ready for harvest) - */ - public static List getPatchesRequiringAttention() { - return Rs2SpiritTreeCache.getPatchesRequiringAttention().stream() - .map(SpiritTreeData::getSpiritTree) - .collect(Collectors.toList()); - } - - /** - * Get priority patches for planting. - * Returns empty patches sorted by farming level requirement and strategic value. - * - * @return List of patches prioritized for planting - */ - public static List getPriorityPlantingPatches() { - return Rs2SpiritTreeCache.getEmptyPatches().stream() - .map(SpiritTreeData::getSpiritTree) - .filter(SpiritTree::hasLevelRequirement) - .filter(SpiritTree::hasQuestRequirements) - .sorted((patch1, patch2) -> { - // Sort by strategic value: lower farming requirement first, then by convenience - int levelDiff = Integer.compare(patch1.getRequiredSkillLevel(), patch2.getRequiredSkillLevel()); - if (levelDiff != 0) { - return levelDiff; - } - - // Prioritize by strategic locations (Grand Exchange area, commonly used locations) - return Integer.compare(getStrategicValue(patch1), getStrategicValue(patch2)); - }) - .collect(Collectors.toList()); - } - - /** - * Calculate strategic value of a spirit tree location - * Higher values indicate more strategically valuable locations - * - * @param patch The spirit tree patch to evaluate - * @return Strategic value score - */ - private static int getStrategicValue(SpiritTree patch) { - switch (patch) { - case PORT_SARIM: - return 10; // High value - good access to boats, farming - case FARMING_GUILD: - return 9; // High value - farming hub - case HOSIDIUS: - return 8; // Good value - Zeah access - case BRIMHAVEN: - return 7; // Medium value - fruit tree nearby - case ETCETERIA: - return 6; // Lower value - remote location - default: - return 5; // Default value - } - } - - /** - * Check if spirit tree patches should be refreshed in the pathfinder. - * This method can be called periodically to update transport availability. - * - * @return true if any farmable spirit tree states have changed significantly - */ - public static boolean shouldRefreshSpiritTreeStates() { - // Check if any farmable patches have recently changed state using cached data - List farmableStates = Rs2SpiritTreeCache.getFarmableTreeStates(); - - // For now, we rely on the cache's automatic update system - // This method can be enhanced with more sophisticated change detection - long staleDataCount = farmableStates.stream() - .filter(data -> data.isStale(10 * 60 * 1000L)) // 10 minutes - .count(); - - if (staleDataCount > 0) { - log.debug("Found {} stale spirit tree entries, refresh recommended", staleDataCount); - return true; - } - - return false; - } - - /** - * Get a summary of spirit tree farming status. - * Useful for reporting to users or logging. - * - * @return Formatted string with farming status summary - */ - public static String getFarmingStatusSummary() { - return Rs2SpiritTreeCache.getFarmingStatusSummary(); - } - - /** - * Force refresh of farming patch states. - * This can be called when the player visits a farming patch to update state. - */ - public static void refreshPatchStates() { - log.debug("Refreshing spirit tree patch states via cache"); - Rs2SpiritTreeCache.refreshFarmableStates(); - } - - /** - * Check if player can use spirit tree transportation. - * Validates basic requirements for spirit tree travel. - * - * @return true if player can use spirit trees - */ - public static boolean canUseSpirituTrees() { - // Check if player has completed the basic quest requirement - return SpiritTree.TREE_GNOME_VILLAGE.hasQuestRequirements(); - } - - /** - * Get recommended next action for spirit tree farming. - * Provides guidance for farming scripts. - * - * @return String describing the recommended action - */ - public static String getRecommendedFarmingAction() { - if (!Rs2SpiritTreeCache.isInitialized()) { - return "Initialize spirit tree cache system"; - } - - List needsAttention = Rs2SpiritTreeCache.getPatchesRequiringAttention(); - if (!needsAttention.isEmpty()) { - SpiritTreeData data = needsAttention.get(0); - SpiritTree patch = data.getSpiritTree(); - CropState state = data.getCropState(); - - if (state != null) { - switch (state) { - case HARVESTABLE: - return "Harvest " + patch.getName() + " spirit tree"; - case UNCHECKED: - return "Check health of " + patch.getName() + " spirit tree"; - case DISEASED: - return "Cure diseased spirit tree at " + patch.getName(); - case DEAD: - return "Clear dead spirit tree at " + patch.getName(); - default: - break; - } - } - } - - List empty = getPriorityPlantingPatches(); - if (!empty.isEmpty()) { - SpiritTree patch = empty.get(0); - return "Plant spirit tree at " + patch.getName() + " (requires level " + patch.getRequiredSkillLevel() + ")"; - } - - return "All spirit tree patches are being maintained"; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java index 4a8d4568c73..b4238bc3f30 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java @@ -14,7 +14,6 @@ import net.runelite.api.widgets.WidgetInfo; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.globval.enums.InterfaceTab; -import net.runelite.client.plugins.microbot.util.cache.Rs2QuestCache; import net.runelite.client.plugins.microbot.util.coords.Rs2WorldPoint; import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; @@ -1423,11 +1422,7 @@ public static int getPoseAnimation() { * @return The {@link QuestState} representing the player's progress in the quest. */ public static QuestState getQuestState(Quest quest) { - if (Microbot.isRs2CacheEnabled) { - return Rs2QuestCache.getQuestState(quest); - } else { - return Microbot.getRs2PlayerCache().getQuestState(quest); - } + return Microbot.getRs2PlayerCache().getQuestState(quest); } /** diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/world/WorldHoppingConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/world/WorldHoppingConfig.java index dc99bb1a12c..7b11eb89c77 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/world/WorldHoppingConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/world/WorldHoppingConfig.java @@ -1,12 +1,7 @@ package net.runelite.client.plugins.microbot.util.world; -import java.util.Map; - import lombok.Builder; import lombok.Getter; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.ShopItemRequirement; -import net.runelite.client.plugins.microbot.util.shop.models.Rs2ShopItem; /** * Configuration for world hopping behavior in shop operations. @@ -77,37 +72,6 @@ public int getHopDelay(int attemptNumber) { } - - public static int estimateWorldHopsNeeded(Map shopItemRequirements) { - try { - int maxWorldHopsNeeded = 0; - - for (ShopItemRequirement itemReq : shopItemRequirements.values()) { - if (itemReq.isCompleted()) { - continue; // Skip completed items - } - - if (itemReq.getShopItem().getBaseStock() <= 0) { - return -1; // Cannot estimate for items with no base stock - } - - int remainingToBuy = itemReq.getRemainingAmount(); - int availablePerWorld = Math.max(itemReq.getShopItem().getBaseStock() - itemReq.getMinimumStockForBuying(), 0); - - if (availablePerWorld <= 0) { - return -1; // Insufficient stock per world for this item - } - - int worldHopsForThisItem = (int) Math.ceil((double) remainingToBuy /(availablePerWorld)); - maxWorldHopsNeeded = Math.max(maxWorldHopsNeeded, worldHopsForThisItem); - } - - return maxWorldHopsNeeded; - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.estimateWorldHopsNeeded", e); - return -1; - } - } /** * Creates default configuration for shop operations */ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/area_map.png b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/area_map.png deleted file mode 100644 index 95ed799ceac5eb77f4740cd8bcbbfafd8fbb0e55..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17509 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 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/chronometer.png b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/chronometer.png deleted file mode 100644 index ec50807c0deaba6e26414eccdc66852d354f2c06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23542 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 HvkCeib_d2= diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/clock.png b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/clock.png deleted file mode 100644 index 4a16b83fecd3bd09432356825407d6fa87b4a729..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53520 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�zEL81>rwiYI@e^mTXn}2_Mn>pFu%Z)n)T4JYD*MgZHm%gjBp?)DB$P)2 zVpMPVl*z{)JMtl0>QzhH)qUBNG3A2n=d07vIi|R@@P3mV)g2ilBmL0fFx!yVNaZ zx_?_+qFqXaT=Z!9_2Vwi3R^><`Sf&$hJQ=kTV}s5OpL)%-UVy+e}v>NA)Dwwe^c0y zl16CNICRj~rYi0xS=WDybh)X8Z6VD!n45P%yQOT>6M_)=+?9x%@q?tf#vA^RPp!=U zkE})@=}L63?SihtsENH+aEs^#naL?%?RXe4H|Vyd$TH(CyRblR2s#eZ(RL;Us0TKO zG{Wgs2+ia+E0DarheG+|>Ww!uAdP%layg|lP$9PPoJaJq$TZm~wHp!I#JuAv#MwI? z9kw)9zTfC_uP-}=T`6G?t4&era`ehTpL9zog*kbM&=D%aKR0DVZ$8&3> zcz5=U1pr7!b#))o40>NW;2w2Ks+nXn@_({857_i?9=e#TPp;~ZW-xpI_GGu%A`Ve@ z!BDfi`lAMliH}||PM;V6%hrIu@M&$5Ug_c118yW@zq0Qwsev9?G)LE9B3b0?C8t{i zqFBqCqeBc2^UR!AsKrym+>X3&|LDO~_iv>>KY`qξ1?c+#`E?wNFLvoVGKajIl4 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 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/delete.png b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/delete.png deleted file mode 100644 index 2993cffb29dabd4bd44c45286ba1f66a366a24ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7842 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 v@;vP(R_q0%s^(>F6B;(G`WmWF^2ZhI#n3F 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 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/loot_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/loot_icon.png deleted file mode 100644 index ac88ed20fe7fd8c4e749947ac31755be8b2e82c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 892 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 BXodg) diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/old-watch.png b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/old-watch.png deleted file mode 100644 index 01b82d9a553a45bbd6096d002b6f9ec7c82d6642..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5617 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 aL%L^HJxh-_9u5G_8o~DPXO%CCP5%!zg)|KS diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/padlock.png b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/padlock.png deleted file mode 100644 index 3b6c462c73c5059f80b47b8847460bda9e3379e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15064 zcmd73X*`r|_%M9USVNYQrEFzn$(j_CwG=}!w#rUI_QFuMnYf7-G`6zKShFM~WGN!s z$izt2l4XoF`#R4#_x*pK5AT=veZIZ@ell}i$9W$6dY)p?MtXS!` ztBTOtIk?yP?i^;OV%K4hk(eeUe`E~X0406*`snE~#g%}$eE1y4Tt1Pwe}3k0m0M2f z(aBdw&~K*>j7-H6$G)BK=un%f3&SEFp@UNXq_5TzF%6kBvl6Oqkvvg;ZP+nd7T0>PAEGDhPoLgdV(mAzn*qe6biZ6;ZFs_o7vJCd zd6y>)pDB>c7?D?7@b$g;r)sprE9HBx(c(0*ELPIv;4uW5{gHwS`L`Pn_)rCln@&Z9 zNr{Kbq5EEOo@o?AeSFBc!ggcR_tlS&HD*SCsc)W{8Oc9p+vQXAl?lp*OF_|zA@Z#2c))l!(q`HY>>N4 zNa=wrY9^NUpooy@R)~^iZ+=AhPSKFoT0haddGOtD?83!*WPqhBoLiI^GQZwft|dot z4hm}aNl8>WjA1o710|@-*Lr*AF*1Uyt5XpWx=I+n0R-9%Kod%o4u zbNaq}JwGR3tdq$BORQDQF!I?9)mjQZXi}RwXsCRXdnbckdp{d;xcWDAzfz*Xj^rVM z(NA?vySp=21kLxZepE~{(7+qyh5Zgbn98sfjv&8+bWborX6Fx#n#@Sni2WCYtHZiU zCcR7!XJ^vSzUmGwGN1FvTy1RbhHwly&_wjq?oL*Dg%0gHo=>p7=)(tITt`T+LV+^H zK5wsI@@9$6ffH37dL04BUGCgmzhD@Y+s1rwV^Y=PSUX(NQYPuRLBV&A&~aKlP}$O3 zB11DD6V!`eqV^gDRktzkuO7>>UN4qjWO8_S?eyv`nWUZArR|&d{FUOb@j~hnWNeYw0rB37LF7ICXY-iH=h0Zg! z@-vA!wheC5u8~+IGVZ3GwvvK+|LlfIr*(vlZ|)BO2?m$0LWQzy+LxYbIWJ}453H0v zMwTCuf>@@5!_*Rb_5wSN8*(e(I6cOQKuJ2V=-Lo_nhlW&O!aK$OytnD06&?eIsyO;1AzID z;oQmqa1ybfMCd~FoQ*ZRDFvBLrn6TLd1@vZG(3-p#O+*$MaQh@Ej1dG^eps@C!QW* zYhF{G`D`Pd@hJXzF4AWNm3H<^2ffE%DjXXW&*;~{AKowfs??fvQV31eE%aLR%^tl zv|II-PZaXp*|UE?hhE+lv8vf;_|n(@CwalifK@7wxkcPB2X z{|tpqX-ho+vts}9@^a_ryT2;d8`6v_hztlGCb3I^tr^tLbHZv#B~-15LFLslQ#sj;u!-kS%cthqp?}OH_L)3NuQS zho%wl(9+V95qarXA00^z5srW{#O45w*-$kGR^CEP(f{=Qo(<2*$r%nB^a{xWye!^= zE5?JkArWdr8jn80CnyWkHN0Hr=p>UgcN+y2ax!Nf2lA-VuQPF~*iCJh zERPJP=t|StL}8KRjc)sO*ZG0H&hap1hXa+BC$1@@*a2fGG-v0*x8-lxO~7dYz|oKc znd<`_D#wGLE!ltvct)LxsHmue+Q5ajF0f+#A2=EY{LmXkGd3k6+2eH`1>DEKHJ%}t zHmn?{?~o03qrncB@r@@t=o%aa7UAqs-t_A+Sq&G|jKY0ST%rcz8^vM@lD{xY!rJuY zQ+B7A_ygWs&p@xA3F6!YME9DcnXzK{t5>hug^d~y`cx2Mx`Ip&L5*URQu0Yi$R-D^ zy0Ak%zh()RJb^1-ne;gLxXLou{PpYCu0cC&3zq&O8m`C$NUcHbBw{!rJ?{q(wyLS* zv8`iNA?apa8lXDFG{Fk2pQh~;w>EV{E1NTtP34@r zzw=VvbDv5gT$hPyLJ^3uH)YKUb(m}kU9%U6IxZq|(6)m~BLbdvVN4U;06
Wmw( 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 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/position.png b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/position.png deleted file mode 100644 index 2c1ce182574d442a96c0f8ff2e05e24d67e1c3c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30852 zcmXtg1yoes`}HtGr+_GpAdPf4(x8NNceivih=NEVnQGg90hr4H3$R>euP5MQNhQF_unh21I=p0;ljXdee?NB(#iXic^C%W3GN?| z*@lmq%#hy1=0E%DE*U1_3#H9HclFy3*c)gPS62P~KA~|!(AsMJ{5Vy`JCM?TKXz7d zd#5r9El(r&5E26Se}>nCIP5DluE^F%fLU&|-R32I>)5Nxzi%75I*@-7xyVG~$8;ulk1AC0#& zQyu)JH!LLbvP^ji zLhmjSAsbTSX<~y2m<~T@0vIN!54jXJ-+dXD7-%6r6YTuWnm<8}*~+JU9(})4kInO{ zEtkqqTokcT?&Q2-YLl1n3CR|rUC#Vl@b=N47-6xQ;Pi?+Cr0o+*9Qb%dBddOEn*}_ zSlkg|D1iZHLNjksY+*HpA9L> zylB4vK{JMdDCsz;bWnbm{l!*cg`q1xeuIIbO=Yjm_?Te1SNR}*?L2NPrhUIU@Nd<> zWyr^KABFH^q~B9ix$HK)f9*H78L*KhO@*Mh?k8xrTNtVnqjKiX@UAQf4q8yj{-$}5 z96ydIl3d_D+auI0XbYNyhc}xd1-BAJBexgz4zDhl(Ybm1*qb;mtD|l}BQ&3$i-$Gb zv@jf7;gy+<9f56N_NY2?&htkWV4&-n#;J*s_LeiJGcWh5=Dheh$2sJ7(}DuV5~@11 z?KIo?D)l#88@A9K8X0mIoCCtI<&pMQ{^ZM5Y9z_2zv>1a=db2-^D^vaJ8#A@KfcOu1UryA!pnAAqO=} z;w4JzK>x6b(Mx(MHNwoSq>?@%U!6BIzxo=qwEotV*VjK!)57FKGVW}M`kG)5W;5%+ zc%Z*X9=6W;?9hn)7?Vq&kR#&qGId~R*k$mjS^p=>m9FMYUthzMq_LXvA2o?Bsy&;w zIDg|2{O|x}73o5$5oC$_xTkeb>L}#F`gP}VFlCK?auq0}P41Ktv>+GpFNjvw4vN+E zC=t=t8m_WDCEwV1>#NvZL}stQ$oFjHz#!oY9h{t**Qj^$?-NZ=eMDZ6Cv*zZ)o18t zv}2GkDXTF3aIoE5$@_$ARBW@do3_l`0>~$<|TFkR2vUl4wk{d z6#oFtB%%FSfTpaw{oT9QVw^9sBf_3+NG+IZ8aCgU2T=sg=(qo9(4wIwnnd+S(pfr@ z)^Hd_mJnn@x?s{`_bh97yVS`m4EJ~XYa+()%R$Ff;bA7Omf&%6uvp4WIjJd=dOeK& z9&T)2;$0tD$t(n(n!$y8!ObzY8BQKOylgs>^P-eYz2k$_QTXvz%qv>-5A995a|2YK z8b<@cLeRZ=nn_Fpl%{>~;GGd?9!q{a7s3Oq;((wDlC-oA4~EBwQ*S~%!o`1SdYm4C zXYMV$>AZ_}DkyLhm_zUfH&Uy4a%%AWMwXZ=gw#xfnBEQG*X#d$yLm06C;g5f>2)5Ryn`PzCS^5Jg>pH_ZS6hs)8+g&!kTi9MY zTKaHfvgHz*M<)pGgnEbuJlxq>lQ6PfDs95#BB;84Afl1*2U$WP74rs0ymlhJXpDDv zpDvVvf3>I`raC^-K0LqB4Qu={t6}h+ybihObB>xU*acdHkTHU>L-jkcn6M(*$48<8 z2Swk0eyfh4DqUzNyxy91mCeDb!?ovCVloG3j{dTu;oNZjL>i(@k}*h8CCAOlLUWD5 z6z1r#wMb^~b>yoVKdOF-Fw~hqfq@h9UszxwK`aE1W@PpBoLB@3+CN@TUK120B0|X$ zR2}A@*t1Y+WZ6P*3a_U7y#uy)1=v-pDz*sP>$k21g@IJY=^XF(Uy~}n8y-viKruQ- zwL*2h?Ojux6{SR>;dM%|eyW;~f!MjhW~2N(oZ2bqEqXO}U>dBN;#ZS+e0zTO=gEeb zuTW!f74F@Szi&sgU3pcj;MNtNZUlb8{GCor0)@|w>lrm!o>laiBHo{O7&Z!fC(ro zeBQG+%S`g)*B3P+UA;>|v<|+ddN?dN|ojNRs*GgFh|GJccf9kj&_Gbq{*0V?+RagTBI^zP9&mwU;^F0LOcfd?Uk{!Hja;u!7F*S>7ddv2ylM*_~*~WFkP8* zt{-GJ4sU*uwO7pk zV&Gp?RRLMJ2cxiF<3-;VMU&ptMa6v(IBMLV-0FP)nhde_dU!O4O)#g|`A$9g5#zF} z)`O*k;TJ;~Qq%+d$CW^@jxlY-4&}woACW!bx8#BtfsJ9&n7hHxQe&EIU(BaOTGSYE z4eK=F>8s5nR-K)`7Tr$@Em$#JT&{eSY~iU%A>>k%UgJ}(o(nLja=>EO^!GP=zCu)7 z)L^KLT{=75yjkU{2R(SqCw>z^i6@@U`I$i_y%_Hvi6e9Bm7$woUS(W_QP6z~)g45s z87Z);Is@Zgr;~)ewHpL8Y%6OPkn^J`*;b|OOH21` zaQ&A;g`0^wf)kRK-1UwDj8hi1|`eR8g9noW#mh$ zb0b%s18-!@koZoJ>#lZL&Ait9q^QToYY(q?6aRXgb214>@w6Tfw?9`DV(5zLX4fK(lG zU9|ngmp#YIW_MmJ^Rvg175OQ_I52(17g$^U?$Z#Z)taS13-98&Z=H?2*c3*fQHeuk zmEG{;XJkb>v1)0M??G2|F7bvTc~=29w+RaVt}D+PLn8&l#n^k_iW!k3Fx<1d+szqN zc#JSF@PwIl^%tyo*19o5+nXjPhd|iMR{D_J>gmbU#A_2iEyEc8 z9a7c$WO@kZqU<7rCyo|y!2Qn_LgD!Na8?c^(Y7Q1uKbCWg1+8Y6U8<^G%Z;mSi zSS>-F>${pJT%`fU)v>C4H_3eAeMh-(Zi=o<`&&!5=96w`J|eSRZjV8rkhV~c*)6g3 zi*PG(Zw#R}Ds@;%g`GzB$y<4+x$&=}GBCJY9!SoAx62=IA@~&$tc37vMAg{)h9TjQ z9oBb5z}{?O0%0`|fb{Kb`xOm!h^nHKsUo zT&a{GW)mF6XJ2ZH+l{$+vRgB`6t*vgZS0gujYZmiq^%!%qlf--KSfiBKIVME8h#wM zau`zBUhg<=6IwtQF_c%+7-Nln#!UP@v&3+=_8VsFM1OG;Hp8d0*o|Y?)sK>%)t(id zS!4L(S_B8ki~fB14h3>$=IVUqP=U7Swo=kJJursoiTJy%Y`yC%Cf}W1qy^Pj4o;Rt zzqJ@kr@+wX%V_PUEp=vOcsoCLJN;QWsB9|iC022-gr4}_==9%l?-1{Fy`{S`Z4;22 z|DNDSch$7ByJ00|-UvcJ9!O3^>Wb2mS^||x6Fw}gb&PEvx;QDYor)sO+m^59OH*>- zT(C^b3gp{BANd8M7+7^nETr-z%veQ@The>T19Pi$wx(-lGeALy*>CAzcHilNBdzXm zLAny>EkZfO)|)5#dw5GoUu#{?gbvC+$(iB`s!y|iYqg1E4E*$<>xX3+B}j3QTj@rz79Dd3!w{&#sOm$b z$j>}q!?HT2p^r|3)WeROncDNh-n=of@Z~b+R`;p++BRe+&`tX|bhVnbNn@l3xUim@ z`d6#q^AM!(B~CTwb=+_|i71K?CTm;hBeKfiiSnci=>SWArrH5wCeg@GG$78^+1QBb zrwI$13=`EYtIdt|w>SPENlWT^U#y|uEd~maER&B!)&E$K0vBH|4of7TnDptUL8Qg0 z#?xCbzO<5cCms|Dn&*O{QFAo`;PkTr$;z1Ch5m{)sGD46K_VQ+Cktv z^xI`&@ZVZ=*lM`m5g<;%c#T4dBCJou*a#+g8yH-D&b|fG(|W&o9npgV)LdG+xQMW| zW8>vl#ObM$e$RUD&Ok1GN;S@3a|~*Uf~TvomZM2yh3|H&^z!)jTY@5FEj%l7uk4$$yq#UC|%tF_WMae-Lm4r7U4K~n-2v-NP zD)Gl_NBsPUC&5CSUnCLrHs^@%c`T3-7AkL&M{~s@mM(n26DvgAFml$12M)a$TD_{E z17-0;sR&H1pa4Zj6oqm||CL!rj1Nj`P1746QA#%Q&)sRET4#Nw(ca~_C}Pl$?1GHf zo3)tBH;Pc+ltfv@a?CXhZhaD|?(-!&|F%VLC_2&>UHB5;U z61-wq$5nd%WxxwjYgF~h(cDngt&}FCv5wmDJu|8J zQ-59v7!^7skXt>9g==cOmO)a2eC_sMq(rXu5EHiJ5Kk@Dyh~`j6rr?Qw?0UH6JoQ0 zQLbRQTK4HIH;TJ=lKX6f5xC*NbyML znl@_P(%O)(?J>a-dGFR~FML)9Y**;E!h?u>Li6d~?xxWEYogSes3rJW-{HbRzP4HE ztK1`R+T?;4EKmk=IaiQI+BIBt)`+{78&NLAl1fZT5yE@;NxUC@5$^|=SU*)UwTsTW z(2ipn+BRLvc>yeQcsi8thhqyLF=o3u+7?M-+>3d6#^++uwt|}?4F$4Cs*47Tf_i%V?!9$Cp8Boc^*!a4`Utf+Q`T@4>1)M|7)xH(no`vF!PX9nP!J&SQRjc) z+f(m!+7X{H{FG4b_yg__z`FRUaZvTjwcdV@Sd{VW*H076rhzZ%L9{!t(j*+@Ga|u; z4JVFqvod79A*n=zsk|7lFq9%|oaw09iU^kdZ=C(J9$xIpPafwBb-!}G$BlWy{O8WK z-xP~JS^>-K?H<$2GNmBK&YmS0MfhRc_V4)#=W5~DU-xqboRMfv|6&6>2%VfZgk~Ze z9!>yqbHV~;v(H*r>TlRUaSXEg#|5+pdX|K@e2y3=VeKLTInh<0++_M~3py@|vlIKw zT``oQT7`?Pia0qdAaxL8si>kpGNg7Zd`TQ&2yNPJJRT)sUJiV-P*tW?} zNas02tvec20*B6GCWXNwxBO<7&3P(qV^+aW%ZZYE@J`;k)XtBYq?;DsBEA{ww zdNeMdhx=Kqm8$Xr>(!hR$k&(qmOlJEIjyRh&(dngX$H~HVI6#RA;Pt9@z$tF=wC?V zk31)_6;neDe>s}E&tEVkEP7G0q`!|Ys8~m?fQ&!#>!U7xVgLCHcs) zCMV{VWUSerV#i%yk|ktFG)+9OZ{Wnbc&@cBL4?GE^znrk1dA4`GN`Z|gVhz@9Lz{$ zo4=mVO(z_+-bac)8H?gzW5>Wd)GgAMj^uX4t)n)=sEN*masVrnN7i23tPNNp#%qd^ z__KEItK)a)KyTUqgy9V{K}m`8@~rmw=p@-As(w(;{v%Nr^l+(}B;F>QNj;v`yR!V8 zCT0TFYK5%2`u<^Yy+Qv%lrvt1eI30tW6kHhxatz65>%0G*^&BQv2rVAi9V z9CM$WxD9-P&oj8c%rGh^|6Ysv2N8Ye@lon1v1xnXRI-*6;hgKmYVjM3j&6ky_rV$_ zmZ9{JJx`%>d76Xsd~9pg$|pB(w!9xsMAsBIqY}|(=cx)s@Uv{AhSU~SdD_50Gm(WX z9t~Q)a$*4m<~mY=IjBpmTrkb(OF3)WV}!!;=}@a0ES8e0`(K-&NK0ALUd{lF^r6Nw zVSM4q>mutU1Ty6>GWlq~R2Qkk5q692D2Qg|wjafovjEm1+#eJ7%|o^G*TQ7a5KGi( z-D&(=l8#&6R3k&{3G((~>&VC9I2n<@ne#~n0x;_)_-Wr|X=d~TOC|V{_(`Ga=#P*4 zi1Y|iwW`2CvX;X)Ih(wtKc*-djM~8mL3z-k;1O1cR~q_k^pfAbQA}93O6vE)zyvM_ zcUi8@!?#TaQ;dAFQY!Ek)sLWLq&Z|*&waZv#EiTJ!$r&Lw0n|kAWaDIx}xLgbpOpF z``EQzr}?ZP;XTN{f!8@y4l#Jz#(V1cg01h1KG9lXlwym(PZLjYc?j!#MH%HgpF-13~F+HB$c zeu_zm{$T?h8L0@z9V8maWnrMob0u8vRXR~mNszErqhH=5B6i*VB!3NiIm+{bnGJ$GiPWNsI4lx_J$7op|yB ztZ=IFzhoxkRrWI5mkUCmAj%eyrafCva$H(6q#=SjBi+f-lnaTaLF!%!siIn>V3Y2j zR-S&Y*E{0>h0<>NP{S%(J}=YYJ5rWP*O06I1{^?2T>22evjiJylT+(Ay-$6a$kL{D z4`zibwu3<-305xbOW|5j{`pTY?ma3Nx;rOcSRTBGGDOHTt)a0W2Z53wGA~g2o+b2Y z8g7xpZz{}VUroYVCn3bsBOC7* z{v03{noHN^z@-Y-*VGjXVpK#2-uquW&inY6^+U=BZaLq=aH4ker5KPjb)p6nusYUM z3WR;&tnd+!E9vZH(PY*fj^2$pD4oMAH-_{5;*}9mUSK4GqM6HTv5$utLWx{JfTy?W zv})W`X{}C`JMW9nCnd7MI$4_zKAm_75*cFg$jfI(V1DEIEdc&UHM+yW@~pxGoNI z&eNwu_=1lvI*DfLoaOQk>vZUbj1$p|qobUV>bhBV&hMiD#wFtZ$NPxjba!EJ8hU$U z_lhK2uOT(eQ(JvxYWpN1l4XBqbrW_fQdjlKgqYr<;PbP;+w)5Q0*tIyEe{U%nB!Q} zud*OAz2UEimG~*I-ve-AUW@we_rG{I?fyC*#XLwZVdRT3@DX9Hr`Wv{o~>--Pd3(T zl+fzzHmx2?@!Q4fCYy0e^dmh@!qx?&m&S}8KQm6gs~~?;BS8PKRFY;DcVWLhbMY&M zh5+Z@Ye3=P*vP!GW2@6<@f>cDF6E)|@CdTtDkZer8g!9o0jh?@eQF)(&Bdmdk(TJKm^HnxcvYre3RNIfX|cv_FTHhHa22p~)!~1HQggi~R-06P+Eqm6)OC?ayyiT|0LudL$@u6>f@> zkpIbn&geP528F00l8csz3zb$dws{NZTXZa}DzFIMUy8qly_%owX`W%(bdws(LLULx ziWh8F>tGdAu-InQ+V>KQ8BQ8G-D1zBKwC#X^cw>YveFQnUiSkbp{ zERZ1~ERL((=wsgp+%lee?5#J*@YS=8SSx4ASmuA&T1}7Id)u`hu3F2nk|uN($l?-& z-y(*X?ND$@jYRD@0*|Xv|5UWV5~YAuj1%R7gvx+O~>ABVSTtb+M+P1b|+*F zxH98b1xw2oVE&xHWSaU-;t*%;5Gmas-3kWce-rwBk=?BFpVo`b;v73wt@qOxENH$c?co(-00rZ5i2~Og%Lawm z|7ihk9*)Il|G>bXJ$?UtbQV7JA*DeC*8LGS!6&A+3K>F^H-;u27QpSL9`ERB$>?Gj z^XnST{3^B{FaOf(9ZxDZD`JW+g^W8qceX4?u=Wo?$~vk}QVtwe_8>76!8MqClyho_ zkEp-an*T^1!OO8YTj*k>JGON*xe6DN#+fm)OHybo}7juXF}$J^0km1ew+}zpmNNJjrIjfyuBZ;qwPV$2dxO=9O4V?1IQXcl`Rn=I(bEC zro66~*&%K&8pL(BW4M%a>dPk@(K7&zS$5un#OxPw28ulp!#vq1enK?E!0H(+x2~5Sr}H>N-B0C?oxA?CtHx) zw|KNWW_WrqH9`rYS3vKIUIb@1EpZ4y2@`5Uku)v!c6&|+ctYYp1RG`A@OSrP-nW3T zwG>(Flscv>CAWu~08zkBBH92Ci3_0C0n`jme{131*6KTLegTg$XZ`~e6XXMy;t)Tl zF`OGx`%-=g+D?`P0F6+}9CL)A_FN#vY7uI?G3}}?N}rmUcjQ?E2S3~qUSD@mZp#Bw zA0um%i`L62uG9LlmjPcMB{x^ZF<(KW-90%6Hdm&G82KgztaJO>`E5qp_XL>e5Ih*6zTi;q5d>L4F+4ryc ziUu+xxqQzjJHjI!@x;rKX}K5O zfzBV)4a?WtGy1|)vkqqtKt%CaaquY>cv+x>nPhmn-H^n2b;e%MR28^;J6%Z9&_6Gq%?C}_&=c@Z_g#tW|Qv#Gt z$Rucx1itB5keNhFtDa{6Hxzvc0CQhF_I{;$(CA1+kf)57hqP_bv-{}n@b_SJn0WUO zX2urTdyv9;$>|KkBSP2WZ7(<$qsQ55s#BV!T>--Quc@mHHpES#N&yn3GtM-~z)=6Q z-hv?i{N4SHwV;JL$8J@$@}c*|mF67DzGT~56O^05E!TTaeZtSJS)TQ-uM$E#J(Cjn ziKCK9sJdSq6JZ1A{vB5-``MX1N`4Bqz=FRV8;U`zStG^yEx%dw>P{>rBXkGl36ee? znk^o3y{)Fj?Rtx1PV+eykrg2XK02^~ni9I?RQTUsc9Ua!8HrS=5<*5F;DJQNL$%7H z@}%3rWa|h6B7_3EHot#qaTajXbYUEJFJIEj^jc+yu-NI_Y;yaZm<6^R?teu#Y1qu! z5(h^vhJ-%kKh(;$?M3{RBy~%**3JBdRdJC^^w@w`g{%o@`AgT>vXUtUV@590p$AZs z&%mJopq;$!3VVRH_FwCVZVN)lY`|Cw^dU+^%DhIbFUD`JUI_vgg*I$5%%i(i!8I0r zBGjb>F1kB6+bHU)_(DDd=G{EKW8B8m8gr=IwH>$dzcKKD3&6rWJ!w^N3=Ryec-y-2 zs9Fav$nZ?OQgQJ`j; z=9G2l25B$HRbhFV2I)yhzIctAQFwjd1yGRVqQS3YwyjvjvE44Wy$OxjeNn=h4vBoNLeD?qt$&1(5Bm}G-zd$+y@Iw@+Zf&D^ClmS z`&m(vk+SJ(m~T>_K*eAw`<7TYp7uV{Ke2~C#+WB9X}0o)3LE&s6{c)u&7sRmXc|;` z7;uqq7>LKm;uf6Sht1U*Wb;_JvFu+M?v%d6O5@NC=BL`l&*WsUQAP)YYks(^+v{d=TW&lfosU$U`)QR? zk=@sp0u`DQR&=7WH;@x(Z5poL#AvsEXj!L1N~$#<(&XH{jLL5ZW8`2*xY{VvSSy%% z{{|+4LkSY;?Qa|ta|%>^j); zHR5p4j$L>q&_mt<Ix8zdP)ishJ=~RH@N7tf1CQf{k{<;7@3tn5@lnIMb8Bk~?uDJ@x6RnDs9~-~ zX!(K*l!LbO!b<~F4@)AF9^!iR8kI%<6NBK8je1+k=oFQOlzQW4xo~hbnC^4E4@*z_ z0duyw<>eKIc$>Osgw&&W5WmNNHh&!aB_GBd@h}3|-h>PjE`!&4h7-&qH>)bZ?!KL+ z^qK^#PM)qqWOLI+(f&@7am?eWn~)(kyb7dN=f^vIL_g!zLnB! z$u()!e*TXRo7aD$CBNT6b6^K~*&4!w^yt5PSRhbckJq-F+v^fxhv~k~DY)OGr_|!x z&T70G@1B{w`80Jbk*^J@{1??)yzvD7=wW(>NIfclkWHR>)_78@t}B6hf|Zx#1>k~8 zI=v@MzhTA?&_k-{z+C`aeGE*DQ?E|H7J_P(y|k^Ylep2Rgr?c;z+iF$ZQU$-n@>a> zNb;R`mYC&LErbT3dajZAnscX~0lxCFU+i=}eM2VdBzAPco@bV8t$%A%!R6^A??9&fAw1`#`%i2&(iV2?OwyN#AgvSTJd~(@KfcHODfmR*&H`yb;dpeqrVnXs z^)T3B%%feY*nB*LHqTYw zEMy40Ul^wHdtGbbPl*Ru^4vo1L ziAOBqCqN+nK&s=M!J9edBPWI>$RHk)w?43_A0PT@P%bCaHR53ix+kl|tSWX5me$22 zaZWd{M5&08b~q_?DB`~mx1bW+Vn7f#?_O(V@W&HXvpxVwUSu=hw{|+0gMZ~BTdn1} z1=>;qN^@|WQT)BtF$-kc`_+F|RSH4>Srx^!%&mS1-|SC__>GLPWk|;cZjCsRGF66B z6agf)PadZH`enoCR(E&>ZXk91MU=l2g;2f12%}mBym~i0-@0 zhYh)T-;0oHc#@XQn4dIi+&;vJ$h*9f3062#dTKgnPM>Yqp#m_LH;0gHP7;o`b<6QL z26^gKWnV{a@HOCeqezJlmJYBpE}+`vc8zj|MWR}z0tCc{EwrC=xJN1GqcYs-3F?oA zNbqjJDDG?eN01R8l-apvE>@lyqLeR;0a%DnV8*>CWpt7-b^}G#T>r$_;i~lZWBJ2Opvvgm%<{Q#-Gx5a*N89L2xx>!g}sn&m^A$122Zrhkzo5kA#gL zA zqRt2WJlzk-7AE`|m=MWrzyAEx^ATGXdZivk%bM3?wHAw7;Rw#X33P+%4CC0|_b-~; zY=4#3u!S#Q`Jzh1(nBgeA+kkgAa|O!6v)4w5LhnirK-c$c|jzCnu%q=h=0+*DMMgt z{^$Aum_LJPf`}gkc17!{sukB_lGU=G1Lrni;u)3k8|%-X#WE0ygswo(<&oKx6&ATf zZ=DB1>6h6VggI}Zd18u$5Fa3qpZW6@ctNlZPUI{#m>k3`7{5m?yN9tvuphrdzVEwG z``3I$WYc#Q$~Q{Q4??V%;{91acN)ZDoE7rzK?u2n9LEw2u{I|{SLhey^0mSZ&ZZtuc~&WxH+{3ZjRqJ$544BW)}N;MaIF9Zh6iNgP7YbzMJF zBl_Xv8KOQ^r%RDBR%x-Oe313BFi zPB;kRbUM2sV{(?DIVYYDtv+Df=EkTw#Z=8YDE3sTs}?d2a;m#{P}_5W)wSEpsan`5 zeFp`~mgQNs!=h_9cx^=k8uKgsa_sHasFdSZ#{%C3wxmr2-}&89U0e>teb%Hh zpZ+AYj3EJ=h9RB+=;a%1(u`{{eJl+kU;oyDlL4>NGF^eVd>4oEKzly+NXOniH zkMU@hZgUuVCvuX2_3zVLw*EZ+1r6c|xZ!}LRP374FHvXkELiN~skgVyl6Nm=Fx3;Z zS*VDd13B(Mr_g2)mgM=3_zRY{;cR8{r|MIQnMo5re_P&(e5Qo_d_boFFme+1>qVI( zABkFk{2N#3`9PJ&@CA8s$yc{s^1oKrremFtnD+e`&)JFPZ0)`pws&SEvyjpKYN5l| zxCLqT&M`=oeQ_nm^V%^lj=c3*x)M!FkVV2(VdP;6X17^MCgz;EWs&Tw@2IC^-~1_B zBZO0$R09Y20BEW3md{uql3AoHolmK2hsZmdZm=7}n$agQXCpNg1=KoW_feX-*EGUt zMnEQ8a^yi_re~>LChbX{ubB<9i{0Xcj4=sWSRK(3k~P+0rdU*1CiY)b#d{P?lu zDS+>TxETe5t)HlP{_rzkIuJI&JFtA#8?Hc}r?I$=Fe(bVzMRee2QmRfU_}&LGCqd#u z#?ce50GvsdfF(hErFtf@SiU>*JA&(b!wnpWJ~$U;dd2bcU0{2@)bpEg7f9nzIx!c0$sOERPQB|Mq~KbQvG|!X&(&mL zb8WGk<*$N`M*j!Wwmwxt-NyTnSNiX})Wyojmp8sc#03S1!JImNJ7g7FsCU=~Q3HRj zT|lY(rWjU|9=s2$Nl>~Gpj_PZVL0f;kA5N2ky~J?MVSoPkFbIQiybe5&|&CZIEg@zj7fuhvit0w14$oyhgL9Urv}3cdBZ2=%IxXd${zzEst~(YY z@D@=-4?9DaQe$u(>#p;BjSuX*;C7Yb(fa8@&o7Lt1206j*M$80fR{Aiue<>hv&=M# zbl;$|5~fDcW-W_j*1M^$aK612VW@U1mL=rDr5(-a?#s+sAo?hw!6*0n9v3(Wl3~-j z>9K81rN5^q01A)(1Cmpp<(mJ-3RCD^l_PspnnCQouO@bK_#=p zu}aq7F(^#5vM4R*l8pV-=GwU12D+GmimZoxZ)=^m8M`H5z{8V$&XzIwOS1dZvP_dY znswFPS3zJQ3t&Twl+2!a*uuCMXyn3 z7e?A;q>ic?8nAFwUj3Wql(AsA&|R2hds`s%b%n0$xQgUC=*>9H$O(^QnFC!@{Fci1 z#&{cZmS~&I>Blz`bg|Z4|6}s+bmFNKO@Yd$by}?}Gac{upQrlkIj$f}aQyY?&fWl{ z&{ynNH2`^KZyZrH4Z9joTCfH)oiBgk_7xmmug($B`WuqR=$#8HrK#fL5 zi6CAnAIC=LzUx~>u7E-Ys3+c^e2@fM<|Myb?aHq;otvA)yzJs3+ucbAfDN7&VR|>< zZG{8GSW02LuutWsk}PPODqsDIk=N3&b%4c}zFN0ON<{xzUmtSqrFB@_%=_0}&MGGC zro;O|6aLVu)7FiCV&y>y=+Z3djaI0(BC=MuY&Q1Zq3+_#(U(+)JaPSDPg8)7g8;M| z>CFuopN}8->xOKTiLKIgxh=(tu#ssYe!bMa5V;~EEM)WE4aiE~ys1Smxm(A)8r`H+ z&3`kruLGNADo4JmLE50kqp(g=Hexd^dO6w>8$cGsF zEM=WYjRH30nkY&PUwgX}Q6J|8)joi;n%#0iYICb>i~ac_EiP9YKotzAdck60VlyQ} z7M5+j$={Ph*Gd#Sw%flhw9j4wozar5!s`KLOJ8MDP|2mHCcpelxe_5X!M4x`6P8Jf zxP5vc41mT-kfgIM!v~4)+ypD0#pkVWM5XKerbMqSDv4-zG~BV>&j-}{)}9fmAC%_= z*#_riln{Glj4Josq<3RQfC{L417pH6q{TtI4iGtm?k5u+J%V;MLZI*N09_txCt3=B zS++M!2NtveQS4zxac9p93ejwV0)`whGnNBpmxkV)emi{~qnWSGTjmmJ3*A36c=A_s zANuCLIL-J=*x7HsIa%6gZtz?eYyFIVb$ovQ>b<6-OdOE5-Tjox-8Lrvvd`yn1AJ0; za=8{X(zoJkFz;!aKT96&{?H)WM$#tz6%BO3-p=IV5{_|GiRHk$z%pr_JdMXTJ2)<7 zuP4Oe!a%9DTz~-=7JVm?Se(p4s{6!}Xe~j_YPR8Rs|M4S5@H(|D+2f*2NH0}&4E!Kj$PmRi_&$NN2k@9h`t1#-H8>B|4r1% z3rfTSD{gd(prV5Y-8YL8n1U8~8z8+o-+#n(1sk7fZQR!G25|7~t|!9#`5qA7i?FCK z>oXCVKFs-V^F8W&BeMh&fE4MY9F$8h-W%1X-^DuhGZFRI@OT|Pb1n+j=nuUKvXK{XZ>0j}kwL78B7+v04YL2lhphA1PZp`QA`T8#3$6 zE*WMM_%o25f@>lW^l_g95{zAwNAW`|mQ6Bd>A-IycuzS=9c4R)zFbhOcXqN&NTla8 zEya2RM)I{ve!e%OtVi$W-<_TC=N)>BSj-ik!;+3qh&8gb2~f#xxuy zGxm*InD2xD^g&uW37Lg2;YNdg#_2sVz|cB#6|42-HH1*J1q3_?K|1^Z2cfw!A0++o z{5Pnx4EPtomlXddXjANbug||%(4_9*ac2##=r|A%Me=x!iqr^@t>A*S+={iEhm)!; zS!kmPDAfxFZ3WHSBB-ocB9}J41VArep~a#Ma8uOfH^%%+kj~$%mdN`$&f8y8Sy(T{ ze8V3+1|*k0R}D{nD$BKyDwWWNR3fcs@{W5V`F#f@m{y@>ntX4&`u7z+EMUBbK!M=) z7~mgACy-kNNj}W$3~h4@&sFPf>E>@mCbZMAUj|2AvRl3#Lu-au4vCnh78}%vo)ck{ zZ0SY68qEWsNs|WM`ci?-)scg)ukQQd1$pmS06Hk0Z?qUh3k5wZPCE6OCOX&1j6^sO z{hGa~apx#-jswTSMa3j~NZj^YlSJu7FS6;2+Zs%u>10Zu`{|2T5%?6H`gg|1y5BF3SeQ9uvME zlVwQh-LnU^ieLu?BCr=J;e`@}W`Q4ntNyk&pjsqAVrTay6WvBP*;z%aR585@oz|E* zQ5%4h{Sy(xdG8i}+M%(!UWX|%A*H7a*SQ@d^-{NlXr{cD;V1#(_HjruOfVl-i}num zm%VL^6&d}Ks0IZD$J3T8vDHj7Yi}(m|0@*jvxkO+-w}(N8E9UbhiWq4G`#Z?Ue-Da zA%yDnD5)2V&?&^U&VU@;R|KVrk?7_saVB>e@bXhyW0iz?1*~02BMqc#DToc`55 z>JT(77gcrWw-kKDmck`QVL-7mk)qUf12kvSZJOa)IuB~#%de$I{A@ZE)rj&KHNw;L?ouc5k5u-!GBbb2=llEjKF&SkwO`NI>zp@^ZBp`D3asj*cxc=HvU;-V zWffX4^d%d5@-dj}vcWQIT}qGDCpTw#f7#@5xh!+5?#}95^$CHCFcLrzKYY_6-R~O^ zR4_W&(#*sFvYwUQHk#)VXOZ-8j>E%Wd8X5e@~x2T959p)Oz5TF*-ZbsRh?fJKpFQ@ zVYN^NammMz!lxCPC!G=};oUn&_0V4-*x)e!z~bzcpuaVoZjIT+3-bZZr{H2y%xr;g zk?v>DGfc6o#g_kP9m)8nR-i5kC#~$ zahvC{B$5{>M9Ws0(H<4_4MdmiZJ)1HtLGW+#&27dP=g2TQlQaa&1{uZOaX@9E4siBmyaQM(TZ}40_z9<$YJ5lSm zs+{I*X@Ef_fyIYls0QudA~lBT61ybBvM=LRK~s5#oZ&J`PgKCQoh=!73LzOAZ1Q4y zyNbg^t=1!Jpc`RIUBULn~1v9)zh z@#SJ5g80i?Y#UU%!9(zS^<37gqI_Lv{%+gV6?JERR{62 zim1(S95Gzx{S==FqY`0H{P|_76O=Q!1EQsZP7qd676N*gbippk9*FyY1GiCO&gnP! zKB`_YGAwXao1qC>l89}#xZJn7vLdH4ipuG!0?MWayvqvj-h5eIMtHMgYA48lg-l^K z2YZKTv3#UFusdYu8tDUwbtLksV*O!TOL*Vej0*lZK_=#260L$FKDdz?P6yEq{-=w2 ze=8TVSsw>+b$6p-nK_e@|P*wiD$!)+lreXb?Xy2_(LhwTxm^ABvrXPlo`A%as5+(qI49w1v!7$4MblLw1a zORouABus%CTDZta_IF7QlxL*I`m;X<)K-`XiI@Mu@yo{J&FXa+)JPzn^-Ycq)Fu!Mt}!gI`39j0+p3Lvm+Vq?1gsP zm{^}}X`J9=EDBjQ<6y*mUv_QPq_ws3Pg?$+(yf~L#_QK=uZ;6LDTixj6Kfnz_uQO? z-=Q3AJe08HT%1MY-_21Jr358Q)8UeY;OJ(i7^=8S&L*v@d(PFTP4v&Uth?Aup`%iP z)v2otv+b*QeF$eyOrj9^&kzOgu)KvR%^5NTIdSLCd_j|#+Txpcgwl0+<#8D@qPhOo z4mkFCm5p=~%t#d|W2F*)sZ4OAgomyIr9Zhwu9nHiNP2OHQ`(q0`U-2pN zGN-8&(CA;FGFJA!SThr<#4Pdla@*|tC%Q~;$`m?QI{E8WPjA4o2%K-nTgT}(f6*E* zxXGw51Wia2{bPgJmqQl;J_&sT7*oCP%X6$1&Y5=l!#)Jx{hn2^A+EfSb2sSRJ<#X$ zCKWoGIF(rYw!!&+u1FaVi+It^(%Eph5ZIq~5H7PjK)M@#VBqJvO3B7ZeWb(T_>e+c zz#U5XUTl1-x7*u$axTwPDtqWE+Tp*U4-}=sUK@~JlTPUS@e_Jb*d%b|oy6YcbjL*$*8->kkp zl+pnwZ{D{s6`ZYDc_)9^IBjo%yA!I(RGD@^^a1+wyrbVe}xK zeL3%ze!IH zcNB&=Jm~crGl!*4h0Z3VEJdmx_`fS}?q2+DlncBvPsX;u&kc-tg2n)3z~c-0CsZSj zU55GR{UHlBSxli99BjP!&)*Eq26mcrxL|5m3P!-pbRn=TFX&yVplr)x*WdVKyRk(d z#R#}dlN8Sjc!ZH{@qzX#q;UtI`E3f|!SWfTEkvgoWlBs!Y+>w2t@L(if4*$qeAgoO zi?WXSQ3r%x^Sg*OcJyFmm?y%7QQjc_+P_^ z$?(c|a-O7W>=?W5FPZVFJ*M5TaEw0CmO%}wl!zdt0E`H5WZ}mWX3JH4<&SA=oVm2n z5?)Ej91-AtsCpYvE9afM|Jtd5x7$_~3?0U4#pIFy^6m257Mdb zA7WxrUkTC3dU!T15gve8UAf-PkC1;l`w?`=uZZYK3X|Sz9;&;&UF7+H|2Zw zo*ir^6h!Y|zk@~HVCBPZI1WFJXt8hp)L>%-xS&!_xuRRH{BR~B{#R}V!>Jd6)`DgqlyfMH0 z(T+o@f$+_Sob87^2QH>K1kRH|XK^^w7dx#7vT`2bq>#=i{QPgK9~R5f(X#9kl2G62 zx78)F?GlV{On|*Qmu&Mg@hEW6(ae;y zEUeKi-}Y|f<@1O#HuT`?+Iz*&GYCF*)GW=S0lvn02l7EiI>SLvPu~-*?+j2wEIru} zQm>^sZptL$YC0NU*upHK#Tkn)+65TC8u-`cga@MlQD`Cuq5wyFuZ4@FiZ0O~7{XJ5 zijVMjD(suC{_?w7{k0iv0POv`5#RxmB4d1swIr{rC$E|O`N^(oL?$hz3;oLTe$30S zK~AU#a>o(Lgi0S!WG`^|kn}E?)EQ`5_D+4M>Z2+%WOAu%vwb7S1@b-j5u7MEyPQWbIJAN@S`&5mR9bSU9RAe zUUc{{K4lwUFO>RnHV7CfC7_lU+yqGJYLu~=@0%)tI9x=Q>MCkuWOA>OW_Eb-2b>ej zd&N|qojJ6BS*{af9I@fg;}*zrJ+013TH@=9bjd+-m`IL!?(`5*;60v0(L^v7^f8yP z8JL{w<;QEr;;0^etvstPskM)({xpaK$45sKFfq2xfmrpxB$DGuLm>>Yz~m?GpCrre z`D0_C&%%9WOZY(JEzj;x$FOoKg93>mjJ2RKsV>G)V9z6DkEE+9#~NQ&)laKB6laLN z-~uMnVXeSMrU;=S8x4ee3m9k^DIG$&g#AfE^V_}h|GAT-Kfh(Uk96u7I9vo>b zjA=rluSx;>uf4QXDKRyOa0UnIOtHq6Maz#5*$r<0p9h(l`!UfjmB1FV@h=;M_IsF4 zilS{it2<~ zABP9`U*(b(H!l_pZ9-!ZzB+<%hfIpUgISV;4Khl=0yJzPUuOqPHIzm9G~NdEh7Sj| z@HZt)L%zI*C->+sZg{1qR zwFJKSv*!FGCsM6hIW-2MD_@WYc@r{~%K;Kp&tu@qXVI4^R@5b}Z+Lw=QJp1YE(th9 z*aZ~>pNzluP$-@d%aRRVDrpjmObl9_FQ0QMY5aLH>3Xr5PcOujByMxGH0n2VBCo)vS?+@Al^JU zJ~;9D=3pf~9fxnqgz||X<32yBV!?};<= z&WN;$R+u`sXXXb62u2@?>KHTR2fI{PrWIXg+z1tr-wd>N!0hiQB}b2eu;+Q9;Z9NQ zb=8Uk5^-=btJ_j`=x^>>&n((>{`ZXT+v_yKt4`J?_sC z&`Ri~{*>03Jci@Zvzb-1pi zVrR#jj-SkP^3cv+$zZE!_t3z+^`bOy2lRO}3jy;o-cR&BwykgdHe`knPjO1p0H3?FTJ$|D>%B*HXgr%ELxy z;?V#OM&A{oFlE30r6dC`TaiukiF7b7>{nzX=BANJ{OsT`1Fc2ii>&!Cl*8|K1Zv1G zU=7n#o6$6(7tCO@WlhLXnX+ABYMkN4UAMzvb+gRR`zFk4S0n9ulI|*3LGUus*asMm zdDw5=H$Jn30*5MDgLq@XUP`1z|K#%cqQy@Bo2~5UdAo}#%uV|j$e&LnZv_sWh4}1? zzq3yfO*7>Ck-&cHJ^2^Htv*R{>d`^yc^uC#Lgx^H+fWnu(?^NDFD)xdE7QUA(Lt7} zn^Gso5<=5L*#4tM&bRFd3y=$pagk2j!l668GBZma;tPgprXHlCZ&1@={ zIf_}$R>HR53M0e|O>4#!07>c5t$9wRL%-B^lY!97B+?A=bWP{F=_*_1#RNyzY%zg|1fG0=qb<=W z1Fx_|tc4D=744e3AyqqKlx$8duBV|o?`-Q1?bOnZ<+-JpyQp%=oxL8+ z{*GOLn!VuOxhM2im8aBw!yv@2(0W<~3_Q8j_V|4Xg0My=QMt#gNKt=u=qXauA@WT0>~<_)9S{xG$7en>f#SvQ=I- z!7DI9GGg-8j{tMG@OxHRQ%D2iE?1l|ije}@Hq-NJK6o*BxrD6eI3eSXELtP>^DL~b zm>|c=X)}aZ`NnB?>6b)w{pz*g`_Duv{AIP1h=SKhF_y3b zNGM;rJ^Vyw|25$Qm9nlA^MDu@>t@M$;gW*x6)O=D4L(9*_430qT)a>?#g+sAw}Y>k zkrm~i)5+xWxzsA*-nADuhW}cOv9O>9;TfN!$amDJOC%OmmS3EjnNP@0M;eLKJwkV- zDh|HGyvA-YlSL!qg1lA3MG_*b`t5Jw5l!5| z@bRFO!YKRz)~A1`JB3;nuZS7aeuMIf~ZXCtt3j{0w8EGW*mQsp-j(Qu_X2Gcis*wYcS=9fok1%^3Iw z8wM-Cu`J=ZJ%spil<~4Zdid6iWpaQz`N>Rw_6hC6yTUy*IicHbhM-N#D)_6|cF?`L z_iQCi>PKQn)43zpT0F;(hThb>q*T-Pzqm@vif}rENl^LPS;r3L&h}?p$L^`xcUV`5T#X)| ziOkk)j)ph%UJP77CB>GEB!uAb))>sM^wZQzcOd`<{YFE zaC-ir%=wYG6k<68`{I~u`1p1;lH_%(3{LHIH z($}#9CmYc6i_`Vs3tvvYlKj_Qc5!eM_3hh7l1^}k!xJrONPA2@#hEb_=|EP0fRWv& z75_W-X-whjl*y&m@GInAQ1>aUHO}c_akTmJU+h=PrS(1hWip6?&yVlzu1)OK4T*ox zaq{^g`-5@n<69+rXBQWH2P}@SpIqK7bETKvc^p(9<-^ZM{QrQv{=fskQ1yN4ta=XiL6pm?#1bU5%< zzDw@kgt2#`@FS#mmEV%t)AJOa`kz9|Go(8HpTF;Ytn6J4!PMy^!jU1m$U%g>!a&+y zzt)1w+0e>6(<6}R@A|1`xf$QPwQ$2v1kpVkQChjJY?&5K2UcB&Gi>Z=_ z8>z25CjaGGYi`>%d}i^kF;1##%ai~a4&st&^TVhA8aC|>n+|ekYF572UDf>UG5)jp zUMngD)tOY=eBrg1xIt&MxbYfRVaSI+168S~MJRA|Cn2%;LF{-etM1P4G0lOTZ5-A_ zFOwK5qCLf*!GojZ0&k!%yX5BUUNfVTWHvJlAiE{kZghfedeBZg3CUVd-80b7b`aYy@ly-8~$DV??_ zKozREFswQFH$N#~Wsgkj`vS?r%V%QZuU)@=6C0cp%xQ<`rlx3qqVgLaO$UYyH$z6- zAx4(a{_D7v#e)LpQLC+C)atN>60XX=KT}^>em!7A!8Hh(vZvP9?i-f_`{9w1(IrErYKG84^1^yf~Joh#|= zR!J=?CCk0b@TRPfD>I^VoP?~Kto%ETdB2D0BKm&i?fCH0$6^Yb<`G)p??{FWvu`qk z9NdELF2wJzh`!-7O1%SyuG&os{)3U9-RCjF5=FtJS)__U&6HPXztZOx_Jl0k{JP9G z@cwjS3ySfurUF=o z%v@ROUk3Y>lU1iF%ZsDk9&k<->rC9fof&r(YK=d`M#kvQ6>lv3lpcxIEHCEORDF(Y zc}zDhaVX==c--z@(721k2K*#c7?8ny+r5RxRds9%2~Xa5(FC%;s=JG2X_7d_9ewQC48Z=8+IEA+f|KtRVF4zUhfUu8 zKAh**c31=RwUyntKx1&Tr{kGK@N#Qf(-vL^uI>)PpyW^*Xxp*+y1OdWA{%#290}*7>haLt$5BMH?19=3VqXm#6{F0g%nHbGBxmcS(0j?uPxU$iC@{}8 z$Y<_4Wa#EBcA=85nGYB!*THbBx<$@q^Nn4*5JL-W#q*P#PijA#_3CGK()_*s+unS6y|w~P3!Rs4``x{L#((|}HPg}a zbtIns%H2VVQx`pxMqxRSq=LiWZ~kuJ=l(Us!`nP{ylD;PVWc+P9L$x&h6pafu8A3V z5xCLVR^#zDHEY{x-Gy6XtV)zDLOdValj`*05RF}^J#E&rM(=7)j(t7zyWWFet^Utf zpD3OowGVD4i4(0?RwrKS=2O*qP`XOJ6m4spN?F9<$eTemymWad3%s6l4=Mvh`h`=D zv}p2`*|&EgB4-H&`Zo&dPla5!Ye*|6XT@)19R=lyt@_n;rReWVMIL1 z9vUMGG_JTYOIRYg?9u7ZBXtwuAA3TfPu3@_s2;-^6=|aEoW~4A^Oe-@@IP+jG8aOS z$>w=MB@J7`l3AE{wK1V)Db9bHueA=E%rch^K6^fp4Ilcs&Ep>=-Kec6Zi1tcPs~i! z?!w=*I67f6T`KsD8reJo=`6fGequvdEj!!e-gmzKz2jku*bId1C6495-OzE_p|g)V zhA^+9DjE3{xASXuUzw+y{GoPMq;nJm$liLswy!yvSPE1tRkld|bGw;X~=p>!s znj-yL1+6mEHErm6maF+;hstbN7%8Ngs{Mk`)`VTJLuh1c5L+ezM2qNJqe2p~@JGLP z_4=ubJ#|v6tL@^|867@;3MY2NDFOcyf92hVO9F`e{oI!D<#E!qgo|Nap3_4Us3fbU6C{sBO3%rI13%m|fyH1?^)vVVly4&}D5W0dJ5^ zBb(+-^wXH%ahn^KILY}!bFbn@7@b#S=~5nF*FEFZg=`7-ZsBNN)#BdZurWW*EE*l| zvk^HrxM;jfmzPt@{2~V1O}#|=(529o{mKXSChL;QwXV!Ce&2U`&v5{PR`%*|ciQ)Dt1v^iK20y4M3>>Mq_!ev{5u)8tj3pW33VE}M2 z$hVvn4syZQ-Tz}B)vEAX8n80IBg~J!2?adwp$>eKrvlB5&qJ5>&@D_zA%WWbu8t8)knA|8--A@LrDdB-Wld;>BphZk*ZP zY-%%G;>QmnM<4t{P67ldFg>Q#(800gep=2)59JoKuL+G|YB?ENSMes%f;>K`TZnXysbBdq)B-<+U(-~P-UPa%cA0i;X4otyrOoqcp+ zt(GOuS?;{4hUsB4gB;cUkIX;4ciXD|==m#=qegLy97ldJIUj3sa$Ls23b8Y%A02WA zMffH+;o{x-yXH^fp1yy_S*P~UPz+hBY5B`}Kh6Fssws~oipHSvR3%!Ile0kJwGGf` z45XQEx(G?|T^*9tt-m#rMQI&zUBBUo-Z9v2NZ(G3teZ@j)cxdRFTXT)_g6eLHddHV*8cbVGV80|-AndRLWBH+H!P#0 zzF4+u;cs{5P|% zmU$rjHKS)FYFgRC5iw}cb*)eTiGv9;q7Yi9P-ovIGqbytY0|@~nZmsnLNU2fT0kgP zvDO5HVj8E&XD6SHcLEa!OCT`{C6r!w_4nN!I3F-KE#wZxX`*>OLnf;NUBl5ps92Q5 zMW;|a(}7n;f=q2tC74i7B7U%o#@u}HyGZ;F?lC(ue*^+F*<1`N8Twf00ncNMVN3=oVzj`yd ztMwAuQa{~r#k`~y@-=`+1=#m0Md%eY=j1P)?=B&^jNsx2E6Xs#?>&I>##$9DAWJq6 zlf;fuj75Pld_X~*_d+C~IqFj!Ha+*Dz!==9lCKjBRJE&hm{R!_OH<6LP$$UJAg+0Uy|d%`IEBcCj#h&F7r65^jy66PaN1nK_hS);Lf?A_yK#*RiPO zU$AYw*M=xS@cl1`64rv;z49W?#SU@xj@`tot zzy79Xji5VCCEZmGX(|MOO*%BA`=C$r_dfzaz$42)D)0GdL!~;2Pa_FYwmh2p%}u)x zQV4YYa+mfzq|4|G@mn3dnAax@A=QaQ7^}0 zqyZEW0Tnr87g8U|B+hSrN0>v^m-8pmti53C_uHyvD)-vevN#X+^sj zo^J<GQP(c_ZmcsDrfS(tZVoSFB@sAcV@w^X8Lk;&iu|TZ&UoP4FXo@1KCb19+_08P%mI`k}}jp|DH2r&t`Rw?@e0y#Qrr{ zvz*G8`N;tST648vit4q8A>2gtB)uT)AG^C;Iq8s20Z@3I9&xAbjv9^Fw{M{&8KW*Q ztpw;CKjlo`NeY*QCwv7g_Q)ZH-14GcFj#zbY-A ziDhWF*ln_6jXTnRP5E1(pShshIZ(ltIq!?_UA(wgALE97fsJej$cF<9K#3zzZs~lm zVI_T={TDE>eBOB|4*(89PSni9PWowm$3^dxx$5?<+sNL7KPP9p@T|cOS`K^9JHcS{ z3Qf`!_Isomc8%5q5LJ(w@bE-2=B&N(wBIrMRTtScHpOgjRjhmki>`i%wsgcy-24F$ z&-xB5mE&5?m*j%nWob_#)D^(-CL|$u7@yO#BKW#bBl2b=vVX@NDuUqTggW8jCK*iL z$AsQqb5^IeI3`hQQxx8mSW41hQ~Za!C4?hn`2P`a3moVHBnbCJSd1d4R(Wn4METkB zy3&)QJp)o@aB$>NuVCk!Gsrx@`)7k zoe8&zhCVyXIB?@ zczqQrlSy+w!_4JRk0_ofNVmT=hPjZGruAv`kIH|YW7z|k)rTdC1W*8ZsaFtrNHu#( zulB|W7i6-!tR`$#6h`(&b`^F705O?ac%1&g==B$;g7*4^1wlMe0$F$af$AXQ8$u9# zhZ7JnbcD(K?=>+6E;_xk29ZKqrW&Gp-m&|$0!a$Jnn^t6%gOvmErXsXAjJV$exzz7 z`=iI15lzV8)o&m!a(V2%_If+Fp>aPy!;f1dd1qtAWM0l)pPRSA(Z@Pom#g`KIRtB`oj7+sLAcn+5;COFDb&iM>*cAwgXr5S`h!VLHnTE-sCAbnP zuz}wwLWarTN=?K`O=zm9^vlffDs+chg4eftkXzNGj`_b2Jnr;0!k{HktJ#*KIQL@IYQ16r^KfJ#YObS{5+nbMnX&4rR4g0Aj9_$@#j=Gy*8RUuu%8uL#@5MK-pcWCJGQ}iq&a|U? z$M)OKJOB#n_}+i=(s7niPacbqF`;Q=v_h}R=Inte#DgZ4%IBOrq@69G0O z;1y~(hO|;6GO~;jG}6hT@#7i7crg7^GvWxLU+4`{%Q03^jfVeM{|z1Q45kTTL-48y zFm^YTpsgsK{=TE!p5VX*HQuoS4Dh~4o)YF~Mj3axfP{jQ;_LX}*nSQ3*USvPohgq( zd`+uf$Yz3)uqBp8Nyd%8X_Xt;#CPDgEI(;mkd_jrh8f8-avVljtfAoNIbAY9Gjy)b z!jN2`SwO!VW%gKAfFIk_FOTqc1f5o4d21Fm;{$p?C(3<49oai085g=z&L;eACC+Pk z8W%_d2b`uF{WZTZ3)90V*pQ!mojMhf94I8sD_80134}kmEGVj5b^{s4eCO|&LecMA zn_f_tQZ^x}@d`826JQ=>CB;|E6m5lnDv@e{t znUBE&_veRUxR%Jry!UP+*!3F~5ahC?veofFjL*fN6q`6bCl^d2mJTc^C+Vj z1ME!sCLxV+&eLsjgR|5bVeI@J;NjuHVr6gRVruO8oyEb~BK=&55C9+t$VrN;d1jnu zx_RoUKM#s}oIgs^oUhM*dyvSL&XtXmaPrVUZTfS%eOlYhG7gD^#MH>77&rYbm)Xrj zq0A{cZi8$_f?Dn%tA$c-{J?A9Dq)=e!Or<0>!JO?uai}l3iFpFMcn`QI0~lQ;t~7z zvJWCvj3*Xx5zSLOz_P3PqGK+As05%VHU{cU2IvOrzr5>HSm7N3!2d31`3K+^5ZDFC zfs2LM-PAq2>xr5`SVXjiNP>(Ar0y~xsir*Il!g1Z?vKwNMv!d88$UK8HaxDjOyEQ; zj4;?xQlPj(=P2YBV^IUZTRQyA#Rkgc1_!0x$zk z0<|9{F4j@smr;d&U8G3-C&(~_R-jy0s2J9kVSUh@4$LZ=>&#v;%|99C;0n<_5lSdt zBV6bVNsR;?360=yL+SqelZ-LjB@ZaR&5?Cr%ij)QFTGI)^Pg%cU}5-3QWPHX)?|Ip zAkSg9rGQ=k?Ld<7^A+`7h)>)Ze6{nYqAwxb|7=XQ_A=yII2dSrW?Ajlg?%RK{NFk~ zGRT+klVXo})6wtG2>1nOgRK93@gYB=p^0uY-uU>f^Oj4=UqXMh|JDhHW&}x9^uCR>n4or1=U9rBW7$ z8xyM26hRY{4%?LO9g`lOWZuX-TbExNIFb~WaOcpc$$ZHLT1AwD{zL-!B{{9G9Pk14 z4x7^RUGm4U#Kxi4^I+c;>$2QL3I22?PyEk4z)MbyQg3vx^v8=o4Nl_TFl7FTYKd#7fN&iV8Mgu1#e+QHQ6MIztPW({g z%CBzeZKzO8L3)B8or`mLnR$cAJvaw)%lzo`5(v2L+VU2^^u zJe7dJnyas06c2U_TrvNO=o3eXQxBL)L~Y7cj-GtiSz-}cY>8s^NLD^^hk1!N*bMYz zmBatF+V%~+2;>g_CK5T17G4+!AwMm1dc^e2Q0*H_Of{fJno~m^Wxg;-qGM%ju&wdx zHOZ}_tP=5lFAKuH_W)hCC%_c|wl|Ais!hrtv-LD<);sZ$LgR9yG|QfjBDr$x=r$at zY69mwb4agRtq~SY6??_CYu$RehEP>f8t}m((LSMHzzduZ$2c5Rx1lSP^^cr+3G*{k zV>Kkgtml?V5{zXYcemyh#rrIAp7vg76r7143TC=-CY)z3z^DGkq2(8AvEDkP2^Tw*W1)7;#|35 zH+5f4Z_|_^#uY~Q_e6SAVpGD0jV$+s5uJ*nJ=Uue^?b0CE%p=aTrO?C^SeFMAfXO; zT(fJR=1KwBObjWNLo%$v z5P}w4`w~+-{f4ZW7?>}ChIbii(>&MkViZS&DVNOA<4^`pp}rj-KI|{=ppPe`g9rZ$ z8dTKkLCBYx{`AW^FQ{2a=9hPanu`f<-?tsc`?sa_jRvFsCKuu&C%yEHxcFhe5I`tnc%rNTng+S)2IEUW zr^VR|CzfpCj_-QMBb8|r+2wNM#e|6wmg$`yc4@4b+rtS2Ijb|!M3_WI zKjzfkTW(%+O%`L)n5jm;K}XSQgi>d&WHkbHCFO;|uL4se=<9&27lXAx(wFzI2>JS3 zc`%C*zjIYTo_u|M1SK}hkodGq%1T!%-W1BJw`*jA~DON z%B*)|%9$C%K129D`2}9UV51Mf@?>UCT@4G(_@J|&1;2JDU*b9Nn0{*=NbS@xSZfRv zoM+1IFPGQ;!0IBqOOc%xn1+az?Fl8=kP!9Uuo(hX?@5qJjDH>5z7T$H(pdn~UC|)z zv&3K0j(8Lb@&kv730EPK?|cGx0;~dcsjX1{-Pc_4{-$QMod=jsR&n(5xW|c>BYq*Z zMZ6%g2w?qGaDJ3^fYIiacd2WG@}6~WZQ)bX1rbh&WF7cJZ9<8`Ir6~veTqr=3k)yd z?Mb*rEsyfwrhWurY)}B1*a-2Z(7BJOms|KKV|K(?Ni1++bwC949_!p3pa;OU&8T8ZMURps0zpo z7z_|}W0P}+-|CbHJDfm*jdOo4kkkjAt44mp;b6foa@M)8TN(S8f175%TeZo~4%(7fRjt zD-z94tDBNY6GmW+YEz!%Z`|o;0RX1li`mY~UN&;%=ksepCLZ;@8Sz2o!?634x3aug z%UaB#A}@729H4evZL5n}N=yBlS$=+=U=TnWXCix80;KGG? zBkDZDKT8Q!h0cJ=h-OtaoJM%meelmnKL<_@SaF!*Airt0=rmibajY|}2Mc}`e5Yh* zqlu>VMllQKWT#qKnOPmWT-vB9ZefqLVu4+s8ZKao`KJN0wE&kZ=2yQS+XzVu#V=nD zp7Of=J2!0E&>d=tpQ0)+=dwsv+pRwdrvCsAkkP?PZ4G3cuWByJEG|v;#C&h#^V|9> z7A-!%quTRZ1waBu9tDgEP#IAUQx8D5SXVmCzqlYG0clkN!@Q**DyC6>34^_vHn=xw z{?5&x@kQ=OOf`+j4513j`M49CXLJ$msF zG5hM^P;u~s+C@vNup!QP7rl_stL1`iTy_}w*lk2N?MU>-W<)m_xF8C^a^)%=o&HYh zAI}nnNQ|Zz!Wz^$BX?FN4OLUhvpr4RXTcXH--MNX-j!-jE7B`&TZ^LmbvFXFRafZW zw|5i=rcFy*kETD~Zb@;eMKI&O1jPCI ze5feSE$Xow_^A6(81ceTAGSxwv`e$4^b;nJbiPpK(h@cMZj62~8wKalp(k&VQ;Yum z{X^>;qKPbh*e7x>~4 zVXFF?PGY{++)|=vZ;_WP3DLu_7r5Ur-w!{rr`YMAPzqiojP;lf(iU;h@fggc(B*%K zZlvz{@Y_s%J%)1d#&T-^<0gz^&pUyGf)I{nq>O#ea5(lPzaOUP#N&umtM4f0T3P(7 zfKwHq7gYU~j{0w-WlyR#ZMlPxw%_*fdAgYUh9|L;3Bh3|Ld?qOkBL5#fRoxbEALJf z_Q%Rc-p<;{I$V$OexRrWwgbhy*?#rf3(WU-RaK57tTVF^Eo^cOd^F$kYIS`iun`If z3<8=Ui>DymU(D0v+QonEqA5~OjR-sHnI;9C4zT-eY772-hnkzE*oVwcYWK~sFl3N=np$;z;dR#`{J=X<4 zdzfarbSGH_>)S7jVtws@vzxLgbcTI zdp0IU#bd6P=jmSRQ`+F8DxBeoGyC@|rh6XVUny>q&dxV{-nUxOgxqOa9T7vUbbZ!> zdcM=FFUVd=sv>IEa4TxFFltXWiH%Y0dh3qb}b_3Qr^RGL(KIbO=wbZR+f&wC`)jy-B z{H7LHm1Q=vJ0+BfE-^M)^ihr_hA07YR5L`)MA?=~eHCRiYfml;KvW%xcemS(${%Wth@9F*0^1s)*J{Wpy<6XM-cZ^W>rwo=%4p4yFHZHxsy+1Z#SNj1Z`0#9C|P22h?#7L^uhLW4=rMsmg;s4_B+e8Nm8!- zTnV(+5dZ>@y`*E(1Lg=uKWGA=-VF~dvs22vB9|uL?e0y%nZr_p{!kVafzvJf>jBWvWtKfZ2=quM`?Vs0 zcjF~kbnS(_EISd^PUXPe>7lhyK>J>hmbN$oRsBjGQVu#+i9?P`IoM%PG{lkhxyJmN zGJ_oo?EEhe$XQ$*yLpA{;4+;p>;{~Fsx`2W9+$=Fm4MfaKeqV2QIeauzfy?Qv z`sw_MMgo7`;!Sc?@6L zIv`20Se2lRW)EvtKQ4Fhkzr5W;iOd`$y#U?1)?kr{e#l0K-e*Zlw5;og>71D`r|qH zcHc(N*tvKD7y5PR<>AC)FE^s%tAe4LNggSU84l|DQwW@OSSZIwXzOG%cCgP*^Q(>sP*1Me80Li?Qs!m z@*;7#Bg;)!=EXixYd_`7}_m{t^d^WBz^TR{=51ZrDx zxL;(aYYf=1MRWohH!1ghKMYoBSc*&=!A@@p&xy|$2O*kHM(guO<<+3*#^{9TTURZ$ z$zD|U1$|LP%P(kA=-;oolNbsWQ<91I)<`giFo(3$lTxvvnej%P5L(df;$e8ey0M|l zy6cvmh=k^=wHr%tTpD4R};bEl^SiV$T)%6*zO0%ME1C()iPeYfxAe2PA_4yR+q9eq!6;VG_ZyxAFvWB zo5ggoqEx9E7JD-`u7vkmjAis3WUcXyrHh{$UnXjo*{!|;pxGMP_d6k&Y!^0siOlN= zF4vOXWp>Zr(RP>9?YG7>4GUUZzuYW_sPCOx+LG(j8s|W^IsPh$1i+J10x*SBAuC|t z#lmobb7n@D1;qBR51uT3_mbKw6u>(Cz3g1W;zN&n)+2ye$@|&Lu}w<-2y7>& zJ$(tlmu0hBBXq~F5^@!s(Tvq?a+Vc%kKPxkYqs?0F#vG*3m5`%Qj>)EZW3*N7So!R zN1}-s38RNuecrmt@LwSRc5PZCIHr|9R@<)*S5zhO7ebZ;i4S16O6$V#Fn#ERwB)od zm2AQ%W;bDShQC*+sSm_5k4UMk8+kZ34-KPiilJJIJp9jzlc!uI-oC+1Y7)1jFu7Cq z(i}L=wuDIbzmRH4*Foq;5)?)Sv&apKuR=_MJR1r>L{%NdIa9xW722B(0$I#ybBb#9 z3GCnt-}8Y!G<@I&mJ*-&0gCx@u9myATJGfFON{Xy`4Tgy@}i8q5ucN?7@N)HiwGF9 zI&3uYebpkq<~e8Lnh)n*bxNyeCALEVFk@A`k%9dp-;V!6lCnDYcJBwA^K_{NJU>8? zkciH{f_8?`vnpuDOyoLUN5%)XL=v2Lj8Q=-0q`(>7_|w{|1Cjk<+ODs#eKPGD(^Px z!nKL7?Z`ydtkXJNn~u%`>eAV*f=0{wi37G|q67|Y5*|%(acQ!IehGf1(b1_FG`6uW zcXI-4_W{j0Aj$s~V|4i7JhRX76dK+Z z7>ia-`+(V2y;ECj^jhP?%Y4-5Wh(SUc-oqAK182elezHkX@gajzK{Badf4zZrd8y1yFwv04QIQ z-gj&dc;}v^f2Bc0L_&McDsyjtsh6of6OGJsKk0B_jglRC1tAiQmEfVj(a;%qi(L2;zicJbSCpuq}IJ9o{Rt>ejXp9 z9R-{8C@L`aH`ds2(|x{?mUH?A`%0u%zTmuenI!QPyi$bB%Xm%0nt6~e6T%dRTk{Y7 z0`8A1IF-2`@9ylC+~z(?vHv|PI1|WosB@~YhoAk%W~Y0?1nh-?xy5HFLzONIbV1deg)=PelAgCw_`EI>F&WMnlE7?Lt+7;a)32?d~LR z(X@Zpax|@hmH1o(x00^=@7qXtcrWPS&Rb9qJ4cEQuBS@1$27zfTCBqB^FQtB6A^)0 z-x+_nK}4(#^@-kozoh4v(VJDkHLNv+^D+P8+INm!zK!0#vhrJk7C3qiNu zs9>pA-yD1)t_Cka9^miYrw@)ruKI|9F#+%0?O}sEg%iz@lq~P&ZC7mFWO56=Tgdv* zT}YmM&6H_}5*jK7{)-Pnq#mL1kg#;?Ec*cHoGl0!^Ri=vu+bU`(%;St6Z~{ z`7wzd4!h?JQhS>cED{D107L=QWCI)21b_^RJ-79+hqbBx*{?sx$U}!sTQJKgoQkin za494@U?eTQ#KlIb?}qprVv0o!<~N+%<~1h6*2kQS7U)w1UZnjA9KvLa8JTdPO;98t z7!xcrNo^oO*H|v-GMYQ_bXnPL@&OqZDw+>Tx~?6Dy3@3_tVbol4l*M21^o?>p9jxs zE|145bZC?8DWR*iSnGwQ%VUR4xH49i3`I^1fsUl_>a;u4WWxrb6|A&L%f$3*a^e8g zRDdPEGDo%^{1O`<{Byn_=S`f-_td`7zoTu(QH2bc!fP=vZpa%!TOSfaFayFPECxWC zY(s)Tc#CSRtExtRU9hg=U>!WQKs*x4!WU!67uyfcoeo%XY&xcWPt7h+*yikaPdGWY z=8CbZ^!IC4JzcQbhF1-CSW6#Uo61i0LW)e6nMQ;-=OXoJYfA zB>%swd$DhyY(Ah-#Jc&%-^((i%PVPX>$>gol1RuiVgyVf*vYFl zg)W@z)y0VN9r`Fl87~YyMBLddT;3>&!bg$N9I}y&T&I1R%GHhS00e_mx&tcIjj9jt z4q6T7?IlnIXc>36Q6hd^%$i6cH@fe5*?P=$)qph=7UEfE5Q9-`{=WZ1B)8@1VG?Mn zI_L;603La5DMo@b=OdF|4ktLVpC2%B;8pX{A7M6HI;;9{NaQ~RMf0Rg5!`v!x$8rIDAv`I?g#XmW$A_51fM|;OE!dWhac%U)q4Q~Ffq87Knc@{cr^1z$ z?BhT`x27!5QO~4|#T^pt!~AFNt%ldoMTeg8KQkz} zViu5GL@`G>l3YpRuk%7$A-w|&=a%s>G#M*{iwj8B*B1;8fKOf{qa*$=tu+SC^~_X4 zt`pgo=uDkNILVUjs0SYaq%u@I)$0VQR{Y8Tq}sAJVweOif}|_tmTo~;DBoljfik+IF=W-WUUy9psg|DQB%=-PCC1X)TSG8AuLKrOG z9m-wL)}0aO$wBtx;TsSsMpRT-6HkzbceW;9r8#?|hq6;@ns6H8k%TrjNQuP3k+E7t z3|8DWg)$7nlh6~!P|$Eqme?85&)y$SW}q7a#^xK(R0~w_e#Nr>#BsLAv2j)bR>GRn zX^NZ~-qPw9ikyC<&5Bv3PecHbr%hq=uQ$|lUc#iuCm|;*LDHt5p*+@|gZ{W|D@SEy z#g|4fyD$X1`Ig1;1A2mm1%)IFprjsgAK$9=$gvE2GUm^uv6K6SkcIkFK{m)rszS743RJ6Vk9`04jH^15D`REYd_pgrwXI^>xa;<=+H_T1_vx&R! z+==y`LMdD&Pc1^E?d!ckI+|uUmlJ117}}&A2CNUOUW^xh*VL&?{Ihd&#!V|KzlRoG zygCf*Th0-A99GGN24M(Vdd3n^ziYJof%=2bw;Gi2C}rUaxDMFr0vK(AMEj4umj@sV zPJQld_cqqLbbND5`yaf;V2+536r4rpcAASXA$&90*O+?x07V6Ur0s5CU~j29`vRB&CF*T(~X z>6@zl*cq>WZ~6uB$J%*LV^Pv!N-Vu&(?Vo ziXXI}z1KllglIsw8MV`5?bMxq- zQ-2-)*?Y1PHHx76W4w##s-xChgJdmPf6k;`bIOHCzFrrEWxkq23TNOILSSMGvwnMQ zIV*q6NVs!ZIl~nq2?KOA*oBqKe2io^t)fo~nFj^>wBAh(Gd#LGi1CZVZc=Zu_*ER~ zxKhF8z=8T*VE$lw0uJ#MgCMC0`A9tR7Ne>^Z!Fbkn`y!S9Y~HXK`QH4e2^!lJ?T49 z0pCd%v-_T`b^XIuAD)Fj{^$0&HStyAOWu3J2mZ#-Wuk9^3s4CfJhVlU4%(M@HZ%eUcj1?&?>E|y| zRut1Of6@Z$0uBOc1K5LU3&?E0KIRds1`u7HUGqW}!nHFB-$ni(7r+9Y4aJc92aDGB z^r73ByyO8IcGhS161A!{dan7oUGuJA+jsNv<59z9952E(&=>aimf?8~2#Bf3u#uHi zN7QM|vu*mX-?W!U=srp-=nm>W{DYTli#EEtQ^uZWC&+#!jwU-VJy~4Mps7M-G0vIn zZM?c3E<`BzUu2mbKcTU&}u(i2sp5b`T5iP-TOj6gU@`| zxH4SIt98LhpYY?Xex75%>LL z+XdQHo=lP07gM;}3qkdFvw3W{S+`|Gw`B~q)1{n*OS7)f-#0A=s4qG)Dme?5HO^B@ zIE86~qudQ~@bI9t2j1tXt`b$saxWKir;%nAJ*RH$`3jxV(TtbUvWm?R3(|u_@eZ%6 zC0eHzz1F`z?m9fhMTfXx;K=CL^AXAc^O!L-WBZGbVh^4i&6C)W=7Wu5uxdOcFP8ht z+nHwVM_xGICww(d>9(7u9;*c5q_xhlMC?TS^6$6(!pdf$Vseb0dIq?_4uu!K>SPHY zu8zFdhxCqs{+zrlguVu29iEnhYfG z(-H90t4jK`pgz#aly<+OGaGDv>W>>T3eZJ!z`hEywBhb%S7^b)=no(1nLid0lNk%I zF324)9^Uce3Z`~QK+88Gx(v_RIQnc0S23Yr8rXwwf4Q!OUg!K zwmm|1DfAQ9?i}lH*J!I z>8q!Tyz-6lAVdi3(pEw4OLa+gnA%Vp%GEw;1zwZDV zo|ovbBA61+2p;6#@FB_aEB|q%sI31dAAO|Gozf8vOlM&N%Ei_c?oPcdC)d!xYFQTt43mcDHIfjjkMR(?JLFY) zHtZjD82sUj#Ew23;m?j6%o^i;Q6X%+P;nQ0(9(1Oc;*GQhq|uqzB*UkM)EnCD4jqeoPMqZ;-KV9-ReuTCb6 zO6}jG5NIL;pB7=h@Z~l{zVLkYx6b1zB<<%z0*QsN@FvHPzccLGX73iwOAVm#2~z#P z#{`1Q`mR`Y`6;f00NAg{prl?TJSEdL;Cu@BmQ8PgT!7NyhaDw!7DWQz(M2ucyk?2SFw2~gFyP{mVm#SxSj>kyL% ztWOZ~!M4IB($)|>(jYp*C8C$%dstCQ82oJIg5{G=w6r!mwEt6JenGu17nBs`#%*g4 z4_r|Sn);fZzJXdY02D3bMx5L3+<(Y}qy47Y{^d~T;LVSZvIzPjm(nVGU>Ucl1C&N) zpj}9RWZ(B2a@^mNu8iH2Hr(aCE)eZJi{;$ya9GyOqMXk{l=T1j^Vd^lF#kGxk zYM2*Bh`zd4!-eDcWa*fLO+{6u$sA0$sb36lmH+=6Yh%8F8+H1 zCkIQM@i;W>;D9PNP6TDZG%v8!(#jm%UPme$d1@$&T-x&~bqf{QHme`}=6OZghmDMp z)oavfQg1c-y2ih=Ve9!YA+aETa=trWtte2zTu#*t?p>zbKNA*pw*35gY@W;S9iw77hbOI!z~s$)pr_)^~XlJx~_~tG5=#Y=+2(EsX}L{O|a3jk@efq zpXEoRO*+_tKA1a!rYeKJ@|C_7^pifH$Fv5wBaNQ!@Fp67A;1FG>b*CNI}JZOcBC-X z3)~G1Yiq@Ea<}IBoQYtjb=Epx-59E%PX9;7>8mg1LBXD*yaC!kUvaE;g9@0JR8G$o zN2?@{^*y54Bs_dDuCyaRHII;;Kg044C-enskI_eZ5pIHZq-P*RMQ%-PgH+$$m=C!wsd+sr97~I z?_EQr;J@)k6BH;%Pq4d4{HoTNp5!hWLJ^mcF!SU(S<`C$+9VxS9#XFHw^%#p-jE%i zU21U0YMR=(nHA)AOe9QmQUcuzaTry7@2rfzF9M%l#=%aXxpL!uy>R}YQFUZJzf*4U#yx+wHfckXv64Dy z=f>_2pl3jR_5BFMk2FtXaR1i!y>!23bL9wypIg(iq7TU6F@IC7YjM*%T5ee}M~m6& zCP$zxoZ%9tDc6D?)jn0a!dT_MD zW8r4v#G=-w<6Zx#k%f7u%w6OyE-LO>~mE ze5FV%x0q66(b(U)@}ws$uikHlc6X@Q&*uGWcJD(J{QK@TH@B5QH!`(dT&{JZ5+D0` zBz>{ov3l|mOg-AcdfQ0%1SE*#On+zwyt6y$rKQH;X{qQEsL>iC4Gz%Wn0I2VrmSSr znO=Ily`-u%rXMs`VG-JdE9PAG$S8wDtjZPDZMN9bc4;$_%^jN^1?Zh~^xx6uE`;+# zJVuhkGcT$oj&j_1H9i=D=|>b#v9^BY;?(1|(m3#%GKgSQ-(7R1m#8jwQKB`!uwZ>| zagL^dBe>!Qsa8iacMKc=`jp_p6(V(F61Mq#=FT5y?K3;mLe($WcJg_;nwx9)JFy*U zJh|?&OG2_jWm%wc+m#U7U(+ujrT3{C^xQ;N$r1v|re(v*AF zr^U#yrE)dZm^96CCRqoD(^?7FNCF!(06|mjN}OWZ*@eOl0m3dH5#=DA8?C0%pnl_m z6*hWo0y=PRa=FaSnewrr;JKP>6}JxdJ~|$>Rw2*UgHVg6W479QUyd}+q9a#MsMl?c z9V8XO5mme;k$ zna&J$FO1v|c-w0?$R2)_!dhnNP}uSMPRv~j?jWqfGvH+e-DMk2&pzZsh{HpwWMm8O@ZEynAEjXF29 zzE7j?45KDxENcsiXTE!_{O~TcnlEaK^^NQ9Gd!%4gF$ZvVoiQJSh587Q<%#4^eGJa zo~~b@zOJrJ0H-73CB&rH@66>-v9$i5*znLD7=Z9K(pYG&7o!EcjT1n1sZZmU-T1+m zCIz*{EpeR{`U5@!3;49C{2@PF&U^83LdEAkAhztwOy4sp9p@sb(=(lYfHZoV1$F9~ z+1K#`>mM_E^KW^ED>{`WG*G~FUhw67e@GlW+v~2$R;~HWgm4<=-%CJp0d$p%;K-pf zFj7~*`A5ucQ_`~4*GJi7Rsi=f)X4J02<^DytZU-!YvSx&CTRtd-lr77P2 zPH5UxtS#JE8he`wcUBSedSN8l=S#Ek9^@A{CbGZ6Z>Y73ttw4VsNYA6V`|^h@qz~g zGH}n+ofvRs%!DT?Qd`LEc4s=7rz`7O(OJB&>A<~UqTemWpg%vVvkhptGzt_PE2fC= z!%Q2>Vd}aA)Qc@8OFV&t8dZ8CN|qJO4FR9fzTPkjXgB#$k7fzU94^-2fclDY@l&aZ z1wO^|7ejuC7x!-zl?f>?CWBKoOOGpXQaC2a$^V^dZ?yP` zXCt>DrVTj=R;mH2wA^<4z@xABXB*+1525E>*-<-ip*>9N6=Hq@#Q@r5cWJxu=gjE+ zAE|sZkI(qCLTJM>rs~8ZkXyhdq9zqepxcRi67#qesF(R{+BzQsosg_I@-%+UO zFu3bL5PI%7r^c1YZAB9?id9>GxCE)ql*?_EA;zfgaI~`BdCt|nq7=D_9&So`rsq)J z2l~5xMDGxo6D6So`+_w!K=lYj-W-TfS7#rxh^4_*Prg||G0FCuGbw)Ao3Lb%Z|p0^ z%H$20hB4ekXcc(vHVm@hkCVsN6*#dPs5BOT@Xq~&Od&#&2qp<*;|hl}6VuH0 zor-;v&XO9Z(ubT^-0Tc)DfDBz=jg)@vl?)NO~oiO_8i@-ezKVogmmB3XbD8QV8U0T{?*|6g>b7T5C&4NO;>= zm~GJmNt>os&o@}!Hhb|0Tms44KA?t0_HYSDzbp#B1c?1jV9$H`|VZLPu-I zJ`4dzgPrvr5r03&ui=4~-G8s{OI+F>L9TdU0z7AN?eDpUIf#&3@azOQ`Qa4#6taUg zLpMEXujX)lrt*>zkWFWWE$U&n*brBlEiAXL4V;=1AmcAy9|^bU7g$l2@fcG2is_~f{|wDx#4 zr!KLx!L(j53~{+xIJ|QnyY!g58$_}atp0V3U5*M>NQGAyk@Zz@EU~Rbi=T3`v}iK{ z$)CxwoB|4Xz8!k$_Jp-R`x}|`d}^b*-!RZ``ThBEk=X#v@RWXQ{cg0`Yfj?de1p^+ z4jAtvE=Jj0doF^R()G1#?Tvnv7w3x-+VWufuHcoi@_IOLD!&MQ6qtjIBFq2B{DdPu zG&682xB9cG{CJ_s9&#F^JJ|miXm@ud)H?OKWzuyOh4^?K)gD!((%*C5gf@$q9^Hfj zw@#hZ(k+V4_r2XGo@6cG*^`fG@TM7ty{2AY8&h2B%S%;UlEs=9h~np4NN`vk!IZil zZ)yUps(~e&-}d)(uuuq4(Z#;~t~zBnwnwxDd#STazVyLXbLL+fXxzaR@<3y$aV{^K>uK?j%*aCYh=(Ow@J(iD}N80w?RZq@5-@2#o&7 zXA_*W*wtm5zh*l31~oNdDg%}DvTKk*A2Q#f)3z60#%hKo2CLxhejhLD#CISzSh_?8n8_{bTw z0ySlH?OI&Hv+lERsNgV(jZ15P@9F72s`EVd`V(lYUo3BmBCMsaI|d`a@3`7-1!nW3 zEx9>>vpell?QJa24%_!f<2Pi1fYo6jqU9?s8cRV-?u~GOusy5lN6n978l~?Ct(&$i zY7s#_NMyMa3J%8ZMUTScMzzHAQDcT_L_a>X{}TO{br)s-#Mrbj3Z~F=M_IYHajT=d z;()7mmo87FHQnJ71Ht9D+=uAI1j7IN5$K+7OXQnQBwrC3&+NuF$OD!KPh1dFD~us( z%gY=$&J4>(`0TbOe4AZ}#su8QW)i?Oj&+u$uixc4W%{X~)L{wez%bI&VG!3E#)C2H z`^qnR{2$EpGMa4_2&uDny5ha|HKz4H({P3CS%sq&=jN&$Ykf?)s|~n=V6yGHPY}j? zFm?GdM0h)c>K^EF*Iui#H*lu4$=t+0-IH^{2w>OE4b@)s9gi<8EI5mUt8Z1n@I1rh zuJImyo8;PBjZ-?Bs)rhgM5LDp;RZmzQI7RlhFCEnA`{F!w7t5gr47z8Oa{4t6}Z?- z6-3%9)U%IzGk7>Pz(cb=*5`-G&TXGD$6NaMoBY}!NV&b)8$ z7f|3PXwOa9PsQ|O39L#2&u=_6!zOuJ+*P>m`6~D&>wAbg>e4c1KBmIi=a`~dNcZO- zVzo-GaxuiTFpsq4@41MacKiTd3j9bglpB1-eqK`gJylXF%KJF}tfbABImlCq>g}Ap z4jv=gd3+LuQKLO!1ye0u6w@A2L10%xcyBv3k>j^G(;CNdY`b-3pB(C8{}GZ@G{kp| zk=x!J4bm$A}RUxhNs0vLqnvtZ-Fde?bkF%wdR z8RV?bJVob8dO4kV>$IC-Xvs*UZF`fI*GZk_(7ATr(i5k4KxNXCdd2`S_j=`Ql!CRy zv_iJFBZZ!&1mFBSA=Bio!6wLdoP3d5D#MwG=lZWoOUnOb!}AwOaZsq=h{|WW68Uk)VsYkH zgs$<_*+v^Pbo*ly#kjl)D^LuZr@YvuT$7}N|8fz{d z&E*(=nqLK5#V59LKrJkeBXH}>n+vrS3BNi>?o%N*+@`?PaMfQXa>XD(^!(b#zi}ng zsfiw5oq}uqBgJv8M@~0!S4c1snM&%j)IeY}q4gr?1yDeDm(p+bJ0C}%?tr(j!8*td zHBqpiI`(t_c6HP`IgQA881B1UGt1r}~(8Nnf#6@LB=svxQZ`ZM}(!#C>2L0RXCN7I}c`~Z)Df-Yu6 zl2HFIu%A+%$Q(Fayfi>X>>5E+7xt1ajPfmx!>nAc-yD1trRa;{rVt!Uq|b-&L@yZh zFAJmRU!(H~kKDU^AwoW^M+m*n(g?%_Wzc?u>#Of?z|PD0vAWW7Q6zSrVg3+ z-#)>D47?o?BQv++R11labXApTD~lXo7qhmzo)`{pFReLc||W3INIe)kcQ1*}^rF~Z+c zy1kLTn;-Mex8S6Hi%x9r*{gmQnpR%8Xhk_bG#g`*xWAm?@$lWytyWg_9|G^i>fB!V zuqx+#Ad>oYmvTekyYV1Gzd^ojjV=g_xQEf@KcGJ+zgV1Ms67yGZKscHqo4V*G)p?+ zfO9dM%5lvkJiZAfqjKm!j1|h=jk(DMIXQvA9qP{!LJX+u?!u1BGiupUZ(?H5E)pa2C ztH2u*B4R@#>fGEM$H4)?>us;v`^yxtq-t8T;cR%!=HD=8Y9QTHLpONt?BBFPY`FGO zo!zhBA^Zt61C&wzL-_T(v0S;GL6<3JhxWKr0>a0DK zP2jb<86mhRJkwLB{sUN{LZhbYiQ9jr`GDyT0I{z1gNv2UcJIj|^R26Vy|+LVJSAJ) zg)X^6UQ*rgM$;W5tW43ccFvEUA~Z61vovtVw~6gnfk+CsUfHwtaWmt>9aHm^i*?zA z!kDU_Ygn3{N0;GvUFs`(qUoPe7pwdJXfXz+JM?1o2V*JiZ_#F(ZXGMUo?TfCF``ch z*+->ouo&qEz5lWU_inHfrh^AGs!~H3|40*_#j`n@bjd&fvfunL>eg|TzF-!5J5V~T zvL!UVvoTsmbkZ|q87#C*PB3PpEp4MIcdDKq5M-A+sNylZ5@ks!0-d}zf!2RvEU%ke ze+ymqlsS(fB*4XEK+cI`a10nKf@?^DiY^@J{992xe2zG{494KnEpRUtf+LpjV>p+Qeac z2$he__27&iz*3WKg2PO%lYu7E+{ylt?N~Q<#*0|jm{_~P;~~iKY5N!TCl%`(Z{v3$ z@6`iVr`eGjJG~At?81XM==>wAubUR`jL=S{#-w$!Irwh?Q8jd5b9wNOG=OidO3 zrw%6rbT(;G!?7|DpnIxBW2VR>CWa@mSZH57^~+>c zzGWrE5BBMB=%hfTlbmB|QElbwxV&e}FY{Ki^UlH|@i+zWc(Fi9^OjD~c>? z+E}Z=Q(uaSWv_Jb?xe4&QYr54Qe_XCvZA5kbs-m_^Uem~yub{iW&6 z$#&O?!!HxT{S5OIWgSBNA4GnsAHDEx$9QwYdYF#h52BXU8}dLxFKCGJSR5^du#3Fs zGnJlpSf9R{^Y!QGFL_W?{SJ2LJzc-!rb-OEHglRVrcsqjq~w(O-_QICAsRMhMkYXu(lT++ z-x?SkcDh)({df*|wzR`s_ipRdx6)%&kY4&tg>A-uw-wEx275xsUGIUC#U;$8vnIl^ z9!iHhxn&@PM*G7W!8Ia89`2cSzlx*s+l&jyiCUC9?K*hgw(zqrwwA8VOWl^k{&$-L z4FNZ^-f*d(TOPm@x#I%y`*#Vp{6Y4UBrKCVysGny>lA>mxKsS_EKS5DBs)&A#$=FI z;eoQMJwfYRKcrp!{ko?2l5*5I&o}ZrMyh!^LXSh@YPW{CXi{Fc(TkAb@AOHgbTg_d zb)p?6=LAbw=`TEt3Liw%^sXKzzoxl(ViWKQ=wGx?i!+bc&2UXopHwngM$yZBn}>n|Z)`yN;V`R`C;c-vuq-%?FM1Z9#S<>Pa@$Ra3zP8bjOcS!jh zp=MxOo10QJCk&D+b2#S9#*bJG>JED&I zn!>jyur5bo0~pkAuIwwkTw1r2b?lMZ=9SaN2tA`p6r^�YxsKvW1$=Dybc4C?N07 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 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/ungroup.png b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/ungroup.png deleted file mode 100644 index 14966d8b5cfe8474b71c1d889c74b03947df2627..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2553 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4Yzkn1=v6E*A2N2Y7q;vrJoCO|{ z#S9F5M?jcysy3fAP*AeOHKHUqKdq!Zu_%?Hyu4g5GcUV1Ik6yBFTW^#_B$IXpvE#! z7srr_Id5-n>L&=a_D>C(N)dbP{Wwl3kYNSN=ne8Qc^ zsgr^(Xt}Tzu}3W_T7H7HqHA&XvpwgF8SUdYKTKV32(+0#z-ih|X1lh>m&^CBIbBu$ ztuJVqn?;?&j-?VP<(~kn}K5iv<{vM-G;kG|E+cvu&u=~kO zu5VC`Co4P#iwPIfzA@bDyT5;~ZKC@3>B}EK{B-$^WI;gQ+`8Q4)ox3+D=I*(9 z8}t8BoCGkuY_^&Gz|w-Zza-r4&%_v(3|8!$@h4!RlO+^4zEiB$QsjjL^{k@W+?C`mZgM85h#tnL}?{4g4|GlTuuJ8Cyi+j&c{@49&50rZZj2qI+MVxVT z>x|q1i`ljh9>o0ob57!M#y6gZZXX68aM@)pGwbWsUiIW2_RZxCF)1`CGG~M83v>1G z?VHUR4!Az!C$+f8=`Erwj6r7tuol^J*Q$2+xqBPBf5-o`l)LvY_ZyFe#_{uY^KBE& zPg_(M7nNF9AKOQRY6X}W7^WWt6-5%se@nmdSs3)E^J@q*Y-s*mzyDYI&v*Umo68R< z?ku&ccATwFO1(@ln1K~HL*Z#)N=y_#y~}1ku+7l-UY Date: Sat, 15 Nov 2025 07:52:03 +0100 Subject: [PATCH 10/42] remove caching and scheduler --- .../client/plugins/microbot/shortestpath/transports.tsv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv index 530e838b97f..1e18df02492 100644 --- a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv +++ b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv @@ -5193,4 +5193,4 @@ 2763 2952 1 2763 2951 0 Climb-down;Ladder;16679 # woodcutting guild 1574 3483 1 1575 3483 0 Climb-down;Rope ladder;28858 -1575 3483 0 1574 3483 1 Climb-up;Rope ladder;28857 "" "" \ No newline at end of file +1575 3483 0 1574 3483 1 Climb-up;Rope ladder;28857 \ No newline at end of file From 530ebd80d570b4106742b4715679b1985b616972 Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 15 Nov 2025 07:54:24 +0100 Subject: [PATCH 11/42] feat(api): add new api for ground item, NPC, and player querying --- .../api/grounditem/GroundItemApiExample.java | 141 ++++++++++++++++++ .../api/grounditem/Rs2GroundItemCache.java | 65 ++++++++ .../grounditem/Rs2GroundItemQueryable.java | 97 ++++++++++++ .../microbot/api/npc/NpcApiExample.java | 71 +++++++++ .../plugins/microbot/api/npc/Rs2NpcCache.java | 41 +++++ .../microbot/api/npc/Rs2NpcQueryable.java | 97 ++++++++++++ .../microbot/api/player/PlayerApiExample.java | 116 ++++++++++++++ .../microbot/api/player/Rs2PlayerCache.java | 47 ++++++ .../api/player/Rs2PlayerQueryable.java | 108 ++++++++++++++ 9 files changed, 783 insertions(+) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/GroundItemApiExample.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemCache.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemQueryable.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/NpcApiExample.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcCache.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcQueryable.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerCache.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerQueryable.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/GroundItemApiExample.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/GroundItemApiExample.java new file mode 100644 index 00000000000..82016a0e52c --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/GroundItemApiExample.java @@ -0,0 +1,141 @@ +package net.runelite.client.plugins.microbot.api.grounditem; + +import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; + +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 GroundItemApiExample { + + public static void examples() { + // Create a new cache instance + Rs2GroundItemCache cache = new Rs2GroundItemCache(); + + // Example 1: Get the nearest ground item + Rs2GroundItemModel nearestItem = cache.query().nearest(); + + // Example 2: Get the nearest ground item within 10 tiles + Rs2GroundItemModel nearestItemWithinRange = cache.query().nearest(10); + + // Example 3: Find a ground item by name + Rs2GroundItemModel coins = cache.query().withName("Coins"); + + // Example 4: Find a ground item by multiple names + Rs2GroundItemModel loot = cache.query().withNames("Dragon bones", "Dragon scale", "Dragon dagger"); + + // Example 5: Find a ground item by ID + Rs2GroundItemModel itemById = cache.query().withId(995); // Coins + + // Example 6: Find a ground item by multiple IDs + Rs2GroundItemModel itemByIds = cache.query().withIds(995, 526, 537); // 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 + Rs2GroundItemModel lootableItem = cache.query() + .where(Rs2GroundItemModel::isLootAble) + .nearest(); + + // Example 9: Find items owned by player + List ownedItems = cache.query() + .where(Rs2GroundItemModel::isOwned) + .toList(); + + // Example 10: Find stackable items + List stackableItems = cache.query() + .where(Rs2GroundItemModel::isStackable) + .toList(); + + // Example 11: Find noted items + List notedItems = cache.query() + .where(Rs2GroundItemModel::isNoted) + .toList(); + + // Example 12: Find items worth high alching + List alchableItems = cache.query() + .where(item -> item.isProfitableToHighAlch(100)) + .toList(); + + // Example 13: Find items about to despawn + List despawningItems = cache.query() + .where(item -> item.willDespawnWithin(30)) + .toList(); + + // Example 14: Find common loot items + List commonLoot = cache.query() + .where(Rs2GroundItemModel::isCommonLoot) + .toList(); + + // Example 15: Find priority items (high value or about to despawn) + List priorityItems = cache.query() + .where(Rs2GroundItemModel::shouldPrioritize) + .toList(); + + // Example 16: Complex query - Find nearest valuable lootable item within 15 tiles + Rs2GroundItemModel target = cache.query() + .where(Rs2GroundItemModel::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(Rs2GroundItemModel::isTradeable) + .toList(); + + // Example 19: Find members items + List membersItems = cache.query() + .where(Rs2GroundItemModel::isMembers) + .toList(); + + // Example 20: Static method to get stream directly + Rs2GroundItemModel firstItem = Rs2GroundItemCache.getGroundItemsStream() + .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(); + + // Example 22: Find items by distance from specific point + List itemsNearBank = cache.query() + .where(item -> item.isWithinDistanceFromPlayer(5)) + .toList(); + + // Example 23: Find items that are clickable (visible in viewport) + List clickableItems = cache.query() + .where(Rs2GroundItemModel::isClickable) + .toList(); + + // Example 24: Find best value item to loot + Rs2GroundItemModel bestValue = cache.query() + .where(Rs2GroundItemModel::isLootAble) + .where(item -> !item.isDespawned()) + .where(item -> item.isWithinDistanceFromPlayer(10)) + .toList() + .stream() + .max((a, b) -> Integer.compare(a.getTotalGeValue(), b.getTotalGeValue())) + .orElse(null); + + // Example 25: Find items worth looting based on minimum value + List worthLooting = cache.query() + .where(item -> item.isWorthLootingGe(10000)) + .toList(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemCache.java new file mode 100644 index 00000000000..4f50aa11aee --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemCache.java @@ -0,0 +1,65 @@ +package net.runelite.client.plugins.microbot.api.grounditem; + +import net.runelite.api.events.ItemDespawned; +import net.runelite.api.events.ItemSpawned; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +public class Rs2GroundItemCache { + + private static int lastUpdateGroundItems = 0; + private static List groundItems = new ArrayList<>(); + + public Rs2GroundItemQueryable query() { + return new Rs2GroundItemQueryable(); + } + + public static void registerEventBus() { + Microbot.getEventBus().register(Rs2GroundItemCache.class); + } + + /** + * Get all ground items in the current scene + * Uses the existing ground item cache from util.cache package + * @return Stream of Rs2GroundItemModel + */ + public static Stream getGroundItemsStream() { + + if (lastUpdateGroundItems >= Microbot.getClient().getTickCount()) { + return groundItems.stream(); + } + + // Use the existing ground item cache + List result = new ArrayList<>(); + + groundItems = result; + lastUpdateGroundItems = Microbot.getClient().getTickCount(); + return result.stream(); + } + + @Subscribe + public void onItemSpawned(ItemSpawned event) + { + /*groundItems.add( + new TileItemEx( + event.getItem(), + WorldPoint.fromLocal(Static.getClient(), event.getTile().getLocalLocation()), + event.getTile().getLocalLocation() + ) + );*/ + } + + @Subscribe + public void onItemDespawned(ItemDespawned event) + { + /*groundItems.removeIf(ex -> ex.getItem().equals(event.getItem()) && + ex.getWorldPoint().equals(WorldPoint.fromLocal(Static.getClient(), event.getTile().getLocalLocation())) && + ex.getLocalPoint().equals(event.getTile().getLocalLocation()) + );*/ + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemQueryable.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemQueryable.java new file mode 100644 index 00000000000..74bb8bde72b --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemQueryable.java @@ -0,0 +1,97 @@ +package net.runelite.client.plugins.microbot.api.grounditem; + +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.api.IEntityQueryable; +import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; + +import java.util.Arrays; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +public final class Rs2GroundItemQueryable + implements IEntityQueryable { + + private Stream source; + + public Rs2GroundItemQueryable() { + this.source = Rs2GroundItemCache.getGroundItemsStream(); + } + + @Override + public Rs2GroundItemQueryable where(java.util.function.Predicate predicate) { + source = source.filter(predicate); + return this; + } + + @Override + public Rs2GroundItemModel first() { + return source.findFirst().orElse(null); + } + + @Override + public Rs2GroundItemModel nearest() { + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc == null) { + return null; + } + + return source + .min(java.util.Comparator.comparingInt( + o -> o.getLocation().distanceTo(playerLoc))) + .orElse(null); + } + + @Override + public Rs2GroundItemModel nearest(int maxDistance) { + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc == null) { + return null; + } + + return source + .filter(x -> x.getLocation().distanceTo(playerLoc) <= maxDistance) + .min(java.util.Comparator.comparingInt( + o -> o.getLocation().distanceTo(playerLoc))) + .orElse(null); + } + + @Override + public Rs2GroundItemModel withName(String name) { + return source.filter(x -> x.getName() != null && x.getName().equalsIgnoreCase(name)) + .findFirst() + .orElse(null); + } + + @Override + public Rs2GroundItemModel withNames(String... names) { + return source.filter(x -> { + if (x.getName() == null) return false; + return Arrays.stream(names) + .anyMatch(name -> x.getName().equalsIgnoreCase(name)); + }).findFirst().orElse(null); + } + + @Override + public Rs2GroundItemModel withId(int id) { + return source.filter(x -> x.getId() == id) + .findFirst() + .orElse(null); + } + + @Override + public Rs2GroundItemModel withIds(int... ids) { + return source.filter(x -> { + for (int id : ids) { + if (x.getId() == id) return true; + } + return false; + }).findFirst().orElse(null); + } + + @Override + public java.util.List toList() { + return source.collect(Collectors.toList()); + } +} 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..6b26777e720 --- /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.util.npc.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"); + + // Example 4: Find an NPC by multiple names + Rs2NpcModel enemy = cache.query().withNames("Goblin", "Guard", "Dark wizard"); + + // Example 5: Find an NPC by ID + Rs2NpcModel npcById = cache.query().withId(1234); + + // Example 6: Find an NPC by multiple IDs + Rs2NpcModel npcByIds = cache.query().withIds(1234, 5678, 9012); + + // 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..a566b135cdd --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcCache.java @@ -0,0 +1,41 @@ +package net.runelite.client.plugins.microbot.api.npc; + +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; + +import java.util.ArrayList; +import java.util.List; +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 + * + * @return Stream of Rs2NpcModel + */ + public static Stream getNpcsStream() { + + if (lastUpdateNpcs >= Microbot.getClient().getTickCount()) { + return npcs.stream(); + } + + List result = Microbot.getClientThread().invoke(() -> Microbot + .getClient() + .getTopLevelWorldView() + .npcs().stream().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..2c8cb8110b8 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcQueryable.java @@ -0,0 +1,97 @@ +package net.runelite.client.plugins.microbot.api.npc; + +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.api.IEntityQueryable; +import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; + +import java.util.Arrays; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +public final class Rs2NpcQueryable + implements IEntityQueryable { + + private Stream source; + + public Rs2NpcQueryable() { + this.source = Rs2NpcCache.getNpcsStream(); + } + + @Override + public Rs2NpcQueryable where(java.util.function.Predicate predicate) { + source = source.filter(predicate); + return this; + } + + @Override + public Rs2NpcModel first() { + return source.findFirst().orElse(null); + } + + @Override + public Rs2NpcModel nearest() { + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc == null) { + return null; + } + + return source + .min(java.util.Comparator.comparingInt( + o -> o.getWorldLocation().distanceTo(playerLoc))) + .orElse(null); + } + + @Override + public Rs2NpcModel nearest(int maxDistance) { + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc == null) { + return null; + } + + return source + .filter(x -> x.getWorldLocation().distanceTo(playerLoc) <= maxDistance) + .min(java.util.Comparator.comparingInt( + o -> o.getWorldLocation().distanceTo(playerLoc))) + .orElse(null); + } + + @Override + public Rs2NpcModel withName(String name) { + return source.filter(x -> x.getName() != null && x.getName().equalsIgnoreCase(name)) + .findFirst() + .orElse(null); + } + + @Override + public Rs2NpcModel withNames(String... names) { + return source.filter(x -> { + if (x.getName() == null) return false; + return Arrays.stream(names) + .anyMatch(name -> x.getName().equalsIgnoreCase(name)); + }).findFirst().orElse(null); + } + + @Override + public Rs2NpcModel withId(int id) { + return source.filter(x -> x.getId() == id) + .findFirst() + .orElse(null); + } + + @Override + public Rs2NpcModel withIds(int... ids) { + return source.filter(x -> { + for (int id : ids) { + if (x.getId() == id) return true; + } + return false; + }).findFirst().orElse(null); + } + + @Override + public java.util.List toList() { + return source.collect(Collectors.toList()); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java new file mode 100644 index 00000000000..51197deec6a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java @@ -0,0 +1,116 @@ +package net.runelite.client.plugins.microbot.api.player; + +import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Example usage of the Player API + * + * This demonstrates how to query players using the new API structure: + * - Rs2PlayerCache: Caches players for efficient querying + * - Rs2PlayerQueryable: Provides a fluent interface for filtering and querying players + */ +public class PlayerApiExample { + + public static void examples() { + // Create a new cache instance + Rs2PlayerCache cache = new Rs2PlayerCache(); + + // Example 1: Get the nearest player (excluding local player) + Rs2PlayerModel nearestPlayer = cache.query().nearest(); + + // Example 2: Get the nearest player within 10 tiles + Rs2PlayerModel nearestPlayerWithinRange = cache.query().nearest(10); + + // Example 3: Find a player by name + Rs2PlayerModel playerByName = cache.query().withName("PlayerName"); + + // Example 4: Find a player by multiple names + Rs2PlayerModel anyOfThesePlayers = cache.query().withNames("Player1", "Player2", "Player3"); + + // Example 5: Find a player by ID + Rs2PlayerModel playerById = cache.query().withId(12345); + + // Example 6: Find a player by multiple IDs + Rs2PlayerModel playerByIds = cache.query().withIds(12345, 67890, 11111); + + // Example 7: Include local player in query + Rs2PlayerModel localPlayer = cache.query() + .includeLocalPlayer() + .where(player -> player.getPlayer() != null) + .first(); + + // Example 8: Find all friends + List friends = cache.query() + .where(Rs2PlayerModel::isFriend) + .toList(); + + // Example 9: Find all clan members + List clanMembers = cache.query() + .where(Rs2PlayerModel::isClanMember) + .toList(); + + // Example 10: Find all friends chat members + List fcMembers = cache.query() + .where(Rs2PlayerModel::isFriendsChatMember) + .toList(); + + // Example 11: Find players in combat (with health bar visible) + List playersInCombat = cache.query() + .where(player -> player.getHealthRatio() != -1) + .toList(); + + // Example 12: Find nearest player who is not in your clan + Rs2PlayerModel nearestNonClan = cache.query() + .where(player -> !player.isClanMember()) + .where(player -> !player.isFriend()) + .nearest(); + + // Example 13: Find players with skull (PvP) + List skulledPlayers = cache.query() + .where(player -> player.getSkullIcon() != -1) + .toList(); + + // Example 14: Find players with prayer active (overhead icon) + List playersWithPrayer = cache.query() + .where(player -> player.getOverheadIcon() != null) + .toList(); + + // Example 15: Find nearest player that is animating (doing something) + Rs2PlayerModel animatingPlayer = cache.query() + .where(player -> player.getAnimation() != -1) + .nearest(); + + // Example 16: Complex query - Find nearest low health enemy player within 5 tiles + Rs2PlayerModel target = cache.query() + .where(player -> !player.isFriend()) + .where(player -> !player.isClanMember()) + .where(player -> player.getHealthRatio() > 0) + .where(player -> player.getHealthRatio() < player.getHealthScale() / 2) + .nearest(5); + + // Example 17: Find all players on the same team (Castle Wars, etc.) + int myTeam = 1; // Example team ID + List teammates = cache.query() + .where(player -> player.getTeam() == myTeam) + .toList(); + + // Example 18: Static method to get stream directly + Rs2PlayerModel firstPlayer = Rs2PlayerCache.getPlayersStream() + .filter(player -> player.getName() != null) + .findFirst() + .orElse(null); + + // Example 19: Get all players including local player + List allPlayersIncludingMe = Rs2PlayerCache.getPlayersStream(true) + .collect(Collectors.toList()); + + // Example 20: Find players by partial name match + Rs2PlayerModel playerContainingName = cache.query() + .where(player -> player.getName() != null && + player.getName().toLowerCase().contains("iron")) + .first(); + } +} 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..be02189541e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerCache.java @@ -0,0 +1,47 @@ +package net.runelite.client.plugins.microbot.api.player; + +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; + +import java.util.ArrayList; +import java.util.List; +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 (excluding local player) + * @return Stream of Rs2PlayerModel + */ + public static Stream getPlayersStream() { + return getPlayersStream(false); + } + + /** + * Get all players in the current scene + * @param includeLocalPlayer whether to include the local player in the results + * @return Stream of Rs2PlayerModel + */ + public static Stream getPlayersStream(boolean includeLocalPlayer) { + + if (lastUpdatePlayers >= Microbot.getClient().getTickCount()) { + return players.stream(); + } + + // Get all players using the existing Rs2Player utility + List result = Rs2Player.getPlayers(player -> true, includeLocalPlayer).collect(Collectors.toList()); + + players = result; + lastUpdatePlayers = Microbot.getClient().getTickCount(); + return result.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..f41339efdb8 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerQueryable.java @@ -0,0 +1,108 @@ +package net.runelite.client.plugins.microbot.api.player; + +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.api.IEntityQueryable; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; + +import java.util.Arrays; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +public final class Rs2PlayerQueryable + implements IEntityQueryable { + + private Stream source; + private boolean includeLocalPlayer = false; + + public Rs2PlayerQueryable() { + this.source = Rs2PlayerCache.getPlayersStream(); + } + + /** + * Include the local player in the query results + * @return this queryable for chaining + */ + public Rs2PlayerQueryable includeLocalPlayer() { + this.includeLocalPlayer = true; + this.source = Rs2PlayerCache.getPlayersStream(true); + return this; + } + + @Override + public Rs2PlayerQueryable where(java.util.function.Predicate predicate) { + source = source.filter(predicate); + return this; + } + + @Override + public Rs2PlayerModel first() { + return source.findFirst().orElse(null); + } + + @Override + public Rs2PlayerModel nearest() { + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc == null) { + return null; + } + + return source + .min(java.util.Comparator.comparingInt( + o -> o.getWorldLocation().distanceTo(playerLoc))) + .orElse(null); + } + + @Override + public Rs2PlayerModel nearest(int maxDistance) { + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc == null) { + return null; + } + + return source + .filter(x -> x.getWorldLocation().distanceTo(playerLoc) <= maxDistance) + .min(java.util.Comparator.comparingInt( + o -> o.getWorldLocation().distanceTo(playerLoc))) + .orElse(null); + } + + @Override + public Rs2PlayerModel withName(String name) { + return source.filter(x -> x.getName() != null && x.getName().equalsIgnoreCase(name)) + .findFirst() + .orElse(null); + } + + @Override + public Rs2PlayerModel withNames(String... names) { + return source.filter(x -> { + if (x.getName() == null) return false; + return Arrays.stream(names) + .anyMatch(name -> x.getName().equalsIgnoreCase(name)); + }).findFirst().orElse(null); + } + + @Override + public Rs2PlayerModel withId(int id) { + return source.filter(x -> x.getId() == id) + .findFirst() + .orElse(null); + } + + @Override + public Rs2PlayerModel withIds(int... ids) { + return source.filter(x -> { + for (int id : ids) { + if (x.getId() == id) return true; + } + return false; + }).findFirst().orElse(null); + } + + @Override + public java.util.List toList() { + return source.collect(Collectors.toList()); + } +} From f754f4cdd157b614adede2a9c89aea347e79205b Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 15 Nov 2025 07:55:30 +0100 Subject: [PATCH 12/42] chore(pom): update microbot version to 2.0.42 --- runelite-client/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runelite-client/pom.xml b/runelite-client/pom.xml index a89f0fe91eb..4047ff04003 100644 --- a/runelite-client/pom.xml +++ b/runelite-client/pom.xml @@ -41,7 +41,7 @@ nogit false false - 2.0.41 + 2.0.42 nogit From ac3660143a361354176350a76c339039e043b916 Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 15 Nov 2025 14:55:47 +0100 Subject: [PATCH 13/42] refactor(api): rename ground item classes to tile item for clarity --- runelite-client/pom.xml | 2 +- .../plugins/microbot/MicrobotPlugin.java | 5 - .../api/grounditem/GroundItemApiExample.java | 141 --- .../api/grounditem/Rs2GroundItemCache.java | 65 -- .../api/tileitem/Rs2TileItemCache.java | 63 ++ .../Rs2TileItemQueryable.java} | 38 +- .../api/tileitem/TileItemApiExample.java | 104 +++ .../api/tileitem/models/Rs2TileItemModel.java | 169 ++++ .../util/grounditem/Rs2GroundItemModel.java | 874 ------------------ 9 files changed, 356 insertions(+), 1105 deletions(-) delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/GroundItemApiExample.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemCache.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/Rs2TileItemCache.java rename runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/{grounditem/Rs2GroundItemQueryable.java => tileitem/Rs2TileItemQueryable.java} (61%) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/TileItemApiExample.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/models/Rs2TileItemModel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItemModel.java diff --git a/runelite-client/pom.xml b/runelite-client/pom.xml index 4047ff04003..f976393de74 100644 --- a/runelite-client/pom.xml +++ b/runelite-client/pom.xml @@ -41,7 +41,7 @@ nogit false false - 2.0.42 + 2.1.0 nogit diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java index 06341e932b5..b3703d6b5fd 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java @@ -16,12 +16,10 @@ import net.runelite.client.events.RuneScapeProfileChanged; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; -import net.runelite.client.plugins.microbot.api.grounditem.Rs2GroundItemCache; import net.runelite.client.plugins.microbot.pouch.PouchOverlay; import net.runelite.client.plugins.microbot.ui.MicrobotPluginConfigurationDescriptor; import net.runelite.client.plugins.microbot.ui.MicrobotPluginListPanel; import net.runelite.client.plugins.microbot.ui.MicrobotTopLevelConfigPanel; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; import net.runelite.client.plugins.microbot.util.inventory.Rs2Gembag; import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; @@ -49,7 +47,6 @@ import java.awt.image.BufferedImage; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; -import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -174,8 +171,6 @@ protected void startUp() throws AWTException Microbot.getPouchScript().startUp(); - Rs2GroundItemCache.registerEventBus(); - if (overlayManager != null) { overlayManager.add(microbotOverlay); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/GroundItemApiExample.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/GroundItemApiExample.java deleted file mode 100644 index 82016a0e52c..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/GroundItemApiExample.java +++ /dev/null @@ -1,141 +0,0 @@ -package net.runelite.client.plugins.microbot.api.grounditem; - -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; - -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 GroundItemApiExample { - - public static void examples() { - // Create a new cache instance - Rs2GroundItemCache cache = new Rs2GroundItemCache(); - - // Example 1: Get the nearest ground item - Rs2GroundItemModel nearestItem = cache.query().nearest(); - - // Example 2: Get the nearest ground item within 10 tiles - Rs2GroundItemModel nearestItemWithinRange = cache.query().nearest(10); - - // Example 3: Find a ground item by name - Rs2GroundItemModel coins = cache.query().withName("Coins"); - - // Example 4: Find a ground item by multiple names - Rs2GroundItemModel loot = cache.query().withNames("Dragon bones", "Dragon scale", "Dragon dagger"); - - // Example 5: Find a ground item by ID - Rs2GroundItemModel itemById = cache.query().withId(995); // Coins - - // Example 6: Find a ground item by multiple IDs - Rs2GroundItemModel itemByIds = cache.query().withIds(995, 526, 537); // 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 - Rs2GroundItemModel lootableItem = cache.query() - .where(Rs2GroundItemModel::isLootAble) - .nearest(); - - // Example 9: Find items owned by player - List ownedItems = cache.query() - .where(Rs2GroundItemModel::isOwned) - .toList(); - - // Example 10: Find stackable items - List stackableItems = cache.query() - .where(Rs2GroundItemModel::isStackable) - .toList(); - - // Example 11: Find noted items - List notedItems = cache.query() - .where(Rs2GroundItemModel::isNoted) - .toList(); - - // Example 12: Find items worth high alching - List alchableItems = cache.query() - .where(item -> item.isProfitableToHighAlch(100)) - .toList(); - - // Example 13: Find items about to despawn - List despawningItems = cache.query() - .where(item -> item.willDespawnWithin(30)) - .toList(); - - // Example 14: Find common loot items - List commonLoot = cache.query() - .where(Rs2GroundItemModel::isCommonLoot) - .toList(); - - // Example 15: Find priority items (high value or about to despawn) - List priorityItems = cache.query() - .where(Rs2GroundItemModel::shouldPrioritize) - .toList(); - - // Example 16: Complex query - Find nearest valuable lootable item within 15 tiles - Rs2GroundItemModel target = cache.query() - .where(Rs2GroundItemModel::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(Rs2GroundItemModel::isTradeable) - .toList(); - - // Example 19: Find members items - List membersItems = cache.query() - .where(Rs2GroundItemModel::isMembers) - .toList(); - - // Example 20: Static method to get stream directly - Rs2GroundItemModel firstItem = Rs2GroundItemCache.getGroundItemsStream() - .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(); - - // Example 22: Find items by distance from specific point - List itemsNearBank = cache.query() - .where(item -> item.isWithinDistanceFromPlayer(5)) - .toList(); - - // Example 23: Find items that are clickable (visible in viewport) - List clickableItems = cache.query() - .where(Rs2GroundItemModel::isClickable) - .toList(); - - // Example 24: Find best value item to loot - Rs2GroundItemModel bestValue = cache.query() - .where(Rs2GroundItemModel::isLootAble) - .where(item -> !item.isDespawned()) - .where(item -> item.isWithinDistanceFromPlayer(10)) - .toList() - .stream() - .max((a, b) -> Integer.compare(a.getTotalGeValue(), b.getTotalGeValue())) - .orElse(null); - - // Example 25: Find items worth looting based on minimum value - List worthLooting = cache.query() - .where(item -> item.isWorthLootingGe(10000)) - .toList(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemCache.java deleted file mode 100644 index 4f50aa11aee..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemCache.java +++ /dev/null @@ -1,65 +0,0 @@ -package net.runelite.client.plugins.microbot.api.grounditem; - -import net.runelite.api.events.ItemDespawned; -import net.runelite.api.events.ItemSpawned; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; - -public class Rs2GroundItemCache { - - private static int lastUpdateGroundItems = 0; - private static List groundItems = new ArrayList<>(); - - public Rs2GroundItemQueryable query() { - return new Rs2GroundItemQueryable(); - } - - public static void registerEventBus() { - Microbot.getEventBus().register(Rs2GroundItemCache.class); - } - - /** - * Get all ground items in the current scene - * Uses the existing ground item cache from util.cache package - * @return Stream of Rs2GroundItemModel - */ - public static Stream getGroundItemsStream() { - - if (lastUpdateGroundItems >= Microbot.getClient().getTickCount()) { - return groundItems.stream(); - } - - // Use the existing ground item cache - List result = new ArrayList<>(); - - groundItems = result; - lastUpdateGroundItems = Microbot.getClient().getTickCount(); - return result.stream(); - } - - @Subscribe - public void onItemSpawned(ItemSpawned event) - { - /*groundItems.add( - new TileItemEx( - event.getItem(), - WorldPoint.fromLocal(Static.getClient(), event.getTile().getLocalLocation()), - event.getTile().getLocalLocation() - ) - );*/ - } - - @Subscribe - public void onItemDespawned(ItemDespawned event) - { - /*groundItems.removeIf(ex -> ex.getItem().equals(event.getItem()) && - ex.getWorldPoint().equals(WorldPoint.fromLocal(Static.getClient(), event.getTile().getLocalLocation())) && - ex.getLocalPoint().equals(event.getTile().getLocalLocation()) - );*/ - } -} 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..3f1db345106 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/Rs2TileItemCache.java @@ -0,0 +1,63 @@ +package net.runelite.client.plugins.microbot.api.tileitem; + +import net.runelite.api.events.ItemDespawned; +import net.runelite.api.events.ItemSpawned; +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.api.tileitem.models.Rs2TileItemModel; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +@Singleton +public class Rs2TileItemCache { + + private static int lastUpdateGroundItems = 0; + private static List groundItems = new ArrayList<>(); + + @Inject + public Rs2TileItemCache(EventBus eventBus) { + eventBus.register(this); + } + + public Rs2TileItemQueryable query() { + return new Rs2TileItemQueryable(); + } + + /** + * Get all ground items in the current scene + * Uses the existing ground item cache from util.cache package + * + * @return Stream of Rs2GroundItemModel + */ + public static Stream getGroundItemsStream() { + + if (lastUpdateGroundItems >= Microbot.getClient().getTickCount()) { + return groundItems.stream(); + } + + // Use the existing ground item cache + List result = new ArrayList<>(); + + groundItems = result; + lastUpdateGroundItems = Microbot.getClient().getTickCount(); + return result.stream(); + } + + @Subscribe + public void onItemSpawned(ItemSpawned event) { + groundItems.add(new Rs2TileItemModel(event.getTile(), event.getItem())); + } + + @Subscribe + public void onItemDespawned(ItemDespawned event) { + groundItems.removeIf(groundItem -> groundItem.getId() == event.getItem().getId() && + groundItem.getWorldLocation().equals(event.getTile().getWorldLocation()) + && groundItem.getLocalLocation().equals(event.getTile().getLocalLocation()) + ); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemQueryable.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/Rs2TileItemQueryable.java similarity index 61% rename from runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemQueryable.java rename to runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/Rs2TileItemQueryable.java index 74bb8bde72b..f8461d6bfaf 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemQueryable.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/Rs2TileItemQueryable.java @@ -1,8 +1,8 @@ -package net.runelite.client.plugins.microbot.api.grounditem; +package net.runelite.client.plugins.microbot.api.tileitem; import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.api.IEntityQueryable; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; +import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import java.util.Arrays; @@ -10,28 +10,28 @@ import java.util.stream.Stream; -public final class Rs2GroundItemQueryable - implements IEntityQueryable { +public final class Rs2TileItemQueryable + implements IEntityQueryable { - private Stream source; + private Stream source; - public Rs2GroundItemQueryable() { - this.source = Rs2GroundItemCache.getGroundItemsStream(); + public Rs2TileItemQueryable() { + this.source = Rs2TileItemCache.getGroundItemsStream(); } @Override - public Rs2GroundItemQueryable where(java.util.function.Predicate predicate) { + public Rs2TileItemQueryable where(java.util.function.Predicate predicate) { source = source.filter(predicate); return this; } @Override - public Rs2GroundItemModel first() { + public Rs2TileItemModel first() { return source.findFirst().orElse(null); } @Override - public Rs2GroundItemModel nearest() { + public Rs2TileItemModel nearest() { WorldPoint playerLoc = Rs2Player.getWorldLocation(); if (playerLoc == null) { return null; @@ -39,33 +39,33 @@ public Rs2GroundItemModel nearest() { return source .min(java.util.Comparator.comparingInt( - o -> o.getLocation().distanceTo(playerLoc))) + o -> o.getWorldLocation().distanceTo(playerLoc))) .orElse(null); } @Override - public Rs2GroundItemModel nearest(int maxDistance) { + public Rs2TileItemModel nearest(int maxDistance) { WorldPoint playerLoc = Rs2Player.getWorldLocation(); if (playerLoc == null) { return null; } return source - .filter(x -> x.getLocation().distanceTo(playerLoc) <= maxDistance) + .filter(x -> x.getWorldLocation().distanceTo(playerLoc) <= maxDistance) .min(java.util.Comparator.comparingInt( - o -> o.getLocation().distanceTo(playerLoc))) + o -> o.getWorldLocation().distanceTo(playerLoc))) .orElse(null); } @Override - public Rs2GroundItemModel withName(String name) { + public Rs2TileItemModel withName(String name) { return source.filter(x -> x.getName() != null && x.getName().equalsIgnoreCase(name)) .findFirst() .orElse(null); } @Override - public Rs2GroundItemModel withNames(String... names) { + public Rs2TileItemModel withNames(String... names) { return source.filter(x -> { if (x.getName() == null) return false; return Arrays.stream(names) @@ -74,14 +74,14 @@ public Rs2GroundItemModel withNames(String... names) { } @Override - public Rs2GroundItemModel withId(int id) { + public Rs2TileItemModel withId(int id) { return source.filter(x -> x.getId() == id) .findFirst() .orElse(null); } @Override - public Rs2GroundItemModel withIds(int... ids) { + public Rs2TileItemModel withIds(int... ids) { return source.filter(x -> { for (int id : ids) { if (x.getId() == id) return true; @@ -91,7 +91,7 @@ public Rs2GroundItemModel withIds(int... ids) { } @Override - public java.util.List toList() { + public java.util.List toList() { return source.collect(Collectors.toList()); } } 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..6b579be3b97 --- /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"); + + // Example 4: Find a ground item by multiple names + Rs2TileItemModel loot = cache.query().withNames("Dragon bones", "Dragon scale", "Dragon dagger"); + + // Example 5: Find a ground item by ID + Rs2TileItemModel itemById = cache.query().withId(995); // Coins + + // Example 6: Find a ground item by multiple IDs + Rs2TileItemModel itemByIds = cache.query().withIds(995, 526, 537); // 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.getGroundItemsStream() + .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..1d9512cb6c6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/models/Rs2TileItemModel.java @@ -0,0 +1,169 @@ +package net.runelite.client.plugins.microbot.api.tileitem.models; + +import net.runelite.api.*; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; + +import java.util.function.Supplier; + +public class Rs2TileItemModel implements TileItem { + + private final Tile tile; + 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(); + } + + 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(); + }); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItemModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItemModel.java deleted file mode 100644 index 4c011502e45..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItemModel.java +++ /dev/null @@ -1,874 +0,0 @@ -package net.runelite.client.plugins.microbot.util.grounditem; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Constants; -import net.runelite.api.ItemComposition; -import net.runelite.api.Perspective; -import net.runelite.api.Point; -import net.runelite.api.Scene; -import net.runelite.api.Tile; -import net.runelite.api.TileItem; -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.util.grandexchange.Rs2GrandExchange; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import net.runelite.client.util.RSTimeUnit; - -import java.awt.Rectangle; - -import java.time.Duration; -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; - -/** - * Enhanced model for ground items with caching, tick tracking, and despawn utilities. - * Provides a comprehensive replacement for the deprecated RS2Item class. - * Includes looting utility methods and value-based filtering for automation. - * - * @author Vox - * @version 2.0 - */ -@Data -@Getter -@EqualsAndHashCode -@Slf4j -public class Rs2GroundItemModel { - - private final TileItem tileItem; - private final Tile tile; - private ItemComposition itemComposition; - private final WorldPoint location; - private final int id; - private final int quantity; - private String name; - private final boolean isOwned; - private final boolean isLootAble; - private final long creationTime; - private final int creationTick; - - // Despawn tracking fields - following GroundItemsOverlay pattern - private final Instant spawnTime; - private final Duration despawnDuration; - private final Duration visibleDuration; - - /** - * Creates a new Rs2GroundItemModel from a TileItem and Tile. - * - * @param tileItem The TileItem from the game - * @param tile The tile the item is on - */ - public Rs2GroundItemModel(TileItem tileItem, Tile tile) { - this.tileItem = tileItem; - this.tile = tile; - this.location = tile.getWorldLocation(); - this.id = tileItem.getId(); - this.quantity = tileItem.getQuantity(); - this.isOwned = tileItem.getOwnership() == TileItem.OWNERSHIP_SELF; - this.isLootAble = !(tileItem.getOwnership() == TileItem.OWNERSHIP_OTHER); - this.creationTime = System.currentTimeMillis(); - this.creationTick = Microbot.getClient().getTickCount(); - - - // Initialize despawn tracking following GroundItemsPlugin.buildGroundItem() pattern - this.spawnTime = Instant.now(); - - // Calculate despawn time exactly like GroundItemsPlugin.buildGroundItem() - // final int despawnTime = item.getDespawnTime() - client.getTickCount(); - // .despawnTime(Duration.of(despawnTime, RSTimeUnit.GAME_TICKS)) - int despawnTime = 0; - int visibleTime = 0; - if (tileItem.getDespawnTime() > this.creationTick){ - despawnTime = tileItem.getDespawnTime() - this.creationTick; - - } - if (tileItem.getVisibleTime() > this.creationTick) { - visibleTime = tileItem.getVisibleTime() - this.creationTick; - } - // Use the exact same pattern as official RuneLite GroundItemsPlugin - this.despawnDuration = Duration.of(despawnTime, RSTimeUnit.GAME_TICKS); - this.visibleDuration = Duration.of(visibleTime, RSTimeUnit.GAME_TICKS); - - // Initialize composition and name as null for lazy loading - this.itemComposition = null; - this.name = null; - log.debug("Created Rs2GroundItemModel: {} x{} at {} | Spawn: {} | Despawn: {} (Local) | tick despawn: {} | current tick: {}", - getName(), quantity, location, - spawnTime.atZone(ZoneOffset.systemDefault()).format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), - getDespawnTime().atZone(ZoneOffset.systemDefault()).format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), - tileItem.getDespawnTime(), - this.creationTick - ); - } - - /** - * Lazy loads the ItemComposition if not already loaded. - * This ensures we can work with ground items while minimizing performance impact. - */ - private void ensureCompositionLoaded() { - if (itemComposition == null && id > 0) { - try { - this.itemComposition = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getClient().getItemDefinition(id) - ).orElse(null); - - if (itemComposition != null) { - this.name = itemComposition.getName(); - } else { - log.warn("Failed to load ItemComposition for ground item id: {}, setting default name", id); - this.name = "Unknown Item"; - } - } catch (Exception e) { - log.warn("Error loading ItemComposition for ground item id: {}, using defaults: {}", id, e.getMessage()); - this.name = "Unknown Item"; - this.itemComposition = null; - } - } - } - - /** - * Gets the item name, loading composition if needed. - * - * @return The item name or "Unknown Item" if composition fails to load - */ - public String getName() { - if (name == null) { - ensureCompositionLoaded(); - } - return name != null ? name : "Unknown Item"; - } - - /** - * Gets the item composition, loading it if needed. - * - * @return The ItemComposition or null if it fails to load - */ - public ItemComposition getItemComposition() { - if (itemComposition == null) { - ensureCompositionLoaded(); - } - return itemComposition; - } - - // ============================================ - // Time and Tick Tracking Methods - // ============================================ - - /** - * Gets the number of ticks since this item was created. - * - * @return The number of ticks since creation - */ - public int getTicksSinceCreation() { - return Microbot.getClient().getTickCount() - creationTick; - } - - /** - * Gets the number of ticks since this item spawned (alias for getTicksSinceCreation). - * - * @return The number of ticks since spawn - */ - public int getTicksSinceSpawn() { - return getTicksSinceCreation(); - } - - /** - * Gets the time in milliseconds since this item was created. - * - * @return Milliseconds since creation - */ - public long getTimeSinceCreation() { - return System.currentTimeMillis() - creationTime; - } - - // ============================================ - // Despawn Tracking Methods - // ============================================ - - /** - * Gets the spawn time as an Instant (following GroundItemsOverlay pattern). - * - * @return Instant when the item spawned - */ - public Instant getSpawnTime() { - return spawnTime; - } - - /** - * Gets the despawn duration for this item. - * - * @return Duration until despawn from spawn time - */ - public Duration getDespawnDuration() { - return despawnDuration; - } - - /** - * Gets the absolute time when this item will despawn (following GroundItemsOverlay pattern). - * - * @return Instant when the item despawns - */ - public Instant getDespawnTime() { - return spawnTime.plus(despawnDuration); - } - - /** - * Gets the UTC timestamp when this item will despawn. - * - * @return UTC timestamp in milliseconds when item despawns - */ - public long getDespawnTimestampUtc() { - return getDespawnTime().toEpochMilli(); - } - - /** - * Gets the duration remaining until this item despawns. - * - * @return Duration until despawn, or Duration.ZERO if already despawned - */ - public Duration getTimeUntilDespawn() { - Instant despawnTime = getDespawnTime(); - Instant now = Instant.now(); - - if (now.isAfter(despawnTime)) { - return Duration.ZERO; - } - - return Duration.between(now, despawnTime); - } - - /** - * Gets the number of ticks remaining until this item despawns. - * - * @return Ticks until despawn, or 0 if already despawned - */ - public int getTicksUntilDespawn() { - // Convert despawnDuration back to ticks using RSTimeUnit pattern - long despawnTicks = despawnDuration.toMillis() / Constants.GAME_TICK_LENGTH; - - int currentTick = Microbot.getClientThread().runOnClientThreadOptional( - () -> Microbot.getClient().getTickCount() - ).orElse((int)(creationTick + despawnTicks + 1)); // Fallback assumes despawned - - int ticksSinceSpawn = currentTick - creationTick; - long ticksRemaining = despawnTicks - ticksSinceSpawn; - return Math.max(0, (int)ticksRemaining); - } - - // ============================================ - // UTC Timestamp Getter Methods - // ============================================ - - /** - * Gets the UTC spawn time as ZonedDateTime. - * - * @return Spawn time in UTC - */ - public ZonedDateTime getSpawnTimeUtc() { - return spawnTime.atZone(ZoneOffset.UTC); - } - - /** - * Gets the UTC spawn timestamp in milliseconds. - * - * @return Spawn timestamp in UTC milliseconds - */ - public long getSpawnTimestampUtc() { - return spawnTime.toEpochMilli(); - } - - /** - * Gets the total number of ticks this item should exist before despawning. - * - * @return Total despawn ticks - */ - public int getDespawnTicks() { - return (int)(despawnDuration.toMillis() / Constants.GAME_TICK_LENGTH); - } - - /** - * Gets the number of seconds remaining until this item despawns. - * - * @return Seconds until despawn, or 0 if already despawned - */ - public long getSecondsUntilDespawn() { - return getTimeUntilDespawn().getSeconds(); - } - - /** - * Checks if this item has despawned based on game ticks. - * This is more accurate than real-time checking since OSRS is tick-based. - * - * @return true if the item should have despawned - */ - public boolean isDespawned() { - if (isPersistened()) { - return false; // Persisted items never despawn - } - - return Instant.now().isAfter(getDespawnTime()); - // Use tick-based calculation for more accurate game timing - //long despawnTicks = despawnDuration.toMillis() / Constants.GAME_TICK_LENGTH; - //int currentTick = Microbot.getClient().getTickCount(); - //int ticksSinceSpawn = currentTick - creationTick; - //return ticksSinceSpawn >= despawnTicks; - } - public boolean isPersistened(){ - // Check if the despawn duration is negative or very large - return despawnDuration.isNegative() ||despawnDuration.isZero() || despawnDuration.toMillis() > 24 * 60 * 60 * 1000; // More than 24 hours - } - - /** - * Checks if this item has despawned based on UTC timestamp (fallback method). - * Less accurate than tick-based method but useful when client is unavailable. - * - * @return true if the item should have despawned based on time - */ - public boolean isDespawnedByTime() { - return ZonedDateTime.now(ZoneOffset.UTC).isAfter(getDespawnTime().atZone(ZoneOffset.UTC)); - } /** - * Checks if this item will despawn within the specified number of seconds. - * - * @param seconds The time threshold in seconds - * @return true if the item will despawn within the given time - */ - public boolean willDespawnWithin(long seconds) { - return getSecondsUntilDespawn() <= seconds; - } - - /** - * Checks if this item will despawn within the specified number of ticks. - * - * @param ticks The time threshold in ticks - * @return true if the item will despawn within the given ticks - */ - public boolean willDespawnWithinTicks(int ticks) { - return getTicksUntilDespawn() <= ticks; - } - - // ============================================ - // Item Property Methods - // ============================================ - - /** - * Checks if this item is stackable. - * - * @return true if stackable, false otherwise - */ - public boolean isStackable() { - try { - ItemComposition composition = getItemComposition(); - return composition != null && composition.isStackable(); - } catch (Exception e) { - log.warn("Error checking if item is stackable for id: {}: {}", id, e.getMessage()); - return false; - } - } - - /** - * Checks if this item is noted. - * - * @return true if noted, false otherwise - */ - public boolean isNoted() { - try { - ItemComposition composition = getItemComposition(); - return composition != null && composition.getNote() != -1; - } catch (Exception e) { - log.warn("Error checking if item is noted for id: {}: {}", id, e.getMessage()); - return false; - } - } - - /** - * Gets the item's store value. - * - * @return The item's store value - */ - public int getValue() { - try { - ItemComposition composition = getItemComposition(); - return composition != null ? composition.getPrice() : 0; - } catch (Exception e) { - log.warn("Error getting value for item id: {}: {}", id, e.getMessage()); - return 0; - } - } - - /** - * Gets the item's Grand Exchange price. - * - * @return The item's GE price - */ - public int getPrice() { - return Rs2GrandExchange.getPrice(this.id); - } - - /** - * Gets the total value of this item stack (quantity * unit value). - * - * @return The total stack value - */ - public int getTotalValue() { - return getValue() * quantity; - } - - /** - * Gets the total Grand Exchange value of this item stack. - * - * @return The total stack GE value - */ - public int getTotalGeValue() { - return getPrice() * quantity; - } - - /** - * Gets the item's high alchemy value. - * - * @return The high alchemy value - */ - public int getHaPrice() { - try { - ItemComposition composition = getItemComposition(); - return composition != null ? composition.getHaPrice() : 0; - } catch (Exception e) { - log.warn("Error getting high alchemy price for item id: {}: {}", id, e.getMessage()); - return 0; - } - } - - /** - * Gets the total high alchemy value of this item stack. - * - * @return The total stack high alchemy value - */ - public int getTotalHaValue() { - return getHaPrice() * quantity; - } - - /** - * Gets the item's low alchemy value. - * This is calculated as 40% of the store price. - * - * @return The low alchemy value - */ - public int getLaValue() { - try { - ItemComposition composition = getItemComposition(); - return composition != null ? (int)(composition.getPrice() * 0.4) : 0; - } catch (Exception e) { - log.warn("Error getting low alchemy value for item id: {}: {}", id, e.getMessage()); - return 0; - } - } - - /** - * Gets the total low alchemy value of this item stack. - * - * @return The total stack low alchemy value - */ - public int getTotalLaValue() { - return getLaValue() * quantity; - } - - /** - * Checks if this item is members-only. - * - * @return true if members-only, false otherwise - */ - public boolean isMembers() { - try { - ItemComposition composition = getItemComposition(); - return composition != null && composition.isMembers(); - } catch (Exception e) { - log.warn("Error checking if item is members-only for id: {}: {}", id, e.getMessage()); - return false; - } - } - - /** - * Checks if this item is tradeable. - * - * @return true if tradeable, false otherwise - */ - public boolean isTradeable() { - try { - ItemComposition composition = getItemComposition(); - return composition != null && composition.isTradeable(); - } catch (Exception e) { - log.warn("Error checking if item is tradeable for id: {}: {}", id, e.getMessage()); - return false; - } - } - - /** - * Gets the item's inventory actions. - * - * @return Array of inventory actions - */ - public String[] getInventoryActions() { - try { - ItemComposition composition = getItemComposition(); - return composition != null ? composition.getInventoryActions() : new String[0]; - } catch (Exception e) { - log.warn("Error getting inventory actions for item id: {}: {}", id, e.getMessage()); - return new String[0]; - } - } - - // ============================================ - // Looting Utility Methods - // ============================================ - - /** - * Checks if this item is worth looting based on minimum value. - * - * @param minValue The minimum value threshold - * @return true if the item's total value meets the threshold - */ - public boolean isWorthLooting(int minValue) { - return getTotalValue() >= minValue; - } - - /** - * Checks if this item is worth looting based on Grand Exchange value. - * - * @param minGeValue The minimum GE value threshold - * @return true if the item's total GE value meets the threshold - */ - public boolean isWorthLootingGe(int minGeValue) { - return getTotalGeValue() >= minGeValue; - } - - /** - * Checks if this item is worth high alching based on profit margin. - * - * @param minProfit The minimum profit threshold - * @return true if high alching would be profitable - */ - public boolean isProfitableToHighAlch(int minProfit) { - // High alch value minus nature rune cost (estimated) - int profit = getTotalHaValue() - (quantity * 200); // Assuming 200gp nature rune cost - return profit >= minProfit; - } - - /** - * Checks if this item is a commonly desired loot type. - * Includes coins, gems, ores, logs, herbs, and high-value items. - * - * @return true if the item is commonly looted - */ - public boolean isCommonLoot() { - if (name == null) return false; - - String lowerName = name.toLowerCase(); - - // Always loot coins - if (lowerName.contains("coins")) return true; - - // High value items - if (getTotalValue() >= 1000) return true; - - // Common valuable items - return lowerName.contains("gem") || - lowerName.contains("ore") || - lowerName.contains("bar") || - lowerName.contains("log") || - lowerName.contains("herb") || - lowerName.contains("seed") || - lowerName.contains("rune") || - lowerName.contains("arrow") || - lowerName.contains("bolt"); - } - - /** - * Checks if this item should be prioritized for urgent looting. - * Based on high value and short despawn time. - * - * @return true if the item should be prioritized - */ - public boolean shouldPrioritize() { - // High value items or items about to despawn - return getTotalValue() >= 5000 || willDespawnWithin(30); - } - - // ============================================ - // Distance and Position Methods - // ============================================ - - /** - * Gets the distance to this item from the player. - * - * @return The distance in tiles - */ - public int getDistanceFromPlayer() { - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - WorldPoint playerLocation = - Rs2Player.getWorldLocation(); - return playerLocation.distanceTo(location); - }).orElse(Integer.MAX_VALUE); - } - - /** - * Checks if this item is within a certain distance from the player. - * - * @param maxDistance The maximum distance in tiles - * @return true if within distance, false otherwise - */ - public boolean isWithinDistanceFromPlayer(int maxDistance) { - return getDistanceFromPlayer() <= maxDistance; - } - - /** - * Gets the distance to this item from a specific point. - * - * @param point The world point to measure from - * @return The distance in tiles - */ - public int getDistanceFrom(WorldPoint point) { - return location.distanceTo(point); - } - - /** - * Checks if this item is within a certain distance from a specific point. - * - * @param point The world point to measure from - * @param maxDistance The maximum distance in tiles - * @return true if within distance, false otherwise - */ - public boolean isWithinDistanceFrom(WorldPoint point, int maxDistance) { - return getDistanceFrom(point) <= maxDistance; - } - - // ============================================ - // Scene and Viewport Detection Methods - // ============================================ - - /** - * Checks if this ground item is still present in the current scene. - * This verifies that the TileItem still exists on its tile in the scene. - * - * @return true if the item is still in the current scene, false otherwise - */ - public boolean isInCurrentScene() { - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - try { - Scene scene = Microbot.getClient().getTopLevelWorldView().getScene(); - - // Check if the world point is within current scene bounds using WorldPoint.isInScene - if (!WorldPoint.isInScene(scene, location.getX(), location.getY())) { - return false; - } - - // Convert world point to local coordinates for scene tile access - LocalPoint localPoint = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), location); - if (localPoint == null) { - return false; - } - - // Get the tile from the scene using local coordinates - Tile[][][] sceneTiles = scene.getTiles(); - int plane = location.getPlane(); - int sceneX = localPoint.getSceneX(); - int sceneY = localPoint.getSceneY(); - - // Validate scene coordinates - if (plane < 0 || plane >= sceneTiles.length || - sceneX < 0 || sceneX >= Constants.SCENE_SIZE || - sceneY < 0 || sceneY >= Constants.SCENE_SIZE) { - return false; - } - - Tile sceneTile = sceneTiles[plane][sceneX][sceneY]; - if (sceneTile == null) { - return false; - } - - // Check if our TileItem is still on this tile - if (sceneTile.getGroundItems() != null) { - for (TileItem item : sceneTile.getGroundItems()) { - if (item.getId() == this.id && - item.getQuantity() == this.quantity && - item.equals(this.tileItem)) { - return true; - } - } - } - - return false; - } catch (Exception e) { - log.warn("Error checking if ground item is in current scene: {}", e.getMessage()); - return false; - } - }).orElse(false); - } - - /** - * Checks if this ground item is visible and clickable in the current viewport. - * This combines scene presence checking with viewport visibility detection. - * - * @return true if the item is visible and clickable in the viewport, false otherwise - */ - public boolean isVisibleInViewport() { - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - try { - // First check if item is still in scene - if (!isInCurrentScene()) { - return false; - } - - // Convert world location to canvas point using Perspective.localToCanvas - LocalPoint localPoint = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), location); - if (localPoint == null) { - return false; - } - - Point canvasPoint = Perspective.localToCanvas(Microbot.getClient(), localPoint, location.getPlane()); - if (canvasPoint == null) { - return false; - } - - // Check if the point is within the viewport bounds - // Following the pattern from Rs2ObjectCacheUtils.isPointInViewport - int viewportX = Microbot.getClient().getViewportXOffset(); - int viewportY = Microbot.getClient().getViewportYOffset(); - int viewportWidth = Microbot.getClient().getViewportWidth(); - int viewportHeight = Microbot.getClient().getViewportHeight(); - - return canvasPoint.getX() >= viewportX && - canvasPoint.getX() <= viewportX + viewportWidth && - canvasPoint.getY() >= viewportY && - canvasPoint.getY() <= viewportY + viewportHeight; - - } catch (Exception e) { - log.warn("Error checking if ground item is visible in viewport: {}", e.getMessage()); - return false; - } - }).orElse(false); - } - - /** - * Gets the canvas point for this ground item if it's visible. - * Useful for click operations and overlay rendering. - * - * @return the Point on the canvas, or null if not visible - */ - public Point getCanvasPoint() { - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - try { - if (!isInCurrentScene()) { - return null; - } - - LocalPoint localPoint = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), location); - if (localPoint == null) { - return null; - } - - return Perspective.localToCanvas(Microbot.getClient(), localPoint, location.getPlane()); - } catch (Exception e) { - log.warn("Error getting canvas point for ground item: {}", e.getMessage()); - return null; - } - }).orElse(null); - } - - /** - * Checks if this ground item is clickable (in scene and in viewport). - * Convenience method that combines isInCurrentScene() and isVisibleInViewport(). - * - * @return true if the item can be clicked, false otherwise - */ - public boolean isClickable() { - return isInCurrentScene() && isVisibleInViewport(); - } - - /** - * Checks if this ground item is currently clickable by the player within a specific distance. - * This combines scene presence, viewport visibility, and distance checks. - * - * @param maxDistance The maximum interaction distance in tiles - * @return true if the item is clickable within the specified distance, false otherwise - */ - public boolean isClickable(int maxDistance) { - // Check if item is lootable first - if (!isLootAble) { - return false; - } - - // Check if item has despawned - if (isDespawned()) { - return false; - } - - // Check distance from player - if (!isWithinDistanceFromPlayer(maxDistance)) { - return false; - } - - // Check if visible in viewport (includes scene presence check) - return isVisibleInViewport(); - } - - /** - * Gets the viewport bounds as a Rectangle for utility calculations. - * Following the pattern from Rs2ObjectCacheUtils.getViewportBounds. - * - * @return Rectangle representing the current viewport bounds, or null if unavailable - */ - public Rectangle getViewportBounds() { - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - try { - int viewportX = Microbot.getClient().getViewportXOffset(); - int viewportY = Microbot.getClient().getViewportYOffset(); - int viewportWidth = Microbot.getClient().getViewportWidth(); - int viewportHeight = Microbot.getClient().getViewportHeight(); - - return new Rectangle(viewportX, viewportY, viewportWidth, viewportHeight); - } catch (Exception e) { - log.warn("Error getting viewport bounds: {}", e.getMessage()); - return null; - } - }).orElse(null); - } - - // ============================================ - // Utility Methods - // ============================================ - - /** - * Gets a string representation of this item with UTC timing information. - * - * @return String representation - */ - @Override - public String toString() { - return String.format("Rs2GroundItemModel{id=%d, name='%s', quantity=%d, location=%s, owned=%s, lootable=%s, value=%d, despawnTicksLeft=%d, spawnTimeUtc='%s', despawnTimeUtc='%s'}", - id, name, quantity, location, isOwned, isLootAble, getTotalValue(), getTicksUntilDespawn(), - getSpawnTimeUtc().toString(), getDespawnTime().atZone(ZoneOffset.UTC).toString()); - } - - /** - * Gets a detailed string representation including all properties. - * - * @return Detailed string representation - */ - public String toDetailedString() { - return String.format( - "Rs2GroundItemModel{" + - "id=%d, name='%s', quantity=%d, location=%s, " + - "owned=%s, lootable=%s, stackable=%s, noted=%s, tradeable=%s, " + - "value=%d, geValue=%d, haValue=%d, totalValue=%d, " + - "ticksSinceSpawn=%d, ticksUntilDespawn=%d, secondsUntilDespawn=%d" + - "}", - id, name, quantity, location, - isOwned, isLootAble, isStackable(), isNoted(), isTradeable(), - getValue(), getPrice(), getHaPrice(), getTotalValue(), - getTicksSinceSpawn(), getTicksUntilDespawn(), getSecondsUntilDespawn() - ); - } -} From 1ab243487899b2d56c8cd71ca215f471df6a9341 Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 15 Nov 2025 15:59:29 +0100 Subject: [PATCH 14/42] refactor(api): simplify ground items retrieval and add line of sight check --- .../api/tileitem/Rs2TileItemCache.java | 16 +--- .../api/tileitem/models/Rs2TileItemModel.java | 90 +++++++++++++++++++ 2 files changed, 93 insertions(+), 13 deletions(-) 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 index 3f1db345106..697fd7a0878 100644 --- 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 @@ -6,6 +6,7 @@ import net.runelite.client.eventbus.Subscribe; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; import javax.inject.Inject; import javax.inject.Singleton; @@ -16,8 +17,7 @@ @Singleton public class Rs2TileItemCache { - private static int lastUpdateGroundItems = 0; - private static List groundItems = new ArrayList<>(); + private static final List groundItems = new ArrayList<>(); @Inject public Rs2TileItemCache(EventBus eventBus) { @@ -35,17 +35,7 @@ public Rs2TileItemQueryable query() { * @return Stream of Rs2GroundItemModel */ public static Stream getGroundItemsStream() { - - if (lastUpdateGroundItems >= Microbot.getClient().getTickCount()) { - return groundItems.stream(); - } - - // Use the existing ground item cache - List result = new ArrayList<>(); - - groundItems = result; - lastUpdateGroundItems = Microbot.getClient().getTickCount(); - return result.stream(); + return groundItems.stream().filter(x -> x.getWorldLocation().getPlane() == Rs2Player.getWorldLocation().getPlane()); } @Subscribe 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 index 1d9512cb6c6..5802e5d84b4 100644 --- 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 @@ -1,15 +1,24 @@ 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.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 { + @Getter private final Tile tile; + @Getter private final TileItem tileItem; public Rs2TileItemModel(Tile tileObject, TileItem tileItem) { @@ -166,4 +175,85 @@ public int getTotalValue() { return price * tileItem.getQuantity(); }); } + + + public boolean hasLineOfSight() { + WorldPoint worldPoint = Rs2Player.getWorldLocation(); + if (worldPoint == null) { + return false; + } + return Microbot.getClientThread().invoke((Supplier) () -> + tile.getWorldLocation() + .toWorldArea() + .hasLineOfSightTo(Microbot.getClient().getTopLevelWorldView(), worldPoint.toWorldArea())); + } + + 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(action, param0, param1, menuAction.getId(), identifier, -1, target), + canvas.getBounds()); + } + } else { + Microbot.doInvoke(new NewMenuEntry(action, param0, param1, menuAction.getId(), identifier, -1, target), + new Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); + + } + } catch (Exception ex) { + Microbot.log(ex.getMessage()); + ex.printStackTrace(); + } + return true; + } } From b0d4aae669c18a527899e0386747fc20c3f29d53 Mon Sep 17 00:00:00 2001 From: chsami Date: Sun, 16 Nov 2025 12:30:45 +0100 Subject: [PATCH 15/42] refactor(api): introduce IEntity interface and update related models --- .../client/plugins/microbot/Microbot.java | 8 +- .../microbot/api/AbstractEntityQueryable.java | 170 ++++ .../client/plugins/microbot/api/IEntity.java | 13 + .../microbot/api/IEntityQueryable.java | 5 + .../microbot/api/npc/Rs2NpcQueryable.java | 86 +- .../microbot/api/player/PlayerApiExample.java | 10 +- .../microbot/api/player/Rs2PlayerCache.java | 18 +- .../api/player/Rs2PlayerQueryable.java | 98 +-- .../api/playerstate/Rs2PlayerStateCache.java | 177 ++++ .../api/tileitem/Rs2TileItemCache.java | 70 +- .../api/tileitem/Rs2TileItemQueryable.java | 88 +- .../api/tileitem/models/Rs2TileItemModel.java | 3 +- .../tileobject/Rs2TileObjectQueryable.java | 85 +- .../tileobject/models/Rs2TileObjectModel.java | 7 +- .../microbot/example/ExampleScript.java | 77 +- .../plugins/microbot/util/ActorModel.java | 2 +- .../microbot/util/npc/Rs2NpcModel.java | 17 +- .../microbot/util/player/Rs2Player.java | 2 +- .../microbot/util/player/Rs2PlayerCache.java | 176 ---- .../microbot/util/player/Rs2PlayerModel.java | 17 +- .../microbot/util/security/LoginManager.java | 766 ++++++++++-------- 21 files changed, 923 insertions(+), 972 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/IEntity.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/playerstate/Rs2PlayerStateCache.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerCache.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java index 25132fbb46a..cf978ff1e9d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java @@ -42,7 +42,7 @@ import net.runelite.client.plugins.microbot.util.mouse.Mouse; import net.runelite.client.plugins.microbot.util.mouse.VirtualMouse; import net.runelite.client.plugins.microbot.util.mouse.naturalmouse.NaturalMouse; -import net.runelite.client.plugins.microbot.util.player.Rs2PlayerCache; +import net.runelite.client.plugins.microbot.api.playerstate.Rs2PlayerStateCache; import net.runelite.client.plugins.microbot.util.security.LoginManager; import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; import net.runelite.client.ui.overlay.infobox.InfoBoxManager; @@ -190,7 +190,7 @@ public class Microbot { @Inject @Getter - private static Rs2PlayerCache rs2PlayerCache; + private static Rs2PlayerStateCache rs2PlayerStateCache; /** * Get the total runtime of the script @@ -215,11 +215,11 @@ public static boolean isDebug() { } public static int getVarbitValue(@Varbit int varbit) { - return rs2PlayerCache.getVarbitValue(varbit); + return rs2PlayerStateCache.getVarbitValue(varbit); } public static int getVarbitPlayerValue(@Varp int varpId) { - return rs2PlayerCache.getVarpValue(varpId); + return rs2PlayerStateCache.getVarpValue(varpId); } public static EnumComposition getEnum(int id) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java new file mode 100644 index 00000000000..d3550c283ee --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java @@ -0,0 +1,170 @@ +package net.runelite.client.plugins.microbot.api; + +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Generic abstract implementation of {@link IEntityQueryable} to reduce duplication. + * + * @param concrete queryable type (self type) + * @param entity model type + */ +public abstract class AbstractEntityQueryable< + Q extends IEntityQueryable, + E extends IEntity + > + implements IEntityQueryable { + + protected Stream source; + + protected AbstractEntityQueryable() { + this.source = initialSource(); + } + + /** + * Provide the initial stream to query against. + */ + protected abstract Stream initialSource(); + + + /** + * Player location used for proximity queries. + */ + protected WorldPoint getPlayerLocation() { + return Rs2Player.getWorldLocation(); + } + + @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 = getPlayerLocation(); + if (playerLoc == null) { + return null; + } + + 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) { + return null; + } + + this.source = this.source + .filter(o -> o.getWorldLocation().distanceTo(anchor) <= distance); + + return (Q) this; + } + + @Override + public E first() { + return source.findFirst().orElse(null); + } + + @Override + public E nearest() { + WorldPoint playerLoc = getPlayerLocation(); + if (playerLoc == null) { + return null; + } + return source + .min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(playerLoc))) + .orElse(null); + } + + @Override + public E nearest(int maxDistance) { + WorldPoint playerLoc = getPlayerLocation(); + if (playerLoc == null) { + return null; + } + return source + .filter(x -> { + WorldPoint loc = x.getWorldLocation(); + return loc != null && loc.distanceTo(playerLoc) <= maxDistance; + }) + .min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(playerLoc))) + .orElse(null); + } + + @Override + public E nearest(WorldPoint anchor, int maxDistance) { + if (anchor == null) { + return null; + } + return source + .filter(x -> { + WorldPoint loc = x.getWorldLocation(); + return loc != null && loc.distanceTo(anchor) <= maxDistance; + }) + .min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(anchor))) + .orElse(null); + } + + @Override + public E withName(String name) { + if (name == null) return null; + return Microbot.getClientThread().invoke(() -> { + return source.filter(x -> { + String n = x.getName(); + return n != null && n.equalsIgnoreCase(name); + }) + .min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(Rs2Player.getWorldLocation()))) + .orElse(null); + }); + } + + @Override + public E withNames(String... names) { + if (names == null || names.length == 0) return null; + return source.filter(x -> { + String n = x.getName(); + if (n == null) return false; + return Arrays.stream(names).anyMatch(n::equalsIgnoreCase); + }).min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(Rs2Player.getWorldLocation()))) + .orElse(null); + } + + @Override + public E withId(int id) { + return source.filter(x -> x.getId() == id).min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(Rs2Player.getWorldLocation()))) + .orElse(null); + } + + @Override + public E withIds(int... ids) { + if (ids == null || ids.length == 0) return null; + return source.filter(x -> { + int entityId = x.getId(); + for (int id : ids) { + if (entityId == id) return true; + } + return false; + }).min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(Rs2Player.getWorldLocation()))) + .orElse(null); + } + + @Override + public List toList() { + return source.collect(Collectors.toList()); + } +} 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..46a0cfe25af --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/IEntity.java @@ -0,0 +1,13 @@ +package net.runelite.client.plugins.microbot.api; + +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; + +public interface IEntity { + int getId(); + String getName(); + WorldPoint getWorldLocation(); + LocalPoint getLocalLocation(); + boolean click(); + boolean click(String action); +} 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 index 94dc3117940..c07ac959d58 100644 --- 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 @@ -1,10 +1,15 @@ package net.runelite.client.plugins.microbot.api; +import net.runelite.api.coords.WorldPoint; + public interface IEntityQueryable, E> { Q where(java.util.function.Predicate predicate); + Q within(int distance); + Q within(WorldPoint anchor, int distance); E first(); E nearest(); E nearest(int maxDistance); + E nearest(WorldPoint anchor, int maxDistance); E withName(String name); E withNames(String...names); E withId(int id); 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 index 2c8cb8110b8..2f54573104c 100644 --- 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 @@ -1,97 +1,21 @@ package net.runelite.client.plugins.microbot.api.npc; import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.api.AbstractEntityQueryable; import net.runelite.client.plugins.microbot.api.IEntityQueryable; import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import java.util.Arrays; -import java.util.stream.Collectors; import java.util.stream.Stream; - -public final class Rs2NpcQueryable +public final class Rs2NpcQueryable extends AbstractEntityQueryable implements IEntityQueryable { - private Stream source; - public Rs2NpcQueryable() { - this.source = Rs2NpcCache.getNpcsStream(); - } - - @Override - public Rs2NpcQueryable where(java.util.function.Predicate predicate) { - source = source.filter(predicate); - return this; - } - - @Override - public Rs2NpcModel first() { - return source.findFirst().orElse(null); - } - - @Override - public Rs2NpcModel nearest() { - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - if (playerLoc == null) { - return null; - } - - return source - .min(java.util.Comparator.comparingInt( - o -> o.getWorldLocation().distanceTo(playerLoc))) - .orElse(null); - } - - @Override - public Rs2NpcModel nearest(int maxDistance) { - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - if (playerLoc == null) { - return null; - } - - return source - .filter(x -> x.getWorldLocation().distanceTo(playerLoc) <= maxDistance) - .min(java.util.Comparator.comparingInt( - o -> o.getWorldLocation().distanceTo(playerLoc))) - .orElse(null); - } - - @Override - public Rs2NpcModel withName(String name) { - return source.filter(x -> x.getName() != null && x.getName().equalsIgnoreCase(name)) - .findFirst() - .orElse(null); - } - - @Override - public Rs2NpcModel withNames(String... names) { - return source.filter(x -> { - if (x.getName() == null) return false; - return Arrays.stream(names) - .anyMatch(name -> x.getName().equalsIgnoreCase(name)); - }).findFirst().orElse(null); - } - - @Override - public Rs2NpcModel withId(int id) { - return source.filter(x -> x.getId() == id) - .findFirst() - .orElse(null); - } - - @Override - public Rs2NpcModel withIds(int... ids) { - return source.filter(x -> { - for (int id : ids) { - if (x.getId() == id) return true; - } - return false; - }).findFirst().orElse(null); + super(); } @Override - public java.util.List toList() { - return source.collect(Collectors.toList()); + protected Stream initialSource() { + return Rs2NpcCache.getNpcsStream(); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java index 51197deec6a..a56ea75dca0 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java @@ -36,12 +36,6 @@ public static void examples() { // Example 6: Find a player by multiple IDs Rs2PlayerModel playerByIds = cache.query().withIds(12345, 67890, 11111); - // Example 7: Include local player in query - Rs2PlayerModel localPlayer = cache.query() - .includeLocalPlayer() - .where(player -> player.getPlayer() != null) - .first(); - // Example 8: Find all friends List friends = cache.query() .where(Rs2PlayerModel::isFriend) @@ -103,8 +97,8 @@ public static void examples() { .findFirst() .orElse(null); - // Example 19: Get all players including local player - List allPlayersIncludingMe = Rs2PlayerCache.getPlayersStream(true) + // Example 19: Get all players + List allPlayersIncludingMe = Rs2PlayerCache.getPlayersStream() .collect(Collectors.toList()); // Example 20: Find players by partial name match 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 index be02189541e..0bbcffeb37b 100644 --- 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 @@ -6,6 +6,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -18,27 +19,22 @@ public Rs2PlayerQueryable query() { return new Rs2PlayerQueryable(); } - /** - * Get all players in the current scene (excluding local player) - * @return Stream of Rs2PlayerModel - */ - public static Stream getPlayersStream() { - return getPlayersStream(false); - } - /** * Get all players in the current scene - * @param includeLocalPlayer whether to include the local player in the results * @return Stream of Rs2PlayerModel */ - public static Stream getPlayersStream(boolean includeLocalPlayer) { + public static Stream getPlayersStream() { if (lastUpdatePlayers >= Microbot.getClient().getTickCount()) { return players.stream(); } // Get all players using the existing Rs2Player utility - List result = Rs2Player.getPlayers(player -> true, includeLocalPlayer).collect(Collectors.toList()); + List result = Microbot.getClient().getTopLevelWorldView().players() + .stream() + .filter(Objects::nonNull) + .map(Rs2PlayerModel::new) + .collect(Collectors.toList()); players = result; lastUpdatePlayers = Microbot.getClient().getTickCount(); 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 index f41339efdb8..f4ab6ef1186 100644 --- 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 @@ -1,108 +1,20 @@ package net.runelite.client.plugins.microbot.api.player; -import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.api.AbstractEntityQueryable; import net.runelite.client.plugins.microbot.api.IEntityQueryable; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; -import java.util.Arrays; -import java.util.stream.Collectors; import java.util.stream.Stream; - -public final class Rs2PlayerQueryable +public final class Rs2PlayerQueryable extends AbstractEntityQueryable implements IEntityQueryable { - private Stream source; - private boolean includeLocalPlayer = false; - public Rs2PlayerQueryable() { - this.source = Rs2PlayerCache.getPlayersStream(); - } - - /** - * Include the local player in the query results - * @return this queryable for chaining - */ - public Rs2PlayerQueryable includeLocalPlayer() { - this.includeLocalPlayer = true; - this.source = Rs2PlayerCache.getPlayersStream(true); - return this; - } - - @Override - public Rs2PlayerQueryable where(java.util.function.Predicate predicate) { - source = source.filter(predicate); - return this; - } - - @Override - public Rs2PlayerModel first() { - return source.findFirst().orElse(null); - } - - @Override - public Rs2PlayerModel nearest() { - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - if (playerLoc == null) { - return null; - } - - return source - .min(java.util.Comparator.comparingInt( - o -> o.getWorldLocation().distanceTo(playerLoc))) - .orElse(null); - } - - @Override - public Rs2PlayerModel nearest(int maxDistance) { - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - if (playerLoc == null) { - return null; - } - - return source - .filter(x -> x.getWorldLocation().distanceTo(playerLoc) <= maxDistance) - .min(java.util.Comparator.comparingInt( - o -> o.getWorldLocation().distanceTo(playerLoc))) - .orElse(null); - } - - @Override - public Rs2PlayerModel withName(String name) { - return source.filter(x -> x.getName() != null && x.getName().equalsIgnoreCase(name)) - .findFirst() - .orElse(null); - } - - @Override - public Rs2PlayerModel withNames(String... names) { - return source.filter(x -> { - if (x.getName() == null) return false; - return Arrays.stream(names) - .anyMatch(name -> x.getName().equalsIgnoreCase(name)); - }).findFirst().orElse(null); - } - - @Override - public Rs2PlayerModel withId(int id) { - return source.filter(x -> x.getId() == id) - .findFirst() - .orElse(null); - } - - @Override - public Rs2PlayerModel withIds(int... ids) { - return source.filter(x -> { - for (int id : ids) { - if (x.getId() == id) return true; - } - return false; - }).findFirst().orElse(null); + super(); } @Override - public java.util.List toList() { - return source.collect(Collectors.toList()); + protected Stream initialSource() { + return Rs2PlayerCache.getPlayersStream(); } } 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 index 697fd7a0878..54de5e425b6 100644 --- 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 @@ -1,53 +1,71 @@ package net.runelite.client.plugins.microbot.api.tileitem; -import net.runelite.api.events.ItemDespawned; -import net.runelite.api.events.ItemSpawned; -import net.runelite.client.eventbus.EventBus; -import net.runelite.client.eventbus.Subscribe; +import net.runelite.api.Player; +import net.runelite.api.Tile; +import net.runelite.api.TileItem; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import javax.inject.Inject; import javax.inject.Singleton; import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; +/** + * Cache for ground items (tile items) in the current scene. + * Uses polling-based approach to ensure reliability, as ItemSpawned/ItemDespawned events + * are not always triggered consistently. + */ @Singleton public class Rs2TileItemCache { - private static final List groundItems = new ArrayList<>(); - - @Inject - public Rs2TileItemCache(EventBus eventBus) { - eventBus.register(this); - } + private static int lastUpdateTick = 0; + private static List groundItems = new ArrayList<>(); public Rs2TileItemQueryable query() { return new Rs2TileItemQueryable(); } /** - * Get all ground items in the current scene - * Uses the existing ground item cache from util.cache package + * Get all ground items in the current scene. + * 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 Rs2GroundItemModel + * @return Stream of Rs2TileItemModel */ public static Stream getGroundItemsStream() { - return groundItems.stream().filter(x -> x.getWorldLocation().getPlane() == Rs2Player.getWorldLocation().getPlane()); - } + // Only refresh once per tick to avoid unnecessary scanning + if (lastUpdateTick >= Microbot.getClient().getTickCount()) { + return groundItems.stream(); + } - @Subscribe - public void onItemSpawned(ItemSpawned event) { - groundItems.add(new Rs2TileItemModel(event.getTile(), event.getItem())); - } + Player player = Microbot.getClient().getLocalPlayer(); + if (player == null) return Stream.empty(); + + List result = new ArrayList<>(); + + // Get all tiles in current plane + var tileValues = Microbot.getClient().getTopLevelWorldView().getScene().getTiles()[Microbot.getClient().getTopLevelWorldView().getPlane()]; + + for (Tile[] tileRow : tileValues) { + for (Tile tile : tileRow) { + if (tile == null) continue; + + List items = tile.getGroundItems(); + if (items == null || items.isEmpty()) continue; + + // Add all items from this tile to the result + for (TileItem item : items) { + if (item != null) { + result.add(new Rs2TileItemModel(tile, item)); + } + } + } + } - @Subscribe - public void onItemDespawned(ItemDespawned event) { - groundItems.removeIf(groundItem -> groundItem.getId() == event.getItem().getId() && - groundItem.getWorldLocation().equals(event.getTile().getWorldLocation()) - && groundItem.getLocalLocation().equals(event.getTile().getLocalLocation()) - ); + groundItems = 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 index f8461d6bfaf..1fce214b829 100644 --- 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 @@ -1,97 +1,21 @@ package net.runelite.client.plugins.microbot.api.tileitem; import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.api.IEntityQueryable; +import net.runelite.client.plugins.microbot.api.AbstractEntityQueryable; +import net.runelite.client.plugins.microbot.api.IEntityQueryable; // optional import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import java.util.Arrays; -import java.util.stream.Collectors; import java.util.stream.Stream; - -public final class Rs2TileItemQueryable +public final class Rs2TileItemQueryable extends AbstractEntityQueryable implements IEntityQueryable { - private Stream source; - public Rs2TileItemQueryable() { - this.source = Rs2TileItemCache.getGroundItemsStream(); - } - - @Override - public Rs2TileItemQueryable where(java.util.function.Predicate predicate) { - source = source.filter(predicate); - return this; - } - - @Override - public Rs2TileItemModel first() { - return source.findFirst().orElse(null); - } - - @Override - public Rs2TileItemModel nearest() { - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - if (playerLoc == null) { - return null; - } - - return source - .min(java.util.Comparator.comparingInt( - o -> o.getWorldLocation().distanceTo(playerLoc))) - .orElse(null); - } - - @Override - public Rs2TileItemModel nearest(int maxDistance) { - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - if (playerLoc == null) { - return null; - } - - return source - .filter(x -> x.getWorldLocation().distanceTo(playerLoc) <= maxDistance) - .min(java.util.Comparator.comparingInt( - o -> o.getWorldLocation().distanceTo(playerLoc))) - .orElse(null); - } - - @Override - public Rs2TileItemModel withName(String name) { - return source.filter(x -> x.getName() != null && x.getName().equalsIgnoreCase(name)) - .findFirst() - .orElse(null); - } - - @Override - public Rs2TileItemModel withNames(String... names) { - return source.filter(x -> { - if (x.getName() == null) return false; - return Arrays.stream(names) - .anyMatch(name -> x.getName().equalsIgnoreCase(name)); - }).findFirst().orElse(null); - } - - @Override - public Rs2TileItemModel withId(int id) { - return source.filter(x -> x.getId() == id) - .findFirst() - .orElse(null); - } - - @Override - public Rs2TileItemModel withIds(int... ids) { - return source.filter(x -> { - for (int id : ids) { - if (x.getId() == id) return true; - } - return false; - }).findFirst().orElse(null); + super(); } @Override - public java.util.List toList() { - return source.collect(Collectors.toList()); + protected Stream initialSource() { + return Rs2TileItemCache.getGroundItemsStream(); } } 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 index 5802e5d84b4..83d850454d5 100644 --- 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 @@ -6,6 +6,7 @@ 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; @@ -14,7 +15,7 @@ import java.util.function.Supplier; @Slf4j -public class Rs2TileItemModel implements TileItem { +public class Rs2TileItemModel implements TileItem, IEntity { @Getter private final Tile tile; 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 index 875460f3996..93bfaf65f4a 100644 --- 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 @@ -2,97 +2,22 @@ import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.Microbot; +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 net.runelite.client.plugins.microbot.util.player.Rs2Player; import java.util.stream.Collectors; import java.util.stream.Stream; - -public final class Rs2TileObjectQueryable +public final class Rs2TileObjectQueryable extends AbstractEntityQueryable implements IEntityQueryable { - private Stream source; - public Rs2TileObjectQueryable() { - this.source = Rs2TileObjectCache.getObjectsStream(); - } - - @Override - public Rs2TileObjectQueryable where(java.util.function.Predicate predicate) { - source = source.filter(predicate); - return this; - } - - @Override - public Rs2TileObjectModel first() { - return source.findFirst().orElse(null); - } - - @Override - public Rs2TileObjectModel nearest() { - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - if (playerLoc == null) { - return null; - } - - return source - .min(java.util.Comparator.comparingInt( - o -> o.getWorldLocation().distanceTo(playerLoc))) - .orElse(null); - } - - @Override - public Rs2TileObjectModel nearest(int maxDistance) { - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - if (playerLoc == null) { - return null; - } - - return source - .filter(x -> x.getWorldLocation().distanceTo(playerLoc) <= maxDistance) - .min(java.util.Comparator.comparingInt( - o -> o.getWorldLocation().distanceTo(playerLoc))) - .orElse(null); - } - - @Override - public Rs2TileObjectModel withName(String name) { - return source.filter(x -> x.getName().equalsIgnoreCase(name)).findFirst().orElse(null); - } - - @Override - public Rs2TileObjectModel withNames(String... names) { - return Microbot.getClientThread().invoke(() -> source.filter(x -> { - for (String name : names) { - if (x.getName().equalsIgnoreCase(name)) { - return true; - } - } - return false; - }).findFirst().orElse(null)); - } - - @Override - public Rs2TileObjectModel withId(int id) { - return source.filter(x -> x.getId() == id).findFirst().orElse(null); - } - - @Override - public Rs2TileObjectModel withIds(int... ids) { - return source.filter(x -> { - for (int id : ids) { - if (x.getId() == id) { - return true; - } - } - return false; - }).findFirst().orElse(null); + super(); } @Override - public java.util.List toList() { - return source.collect(Collectors.toList()); + 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 index 218ba4ffe5c..5b500d72a5d 100644 --- 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 @@ -6,6 +6,7 @@ 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.camera.Rs2Camera; import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; import net.runelite.client.plugins.microbot.util.menu.NewMenuEntry; @@ -19,7 +20,7 @@ import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; -public class Rs2TileObjectModel implements TileObject { +public class Rs2TileObjectModel implements TileObject, IEntity { public Rs2TileObjectModel(GameObject gameObject) { this.tileObject = gameObject; @@ -150,6 +151,10 @@ public ObjectComposition getObjectComposition() { }); } + public boolean click() { + return click(""); + } + /** * Clicks on the specified tile object with no specific action. * Delegates to Rs2GameObject.clickObject. 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 ca0e5d2ff74..266b92a3b41 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 @@ -3,28 +3,37 @@ 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.util.tileobject.Rs2TileObjectApi; -import net.runelite.client.plugins.microbot.util.tileobject.Rs2TileObjectModel; +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.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.api.player.Rs2PlayerCache; -import java.util.List; +import javax.inject.Inject; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; -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. */ @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. */ @@ -33,46 +42,28 @@ public boolean run() { try { if (!Microbot.isLoggedIn()) return; - // Performance test: Loop over game objects and get compositions long startTime = System.currentTimeMillis(); - AtomicLong endTime = new AtomicLong(); + var groundItems = rs2TileItemCache.query().toList(); + var objects = rs2TileObjectCache.query().toList(); + var rs2Players = rs2PlayerCache.query().toList(); + var rs2Npcs = rs2NpcCache.query().toList(); - var tileObjects = Microbot.getClientThread().invoke(() -> { - // List _tileObjects = Rs2TileObjectApi.getObjectsStream().filter(x -> x.getName() != null && !x.getName().isEmpty() && x.getName() != "null").collect(Collectors.toList()); - Rs2TileObjectModel test = Rs2TileObjectApi.getNearest(tile -> tile.getName() != null && tile.getName().toLowerCase().contains("tree")); - endTime.set(System.currentTimeMillis()); - System.out.println("Retrieved " + test.getName() + " game objects in " + (endTime.get() - startTime) + " ms"); + //groundItems.get(0).click("Take"); + long endTime = System.currentTimeMillis(); + long totalTime = endTime - startTime; + System.out.println("fetched " + rs2Players.size() + " players and " + rs2Npcs.size() + " npcs."); + System.out.println("fetched " + objects.size() + " objects."); + System.out.println("fetched " + groundItems.size() + " ground items."); + System.out.println("Player location: " + Rs2Player.getWorldLocation()); + System.out.println("fetched " + groundItems.size() + " ground items."); + System.out.println("all in time: " + totalTime + " ms"); + /*var tree = rs2TileObjectCache.query().within(Rs2Player.getWorldLocation(), 20).withName("Tree"); - /*for (Rs2TileObjectModel rs2TileObjectModel: _tileObjects) { - var name = rs2TileObjectModel.getName(); // Access name to simulate some processing - System.out.println("Object Name: " + name); - } -*/ - return Rs2TileObjectApi.getObjectsStream().collect(Collectors.toList()); - }); + tree.click(); - - int compositionCount = 0; - - /*for (Rs2TileObjectModel tileObject : tileObjects) { - var name = tileObject.getName(); // Access name to simulate some processing - if (name != null) { - compositionCount++; - System.out.println("composition " + compositionCount + ": " + name); - } - }*/ - - endTime.set(System.currentTimeMillis()); - long durationMs = (endTime.get() - startTime); - -/* - log.info("Performance Test Results:"); - log.info(" Total GameObjects: {}", tileObjects.size()); - log.info(" Compositions retrieved: {}", compositionCount); - log.info(" Time taken: {} ms", durationMs); - log.info(" Average time per object: {} Ξs", - tileObjects.size() > 0 ? (endTime.get() - startTime) / 1000 / tileObjects.size() : 0); -*/ + System.out.println(tree.getId()); + System.out.println(tree.getName()); + */ } catch (Exception ex) { log.error("Error in performance test loop", ex); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/ActorModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/ActorModel.java index ddfc8d29cdc..e5942c52182 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/ActorModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/ActorModel.java @@ -37,7 +37,7 @@ public int getCombatLevel() return Microbot.getClientThread().runOnClientThreadOptional(actor::getCombatLevel).orElse(0); } - @Override + @Override public @Nullable String getName() { return Microbot.getClientThread().runOnClientThreadOptional(actor::getName).orElse(null); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcModel.java index 0f3245c9767..188df4e96e0 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcModel.java @@ -8,7 +8,9 @@ import net.runelite.api.NpcOverrides; 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.ActorModel; +import org.apache.commons.lang3.NotImplementedException; import org.jetbrains.annotations.Nullable; import java.util.Arrays; @@ -16,7 +18,7 @@ @Getter @EqualsAndHashCode(callSuper = true) // Ensure equality checks include ActorModel fields -public class Rs2NpcModel extends ActorModel implements NPC +public class Rs2NpcModel extends ActorModel implements NPC, IEntity { private final NPC runeliteNpc; @@ -33,7 +35,8 @@ public int getId() return runeliteNpc.getId(); } - @Override + + @Override public int getIndex() { return runeliteNpc.getIndex(); @@ -190,4 +193,14 @@ public HeadIcon getHeadIcon() { return null; } + + @Override + public boolean click() { + throw new NotImplementedException("click() not implemented yet for Rs2NpcModel"); + } + + @Override + public boolean click(String action) { + throw new NotImplementedException("click() not implemented yet for Rs2NpcModel"); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java index b4238bc3f30..2eca00bac5e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java @@ -1422,7 +1422,7 @@ public static int getPoseAnimation() { * @return The {@link QuestState} representing the player's progress in the quest. */ public static QuestState getQuestState(Quest quest) { - return Microbot.getRs2PlayerCache().getQuestState(quest); + return Microbot.getRs2PlayerStateCache().getQuestState(quest); } /** diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerCache.java deleted file mode 100644 index 828f247d0e1..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerCache.java +++ /dev/null @@ -1,176 +0,0 @@ -package net.runelite.client.plugins.microbot.util.player; - -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-related data such as quest states, varbits, and varps. - */ -@Singleton -@Slf4j -public final class Rs2PlayerCache { - @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 Rs2PlayerCache(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; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerModel.java index 849a0f49c6d..762d5c56c81 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerModel.java @@ -5,11 +5,12 @@ import net.runelite.api.HeadIcon; import net.runelite.api.Player; import net.runelite.api.PlayerComposition; +import net.runelite.client.plugins.microbot.api.IEntity; import net.runelite.client.plugins.microbot.util.ActorModel; +import org.apache.commons.lang3.NotImplementedException; @Getter -public class Rs2PlayerModel extends ActorModel implements Player -{ +public class Rs2PlayerModel extends ActorModel implements Player, IEntity { private final Player player; @@ -25,7 +26,7 @@ public int getId() return player.getId(); } - @Override + @Override public PlayerComposition getPlayerComposition() { return player.getPlayerComposition(); @@ -84,4 +85,14 @@ public int getFootprintSize() { return 0; } + + @Override + public boolean click() { + throw new NotImplementedException("click() not implemented yet for Rs2PlayerModel"); + } + + @Override + public boolean click(String action) { + throw new NotImplementedException("click() not implemented yet for Rs2PlayerModel"); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/security/LoginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/security/LoginManager.java index 865e1247d1c..34a7ad772f1 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/security/LoginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/security/LoginManager.java @@ -35,373 +35,421 @@ @Slf4j public final class LoginManager { - private static final int MAX_PLAYER_COUNT = 1950; - private static final Object LOGIN_LOCK = new Object(); - private static final AtomicBoolean LOGIN_ATTEMPT_ACTIVE = new AtomicBoolean(false); - private static final AtomicReference LAST_LOGIN_ATTEMPT = new AtomicReference<>(null); + private static final int MAX_PLAYER_COUNT = 1950; + private static final Object LOGIN_LOCK = new Object(); + private static final AtomicBoolean LOGIN_ATTEMPT_ACTIVE = new AtomicBoolean(false); + private static final AtomicReference LAST_LOGIN_ATTEMPT = new AtomicReference<>(null); - @Getter - private static Instant lastLoginTimestamp = null; + @Getter + private static Instant lastLoginTimestamp = null; @Setter - public static ConfigProfile activeProfile = null; + public static ConfigProfile activeProfile = null; - public static ConfigProfile getActiveProfile() { + public static ConfigProfile getActiveProfile() { return Microbot.getConfigManager().getProfile(); - } + } - private LoginManager() { - // Utility class - } + private LoginManager() { + // Utility class + } /** - * Returns the current RuneLite client GameState or UNKNOWN if client not available. - */ - public static GameState getGameState() { - Client client = Microbot.getClient(); - return client != null ? client.getGameState() : GameState.UNKNOWN; - } - - /** - * Returns true if the client is currently considered logged in. - */ - public static boolean isLoggedIn() { - Client client = Microbot.getClient(); - return client != null && client.getGameState() == GameState.LOGGED_IN; - } - - /** - * Returns true if a login attempt is currently being processed. - */ - public static boolean isLoginAttemptActive() { - return LOGIN_ATTEMPT_ACTIVE.get(); - } - - /** - * Marks the client as logged in by updating timestamps. This should be called when GameState transitions. - */ - public static void markLoggedIn() { - // Only set timestamp if client reports logged in. - if (isLoggedIn()) { - LOGIN_ATTEMPT_ACTIVE.set(false); - lastLoginTimestamp = Instant.now(); - } - } - - /** - * Marks the client as logged out. Should be triggered whenever the game enters the login screen. - */ - public static void markLoggedOut() { - LOGIN_ATTEMPT_ACTIVE.set(false); - } - - /** - * Returns the duration the account has been logged in for. Equivalent to Microbot.getLoginTime(). - */ - public static Duration getLoginDuration() { - if (lastLoginTimestamp == null || !isLoggedIn()) { - return Duration.of(0, ChronoUnit.MILLIS); - } - return Duration.between(lastLoginTimestamp, Instant.now()); - } - - /** - * Attempts a login using the active profile and an intelligent world selection. - */ - public static boolean login() { - if (getActiveProfile() == null) { - log.warn("No active profile available for login"); - return false; - } - System.out.println(getActiveProfile()); - Client client = Microbot.getClient(); - if (client == null) { - log.warn("Cannot login - client is not initialised"); - return false; - } - int targetWorld = getRandomWorld(getActiveProfile().isMember()); - return login(getActiveProfile().getName(), getActiveProfile().getPassword(), targetWorld); - } - - /** - * Attempts a login using the active profile into a specific world. - */ - public static boolean login(int worldId) { - if (getActiveProfile() == null) { - log.warn("No active profile available for world specific login"); - return false; - } - return login(getActiveProfile().getName(), getActiveProfile().getPassword(), worldId); - } - - /** - * Attempts a login with explicit credentials and world target. - */ - public static boolean login(String username, String encryptedPassword, int worldId) { - if (username == null || username.isBlank()) { - log.warn("Cannot login without username"); - return false; - } - if (isLoggedIn()) { - return true; - } - if (LOGIN_ATTEMPT_ACTIVE.get()) { - log.debug("Login attempt already active - skipping duplicate request"); - return false; - } - final Client client = Microbot.getClient(); - if (client == null) { - log.warn("Cannot login - client is not initialised"); - return false; - } - synchronized (LOGIN_LOCK) { - Instant lastAttempt = LAST_LOGIN_ATTEMPT.get(); - Instant now = Instant.now(); - if (lastAttempt != null && Duration.between(lastAttempt, now).toMillis() < 1500) { - log.debug("Login throttled - last attempt {}ms ago", Duration.between(lastAttempt, now).toMillis()); - return false; - } - LAST_LOGIN_ATTEMPT.set(now); - LOGIN_ATTEMPT_ACTIVE.set(true); - } - - try { - handleDisconnectDialogs(client); - triggerLoginScreen(); - trySetWorld(worldId); - setCredentials(client, username, encryptedPassword); - submitLogin(); - handleBlockingDialogs(client); - return true; - } catch (Exception ex) { - log.error("Error during login attempt", ex); - return false; - } finally { - // Keep attempt active until either logged in state observed or logout recorded externally - LOGIN_ATTEMPT_ACTIVE.set(false); - } - } - - private static void handleDisconnectDialogs(Client client) { - if (client == null) { - return; - } - int loginIndex = client.getLoginIndex(); - if (loginIndex == 3 || loginIndex == 24) { - int loginScreenWidth = 804; - int startingWidth = (client.getCanvasWidth() / 2) - (loginScreenWidth / 2); - Microbot.getMouse().click(365 + startingWidth, 308); - sleep(600); - } - } - - private static void triggerLoginScreen() { - Rs2Keyboard.keyPress(KeyEvent.VK_ENTER); - sleep(600); - } - - private static void trySetWorld(int worldId) { - if (worldId <= 0) { - return; - } - try { - setWorld(worldId); - } catch (Exception e) { - log.warn("Changing world failed for {}", worldId, e); - } - } - - private static void setCredentials(Client client, String username, String encryptedPassword) { - client.setUsername(username); - if (encryptedPassword == null || encryptedPassword.isBlank()) { - return; - } - try { - client.setPassword(Encryption.decrypt(encryptedPassword)); - } catch (Exception e) { - log.warn("Unable to decrypt stored password", e); - } - sleep(300); - } - - private static void submitLogin() { - Rs2Keyboard.keyPress(KeyEvent.VK_ENTER); - sleep(300); - Rs2Keyboard.keyPress(KeyEvent.VK_ENTER); - } - - private static void handleBlockingDialogs(Client client) { - if (client == null) { - return; - } - int loginIndex = client.getLoginIndex(); - int loginScreenWidth = 804; - int startingWidth = (client.getCanvasWidth() / 2) - (loginScreenWidth / 2); - - if (loginIndex == 10) { - Microbot.getMouse().click(365 + startingWidth, 250); - } else if (loginIndex == 9) { - Microbot.getMouse().click(365 + startingWidth, 300); - } - } - - public static int getRandomWorld(boolean isMembers, WorldRegion region) { - WorldResult worldResult = Microbot.getWorldService().getWorlds(); - if (worldResult == null) { - return isMembers ? 360 : 383; - } - List worlds = worldResult.getWorlds(); - boolean isInSeasonalWorld; - if (Microbot.getClient() != null && Microbot.getClient().getWorldType() != null) { - isInSeasonalWorld = Microbot.getClient().getWorldType().contains(net.runelite.api.WorldType.SEASONAL); - } else { - isInSeasonalWorld = false; - } - - List filteredWorlds = worlds.stream() - .filter(x -> !x.getTypes().contains(WorldType.PVP) && - !x.getTypes().contains(WorldType.HIGH_RISK) && - !x.getTypes().contains(WorldType.BOUNTY) && - !x.getTypes().contains(WorldType.SKILL_TOTAL) && - !x.getTypes().contains(WorldType.LAST_MAN_STANDING) && - !x.getTypes().contains(WorldType.QUEST_SPEEDRUNNING) && - !x.getTypes().contains(WorldType.BETA_WORLD) && - !x.getTypes().contains(WorldType.DEADMAN) && - !x.getTypes().contains(WorldType.PVP_ARENA) && - !x.getTypes().contains(WorldType.TOURNAMENT) && - !x.getTypes().contains(WorldType.NOSAVE_MODE) && - !x.getTypes().contains(WorldType.LEGACY_ONLY) && - !x.getTypes().contains(WorldType.EOC_ONLY) && - !x.getTypes().contains(WorldType.FRESH_START_WORLD) && - x.getPlayers() < MAX_PLAYER_COUNT && - x.getPlayers() >= 0) - .filter(x -> isInSeasonalWorld == x.getTypes().contains(WorldType.SEASONAL)) - .collect(Collectors.toList()); - - filteredWorlds = isMembers - ? filteredWorlds.stream().filter(x -> x.getTypes().contains(WorldType.MEMBERS)).collect(Collectors.toList()) - : filteredWorlds.stream().filter(x -> !x.getTypes().contains(WorldType.MEMBERS)).collect(Collectors.toList()); - - if (region != null) { - filteredWorlds = filteredWorlds.stream() - .filter(x -> x.getRegion() == region) - .collect(Collectors.toList()); - } - - if (filteredWorlds.isEmpty()) { - return isMembers ? 360 : 383; - } - - Random random = new Random(); - World world = filteredWorlds.get(random.nextInt(filteredWorlds.size())); - - return (world != null) ? world.getId() : (isMembers ? 360 : 383); - } - - public static int getRandomWorld(boolean isMembers) { - return getRandomWorld(isMembers, null); - } - - public static int getNextWorld(boolean isMembers) { - return getNextWorld(isMembers, null); - } - - public static int getNextWorld(boolean isMembers, WorldRegion region) { - WorldResult worldResult = Microbot.getWorldService().getWorlds(); - if (worldResult == null) { - return isMembers ? 360 : 383; - } - - List worlds = worldResult.getWorlds(); - boolean isInSeasonalWorld; - if (Microbot.getClient() != null && Microbot.getClient().getWorldType() != null) { - isInSeasonalWorld = Microbot.getClient().getWorldType().contains(net.runelite.api.WorldType.SEASONAL); - } else { - isInSeasonalWorld = false; - } - - List filteredWorlds = worlds.stream() - .filter(x -> !x.getTypes().contains(WorldType.PVP) && - !x.getTypes().contains(WorldType.HIGH_RISK) && - !x.getTypes().contains(WorldType.BOUNTY) && - !x.getTypes().contains(WorldType.SKILL_TOTAL) && - !x.getTypes().contains(WorldType.LAST_MAN_STANDING) && - !x.getTypes().contains(WorldType.QUEST_SPEEDRUNNING) && - !x.getTypes().contains(WorldType.BETA_WORLD) && - !x.getTypes().contains(WorldType.DEADMAN) && - !x.getTypes().contains(WorldType.PVP_ARENA) && - !x.getTypes().contains(WorldType.TOURNAMENT) && - !x.getTypes().contains(WorldType.NOSAVE_MODE) && - !x.getTypes().contains(WorldType.LEGACY_ONLY) && - !x.getTypes().contains(WorldType.EOC_ONLY) && - !x.getTypes().contains(WorldType.FRESH_START_WORLD) && - x.getPlayers() < MAX_PLAYER_COUNT && - x.getPlayers() >= 0) - .filter(x -> isInSeasonalWorld == x.getTypes().contains(WorldType.SEASONAL)) - .collect(Collectors.toList()); - - filteredWorlds = isMembers - ? filteredWorlds.stream().filter(x -> x.getTypes().contains(WorldType.MEMBERS)).collect(Collectors.toList()) - : filteredWorlds.stream().filter(x -> !x.getTypes().contains(WorldType.MEMBERS)).collect(Collectors.toList()); - - if (region != null) { - filteredWorlds = filteredWorlds.stream() - .filter(x -> x.getRegion() == region) - .collect(Collectors.toList()); - } - - int currentWorldId = Microbot.getClient() != null ? Microbot.getClient().getWorld() : -1; - int currentIndex = -1; - - for (int i = 0; i < filteredWorlds.size(); i++) { - if (filteredWorlds.get(i).getId() == currentWorldId) { - currentIndex = i; - break; - } - } - - if (currentIndex != -1) { - int nextIndex = (currentIndex + 1) % filteredWorlds.size(); - return filteredWorlds.get(nextIndex).getId(); - } else if (!filteredWorlds.isEmpty()) { - return filteredWorlds.get(0).getId(); - } - - return isMembers ? 360 : 383; - } - - public static void setWorld(int worldNumber) { - try { - if (Microbot.getWorldService() == null || Microbot.getClient() == null) { - log.warn("Cannot change world - client or world service unavailable"); - return; - } - WorldResult worldResult = Microbot.getWorldService().getWorlds(); - if (worldResult == null) { - log.warn("Cannot change world - world service returned no data"); - return; - } - net.runelite.http.api.worlds.World world = worldResult.findWorld(worldNumber); - if (world == null) { - log.warn("Failed to find world {}", worldNumber); - return; - } - final net.runelite.api.World rsWorld = Microbot.getClient().createWorld(); - if (rsWorld == null) { - log.warn("Failed to create world instance for {}", worldNumber); - return; - } - rsWorld.setActivity(world.getActivity()); - rsWorld.setAddress(world.getAddress()); - rsWorld.setId(world.getId()); - rsWorld.setPlayerCount(world.getPlayers()); - rsWorld.setLocation(world.getLocation()); - rsWorld.setTypes(WorldUtil.toWorldTypes(world.getTypes())); - Microbot.getClient().changeWorld(rsWorld); - } catch (Exception ex) { - log.warn("Failed to set target world {}", worldNumber, ex); - } - } + * Returns the current RuneLite client GameState or UNKNOWN if client not available. + */ + public static GameState getGameState() { + Client client = Microbot.getClient(); + return client != null ? client.getGameState() : GameState.UNKNOWN; + } + + /** + * Returns true if the client is currently considered logged in. + */ + public static boolean isLoggedIn() { + Client client = Microbot.getClient(); + return client != null && client.getGameState() == GameState.LOGGED_IN; + } + + /** + * Returns true if a login attempt is currently being processed. + */ + public static boolean isLoginAttemptActive() { + return LOGIN_ATTEMPT_ACTIVE.get(); + } + + /** + * Marks the client as logged in by updating timestamps. This should be called when GameState transitions. + */ + public static void markLoggedIn() { + // Only set timestamp if client reports logged in. + if (isLoggedIn()) { + LOGIN_ATTEMPT_ACTIVE.set(false); + lastLoginTimestamp = Instant.now(); + } + } + + /** + * Marks the client as logged out. Should be triggered whenever the game enters the login screen. + */ + public static void markLoggedOut() { + LOGIN_ATTEMPT_ACTIVE.set(false); + } + + /** + * Returns the duration the account has been logged in for. Equivalent to Microbot.getLoginTime(). + */ + public static Duration getLoginDuration() { + if (lastLoginTimestamp == null || !isLoggedIn()) { + return Duration.of(0, ChronoUnit.MILLIS); + } + return Duration.between(lastLoginTimestamp, Instant.now()); + } + + /** + * Attempts a login using the active profile and an intelligent world selection. + */ + public static boolean login() { + if (getActiveProfile() == null) { + log.warn("No active profile available for login"); + return false; + } + System.out.println(getActiveProfile()); + Client client = Microbot.getClient(); + if (client == null) { + log.warn("Cannot login - client is not initialised"); + return false; + } + + if (getActiveProfile().isMember() && !isCurrentWorldMembers() || + !getActiveProfile().isMember() && isCurrentWorldMembers()) { + int targetWorld = getRandomWorld(getActiveProfile().isMember()); + return login(getActiveProfile().getName(), getActiveProfile().getPassword(), targetWorld); + } + + return login(getActiveProfile().getName(), getActiveProfile().getPassword(), Microbot.getClient().getWorld()); + + } + + /** + * Attempts a login using the active profile into a specific world. + */ + public static boolean login(int worldId) { + if (getActiveProfile() == null) { + log.warn("No active profile available for world specific login"); + return false; + } + return login(getActiveProfile().getName(), getActiveProfile().getPassword(), worldId); + } + + /** + * Attempts a login with explicit credentials and world target. + */ + public static boolean login(String username, String encryptedPassword, int worldId) { + if (username == null || username.isBlank()) { + log.warn("Cannot login without username"); + return false; + } + if (isLoggedIn()) { + return true; + } + if (LOGIN_ATTEMPT_ACTIVE.get()) { + log.debug("Login attempt already active - skipping duplicate request"); + return false; + } + final Client client = Microbot.getClient(); + if (client == null) { + log.warn("Cannot login - client is not initialised"); + return false; + } + synchronized (LOGIN_LOCK) { + Instant lastAttempt = LAST_LOGIN_ATTEMPT.get(); + Instant now = Instant.now(); + if (lastAttempt != null && Duration.between(lastAttempt, now).toMillis() < 1500) { + log.debug("Login throttled - last attempt {}ms ago", Duration.between(lastAttempt, now).toMillis()); + return false; + } + LAST_LOGIN_ATTEMPT.set(now); + LOGIN_ATTEMPT_ACTIVE.set(true); + } + + try { + handleDisconnectDialogs(client); + triggerLoginScreen(); + trySetWorld(worldId); + setCredentials(client, username, encryptedPassword); + submitLogin(); + handleBlockingDialogs(client); + return true; + } catch (Exception ex) { + log.error("Error during login attempt", ex); + return false; + } finally { + // Keep attempt active until either logged in state observed or logout recorded externally + LOGIN_ATTEMPT_ACTIVE.set(false); + } + } + + private static void handleDisconnectDialogs(Client client) { + if (client == null) { + return; + } + int loginIndex = client.getLoginIndex(); + if (loginIndex == 3 || loginIndex == 24) { + int loginScreenWidth = 804; + int startingWidth = (client.getCanvasWidth() / 2) - (loginScreenWidth / 2); + Microbot.getMouse().click(365 + startingWidth, 308); + sleep(600); + } + } + + private static void triggerLoginScreen() { + Rs2Keyboard.keyPress(KeyEvent.VK_ENTER); + sleep(600); + } + + private static void trySetWorld(int worldId) { + if (worldId <= 0) { + return; + } + try { + setWorld(worldId); + } catch (Exception e) { + log.warn("Changing world failed for {}", worldId, e); + } + } + + private static void setCredentials(Client client, String username, String encryptedPassword) { + client.setUsername(username); + if (encryptedPassword == null || encryptedPassword.isBlank()) { + return; + } + try { + client.setPassword(Encryption.decrypt(encryptedPassword)); + } catch (Exception e) { + log.warn("Unable to decrypt stored password", e); + } + sleep(300); + } + + private static void submitLogin() { + Rs2Keyboard.keyPress(KeyEvent.VK_ENTER); + sleep(300); + Rs2Keyboard.keyPress(KeyEvent.VK_ENTER); + } + + private static void handleBlockingDialogs(Client client) { + if (client == null) { + return; + } + int loginIndex = client.getLoginIndex(); + int loginScreenWidth = 804; + int startingWidth = (client.getCanvasWidth() / 2) - (loginScreenWidth / 2); + + if (loginIndex == 10) { + Microbot.getMouse().click(365 + startingWidth, 250); + } else if (loginIndex == 9) { + Microbot.getMouse().click(365 + startingWidth, 300); + } + } + + public static int getRandomWorld(boolean isMembers, WorldRegion region) { + WorldResult worldResult = Microbot.getWorldService().getWorlds(); + if (worldResult == null) { + return isMembers ? 360 : 383; + } + List worlds = worldResult.getWorlds(); + boolean isInSeasonalWorld; + if (Microbot.getClient() != null && Microbot.getClient().getWorldType() != null) { + isInSeasonalWorld = Microbot.getClient().getWorldType().contains(net.runelite.api.WorldType.SEASONAL); + } else { + isInSeasonalWorld = false; + } + + List filteredWorlds = worlds.stream() + .filter(x -> !x.getTypes().contains(WorldType.PVP) && + !x.getTypes().contains(WorldType.HIGH_RISK) && + !x.getTypes().contains(WorldType.BOUNTY) && + !x.getTypes().contains(WorldType.SKILL_TOTAL) && + !x.getTypes().contains(WorldType.LAST_MAN_STANDING) && + !x.getTypes().contains(WorldType.QUEST_SPEEDRUNNING) && + !x.getTypes().contains(WorldType.BETA_WORLD) && + !x.getTypes().contains(WorldType.DEADMAN) && + !x.getTypes().contains(WorldType.PVP_ARENA) && + !x.getTypes().contains(WorldType.TOURNAMENT) && + !x.getTypes().contains(WorldType.NOSAVE_MODE) && + !x.getTypes().contains(WorldType.LEGACY_ONLY) && + !x.getTypes().contains(WorldType.EOC_ONLY) && + !x.getTypes().contains(WorldType.FRESH_START_WORLD) && + x.getPlayers() < MAX_PLAYER_COUNT && + x.getPlayers() >= 0) + .filter(x -> isInSeasonalWorld == x.getTypes().contains(WorldType.SEASONAL)) + .collect(Collectors.toList()); + + filteredWorlds = isMembers + ? filteredWorlds.stream().filter(x -> x.getTypes().contains(WorldType.MEMBERS)).collect(Collectors.toList()) + : filteredWorlds.stream().filter(x -> !x.getTypes().contains(WorldType.MEMBERS)).collect(Collectors.toList()); + + if (region != null) { + filteredWorlds = filteredWorlds.stream() + .filter(x -> x.getRegion() == region) + .collect(Collectors.toList()); + } + + if (filteredWorlds.isEmpty()) { + return isMembers ? 360 : 383; + } + + Random random = new Random(); + World world = filteredWorlds.get(random.nextInt(filteredWorlds.size())); + + return (world != null) ? world.getId() : (isMembers ? 360 : 383); + } + + public static int getRandomWorld(boolean isMembers) { + return getRandomWorld(isMembers, null); + } + + public static int getNextWorld(boolean isMembers) { + return getNextWorld(isMembers, null); + } + + public static int getNextWorld(boolean isMembers, WorldRegion region) { + WorldResult worldResult = Microbot.getWorldService().getWorlds(); + if (worldResult == null) { + return isMembers ? 360 : 383; + } + + List worlds = worldResult.getWorlds(); + boolean isInSeasonalWorld; + if (Microbot.getClient() != null && Microbot.getClient().getWorldType() != null) { + isInSeasonalWorld = Microbot.getClient().getWorldType().contains(net.runelite.api.WorldType.SEASONAL); + } else { + isInSeasonalWorld = false; + } + + List filteredWorlds = worlds.stream() + .filter(x -> !x.getTypes().contains(WorldType.PVP) && + !x.getTypes().contains(WorldType.HIGH_RISK) && + !x.getTypes().contains(WorldType.BOUNTY) && + !x.getTypes().contains(WorldType.SKILL_TOTAL) && + !x.getTypes().contains(WorldType.LAST_MAN_STANDING) && + !x.getTypes().contains(WorldType.QUEST_SPEEDRUNNING) && + !x.getTypes().contains(WorldType.BETA_WORLD) && + !x.getTypes().contains(WorldType.DEADMAN) && + !x.getTypes().contains(WorldType.PVP_ARENA) && + !x.getTypes().contains(WorldType.TOURNAMENT) && + !x.getTypes().contains(WorldType.NOSAVE_MODE) && + !x.getTypes().contains(WorldType.LEGACY_ONLY) && + !x.getTypes().contains(WorldType.EOC_ONLY) && + !x.getTypes().contains(WorldType.FRESH_START_WORLD) && + x.getPlayers() < MAX_PLAYER_COUNT && + x.getPlayers() >= 0) + .filter(x -> isInSeasonalWorld == x.getTypes().contains(WorldType.SEASONAL)) + .collect(Collectors.toList()); + + filteredWorlds = isMembers + ? filteredWorlds.stream().filter(x -> x.getTypes().contains(WorldType.MEMBERS)).collect(Collectors.toList()) + : filteredWorlds.stream().filter(x -> !x.getTypes().contains(WorldType.MEMBERS)).collect(Collectors.toList()); + + if (region != null) { + filteredWorlds = filteredWorlds.stream() + .filter(x -> x.getRegion() == region) + .collect(Collectors.toList()); + } + + int currentWorldId = Microbot.getClient() != null ? Microbot.getClient().getWorld() : -1; + int currentIndex = -1; + + for (int i = 0; i < filteredWorlds.size(); i++) { + if (filteredWorlds.get(i).getId() == currentWorldId) { + currentIndex = i; + break; + } + } + + if (currentIndex != -1) { + int nextIndex = (currentIndex + 1) % filteredWorlds.size(); + return filteredWorlds.get(nextIndex).getId(); + } else if (!filteredWorlds.isEmpty()) { + return filteredWorlds.get(0).getId(); + } + + return isMembers ? 360 : 383; + } + + /** + * Determine if the provided world id corresponds to a members world. + * + * @param worldId target world id (e.g. 301, 302, etc.) + * @return true if world exists and has the MEMBERS type; false otherwise or if data unavailable + */ + public static boolean isMemberWorld(int worldId) { + if (worldId <= 0) { + return false; + } + if (Microbot.getWorldService() == null) { + return false; + } + try { + WorldResult result = Microbot.getWorldService().getWorlds(); + if (result == null) { + return false; + } + World world = result.findWorld(worldId); + if (world == null || world.getTypes() == null) { + return false; + } + return world.getTypes().contains(WorldType.MEMBERS); + } catch (Exception e) { + log.debug("Failed to determine membership for world {}", worldId, e); + return false; + } + } + + /** + * Convenience method to check if the current client world is a members world. + * @return true if client available and current world is members, false otherwise. + */ + public static boolean isCurrentWorldMembers() { + Client client = Microbot.getClient(); + if (client == null) { + return false; + } + return isMemberWorld(client.getWorld()); + } + + public static void setWorld(int worldNumber) { + try { + if (Microbot.getWorldService() == null || Microbot.getClient() == null) { + log.warn("Cannot change world - client or world service unavailable"); + return; + } + WorldResult worldResult = Microbot.getWorldService().getWorlds(); + if (worldResult == null) { + log.warn("Cannot change world - world service returned no data"); + return; + } + net.runelite.http.api.worlds.World world = worldResult.findWorld(worldNumber); + if (world == null) { + log.warn("Failed to find world {}", worldNumber); + return; + } + final net.runelite.api.World rsWorld = Microbot.getClient().createWorld(); + if (rsWorld == null) { + log.warn("Failed to create world instance for {}", worldNumber); + return; + } + rsWorld.setActivity(world.getActivity()); + rsWorld.setAddress(world.getAddress()); + rsWorld.setId(world.getId()); + rsWorld.setPlayerCount(world.getPlayers()); + rsWorld.setLocation(world.getLocation()); + rsWorld.setTypes(WorldUtil.toWorldTypes(world.getTypes())); + Microbot.getClient().changeWorld(rsWorld); + } catch (Exception ex) { + log.warn("Failed to set target world {}", worldNumber, ex); + } + } } From 423d4c34364ddc35cbe8c4f174b8044bc5907bbd Mon Sep 17 00:00:00 2001 From: chsami Date: Sun, 16 Nov 2025 12:40:52 +0100 Subject: [PATCH 16/42] refactor(api): reorganize player and NPC model imports and structure --- .../microbot/api/actor/Rs2ActorModel.java | 435 ++++++++++++++++++ .../plugins/microbot/api/npc/Rs2NpcCache.java | 2 +- .../microbot/api/npc/Rs2NpcQueryable.java | 3 +- .../microbot/api/npc/models/Rs2NpcModel.java | 206 +++++++++ .../microbot/api/player/PlayerApiExample.java | 2 +- .../microbot/api/player/Rs2PlayerCache.java | 3 +- .../api/player/Rs2PlayerQueryable.java | 2 +- .../api/player/models/Rs2PlayerModel.java | 98 ++++ .../microbot/util/npc/Rs2NpcModel.java | 12 +- .../microbot/util/player/Rs2PlayerModel.java | 12 +- 10 files changed, 746 insertions(+), 29 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/actor/Rs2ActorModel.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/models/Rs2NpcModel.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/models/Rs2PlayerModel.java 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..bb11972cecc --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/actor/Rs2ActorModel.java @@ -0,0 +1,435 @@ +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 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 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() + { + 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(); + } +} 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 index a566b135cdd..2f52c65c088 100644 --- 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 @@ -1,7 +1,7 @@ package net.runelite.client.plugins.microbot.api.npc; import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; +import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; import java.util.ArrayList; import java.util.List; 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 index 2f54573104c..052fddee7d1 100644 --- 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 @@ -1,9 +1,8 @@ package net.runelite.client.plugins.microbot.api.npc; -import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.api.AbstractEntityQueryable; import net.runelite.client.plugins.microbot.api.IEntityQueryable; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; +import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; import java.util.stream.Stream; 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..bc2b2071686 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/models/Rs2NpcModel.java @@ -0,0 +1,206 @@ +package net.runelite.client.plugins.microbot.api.npc.models; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import net.runelite.api.HeadIcon; +import net.runelite.api.NPC; +import net.runelite.api.NPCComposition; +import net.runelite.api.NpcOverrides; +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.ActorModel; +import org.apache.commons.lang3.NotImplementedException; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.function.Predicate; + +@Getter +@EqualsAndHashCode(callSuper = true) // Ensure equality checks include ActorModel fields +public class Rs2NpcModel extends ActorModel implements NPC, IEntity +{ + + private final NPC runeliteNpc; + + public Rs2NpcModel(final NPC npc) + { + super(npc); + this.runeliteNpc = npc; + } + + @Override + public int getId() + { + return runeliteNpc.getId(); + } + + + @Override + public int getIndex() + { + return runeliteNpc.getIndex(); + } + + @Override + public NPCComposition getComposition() + { + return runeliteNpc.getComposition(); + } + + @Override + public @Nullable NPCComposition getTransformedComposition() + { + return runeliteNpc.getTransformedComposition(); + } + + @Override + public @Nullable NpcOverrides getModelOverrides() + { + return runeliteNpc.getModelOverrides(); + } + + @Override + public @Nullable NpcOverrides getChatheadOverrides() + { + return runeliteNpc.getChatheadOverrides(); + } + + @Override + public int @Nullable [] getOverheadArchiveIds() + { + return runeliteNpc.getOverheadArchiveIds(); + } + + @Override + public short @Nullable [] getOverheadSpriteIds() + { + return runeliteNpc.getOverheadSpriteIds(); + } + + // 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 this.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 (runeliteNpc == null) { + return null; + } + + if (runeliteNpc.getOverheadSpriteIds() == null) { + Microbot.log("Failed to find the correct overhead prayer."); + return null; + } + + for (int i = 0; i < runeliteNpc.getOverheadSpriteIds().length; i++) { + int overheadSpriteId = runeliteNpc.getOverheadSpriteIds()[i]; + + if (overheadSpriteId == -1) continue; + + return HeadIcon.values()[overheadSpriteId]; + } + + Microbot.log("Found overheadSpriteIds: " + Arrays.toString(runeliteNpc.getOverheadSpriteIds()) + " but failed to find valid overhead prayer."); + + return null; + } + + @Override + public boolean click() { + throw new NotImplementedException("click() not implemented yet for Rs2NpcModel"); + } + + @Override + public boolean click(String action) { + throw new NotImplementedException("click() not implemented yet for Rs2NpcModel"); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java index a56ea75dca0..4fa997916b5 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java @@ -1,6 +1,6 @@ package net.runelite.client.plugins.microbot.api.player; -import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; +import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; import java.util.List; import java.util.stream.Collectors; 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 index 0bbcffeb37b..bd34518e572 100644 --- 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 @@ -1,8 +1,7 @@ package net.runelite.client.plugins.microbot.api.player; import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; +import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; import java.util.ArrayList; import java.util.List; 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 index f4ab6ef1186..78f8f7a16c8 100644 --- 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 @@ -2,7 +2,7 @@ import net.runelite.client.plugins.microbot.api.AbstractEntityQueryable; import net.runelite.client.plugins.microbot.api.IEntityQueryable; -import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; +import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; import java.util.stream.Stream; 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..ffb81a67c60 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/models/Rs2PlayerModel.java @@ -0,0 +1,98 @@ +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.client.plugins.microbot.api.IEntity; +import net.runelite.client.plugins.microbot.util.ActorModel; +import org.apache.commons.lang3.NotImplementedException; + +@Getter +public class Rs2PlayerModel extends ActorModel implements Player, IEntity { + + private final Player player; + + public Rs2PlayerModel(final Player player) + { + super(player); + this.player = player; + } + + @Override + public int getId() + { + return player.getId(); + } + + @Override + public PlayerComposition getPlayerComposition() + { + return player.getPlayerComposition(); + } + + @Override + public Polygon[] getPolygons() + { + return player.getPolygons(); + } + + @Override + public int getTeam() + { + return player.getTeam(); + } + + @Override + public boolean isFriendsChatMember() + { + return player.isFriendsChatMember(); + } + + @Override + public boolean isFriend() + { + return player.isFriend(); + } + + @Override + public boolean isClanMember() + { + return player.isClanMember(); + } + + @Override + public HeadIcon getOverheadIcon() + { + return player.getOverheadIcon(); + } + + @Override + public int getSkullIcon() + { + return player.getSkullIcon(); + } + + @Override + public void setSkullIcon(int skullIcon) + { + player.setSkullIcon(skullIcon); + } + + @Override + public int getFootprintSize() + { + return 0; + } + + @Override + public boolean click() { + throw new NotImplementedException("click() not implemented yet for Rs2PlayerModel"); + } + + @Override + public boolean click(String action) { + throw new NotImplementedException("click() not implemented yet for Rs2PlayerModel"); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcModel.java index 188df4e96e0..8f04373be24 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcModel.java @@ -18,7 +18,7 @@ @Getter @EqualsAndHashCode(callSuper = true) // Ensure equality checks include ActorModel fields -public class Rs2NpcModel extends ActorModel implements NPC, IEntity +public class Rs2NpcModel extends ActorModel implements NPC { private final NPC runeliteNpc; @@ -193,14 +193,4 @@ public HeadIcon getHeadIcon() { return null; } - - @Override - public boolean click() { - throw new NotImplementedException("click() not implemented yet for Rs2NpcModel"); - } - - @Override - public boolean click(String action) { - throw new NotImplementedException("click() not implemented yet for Rs2NpcModel"); - } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerModel.java index 762d5c56c81..96dbd69894f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerModel.java @@ -10,7 +10,7 @@ import org.apache.commons.lang3.NotImplementedException; @Getter -public class Rs2PlayerModel extends ActorModel implements Player, IEntity { +public class Rs2PlayerModel extends ActorModel implements Player { private final Player player; @@ -85,14 +85,4 @@ public int getFootprintSize() { return 0; } - - @Override - public boolean click() { - throw new NotImplementedException("click() not implemented yet for Rs2PlayerModel"); - } - - @Override - public boolean click(String action) { - throw new NotImplementedException("click() not implemented yet for Rs2PlayerModel"); - } } From f036e930d539f94274d90d6f93fe14e41094c14f Mon Sep 17 00:00:00 2001 From: chsami Date: Sun, 16 Nov 2025 12:50:54 +0100 Subject: [PATCH 17/42] refactor(api): mark legacy game object classes as deprecated --- .../client/plugins/microbot/util/gameobject/Rs2GameObject.java | 1 + .../client/plugins/microbot/util/grounditem/Rs2GroundItem.java | 1 + .../net/runelite/client/plugins/microbot/util/npc/Rs2Npc.java | 1 + .../runelite/client/plugins/microbot/util/player/Rs2Player.java | 1 + 4 files changed, 4 insertions(+) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java index 1d4b4dee52a..8f177cf5f2b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java @@ -33,6 +33,7 @@ /** * TODO: This class should be cleaned up, less methods by passing filters instead of multiple parameters */ +@Deprecated(since = "2.1.0 - Use Rs2TileObjectQueryable instead", forRemoval = true) public class Rs2GameObject { /** * Extracts all {@link GameObject}s located on a given {@link Tile}. diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItem.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItem.java index 10f66852f6e..953722f1d17 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItem.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItem.java @@ -31,6 +31,7 @@ * Todo: rework this class to not be dependant on the grounditem plugin */ @Slf4j +@Deprecated(since = "2.1.0 - Use Rs2TileItemCache/Rs2TileItemQuery instead", forRemoval = true) public class Rs2GroundItem { private static final int DESPAWN_DELAY_THRESHOLD_TICKS = 150; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2Npc.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2Npc.java index aad28be8d7a..0c7938227b7 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2Npc.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2Npc.java @@ -28,6 +28,7 @@ import static net.runelite.api.Perspective.LOCAL_TILE_SIZE; @Slf4j +@Deprecated(since = "2.1.0 - Use Rs2NpcCache/Rs2NpcQuery instead", forRemoval = true) public class Rs2Npc { /** * Retrieves an NPC by its index, returning an {@link Rs2NpcModel}. diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java index 2eca00bac5e..8319a30dfa9 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java @@ -49,6 +49,7 @@ import static net.runelite.api.MenuAction.CC_OP; import static net.runelite.client.plugins.microbot.util.Global.*; +@Deprecated(since = "2.1.0 - Use Rs2PlayerCache/Rs2PlayerQueryable", forRemoval = true) public class Rs2Player { static int VENOM_VALUE_CUTOFF = -38; private static int antiFireTime = -1; From a28789e82fd526e2df50669b830ab5e84f64a021 Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 15 Nov 2025 07:51:54 +0100 Subject: [PATCH 18/42] remove caching and scheduler --- docs/scheduler/README.md | 195 - docs/scheduler/api/schedulable-plugin.md | 796 ---- docs/scheduler/combat-lock-examples.md | 307 -- docs/scheduler/condition-manager.md | 424 -- .../conditions/location-conditions.md | 136 - .../conditions/logical-conditions.md | 192 - docs/scheduler/conditions/npc-conditions.md | 125 - .../conditions/resource-conditions.md | 177 - docs/scheduler/conditions/skill-conditions.md | 230 -- docs/scheduler/conditions/time-conditions.md | 128 - docs/scheduler/defining-conditions.md | 584 --- .../plugin-schedule-entry-finished-event.md | 175 - .../plugin-schedule-entry-soft-stop-event.md | 153 - .../scheduler/plugin-schedule-entry-merged.md | 753 ---- docs/scheduler/plugin-writers-guide.md | 626 --- .../scheduler/predicate-condition-examples.md | 257 -- docs/scheduler/roadmap.md | 152 - docs/scheduler/schedulable-example-plugin.md | 364 -- docs/scheduler/scheduler-plugin.md | 1009 ----- docs/scheduler/tasks/README.md | 123 - .../tasks/enhanced-schedulable-plugin-api.md | 237 -- docs/scheduler/tasks/plugin-writers-guide.md | 225 - .../tasks/requirements-integration.md | 259 -- docs/scheduler/tasks/requirements-system.md | 197 - .../scheduler/tasks/task-management-system.md | 218 - docs/scheduler/user-guide.md | 469 --- .../client/plugins/microbot/Microbot.java | 14 - .../plugins/microbot/MicrobotConfig.java | 12 - .../plugins/microbot/MicrobotOverlay.java | 286 -- .../plugins/microbot/MicrobotPlugin.java | 146 +- .../client/plugins/microbot/Script.java | 6 - .../GroundItemFilterPreset.java | 141 - .../rs2cachedebugger/NpcFilterPreset.java | 111 - .../rs2cachedebugger/ObjectFilterPreset.java | 159 - .../rs2cachedebugger/RenderStyle.java | 32 - .../Rs2CacheDebuggerConfig.java | 908 ---- .../Rs2CacheDebuggerGroundItemOverlay.java | 157 - .../Rs2CacheDebuggerInfoPanel.java | 326 -- .../Rs2CacheDebuggerNpcOverlay.java | 168 - .../Rs2CacheDebuggerObjectOverlay.java | 231 -- .../Rs2CacheDebuggerPlugin.java | 637 --- .../LocationStartNotificationOverlay.java | 145 - .../VoxPlugins/schedulable/example/README.md | 499 --- .../example/SchedulableExampleConfig.java | 889 ---- .../example/SchedulableExampleOverlay.java | 139 - .../example/SchedulableExamplePlugin.java | 978 ----- ...bleExamplePrePostScheduleRequirements.java | 640 --- ...chedulableExamplePrePostScheduleTasks.java | 186 - .../example/SchedulableExampleScript.java | 495 --- .../example/enums/SpellbookOption.java | 49 - .../example/enums/UnifiedLocation.java | 190 - .../breakhandler/BreakHandlerOverlay.java | 17 +- .../breakhandler/BreakHandlerScript.java | 3 +- .../MicrobotPluginManager.java | 27 +- .../pluginscheduler/SchedulerConfig.java | 336 -- .../pluginscheduler/SchedulerInfoOverlay.java | 345 -- .../pluginscheduler/SchedulerPlugin.java | 3667 ----------------- .../pluginscheduler/SchedulerState.java | 115 - .../api/SchedulablePlugin.java | 687 --- .../pluginscheduler/condition/Condition.java | 304 -- .../condition/ConditionManager.java | 2691 ------------ .../condition/ConditionType.java | 44 - .../condition/location/AreaCondition.java | 153 - .../condition/location/LocationCondition.java | 278 -- .../condition/location/PositionCondition.java | 248 -- .../condition/location/RegionCondition.java | 149 - .../condition/location/readme.md | 28 - .../serialization/AreaConditionAdapter.java | 81 - .../PositionConditionAdapter.java | 76 - .../serialization/RegionConditionAdapter.java | 79 - .../location/ui/LocationConditionUtil.java | 831 ---- .../condition/logical/AndCondition.java | 245 -- .../condition/logical/LockCondition.java | 167 - .../condition/logical/LogicalCondition.java | 1504 ------- .../condition/logical/NotCondition.java | 263 -- .../condition/logical/OrCondition.java | 234 -- .../condition/logical/PredicateCondition.java | 196 - .../condition/logical/enums/UpdateOption.java | 28 - .../LogicalConditionAdapter.java | 117 - .../serialization/NotConditionAdapter.java | 69 - .../condition/npc/NpcCondition.java | 46 - .../condition/npc/NpcKillCountCondition.java | 470 --- .../pluginscheduler/condition/npc/ReadMe.md | 14 - .../NpcKillCountConditionAdapter.java | 77 - .../resource/BankItemCountCondition.java | 364 -- .../resource/GatheredResourceCondition.java | 645 --- .../resource/InventoryItemCountCondition.java | 383 -- .../condition/resource/LootItemCondition.java | 884 ---- .../resource/ProcessItemCondition.java | 804 ---- .../condition/resource/README.md | 30 - .../condition/resource/ResourceCondition.java | 397 -- .../BankItemCountConditionAdapter.java | 76 - .../GatheredResourceConditionAdapter.java | 106 - .../InventoryItemCountConditionAdapter.java | 90 - .../LootItemConditionAdapter.java | 84 - .../ProcessItemConditionAdapter.java | 147 - .../ResourceConditionAdapter.java | 66 - .../ui/ResourceConditionPanelUtil.java | 1405 ------- .../condition/skill/SkillCondition.java | 422 -- .../condition/skill/SkillLevelCondition.java | 450 -- .../condition/skill/SkillXpCondition.java | 455 -- .../SkillLevelConditionAdapter.java | 85 - .../SkillXpConditionAdapter.java | 84 - .../skill/ui/SkillConditionPanelUtil.java | 662 --- .../condition/time/DayOfWeekCondition.java | 1057 ----- .../condition/time/IntervalCondition.java | 781 ---- .../time/SingleTriggerTimeCondition.java | 281 -- .../condition/time/TimeCondition.java | 487 --- .../condition/time/TimeWindowCondition.java | 1292 ------ .../condition/time/enums/RepeatCycle.java | 41 - .../DayOfWeekConditionAdapter.java | 125 - .../time/serialization/DurationAdapter.java | 37 - .../IntervalConditionAdapter.java | 258 -- .../time/serialization/LocalDateAdapter.java | 32 - .../time/serialization/LocalTimeAdapter.java | 33 - .../SingleTriggerTimeConditionAdapter.java | 103 - .../serialization/TimeConditionAdapter.java | 57 - .../TimeWindowConditionAdapter.java | 217 - .../time/ui/TimeConditionPanelUtil.java | 1492 ------- .../time/util/TimeConditionUtil.java | 730 ---- .../condition/ui/ConditionConfigPanel.java | 2883 ------------- .../ui/callback/ConditionUpdateCallback.java | 53 - .../renderer/ConditionTreeCellRenderer.java | 290 -- .../ui/util/ConditionConfigPanelUtil.java | 868 ---- .../condition/varbit/VarbitCondition.java | 559 --- .../condition/varbit/VarbitUtil.java | 249 -- .../serialization/VarbitConditionAdapter.java | 135 - .../varbit/ui/VarbitConditionPanelUtil.java | 940 ----- .../config/ScheduleEntryConfigManager.java | 612 --- .../config/ui/HotkeyButton.java | 89 - .../ui/ScheduleEntryConfigManagerPanel.java | 619 --- .../event/ExecutionResult.java | 93 - ...ginScheduleEntryMainTaskFinishedEvent.java | 55 - ...ginScheduleEntryPostScheduleTaskEvent.java | 38 - ...uleEntryPostScheduleTaskFinishedEvent.java | 53 - ...uginScheduleEntryPreScheduleTaskEvent.java | 41 - ...duleEntryPreScheduleTaskFinishedEvent.java | 53 - .../PluginScheduleEntrySoftStopEvent.java | 45 - .../model/PluginScheduleEntry.java | 3584 ---------------- ...sientAndNonSerializableFieldsStrategy.java | 47 - .../serialization/ScheduledSerializer.java | 205 - .../adapter/ConditionManagerAdapter.java | 105 - .../adapter/ConditionTypeAdapter.java | 303 -- .../adapter/PluginScheduleEntryAdapter.java | 275 -- .../adapter/ZonedDateTimeAdapter.java | 36 - .../adapter/config/AlphaAdapter.java | 30 - .../config/ConfigDescriptorAdapter.java | 87 - .../adapter/config/ConfigGroupAdapter.java | 41 - .../config/ConfigInformationAdapter.java | 40 - .../adapter/config/ConfigItemAdapter.java | 90 - .../config/ConfigItemDescriptorAdapter.java | 107 - .../adapter/config/ConfigSectionAdapter.java | 62 - .../ConfigSectionDescriptorAdapter.java | 51 - .../adapter/config/RangeAdapter.java | 47 - .../adapter/config/UnitsAdapter.java | 40 - .../tasks/AbstractPrePostScheduleTasks.java | 1102 ----- .../microbot/pluginscheduler/tasks/README.md | 263 -- .../ExamplePrePostScheduleRequirements.java | 151 - .../examples/ExamplePrePostScheduleTasks.java | 206 - ...PrePostScheduleTasksOverlayComponents.java | 480 --- .../PrePostScheduleRequirements.java | 1513 ------- .../data/ItemRequirementCollection.java | 1444 ------- .../requirements/enums/OrRequirementMode.java | 20 - .../requirements/enums/RequirementMode.java | 25 - .../enums/RequirementPriority.java | 21 - .../requirements/enums/RequirementType.java | 64 - .../tasks/requirements/enums/TaskContext.java | 7 - .../registry/RequirementRegistry.java | 2833 ------------- .../InventorySetupRequirement.java | 172 - .../requirements/requirement/Requirement.java | 235 -- .../requirement/SpellbookRequirement.java | 520 --- .../collection/LootRequirement.java | 600 --- .../conditional/ConditionalRequirement.java | 522 --- .../conditional/OrderedRequirement.java | 427 -- .../item/InventorySetupPlanner.java | 3097 -------------- .../requirement/item/ItemRequirement.java | 1947 --------- .../item/RunePouchRequirement.java | 328 -- .../requirement/location/LocationOption.java | 148 - .../location/LocationRequirement.java | 759 ---- .../location/ResourceLocationOption.java | 145 - .../logical/LogicalRequirement.java | 879 ---- .../requirement/logical/OrRequirement.java | 543 --- .../requirement/shop/ShopItemRequirement.java | 217 - .../requirement/shop/ShopRequirement.java | 2572 ------------ .../shop/models/CancelledOfferState.java | 171 - .../shop/models/MultiItemConfig.java | 57 - .../shop/models/ShopOperation.java | 9 - .../util/ConditionalRequirementBuilder.java | 232 -- .../util/RequirementSelector.java | 314 -- .../requirements/util/RequirementSolver.java | 347 -- .../tasks/state/FulfillmentStep.java | 72 - .../tasks/state/TaskExecutionState.java | 441 -- .../ui/PrePostScheduleTasksInfoPanel.java | 258 -- .../tasks/ui/RequirementsStatusPanel.java | 434 -- .../tasks/ui/TaskExecutionStatePanel.java | 289 -- .../ui/Antiban/AntibanDialogWindow.java | 53 - .../ui/Antiban/AntibanWindowManager.java | 76 - .../PrioritySpinnerEditor.java | 113 - .../ScheduleFormPanel.java | 1339 ------ .../ScheduleTableModel.java | 17 - .../ScheduleTablePanel.java | 2038 --------- .../ui/SchedulerInfoPanel.java | 1494 ------- .../pluginscheduler/ui/SchedulerPanel.java | 792 ---- .../pluginscheduler/ui/SchedulerWindow.java | 999 ----- .../ui/components/DatePickerPanel.java | 254 -- .../ui/components/DateRangePanel.java | 160 - .../ui/components/DateTimePickerPanel.java | 88 - .../ui/components/InitialDelayPanel.java | 156 - .../ui/components/IntervalPickerPanel.java | 583 --- .../components/SingleDateTimePickerPanel.java | 136 - .../ui/components/TimePickerPanel.java | 184 - .../ui/components/TimeRangePanel.java | 184 - .../ui/layout/DynamicFlowLayout.java | 300 -- .../ui/util/SchedulerUIUtils.java | 155 - .../pluginscheduler/ui/util/UIUtils.java | 515 --- .../util/PluginFilterUtil.java | 531 --- .../util/SchedulerPluginUtil.java | 1223 ------ .../pathfinder/PathfinderConfig.java | 36 +- .../plugins/microbot/util/bank/Rs2Bank.java | 356 -- .../microbot/util/cache/CacheMode.java | 41 - .../util/cache/MemorySizeCalculator.java | 514 --- .../plugins/microbot/util/cache/Rs2Cache.java | 1356 ------ .../microbot/util/cache/Rs2CacheManager.java | 1184 ------ .../util/cache/Rs2GroundItemCache.java | 787 ---- .../microbot/util/cache/Rs2NpcCache.java | 502 --- .../microbot/util/cache/Rs2ObjectCache.java | 604 --- .../microbot/util/cache/Rs2QuestCache.java | 669 --- .../microbot/util/cache/Rs2SkillCache.java | 523 --- .../util/cache/Rs2SpiritTreeCache.java | 688 ---- .../util/cache/Rs2VarPlayerCache.java | 423 -- .../microbot/util/cache/Rs2VarbitCache.java | 507 --- .../microbot/util/cache/model/SkillData.java | 90 - .../util/cache/model/SpiritTreeData.java | 226 - .../microbot/util/cache/model/VarbitData.java | 138 - .../cache/overlay/HoverInfoContainer.java | 142 - .../cache/overlay/Rs2BaseCacheOverlay.java | 215 - .../cache/overlay/Rs2CacheInfoBoxOverlay.java | 154 - .../cache/overlay/Rs2CacheOverlayManager.java | 229 - .../overlay/Rs2GroundItemCacheOverlay.java | 645 --- .../cache/overlay/Rs2NpcCacheOverlay.java | 451 -- .../cache/overlay/Rs2ObjectCacheOverlay.java | 841 ---- .../serialization/CacheSerializable.java | 35 - .../CacheSerializationManager.java | 757 ---- .../cache/serialization/QuestAdapter.java | 54 - .../serialization/QuestStateAdapter.java | 54 - .../cache/serialization/SkillAdapter.java | 54 - .../cache/serialization/SkillDataAdapter.java | 88 - .../serialization/SpiritTreeDataAdapter.java | 147 - .../serialization/SpiritTreePatchAdapter.java | 53 - .../serialization/VarbitDataAdapter.java | 147 - .../util/cache/strategy/CacheOperations.java | 95 - .../cache/strategy/CacheUpdateStrategy.java | 64 - .../util/cache/strategy/PredicateQuery.java | 48 - .../util/cache/strategy/QueryCriteria.java | 15 - .../util/cache/strategy/QueryStrategy.java | 29 - .../util/cache/strategy/ValueWrapper.java | 35 - .../entity/GroundItemUpdateStrategy.java | 536 --- .../strategy/entity/NpcUpdateStrategy.java | 390 -- .../strategy/entity/ObjectUpdateStrategy.java | 720 ---- .../farming/SpiritTreeUpdateStrategy.java | 385 -- .../strategy/simple/QuestUpdateStrategy.java | 387 -- .../strategy/simple/SkillUpdateStrategy.java | 82 - .../simple/VarPlayerUpdateStrategy.java | 103 - .../strategy/simple/VarbitUpdateStrategy.java | 103 - .../util/cache/util/LogOutputMode.java | 24 - .../util/cache/util/Rs2CacheLoggingUtils.java | 492 --- .../cache/util/Rs2GroundItemCacheUtils.java | 1480 ------- .../util/cache/util/Rs2NpcCacheUtils.java | 1185 ------ .../util/cache/util/Rs2ObjectCacheUtils.java | 1423 ------- .../microbot/util/farming/SpiritTree.java | 5 - .../util/farming/SpiritTreeHelper.java | 248 -- .../microbot/util/player/Rs2Player.java | 7 +- .../util/world/WorldHoppingConfig.java | 36 - .../microbot/pluginscheduler/area_map.png | Bin 17509 -> 0 bytes .../pluginscheduler/calendar-icon.png | Bin 1078 -> 0 bytes .../microbot/pluginscheduler/chronometer.png | Bin 23542 -> 0 bytes .../microbot/pluginscheduler/clock.png | Bin 53520 -> 0 bytes .../microbot/pluginscheduler/delete.png | Bin 7842 -> 0 bytes .../microbot/pluginscheduler/location_map.png | Bin 49758 -> 0 bytes .../pluginscheduler/logic-gate-and.png | Bin 5314 -> 0 bytes .../pluginscheduler/logic-gate-or.png | Bin 5218 -> 0 bytes .../microbot/pluginscheduler/loot_icon.png | Bin 892 -> 0 bytes .../microbot/pluginscheduler/not-equal.png | Bin 19767 -> 0 bytes .../microbot/pluginscheduler/old-watch.png | Bin 5617 -> 0 bytes .../microbot/pluginscheduler/padlock.png | Bin 15064 -> 0 bytes .../microbot/pluginscheduler/position.png | Bin 30852 -> 0 bytes .../microbot/pluginscheduler/region.png | Bin 65293 -> 0 bytes .../microbot/pluginscheduler/ungroup.png | Bin 2553 -> 0 bytes 288 files changed, 35 insertions(+), 112952 deletions(-) delete mode 100644 docs/scheduler/README.md delete mode 100644 docs/scheduler/api/schedulable-plugin.md delete mode 100644 docs/scheduler/combat-lock-examples.md delete mode 100644 docs/scheduler/condition-manager.md delete mode 100644 docs/scheduler/conditions/location-conditions.md delete mode 100644 docs/scheduler/conditions/logical-conditions.md delete mode 100644 docs/scheduler/conditions/npc-conditions.md delete mode 100644 docs/scheduler/conditions/resource-conditions.md delete mode 100644 docs/scheduler/conditions/skill-conditions.md delete mode 100644 docs/scheduler/conditions/time-conditions.md delete mode 100644 docs/scheduler/defining-conditions.md delete mode 100644 docs/scheduler/event/plugin-schedule-entry-finished-event.md delete mode 100644 docs/scheduler/event/plugin-schedule-entry-soft-stop-event.md delete mode 100644 docs/scheduler/plugin-schedule-entry-merged.md delete mode 100644 docs/scheduler/plugin-writers-guide.md delete mode 100644 docs/scheduler/predicate-condition-examples.md delete mode 100644 docs/scheduler/roadmap.md delete mode 100644 docs/scheduler/schedulable-example-plugin.md delete mode 100644 docs/scheduler/scheduler-plugin.md delete mode 100644 docs/scheduler/tasks/README.md delete mode 100644 docs/scheduler/tasks/enhanced-schedulable-plugin-api.md delete mode 100644 docs/scheduler/tasks/plugin-writers-guide.md delete mode 100644 docs/scheduler/tasks/requirements-integration.md delete mode 100644 docs/scheduler/tasks/requirements-system.md delete mode 100644 docs/scheduler/tasks/task-management-system.md delete mode 100644 docs/scheduler/user-guide.md delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/GroundItemFilterPreset.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/NpcFilterPreset.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/ObjectFilterPreset.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/RenderStyle.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerConfig.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerGroundItemOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerInfoPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerNpcOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerObjectOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerPlugin.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/LocationStartNotificationOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/README.md delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleConfig.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePlugin.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePrePostScheduleRequirements.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePrePostScheduleTasks.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleScript.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/enums/SpellbookOption.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/enums/UnifiedLocation.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/SchedulerConfig.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/SchedulerInfoOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/SchedulerPlugin.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/SchedulerState.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/api/SchedulablePlugin.java delete mode 100755 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/Condition.java delete mode 100755 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ConditionManager.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ConditionType.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/AreaCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/LocationCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/PositionCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/RegionCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/readme.md delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/AreaConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/PositionConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/RegionConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/ui/LocationConditionUtil.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/AndCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/LockCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/LogicalCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/NotCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/OrCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/PredicateCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/enums/UpdateOption.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/serialization/LogicalConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/serialization/NotConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/NpcCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/NpcKillCountCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/ReadMe.md delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/serialization/NpcKillCountConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/BankItemCountCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/GatheredResourceCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/InventoryItemCountCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/LootItemCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ProcessItemCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/README.md delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ResourceCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/BankItemCountConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/GatheredResourceConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/InventoryItemCountConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/LootItemConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/ProcessItemConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/ResourceConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ui/ResourceConditionPanelUtil.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillCondition.java delete mode 100755 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillLevelCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillXpCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/serialization/SkillLevelConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/serialization/SkillXpConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/ui/SkillConditionPanelUtil.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/DayOfWeekCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/IntervalCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/SingleTriggerTimeCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/TimeCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/TimeWindowCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/enums/RepeatCycle.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/DayOfWeekConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/DurationAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/IntervalConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/LocalDateAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/LocalTimeAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/SingleTriggerTimeConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/TimeConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/TimeWindowConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/ui/TimeConditionPanelUtil.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/util/TimeConditionUtil.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/ConditionConfigPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/callback/ConditionUpdateCallback.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/renderer/ConditionTreeCellRenderer.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/util/ConditionConfigPanelUtil.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/VarbitCondition.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/VarbitUtil.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/serialization/VarbitConditionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/ui/VarbitConditionPanelUtil.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ScheduleEntryConfigManager.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ui/HotkeyButton.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ui/ScheduleEntryConfigManagerPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/ExecutionResult.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryMainTaskFinishedEvent.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPostScheduleTaskEvent.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPostScheduleTaskFinishedEvent.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPreScheduleTaskEvent.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPreScheduleTaskFinishedEvent.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntrySoftStopEvent.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/model/PluginScheduleEntry.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/ExcludeTransientAndNonSerializableFieldsStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/ScheduledSerializer.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ConditionManagerAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ConditionTypeAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/PluginScheduleEntryAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ZonedDateTimeAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/AlphaAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigDescriptorAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigGroupAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigInformationAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigItemAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigItemDescriptorAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigSectionAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigSectionDescriptorAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/RangeAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/UnitsAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/AbstractPrePostScheduleTasks.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/README.md delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleRequirements.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleTasks.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/overlay/PrePostScheduleTasksOverlayComponents.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/PrePostScheduleRequirements.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/data/ItemRequirementCollection.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/OrRequirementMode.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementMode.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementPriority.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementType.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/TaskContext.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/registry/RequirementRegistry.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/InventorySetupRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/Requirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/SpellbookRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/collection/LootRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/conditional/ConditionalRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/conditional/OrderedRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/InventorySetupPlanner.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/ItemRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/RunePouchRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/LocationOption.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/LocationRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/ResourceLocationOption.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/logical/LogicalRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/logical/OrRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/ShopItemRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/ShopRequirement.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/CancelledOfferState.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/MultiItemConfig.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/ShopOperation.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/ConditionalRequirementBuilder.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSelector.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSolver.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/state/FulfillmentStep.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/state/TaskExecutionState.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/PrePostScheduleTasksInfoPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/RequirementsStatusPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/TaskExecutionStatePanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/Antiban/AntibanDialogWindow.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/Antiban/AntibanWindowManager.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/PrioritySpinnerEditor.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleFormPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleTableModel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleTablePanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerInfoPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerWindow.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DatePickerPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DateRangePanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DateTimePickerPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/InitialDelayPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/IntervalPickerPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/SingleDateTimePickerPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/TimePickerPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/TimeRangePanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/layout/DynamicFlowLayout.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/util/SchedulerUIUtils.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/util/UIUtils.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/util/PluginFilterUtil.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/util/SchedulerPluginUtil.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/CacheMode.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/MemorySizeCalculator.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2Cache.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2CacheManager.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2GroundItemCache.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2NpcCache.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2ObjectCache.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2QuestCache.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2SkillCache.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2SpiritTreeCache.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2VarPlayerCache.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2VarbitCache.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/SkillData.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/SpiritTreeData.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/VarbitData.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/HoverInfoContainer.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2BaseCacheOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2CacheInfoBoxOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2CacheOverlayManager.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2GroundItemCacheOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2NpcCacheOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2ObjectCacheOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/CacheSerializable.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/CacheSerializationManager.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/QuestAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/QuestStateAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SkillAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SkillDataAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SpiritTreeDataAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SpiritTreePatchAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/VarbitDataAdapter.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/CacheOperations.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/CacheUpdateStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/PredicateQuery.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/QueryCriteria.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/QueryStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/ValueWrapper.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/GroundItemUpdateStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/NpcUpdateStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/ObjectUpdateStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/farming/SpiritTreeUpdateStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/QuestUpdateStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/SkillUpdateStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/VarPlayerUpdateStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/VarbitUpdateStrategy.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/LogOutputMode.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2CacheLoggingUtils.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2GroundItemCacheUtils.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2NpcCacheUtils.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2ObjectCacheUtils.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/farming/SpiritTreeHelper.java delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/area_map.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/calendar-icon.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/chronometer.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/clock.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/delete.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/location_map.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/logic-gate-and.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/logic-gate-or.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/loot_icon.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/not-equal.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/old-watch.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/padlock.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/position.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/region.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/ungroup.png diff --git a/docs/scheduler/README.md b/docs/scheduler/README.md deleted file mode 100644 index ec9084f7638..00000000000 --- a/docs/scheduler/README.md +++ /dev/null @@ -1,195 +0,0 @@ -# Plugin Scheduler System - -## Overview - -The Plugin Scheduler is a sophisticated system that allows for the automatic scheduling and management of plugins based on various conditions. It provides a flexible framework for defining when plugins should start and stop, using a powerful condition-based approach. - -## User Guides - -- **[User Guide](user-guide.md)**: Comprehensive guide on using the Scheduler Plugin UI -- **[Defining Conditions](defining-conditions.md)**: Detailed instructions on setting up start and stop conditions - -## Key Components - -The Plugin Scheduler system consists of several key components: - -1. **[SchedulerPlugin](scheduler-plugin.md)**: The main plugin that manages the scheduling of other plugins. -2. **[PluginScheduleEntry](plugin-schedule-entry-merged.md)**: Represents a scheduled plugin with start and stop conditions. -3. **[ConditionManager](conditions/README.md)**: Manages logical conditions for plugin scheduling in a hierarchical structure. -4. **[Condition](conditions/README.md)**: The base interface for all conditions that determine when plugins should run. -5. **[SchedulablePlugin](schedulable-plugin.md)**: Interface that plugins must implement to be schedulable by the Scheduler. - -## Making Your Plugin Schedulable - -To make your plugin schedulable by the Plugin Scheduler, follow these steps: - - - -1. **Implement the `SchedulablePlugin` interface**: - ```java - public class MyPlugin extends Plugin implements SchedulablePlugin { - // Plugin implementation... - } - ``` - -2. **Define stop conditions**: - ```java - @Override - public LogicalCondition getStopCondition() { - // Create conditions that determine when your plugin should stop - OrCondition orCondition = new OrCondition(); - - // Add time-based condition to stop after 30 minutes - orCondition.addCondition(IntervalCondition.createRandomized( - Duration.ofMinutes(25), - Duration.ofMinutes(30) - )); - - // Add inventory-based condition to stop when inventory is full - // Add other conditions as needed - - return orCondition; - } - ``` - -3. **Define optional start conditions**: - ```java - @Override - public LogicalCondition getStartCondition() { - // Create conditions that determine when your plugin can start - // Return null if the plugin can start anytime - } - ``` - -4. **Implement the soft stop event handler**: - ```java - @Override - @Subscribe - public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - if (event.getPlugin() == this) { - // Save state if needed - // Clean up resources - } - } - ``` - -For complete details about implementing the SchedulablePlugin interface, see the [SchedulablePlugin documentation](schedulable-plugin.md). - -## Condition System - -The heart of the Plugin Scheduler is its condition system, which allows for complex logic to determine when plugins should start and stop. Conditions can be combined using logical operators (AND/OR) to create sophisticated scheduling rules. - -### Condition Types - -The system supports various types of conditions, each serving a specific purpose: - -1. **[Time Conditions](conditions/time-conditions.md)**: Schedule plugins based on time-related factors such as intervals, specific times, or day of week. -2. **[Skill Conditions](conditions/skill-conditions.md)**: Trigger plugins based on skill levels or experience. -3. **[Resource Conditions](conditions/resource-conditions.md)**: Manage plugins based on inventory items, gathered resources, or loot. -4. **[Location Conditions](conditions/location-conditions.md)**: Control plugins based on player position or area. -5. **[NPC Conditions](conditions/npc-conditions.md)**: Trigger plugins based on NPC-related events. -6. **[Logical Conditions](conditions/logical-conditions.md)**: Combine other conditions using logical operators (AND, OR, NOT). - -### Lock Condition - -The `LockCondition` is a special condition that can prevent a plugin from being stopped during critical operations: - -```java -// In your plugin's getStopCondition method: -LockCondition lockCondition = new LockCondition("Critical banking operation in progress", true); -AndCondition andCondition = new AndCondition(); -andCondition.addCondition(orCondition); // Other stop conditions -andCondition.addCondition(lockCondition); // Add the lock condition -return andCondition; -``` - -You can then control the lock in your plugin code: - -```java -// Lock to prevent stopping during critical operations -lockCondition.lock(); - -// Critical operation here... - -// Unlock when safe to stop -lockCondition.unlock(); -``` - -The lock condition ensures that your plugin won't be interrupted during critical operations, such as banking, trading, or complex interactions that should not be interrupted. - -## Start and Stop Conditions - -Each scheduled plugin can have both start and stop conditions: - -- **Start Conditions**: Determine when a plugin should be activated. -- **Stop Conditions**: Determine when a plugin should be deactivated. - -These conditions operate independently, allowing for flexible plugin lifecycle management. The PluginScheduleEntry class manages these conditions through separate ConditionManager instances. - -## Plugin Scheduling Events - -The scheduler uses events to communicate with plugins about their lifecycle: - -- **[Plugin Schedule Entry Soft Stop Event](plugin-schedule-entry-soft-stop-event.md)**: Sent by the scheduler to request plugins to stop gracefully -- **[Plugin Schedule Entry Finished Event](plugin-schedule-entry-finished-event.md)**: Sent by plugins to notify the scheduler they've completed their task - -## Usage Examples - -### Basic Scheduling - -```java -// Schedule a plugin to run every 30 minutes -PluginScheduleEntry entry = new PluginScheduleEntry( - "MyPlugin", - Duration.ofMinutes(30), - true, // enabled - true // allow random scheduling -); -``` - -### Advanced Condition-Based Scheduling - -```java -// Create a schedule entry -PluginScheduleEntry entry = new PluginScheduleEntry("MyPlugin", true); - -// Add a time window condition (run between 9 AM and 5 PM) -entry.addStartCondition(new TimeWindowCondition( - LocalTime.of(9, 0), - LocalTime.of(17, 0) -)); - -// Add a stop condition (stop when inventory is full) -entry.addStopCondition(new InventoryItemCountCondition( - ItemID.ANY, - 28, - -)); - -// Register the scheduled entry -schedulerPlugin.registerScheduledPlugin(entry); -``` - -## Example Implementation - -For a complete example of a schedulable plugin, see the [SchedulableExamplePlugin](schedulable-example-plugin.md), which demonstrates all aspects of making a plugin work with the scheduler system. - -## Further Documentation - -For more detailed information about each component, refer to the specific documentation files: - -- [SchedulerPlugin](scheduler-plugin.md) -- [PluginScheduleEntry](plugin-schedule-entry-merged.md) -- [SchedulablePlugin](api/schedulable-plugin.md) -- [Plugin Schedule Entry Soft Stop Event](plugin-schedule-entry-soft-stop-event.md) -- [Plugin Schedule Entry Finished Event](plugin-schedule-entry-finished-event.md) -- [SchedulableExamplePlugin](schedulable-example-plugin.md) - -For condition-specific documentation: - -- [Time Conditions](conditions/time-conditions.md) -- [Skill Conditions](conditions/skill-conditions.md) -- [Resource Conditions](conditions/resource-conditions.md) -- [Location Conditions](conditions/location-conditions.md) -- [NPC Conditions](conditions/npc-conditions.md) -- [Logical Conditions](conditions/logical-conditions.md) \ No newline at end of file diff --git a/docs/scheduler/api/schedulable-plugin.md b/docs/scheduler/api/schedulable-plugin.md deleted file mode 100644 index e62328e3684..00000000000 --- a/docs/scheduler/api/schedulable-plugin.md +++ /dev/null @@ -1,796 +0,0 @@ -# SchedulablePlugin Interface - -## Overview - -The `SchedulablePlugin` interface is the core integration point between standard RuneLite plugins and the Microbot Plugin Scheduler system. It defines a contract that plugins must implement to participate in the automated scheduling system, allowing them to be started and stopped based on configurable conditions while maintaining control over their execution lifecycle. - -This interface leverages the event-based architecture of RuneLite to enable communication between the scheduler and plugins, with methods that define start and stop conditions, handle state transitions, and provide mechanisms for critical section protection. - -## Interface Architecture - -The `SchedulablePlugin` interface is designed with a combination of required methods that must be implemented by each plugin and default methods that provide standardized behavior. This approach allows plugins to focus on their specific scheduling requirements while inheriting common functionality from the interface. - -The interface follows these core design principles: - -1. **Condition-based Scheduling**: Uses logical conditions to determine when plugins should start or stop -2. **Event-driven Communication**: Relies on RuneLite's event system for lifecycle notifications -3. **Graceful Termination**: Provides mechanisms for both soft and hard stops -4. **Critical Section Protection**: Implements a locking system to prevent interruption during sensitive operations - -## Key Methods - -### Essential Methods for Implementation - -#### `void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event)` - -This method handles the post-schedule task event from the scheduler, which is triggered when stop conditions are met or when manual stop is initiated. While the SchedulablePlugin interface provides a default no-op implementation, plugins should override this method to ensure they stop gracefully, preserving state and cleaning up resources before terminating. - -The implementation is expected to: - -1. Verify the event is targeted at this specific plugin -2. Save any current state if needed -3. Clean up resources and close interfaces -4. Schedule the actual stop on the RuneLite client thread -5. Disable and stop the plugin through the Microbot plugin manager - -### Start and Stop Condition Methods - -#### `LogicalCondition getStartCondition()` - -Defines when a plugin is eligible to start. The default implementation returns an empty `AndCondition`, meaning the plugin can start at any time if no specific condition is provided. Plugins can override this to specify precise starting conditions like time windows, player locations, or game states. - -The scheduler evaluates this condition before starting the plugin. If it returns `null` or the condition is met, the plugin is eligible to start. - -#### `LogicalCondition getStopCondition()` - -Specifies when a plugin should stop running. The default implementation returns an empty `AndCondition`, meaning the plugin would run indefinitely unless manually stopped. Plugins should override this to define appropriate stop conditions, which might include: - -- Time limits or specific time windows -- Resource counts (items collected, XP gained) -- Player state (inventory full, health low) -- Game state (logged out, in combat) - -The scheduler continuously monitors these conditions while the plugin runs. - -### State Management Methods - -#### `void onStopConditionCheck()` - -This method is called periodically (approximately once per second) while the plugin is running, just before the stop conditions are evaluated. Its purpose is to allow plugins to update any dynamic state information that might affect condition evaluation. - -This is particularly useful when conditions depend on changing game state, such as inventory contents or skill levels. Plugins can use this hook to keep condition evaluation accurate without having to implement separate timer logic. - -#### `void reportFinished(String reason, boolean success)` - -Provides a way for plugins to proactively indicate completion without waiting for stop conditions to trigger. This method posts a `PluginScheduleEntryMainTaskFinishedEvent` that notifies the scheduler the plugin has finished its task. - -The implementation handles various edge cases: - -- If the scheduler isn't loaded, it directly stops the plugin -- If no plugin is currently running in the scheduler, it stops itself -- If another plugin is running, it gracefully handles the mismatch - -This method is commonly used when a plugin has met its objective (like completing a quest) or encountered a situation where it cannot continue (like running out of resources). - -#### `boolean allowHardStop()` - -Indicates whether a plugin supports being forcibly terminated if it doesn't respond to a soft stop request. The default implementation returns `false`, meaning plugins will only be stopped gracefully. Plugins can override this to allow hard stops in specific situations. - -### Lock Management Methods - -The interface includes a comprehensive locking system that prevents plugins from being stopped during critical operations. - -#### `LockCondition getLockCondition(Condition stopConditions)` - -Retrieves the lock condition associated with a plugin's stop conditions. The default implementation recursively searches through the condition structure to find a `LockCondition` instance. - -#### `boolean isLocked(Condition stopConditions)` - -Checks if the plugin is currently locked, preventing it from being stopped. - -#### `boolean lock(Condition stopConditions)` - -Activates the lock to prevent the plugin from being stopped. Returns `true` if successful. - -#### `boolean unlock(Condition stopConditions)` - -Deactivates the lock, allowing the plugin to be stopped when conditions are met. Returns `true` if successful. - -#### `Boolean toggleLock(Condition stopConditions)` - -Toggles the lock state and returns the new state (`true` for locked, `false` for unlocked). - -## Stop Mechanisms - -Plugins can be stopped through various mechanisms: - -1. **Manual Stop**: User explicitly stops the plugin - - Appears as `StopReason.MANUAL_STOP` - - Highest priority, will always attempt to stop - - Flow: User Interface → SchedulerPlugin.forceStopCurrentPluginScheduleEntry() → PluginScheduleEntry.stop() → Plugin stops - -2. **Plugin Finished**: Plugin self-reports completion using `reportFinished()` - - Appears as `StopReason.PLUGIN_FINISHED` - - Indicates normal completion - - Flow: Plugin.reportFinished() → PluginScheduleEntryMainTaskFinishedEvent → SchedulerPlugin.onPluginScheduleEntryMainTaskFinishedEvent() → PluginScheduleEntry.stop() → Plugin stops - -3. **Stop Conditions Met**: When plugin or user-defined stop conditions are satisfied - - Appears as `StopReason.SCHEDULED_STOP` - - Follows the soft-stop/hard-stop pattern - - Flow: SchedulerPlugin.checkCurrentPlugin() → PluginScheduleEntry.checkConditionsAndStop() → PluginScheduleEntry.softStop() → PluginScheduleEntryPostScheduleTaskEvent → Plugin.onPluginScheduleEntryPostScheduleTaskEvent() → Plugin stops - -4. **Error**: An exception occurs in the plugin - - Appears as `StopReason.ERROR` - - Immediate stop without soft-stop sequence - - Flow: Exception → PluginScheduleEntry.setLastStopReasonType(ERROR) → Plugin disabled → SchedulerPlugin returns to SCHEDULING state - -5. **Hard Stop**: Forced termination after soft-stop timeout - - Appears as `StopReason.HARD_STOP` - - Last resort when plugin doesn't respond to soft-stop - - Flow: Timeout after soft-stop → PluginScheduleEntry.hardStop() → Microbot.stopPlugin() → Plugin forcibly terminated - -## Integration with Scheduler Architecture - -The `SchedulablePlugin` interface integrates with the broader scheduler architecture in several key ways: - -1. **Plugin Registry**: The scheduler maintains a registry of `PluginScheduleEntry` objects, each referencing a `Plugin` that implements `SchedulablePlugin`. - -2. **Condition Management**: The scheduler continuously evaluates both start and stop conditions through a `ConditionManager` that separates plugin-defined conditions from user-defined ones. - -3. **Event Communication**: The scheduler posts events like `PluginScheduleEntryPostScheduleTaskEvent` to initiate plugin stops, and receives events like `PluginScheduleEntryMainTaskFinishedEvent` when plugins self-report completion. - -4. **Lifecycle Management**: The scheduler controls when plugins are enabled or disabled based on their schedule and conditions, but delegates the actual stopping process to the plugins themselves through the interface methods. - -The relationship can be visualized as follows: - -```ascii -┌─────────────────────┐ schedules ┌───────────────────┐ -│ SchedulerPlugin ├────────────────────â”Ī PluginScheduleEntry│ -│ (Orchestrator) │ │ (Data Model) │ -└─────────┮───────────┘ └─────────┮─────────┘ - │ │ - │ manages │ - │ │ - ▾ ▾ -┌─────────────────────┐ implements ┌───────────────────────┐ -│ Regular RuneLite │◄───────────────â”Ī SchedulablePlugin │ -│ Plugin │ │ (API) │ -└─────────────────────┘ └───────────────────────┘ -``` - -When the scheduler is running: - -1. The `SchedulerPlugin` periodically checks each registered `PluginScheduleEntry` -2. If a plugin implements `SchedulablePlugin`, its conditions are retrieved and evaluated -3. The `SchedulerPlugin` makes decisions about starting/stopping based on these conditions -4. Events are sent back to the plugin through interface methods like `onPluginScheduleEntryPostScheduleTaskEvent` - -## Plugin Conditions vs. User Conditions - -An important concept in the scheduler system is the distinction between plugin conditions and user conditions: - -### Plugin Conditions - -- **Source**: Defined programmatically by implementing `getStartCondition()` and `getStopCondition()` -- **Purpose**: Express the plugin's intrinsic requirements and business logic -- **Control**: Controlled by the plugin developer -- **Example**: A mining plugin might define "stop when inventory is full" as a plugin condition because it's fundamental to the plugin's functionality -- **Default Behavior**: When a plugin doesn't define specific conditions (returns empty `AndCondition`), it has no inherent restrictions on when it can start or stop - -### User Conditions - -- **Source**: Added through UI or configuration by the end user -- **Purpose**: Express user preferences and personalization -- **Control**: Controlled by the end user -- **Example**: A user might add "only run between 8pm-2am" as a user condition because it's their preferred play time -- **Default Behavior**: If no user conditions are defined, the plugin will run continuously until manually stopped - -### How They Work Together - -The `PluginScheduleEntry` class maintains both sets of conditions using separate logical structures: - -1. **Start Logic**: Plugin AND User start conditions must be met for the plugin to start -2. **Stop Logic**: Plugin OR User stop conditions must be met for the plugin to stop - -This gives both the plugin developer and the end user appropriate control while ensuring proper plugin operation: - -- The plugin can't start unless both the plugin requirements AND user preferences are satisfied -- The plugin will stop if EITHER the plugin determines it should stop OR the user's conditions determine it should stop - -### Full User Control Scenario - -When a plugin implements `SchedulablePlugin` but doesn't override `getStartCondition()` or `getStopCondition()` (or returns empty conditions), the execution is fully controlled by user-defined conditions: - -1. **Starting**: The plugin can start any time, but only when user-defined start conditions are met -2. **Stopping**: The plugin will only stop when user-defined stop conditions are met or the user manually stops it -3. **Self-Reporting**: Even without defined conditions, the plugin can still use `reportFinished()` to signal completion - -This design allows plugins to be made schedulable with minimal implementation effort while still giving users complete control over when they run. - -## Implementation Guidelines - -### Lock Condition Management - -Critical operations in plugins should be protected with the locking mechanism: - -1. Create a `LockCondition` in your stop condition structure -2. Call `lock()` before entering critical sections -3. Always call `unlock()` in a finally block to ensure the lock is released -4. Avoid long-running operations while locked, as this prevents the scheduler from stopping the plugin - -### Condition State Updates - -For plugins with dynamic stop conditions: - -1. Override `onStopConditionCheck()` to update condition state -2. Keep these updates lightweight and focused -3. Avoid heavy computation or network operations -4. Update counters, flags, or other simple state variables - -### Graceful Stopping - -When implementing the stop event handler: - -1. Check that the event is intended for this plugin -2. Save any critical state information -3. Close any open interfaces or dialogs -4. Release resources and cancel any pending operations -5. Use the client thread to ensure thread safety - -## Event-Driven Communication Mechanism - -The Plugin Scheduler system relies heavily on RuneLite's event bus for communication between components. Two primary events facilitate this communication: - -### 1. PluginScheduleEntryPostScheduleTaskEvent - -This event represents a request from the scheduler to a plugin asking it to gracefully stop execution. - -**Flow:** - -1. **Event Creation:** When stop conditions are met, PluginScheduleEntry.softStop() creates the event -2. **Event Posting:** The event is posted to the RuneLite EventBus -3. **Event Handling:** The target plugin's onPluginScheduleEntryPostScheduleTaskEvent() method is called -4. **Response:** The plugin performs cleanup operations and stops itself - -**Example:** - -```java -// Inside PluginScheduleEntry.softStop() -Microbot.getClientThread().runOnSeperateThread(() -> { - Microbot.getEventBus().post(new PluginScheduleEntryPostScheduleTaskEvent(plugin, ZonedDateTime.now())); - return false; -}); - -// Inside the plugin implementation -@Subscribe -@Override -public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - if (event.getPlugin() == this) { - // Cleanup operations - Microbot.getClientThread().invokeLater(() -> { - Microbot.stopPlugin(this); - }); - } -} -``` - -### 2. PluginScheduleEntryMainTaskFinishedEvent - -This event allows plugins to proactively inform the scheduler that they have completed their task and should be stopped. - -**Flow:** - -1. **Event Creation:** Plugin calls reportFinished() when its task is complete -2. **Validation:** reportFinished() verifies the scheduler is active and this plugin is the current running plugin -3. **Event Posting:** A PluginScheduleEntryMainTaskFinishedEvent is posted to the RuneLite EventBus -4. **Event Handling:** SchedulerPlugin.onPluginScheduleEntryMainTaskFinishedEvent() processes the event -5. **Plugin Stopping:** The scheduler initiates a stop with PLUGIN_FINISHED reason - -**Example:** - -```java -// Inside the plugin when a task is complete -reportFinished("Mining task completed - inventory full", true); - -// Inside the reportFinished() method -Microbot.getEventBus().post(new PluginScheduleEntryMainTaskFinishedEvent( - (Plugin) this, - "Plugin [" + this.getClass().getSimpleName() + "] finished: " + reason, - success -)); - -// Inside the SchedulerPlugin -@Subscribe -public void onPluginScheduleEntryMainTaskFinishedEvent(PluginScheduleEntryMainTaskFinishedEvent event) { - if (currentPlugin != null && event.getPlugin() == currentPlugin.getPlugin()) { - // Stop the plugin with the success state from the event - currentPlugin.stop(event.isSuccess(), StopReason.PLUGIN_FINISHED, event.getReason()); - } -} -``` - -### Self-Reporting Plugin Completion - -The `reportFinished()` method is a key component that allows plugins to proactively signal completion. It works as follows: - -1. **Validation Checks:** - - Verifies the SchedulerPlugin is loaded - - Confirms there's a current plugin running - - Ensures the current plugin matches this plugin instance - -2. **Event Creation and Posting:** - - Creates a PluginScheduleEntryMainTaskFinishedEvent with: - - Reference to the plugin - - Formatted reason message - - Success status flag - - Posts the event to the RuneLite EventBus - -3. **Fallback Handling:** - - If validation checks fail, the plugin stops itself directly - - This ensures plugins can always stop themselves, even if the scheduler isn't functioning properly - -The self-reporting mechanism gives plugins control over their lifecycle while still maintaining the scheduler's orchestration role. - -## Technical Notes - -### Thread Safety Considerations - -The `SchedulablePlugin` interface methods may be called from different threads: - -- Events are typically dispatched on the RuneLite event thread -- Condition checks are called from the scheduler's timer thread -- Plugin operations should be performed on the client thread for game state interactions - -Proper thread management is essential for stable operation. Use `Microbot.getClientThread().invokeLater()` for game state interactions. - -### Serialization Impact - -The `PluginScheduleEntry` class handles serialization of plugin schedules, but plugin instances themselves are marked as `transient`. This means: - -1. Any plugin-specific state must be managed separately -2. Plugin references are re-established when schedules are loaded -3. Condition objects are serialized and deserialized, so they should be designed with serialization in mind - - -## Best Practices - -When implementing the `SchedulablePlugin` interface, consider the following best practices: - -1. **Clear Conditions**: Define explicit start and stop conditions that clearly express your plugin's requirements. - -2. **Respect Soft Stops**: Implement `onPluginScheduleEntryPostScheduleTaskEvent` to clean up resources properly. - -3. **Use Locks Carefully**: Lock your plugin only during critical operations that should not be interrupted. - -4. **Self-Report Completion**: Use `reportFinished()` when your plugin naturally completes its task. - -5. **Handle Time Appropriately**: Include time-based conditions in your stop conditions to ensure your plugin doesn't run indefinitely. - -6. **Update Conditions**: Use `onStopConditionCheck()` to refresh dynamic conditions based on changing game state. - -## Component Relationships - -The `SchedulablePlugin` interface is part of a larger system that enables automatic scheduling of plugins. Here's how the components interact: - -### System Architecture - -```ascii -┌───────────────────────────────┐ -│ SchedulerPlugin │ -│ (Central Orchestrator) │ -├───────────────────────────────â”Ī -│ - Manages scheduling cycle │ -│ - Handles state transitions │ -│ - Evaluates conditions │ -│ - Starts & stops plugins │ -└─────────────────┮─────────────┘ - │ manages - │ multiple - ▾ -┌───────────────────────────────┐ -│ PluginScheduleEntry │ -│ (Data Model) │ -├───────────────────────────────â”Ī -│ - Stores config & state │ -│ - Tracks execution metrics │ -│ - Contains conditions │ -│ - References plugin instance │ -└─────────────────┮─────────────┘ - │ references - │ - ▾ -┌───────────────────────────────┐ ┌───────────────────────────┐ -│ Plugin implementing │ │ │ -│ SchedulablePlugin │◄────────â”Ī Regular RuneLite │ -│ (Plugin API Contract) │implements│ Plugin │ -├───────────────────────────────â”Ī ├───────────────────────────â”Ī -│ - Defines start/stop logic │ │ - Standard RuneLite │ -│ - Handles events │ │ plugin functionality │ -│ - Reports completion │ │ │ -└───────────────────────────────┘ └───────────────────────────┘ -``` - -### Component Responsibilities - -#### 1. SchedulerPlugin (Orchestrator) - -- **Primary Role:** Central controller that manages the entire scheduling system -- **Responsibilities:** - - Maintains the scheduler's state machine (16 distinct states) - - Executes the scheduling algorithm to determine which plugin runs next - - Processes condition evaluations to start/stop plugins - - Manages integration with other systems (BreakHandler, AutoLogin) - - Provides UI for configuration and monitoring - -#### 2. PluginScheduleEntry (Data Model) - -- **Primary Role:** Container for plugin scheduling configuration and execution state -- **Responsibilities:** - - Stores start and stop conditions for a specific plugin - - Tracks execution metrics (run count, duration, last run time) - - Maintains plugin priority and randomization settings - - Records state information (enabled/disabled, running/stopped) - - Handles watchdog functionality to monitor plugin execution - -#### 3. SchedulablePlugin (API Contract) - -- **Primary Role:** Interface implemented by plugins to participate in scheduling -- **Responsibilities:** - - Defines plugin-specific start and stop conditions - - Responds to scheduler events (start request, soft stop, hard stop) - - Reports task completion back to the scheduler - - Protects critical sections during execution - - Provides hooks for condition evaluation - -### Data Flow Between Components - -1. **Registration Flow:** - - ```text - Plugin implements SchedulablePlugin → User configures in UI → - SchedulerPlugin creates PluginScheduleEntry → Entry stored in scheduler - ``` - -2. **Startup Flow:** - - ```text - SchedulerPlugin evaluates conditions → Matches found → SchedulerPlugin references - PluginScheduleEntry → Entry points to Plugin → Plugin starts - ``` - -3. **Stopping Flow:** - - ```text - Stop conditions met → SchedulerPlugin posts event → - Plugin's onPluginScheduleEntryPostScheduleTaskEvent handler called → - Plugin stops gracefully → PluginScheduleEntry updated - ``` - -4. **Self-Completion Flow:** - - ```text - Plugin determines it's finished → Plugin calls reportFinished() → - SchedulerPlugin processes finish event → Updates PluginScheduleEntry → - Selects next plugin - ``` - -### Integration Points - -1. **Condition System Integration:** - - - `SchedulablePlugin.getStartCondition()` - Plugin defines when it can start - - `SchedulablePlugin.getStopCondition()` - Plugin defines when it should stop - - These combine with user-configured conditions in the PluginScheduleEntry - -2. **Event System Integration:** - - - `PluginScheduleEntryPostScheduleTaskEvent` - Sent from scheduler to plugin requesting stop - - `PluginScheduleEntryMainTaskFinishedEvent` - Sent from plugin to scheduler reporting completion - -3. **State Protection Integration:** - - - `SchedulablePlugin.lockPlugin()` - Prevents scheduler from stopping during critical operations - - `SchedulablePlugin.unlockPlugin()` - Releases lock when safe to stop - -## Practical Implementation - -When implementing the `SchedulablePlugin` interface in your plugin, consider the following workflow: - -1. **Define Start and Stop Conditions:** - - ```java - @Override - public LogicalCondition getStartCondition() { - AndCondition startConditions = new AndCondition(); - startConditions.addCondition(new TimeWindowCondition( - LocalTime.of(9, 0), LocalTime.of(17, 0) - )); - startConditions.addCondition(new InventoryItemCountCondition( - ItemID.COINS, 1000, ComparisonType.MORE_THAN - )); - return startConditions; - } - - @Override - public LogicalCondition getStopCondition() { - OrCondition stopConditions = new OrCondition(); - stopConditions.addCondition(new InventoryItemCountCondition( - ItemID.DRAGON_BONES, 28 - )); - stopConditions.addCondition(new PlayerHealthCondition( - 10, ComparisonType.LESS_THAN_OR_EQUAL - )); - return stopConditions; - } - ``` - -2. **Implement Stop Event Handler:** - - ```java - @Subscribe - public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - // Ensure this event is for our plugin - if (event.getPlugin() != this) { - return; - } - - // Save state if needed - saveCurrentProgress(); - - // Log the stop reason - log.info("Plugin stopping due to: " + event.getReason()); - - // Allow the plugin to be stopped - Microbot.stopPlugin(this); - } - ``` - -3. **Report Completion When Done:** - - ```java - private void checkTaskCompletion() { - if (isTaskComplete()) { - // Report we're done to the scheduler - reportFinished("Task completed successfully", true); - } else if (isTaskFailed()) { - reportFinished("Task failed: out of resources - inventory setup dont match", false); //USE with CAUTION only if you want your plugin is not started agin by the scheduler plugin can not be started again by the scheduler until it is enable again by the user in the scheduler plan (UI) - } - } - ``` - -4. **Protect Critical Sections:** - - ```java - @Override - public void onGameTick() { - try { - // Lock to prevent interruption during critical operation - lockPlugin(); - - // Perform sensitive operation that shouldn't be interrupted - performBankTransaction(); - } finally { - // Always unlock when done - unlockPlugin(); - } - } - ``` - -## How the Scheduler Selects the Next Plugin - -The Plugin Scheduler uses a sophisticated algorithm to determine which plugin to run next. Understanding this algorithm helps when configuring your plugin's schedule settings. - -### Multi-Factor Selection Algorithm - -```text -START SELECTION - Filter out disabled plugins - Filter out plugins currently running - Group remaining plugins by priority (highest first) - - FOR EACH priority group: - Split into non-default and default plugins - - IF non-default plugins exist: - Evaluate start conditions for each plugin - Separate into "can start" and "cannot start" groups - IF "can start" group is not empty: - IF randomization enabled: - Apply weighted random selection based on run count - ELSE: - Select first plugin - ELSE: - Continue to next sub-group - - IF default plugins exist AND no non-default plugin selected: - Evaluate start conditions for each plugin - Separate into "can start" and "cannot start" groups - IF "can start" group is not empty: - IF randomization enabled: - Apply weighted random selection based on run count - ELSE: - Select first plugin - ELSE: - Continue to next priority group - - IF no plugin selected: - Return null (scheduler will enter BREAK state) - ELSE: - Return selected plugin -END SELECTION -``` - -### Selection Factors (in order of importance) - -1. **Plugin Priority**: Higher priority plugins (larger number) are always evaluated first -2. **Plugin Type**: Non-default plugins take precedence over default plugins -3. **Start Conditions**: Only plugins whose start conditions are met are considered -4. **Randomization Setting**: Controls whether selection within a group is deterministic or random -5. **Run Count Balance**: Less frequently run plugins get higher weighting during random selection - -### Weighting Formula for Random Selection - -When randomization is enabled, plugins are selected using a weighted algorithm: - -```java -// For each plugin in the eligible group -double weight = BASE_WEIGHT; - -// Adjust based on how often this plugin has run compared to others -if (averageRunCount > 0 && plugin.getRunCount() < averageRunCount) { - // Plugin has run less than average, increase its chance of selection - double runCountRatio = (double) plugin.getRunCount() / averageRunCount; - weight += (1.0 - runCountRatio) * RUN_COUNT_WEIGHT; -} - -// Plugins with higher priority get a slight boost even within their priority group -weight += (plugin.getPriority() - baseGroupPriority) * PRIORITY_BONUS; - -// Add weight to selection map -weightMap.put(plugin, weight); -``` - -This approach ensures all plugins get fair execution time while still respecting priorities. - -## State Transition Flow and Condition Evaluation - -The scheduler manages a complex state machine that determines when and how plugins are executed. Here's how your plugin interacts with this state machine: - -### Key State Transitions - -1. **SCHEDULING → STARTING_PLUGIN**: - - Triggered when: Your plugin is selected to run next - - Requirements: All start conditions must be met - - Actions: Scheduler enables your plugin and starts watching it - -2. **STARTING_PLUGIN → RUNNING_PLUGIN**: - - Triggered when: Your plugin activates successfully - - Requirements: Plugin starts without errors - - Actions: Scheduler begins monitoring stop conditions - -3. **RUNNING_PLUGIN → SOFT_STOPPING_PLUGIN**: - - Triggered when: Any stop condition is met OR your plugin calls reportFinished() - - Actions: Scheduler sends PluginScheduleEntryPostScheduleTaskEvent to your plugin - -4. **SOFT_STOPPING_PLUGIN → HARD_STOPPING_PLUGIN**: - - Triggered when: Your plugin doesn't stop within timeout period - - Actions: Scheduler forcibly disables the plugin - -### Condition Evaluation Cycle - -```text -While in RUNNING_PLUGIN state: - Every ~1 second: - Call plugin.onStopConditionCheck() to refresh condition state - Evaluate plugin's stop conditions - Evaluate user-defined stop conditions - IF any condition is true: - Transition to SOFT_STOPPING_PLUGIN - Send soft stop event to plugin - ELSE: - Continue monitoring -``` - -This dual-layer condition system (plugin-defined + user-defined) provides maximum flexibility while maintaining plugin control over its execution criteria. - -## Best Practices for SchedulablePlugin Implementation - -To make the most of the Scheduler system, follow these guidelines when implementing `SchedulablePlugin`: - -1. **Use Specific Conditions**: Define clear, specific conditions that accurately represent when your plugin should start and stop. - - ```java - // Good: Specific condition - new InventoryItemCountCondition(ItemID.DRAGON_BONES, 28) - - // Avoid: Overly general condition - new TimeElapsedCondition(Duration.ofMinutes(30)) - ``` - -2. **Implement Graceful Stopping**: Always handle the `onPluginScheduleEntryPostScheduleTaskEvent` properly to ensure your plugin can be stopped cleanly. - - ```java - @Subscribe - public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - if (event.getPlugin() != this) { - return; - } - - // Save progress - savePluginState(); - - // Release resources - cleanupResources(); - - // Stop the plugin - Microbot.stopPlugin(this); - } - ``` - -3. **Use Critical Section Protection**: Lock the plugin during critical operations to prevent interruption. - - ```java - // Banking is a critical operation that shouldn't be interrupted - try { - lockPlugin(); - performBankingOperation(); - } finally { - unlockPlugin(); - } - ``` - -4. **Self-Report Completion**: Use `reportFinished()` when your plugin completes its task or encounters a situation where it should stop. - - ```java - if (isInventoryFull() && task.isComplete()) { - reportFinished("Task completed successfully", true); - return; - } - - if (isOutOfSupplies()) { - reportFinished("Out of required supplies", false); - return; - } - ``` - -5. **Update Condition State**: Implement `onStopConditionCheck()` to refresh any dynamic condition state. - - ```java - @Override - public void onStopConditionCheck() { - // Update our cached values that conditions might use - this.currentPlayerHealth = getPlayerHealth(); - this.nearbyEnemyCount = countNearbyEnemies(); - } - ``` - -6. **Design for Recovery**: Make your plugin resilient to interruptions by saving state periodically. - -7. **Balance Priorities**: Set appropriate priorities for your plugin to ensure it runs when most appropriate. - - - High priority (50+): Critical scripts that should run first - - Medium priority (20-49): Standard task scripts - - Low priority (1-19): Background or maintenance scripts - - Default priority (0): Last to run - -8. **Consider Randomization**: Enable randomization for most plugins to create more natural bot behavior patterns. - -## Conclusion - -The `SchedulablePlugin` interface serves as the foundation of Microbot's advanced plugin scheduling system, enabling sophisticated automation workflows with natural-looking behavior patterns. By implementing this interface, your plugins become part of an intelligent ecosystem that can coordinate multiple activities, respect constraints, and adapt to changing conditions. - -Key benefits of using the scheduler system include: - -- Reduced detection risk through coordinated breaks and natural activity patterns -- Lower development burden by leveraging shared infrastructure for timing and conditions -- More sophisticated automation flows by chaining plugins together -- Better user experience through consistent configuration and monitoring - -For comprehensive examples of `SchedulablePlugin` implementations, refer to the following reference plugins: - -- `WoodcuttingPlugin`: Demonstrates basic resource gathering with simple conditions -- `FletchingPlugin`: Shows workflow stages with progress reporting -- `CombatPlugin`: Illustrates complex condition structures and critical section protection - -These reference implementations provide patterns you can adapt for your own plugins to integrate seamlessly with the scheduler system. - diff --git a/docs/scheduler/combat-lock-examples.md b/docs/scheduler/combat-lock-examples.md deleted file mode 100644 index 3d064619172..00000000000 --- a/docs/scheduler/combat-lock-examples.md +++ /dev/null @@ -1,307 +0,0 @@ -## LockCondition Usage in Combat Plugins - -Combat and bossing plugins often require careful management of the LockCondition to prevent interruption during critical combat sequences. This section provides examples and best practices for using LockCondition in combat scenarios. - -### Example: Boss Fight Lock Management - -For a boss fight plugin, you would typically want to lock the plugin when the fight begins and unlock it when the fight ends, similar to how the GotrPlugin manages lock state: - -```java -public class BossPlugin extends Plugin implements SchedulablePlugin { - - private LockCondition lockCondition; - private LogicalCondition stopCondition = null; - - @Override - public LogicalCondition getStopCondition() { - if (this.stopCondition == null) { - this.stopCondition = createStopCondition(); - } - return this.stopCondition; - } - - private LogicalCondition createStopCondition() { - // Create a lock condition specifically for boss combat - this.lockCondition = new LockCondition("Locked during boss fight"); //ensure unlock on shutdown of the plugin ! - - // Create stop conditions - OrCondition stopTriggers = new OrCondition(); - - // Add various stop conditions (resource depletion, time limit, etc.) - stopTriggers.addCondition(new InventoryItemCountCondition("Prayer potion", 0, true)); - // NOTE: HealthPercentCondition is not yet implemented - this is a placeholder - // stopTriggers.addCondition(new HealthPercentCondition(15)); - - // Use SingleTriggerTimeCondition for a time limit (60 minutes from now) - stopTriggers.addCondition(SingleTriggerTimeCondition.afterDelay(60 * 60)); // 60 minutes in seconds - - // Combine the stop triggers with the lock condition using AND logic - // This ensures the plugin won't stop if locked, even if other conditions are met - AndCondition andCondition = new AndCondition(); - andCondition.addCondition(stopTriggers); - andCondition.addCondition(lockCondition); - - return andCondition; - } - - @Subscribe - public void onChatMessage(ChatMessage chatMessage) { - if (chatMessage.getType() != ChatMessageType.GAMEMESSAGE) { - return; - } - - String message = chatMessage.getMessage(); - - // Lock the plugin when the boss fight begins - if (message.contains("You've entered the boss arena.") || - message.contains("The boss has appeared!")) { - if (lockCondition != null) { - lockCondition.lock(); - log.debug("Boss fight started - locked plugin"); - } - } - // Unlock the plugin when the boss fight ends - else if (message.contains("Congratulations, you've defeated the boss!") || - message.contains("You have been defeated.")) { - if (lockCondition != null) { - lockCondition.unlock(); - log.debug("Boss fight ended - unlocked plugin"); - } - } - } - - @Subscribe - public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - if (event.getPlugin() == this) { - log.info("Scheduler requesting plugin shutdown"); - - // Setup a scheduled task to check if it's safe to exit - ScheduledExecutorService exitExecutor = Executors.newSingleThreadScheduledExecutor(); - exitExecutor.scheduleWithFixedDelay(() -> { - try { - // Check if we're in a critical phase (boss fight) - if (lockCondition != null && lockCondition.isLocked()) { - log.info("Cannot exit during boss fight - waiting for fight to end"); - return; // Try again later - } - - // Safe to exit, perform cleanup - log.info("Safe to exit - performing cleanup"); - leaveBossArea(); // Method to safely teleport away or exit - - // Stop the plugin - Microbot.getClientThread().invokeLater(() -> { - Microbot.stopPlugin(this); - return true; - }); - - // Shutdown the executor - exitExecutor.shutdown(); - } catch (Exception ex) { - log.error("Error during safe exit", ex); - // Force stop in case of error - Microbot.getClientThread().invokeLater(() -> { - Microbot.stopPlugin(this); - return true; - }); - - // Shutdown the executor - exitExecutor.shutdown(); - } - }, 0, 2, java.util.concurrent.TimeUnit.SECONDS); - } - } - - /** - * Safely leaves the boss area, e.g., via teleport or exit portal - */ - private void leaveBossArea() { - // Implementation to safely leave the boss area - // This might involve clicking an exit portal, using a teleport item, etc. - } -} -``` - -### Combat-Specific Lock Patterns - -When developing combat plugins, consider these lock patterns: - -1. **Phase-Based Locking**: Lock during specific boss phases that shouldn't be interrupted - -```java -@Subscribe -public void onNpcChanged(NpcChanged event) { - NPC npc = event.getNpc(); - - // Detect phase change on the boss - if (npc.getId() == BOSS_ID && npc.getAnimation() == PHASE_CHANGE_ANIM) { - // Lock during the special phase - lockCondition.lock(); - - // Schedule unlock after the phase should be complete - ScheduledExecutorService phaseExecutor = Executors.newSingleThreadScheduledExecutor(); - phaseExecutor.schedule(() -> { - lockCondition.unlock(); - phaseExecutor.shutdown(); - }, 30, java.util.concurrent.TimeUnit.SECONDS); - } -} -``` - -2. **Special Attack Locking**: Lock during special attack execution - -```java -private void executeSpecialAttack() { - try { - // Lock before starting the special attack sequence - lockCondition.lock(); - - // Perform special attack actions - Rs2Combat.toggleSpecialAttack(true); - Rs2Equipment.equipItem("Dragon dagger(p++)", "Wield"); - Rs2Npc.interact(targetNpc, "Attack"); - - // Wait for animation to complete - Global.sleepUntil(() -> Rs2Player.getAnimationId() == -1, 3000); - - // Switch back to main weapon - Rs2Equipment.equipItem("Abyssal whip", "Wield"); - } finally { - // Always unlock when the sequence is complete - lockCondition.unlock(); - } -} -``` - -3. **Prayer Flicking Lock**: Lock during prayer flicking sequences - -```java -private void startPrayerFlicking() { - // Lock while the prayer flicking routine is active - lockCondition.lock(); - - ScheduledExecutorService flickingExecutor = Executors.newSingleThreadScheduledExecutor(); - prayerFlickingFuture = flickingExecutor.scheduleAtFixedRate(() -> { - try { - // Detect if we should stop flicking - // Note: HealthPercentCondition is not implemented yet, this is just an example - // This would need to be replaced with actual health checking logic - if (/* Rs2Player.getHealthPercent() < 25 || */ !inCombat()) { - stopPrayerFlicking(); - return; - } - - // Toggle the appropriate prayers based on boss attacks - toggleProtectionPrayers(); - } catch (Exception e) { - log.error("Error in prayer flicking", e); - stopPrayerFlicking(); - } - }, 0, 600, java.util.concurrent.TimeUnit.MILLISECONDS); -} - -private void stopPrayerFlicking() { - if (prayerFlickingFuture != null) { - prayerFlickingFuture.cancel(false); - prayerFlickingFuture = null; - } - - // Turn off prayers - Rs2Prayer.toggle(Prayer.PROTECT_FROM_MAGIC, false); - Rs2Prayer.toggle(Prayer.PROTECT_FROM_MISSILES, false); - Rs2Prayer.toggle(Prayer.PROTECT_FROM_MELEE, false); - - // Unlock after prayer flicking stops - lockCondition.unlock(); -} -``` - -### Best Practices for Combat Lock Management - -1. **Always use try-finally blocks** when manually locking to ensure the lock is released even if exceptions occur: - -```java -try { - lockCondition.lock(); - // Critical combat sequence -} finally { - lockCondition.unlock(); -} -``` - -2. **Keep lock scopes narrow** - only lock during truly critical operations: - -```java -// BAD: Locking for the entire method -public void doBossFight() { - lockCondition.lock(); - setupGear(); - walkToBoss(); - fightBoss(); // Only this part is truly critical - collectLoot(); - lockCondition.unlock(); -} - -// GOOD: Locking only the critical part -public void doBossFight() { - setupGear(); - walkToBoss(); - - try { - lockCondition.lock(); - fightBoss(); // Only lock during the actual fight - } finally { - lockCondition.unlock(); - } - - collectLoot(); -} -``` - -3. **Use meaningful lock reason messages** to aid debugging: - -```java -// BAD -this.lockCondition = new LockCondition(); //ensure unlock on shutdown of the plugin ! - -// GOOD -this.lockCondition = new LockCondition("Locked during Zulrah's blue phase"); //ensure unlock on shutdown of the plugin ! -``` - -4. **Consider timeout mechanisms** for locks that might get stuck: - -```java -// Set a maximum lock duration for safety -final long MAX_LOCK_DURATION = 120_000; // 2 minutes -long lockStartTime = System.currentTimeMillis(); - -try { - lockCondition.lock(); - - while (bossStillFighting()) { - // Combat logic... - - // Safety timeout check - if (System.currentTimeMillis() - lockStartTime > MAX_LOCK_DURATION) { - log.warn("Lock timeout exceeded - forcing unlock"); - break; - } - - Global.sleep(100); - } -} finally { - lockCondition.unlock(); -} -``` - -5. **Take advantage of LockCondition constructor options**: - -```java -// Create a locked condition from the start -this.lockCondition = new LockCondition("Locked during combat sequence", true); //ensure unlock on shutdown of the plugin ! - -// Later when it's safe to unlock -this.lockCondition.unlock(); -``` - -By following these patterns, your combat plugins will safely integrate with the scheduler system while ensuring critical combat sequences are never interrupted at dangerous moments. diff --git a/docs/scheduler/condition-manager.md b/docs/scheduler/condition-manager.md deleted file mode 100644 index 45ef8c6478c..00000000000 --- a/docs/scheduler/condition-manager.md +++ /dev/null @@ -1,424 +0,0 @@ -# ConditionManager - -## Overview - -The `ConditionManager` class is a sophisticated component of the Plugin Scheduler system responsible for handling the hierarchical structure of conditions that determine when plugins should start and stop. It manages two separate condition hierarchies - one for plugin-defined conditions and another for user-defined conditions - and provides methods to evaluate, combine, and manipulate these conditions. - -This class serves as the "brain" of the condition evaluation system, determining whether a plugin should start or stop based on complex logical structures of conditions. - -## Class Definition - -```java -@Slf4j -public class ConditionManager implements AutoCloseable { - // Condition hierarchies - private LogicalCondition pluginCondition; - private LogicalCondition userLogicalCondition; - - // Event handling - private final EventBus eventBus; - private boolean eventsRegistered; - - // Watchdog system - private boolean watchdogsRunning; - private UpdateOption currentWatchdogUpdateOption; - private long currentWatchdogInterval; - private Supplier currentWatchdogSupplier; - - // Other implementation details... -} -``` - -## Key Features - -### Dual Condition Hierarchy System - -The `ConditionManager` maintains two separate hierarchical structures: - -1. **Plugin Conditions**: Defined programmatically by the plugin through the `SchedulablePlugin` interface - - Controlled by the plugin developer - - Typically express the plugin's business logic requirements - - Cannot be modified by the user through the UI - -2. **User Conditions**: Defined by the end-user through configuration - - Controlled by the user - - Express user preferences about when the plugin should run - - Can be modified through the UI - -When evaluating the complete condition set, the plugin conditions and user conditions are combined using configurable logic (typically AND): - -``` -Final Condition = Plugin Conditions AND User Conditions -``` - -This means both sets of conditions must be satisfied for the final result to be satisfied, giving both the developer and user appropriate control. - -### Hierarchical Logical Structure - -Each condition hierarchy can contain complex nested structures of logical operators (AND, OR, NOT) and various condition types: - -``` -UserLogicalCondition (AND) -├── TimeWindowCondition -├── InventoryItemCountCondition -└── OrCondition - ├── SkillLevelCondition - └── PlayerInAreaCondition -``` - -This allows for sophisticated condition combinations like: -- "Execute only between 8pm-10pm AND when inventory isn't full OR when in the mining area" - -### Logic Types - -The `ConditionManager` supports two primary logic types: - -1. **Require All**: All conditions must be met (AND logic) - ```java - conditionManager.setRequireAll(); // Use AND logic - ``` - -2. **Require Any**: Any condition can be met (OR logic) - ```java - conditionManager.setRequireAny(); // Use OR logic - ``` - -### Condition Management Methods - -The class provides various methods to manipulate and query its condition structures: - -```java -// Add a user condition -public void addUserCondition(Condition condition) { - ensureUserLogicalExists(); - userLogicalCondition.addCondition(condition); -} - -// Remove a condition from both hierarchies -public boolean removeCondition(Condition condition) { - boolean removed = false; - if (userLogicalCondition != null) { - removed |= userLogicalCondition.removeCondition(condition); - } - if (pluginCondition != null) { - removed |= pluginCondition.removeCondition(condition); - } - return removed; -} - -// Check if a condition exists in either hierarchy -public boolean containsCondition(Condition condition) { - ensureUserLogicalExists(); - // Check user conditions - if (userLogicalCondition.contains(condition)) { - return true; - } - // Check plugin conditions - return pluginCondition != null && pluginCondition.contains(condition); -} -``` - -### Condition Evaluation - -The `ConditionManager` provides methods to evaluate whether its conditions are satisfied: - -```java -// Check if all conditions (both plugin and user) are met -public boolean areAllConditionsMet() { - // Check if plugin conditions are met (or if none exist) - boolean pluginConditionsMet = !hasPluginConditions() || arePluginConditionsMet(); - - // Check if user conditions are met (or if none exist) - boolean userConditionsMet = !hasUserConditions() || areUserConditionsMet(); - - // Both must be satisfied - return pluginConditionsMet && userConditionsMet; -} - -// Check only plugin-defined conditions -public boolean arePluginConditionsMet() { - if (pluginCondition == null || pluginCondition.getConditions().isEmpty()) { - return true; // No plugin conditions means this requirement is satisfied - } - return pluginCondition.isSatisfied(); -} - -// Check only user-defined conditions -public boolean areUserConditionsMet() { - if (userLogicalCondition == null || userLogicalCondition.getConditions().isEmpty()) { - return true; // No user conditions means this requirement is satisfied - } - return userLogicalCondition.isSatisfied(); -} -``` - -### Condition Watchdog System - -The `ConditionManager` includes a sophisticated watchdog system that periodically updates conditions from a supplier function (typically from the plugin itself): - -```java -/** - * Schedules a periodic task to update conditions from the given supplier. - * This allows plugins to dynamically update their conditions based on changing game state. - */ -public ScheduledFuture scheduleConditionWatchdog( - Supplier conditionSupplier, - long checkIntervalMillis, - UpdateOption updateOption -) { - // Store the current settings - this.currentWatchdogSupplier = conditionSupplier; - this.currentWatchdogInterval = checkIntervalMillis; - this.currentWatchdogUpdateOption = updateOption; - - // Create and schedule the watchdog task - ScheduledFuture future = SHARED_WATCHDOG_EXECUTOR.scheduleAtFixedRate( - () -> { - try { - // Get the latest condition from the supplier - LogicalCondition newCondition = conditionSupplier.get(); - if (newCondition != null) { - // Update the plugin condition with the new one - updatePluginCondition(newCondition, updateOption); - } - } catch (Exception e) { - log.error("Error in condition watchdog", e); - } - }, - checkIntervalMillis, // Initial delay - checkIntervalMillis, // Periodic interval - TimeUnit.MILLISECONDS - ); - - // Track the future so it can be canceled later - watchdogFutures.add(future); - watchdogsRunning = true; - - return future; -} -``` - -This watchdog system enables plugins to dynamically update their conditions based on changing game state, supporting more sophisticated automation patterns. - -### Condition Update Options - -The watchdog system supports several update strategies for merging new conditions with existing ones: - -1. **ADD_ONLY**: Only add new conditions, never remove existing ones -2. **REMOVE_ONLY**: Only remove conditions that no longer exist -3. **SYNC**: Fully synchronize the condition structure with the new one (default) -4. **REPLACE**: Replace the entire condition structure with the new one - -```java -/** - * Updates the plugin condition structure using the specified update option. - */ -public boolean updatePluginCondition(LogicalCondition newCondition, UpdateOption option) { - if (newCondition == null) { - return false; - } - - switch (option) { - case ADD_ONLY: - // Only add new conditions, don't remove existing ones - return mergeAddOnly(pluginCondition, newCondition); - - case REMOVE_ONLY: - // Only remove conditions that are no longer present - return mergeRemoveOnly(pluginCondition, newCondition); - - case SYNC: - // Full synchronization - add new ones, remove old ones - return synchronizeConditions(pluginCondition, newCondition); - - case REPLACE: - // Complete replacement - setPluginCondition(newCondition); - return true; - - default: - // Default to sync behavior - return synchronizeConditions(pluginCondition, newCondition); - } -} -``` - -### Event System Integration - -The `ConditionManager` integrates with RuneLite's event system to update conditions based on in-game events: - -```java -/** - * Registers event listeners with the RuneLite event bus to receive relevant game events. - * This enables conditions to update based on game state changes. - */ -public void registerEvents() { - if (!eventsRegistered) { - eventBus.register(this); - eventsRegistered = true; - log.debug("Registered condition event listeners"); - } -} - -/** - * Unregisters event listeners to prevent receiving events when not needed. - */ -public void unregisterEvents() { - if (eventsRegistered) { - eventBus.unregister(this); - eventsRegistered = false; - log.debug("Unregistered condition event listeners"); - } -} - -/** - * Example of an event handler that updates conditions based on game events - */ -@Subscribe -public void onGameTick(GameTick event) { - // Update all conditions with the latest game state - for (Condition condition : getConditions()) { - condition.update(); - } -} -``` - -This event integration ensures conditions are kept up-to-date with the current game state. - -### Time Condition Management - -The `ConditionManager` provides special handling for time-based conditions, which have unique properties like future trigger times: - -```java -/** - * Gets the next scheduled trigger time from all time conditions. - * This is the earliest time when any time condition would be satisfied. - */ -public Optional getCurrentTriggerTime() { - List timeConditions = getAllTimeConditions(); - if (timeConditions.isEmpty()) { - return Optional.empty(); - } - - // Find the earliest trigger time among all time conditions - return timeConditions.stream() - .map(TimeCondition::getCurrentTriggerTime) - .filter(Optional::isPresent) - .map(Optional::get) - .min(ZonedDateTime::compareTo); -} - -/** - * Checks if all time-only conditions would be satisfied, ignoring non-time conditions. - * This is useful for diagnostic purposes to determine if time conditions are blocking execution. - */ -public boolean wouldBeTimeOnlySatisfied() { - // Check if we have any time conditions - List timeConditions = getAllTimeConditions(); - if (timeConditions.isEmpty()) { - return true; // No time conditions means they're not blocking - } - - // Check if all time conditions are satisfied - return timeConditions.stream().allMatch(Condition::isSatisfied); -} -``` - -### Progress Tracking - -The `ConditionManager` can calculate progress toward conditions being met: - -```java -/** - * Gets the overall progress percentage toward the next trigger time. - * Useful for UI display of how close a plugin is to running. - */ -public double getProgressTowardNextTrigger() { - // Implementation to calculate progress between 0-100% -} - -/** - * Gets the percentage of conditions that are currently satisfied. - */ -public double getFullConditionProgress() { - List conditions = getConditions(); - if (conditions.isEmpty()) { - return 100.0; // No conditions means 100% done - } - - // Calculate percentage of satisfied conditions - long satisfiedCount = conditions.stream() - .filter(Condition::isSatisfied) - .count(); - - return (satisfiedCount * 100.0) / conditions.size(); -} -``` - -## Relationship with PluginScheduleEntry - -Each `PluginScheduleEntry` contains two `ConditionManager` instances: - -1. `startConditionManager`: Manages conditions that determine when to start the plugin -2. `stopConditionManager`: Manages conditions that determine when to stop the plugin - -The `PluginScheduleEntry` uses these managers to evaluate: -- Whether the plugin is "due to run" (should start) -- Whether the plugin should stop -- The next scheduled activation time -- Progress toward starting or stopping - -```java -// In PluginScheduleEntry -public boolean isDueToRun() { - // Check if we're already running - if (isRunning()) { - return false; - } - - // Check if start conditions are met - return startConditionManager.areAllConditionsMet(); -} - -public boolean shouldStop() { - if (!isRunning()) { - return false; - } - - // Check if stop conditions are met - return stopConditionManager.areAllConditionsMet(); -} -``` - -## Best Practices - -When working with `ConditionManager`: - -1. **Appropriate Logic Type**: Choose the appropriate logic type (AND/OR) based on your requirements - - Use AND (requireAll) when all conditions must be satisfied - - Use OR (requireAny) when any condition can trigger the action - -2. **Performance Considerations**: Avoid excessive condition nesting and large condition trees - - Very complex condition structures can impact performance - - Use logical grouping to optimize evaluation - -3. **Condition Registration**: Ensure conditions are registered with the appropriate manager - - User conditions should be added via `addUserCondition()` - - Plugin conditions should be set via `setPluginCondition()` or `updatePluginCondition()` - -4. **Resource Cleanup**: Always call `close()` when the manager is no longer needed - - This ensures watchdog tasks are properly canceled - - Event listeners are unregistered - -5. **Watchdog Usage**: Use watchdogs judiciously - - Frequent updates can impact performance - - Consider appropriate update intervals based on your plugin's needs - -6. **Condition Synchronization**: Choose the right update option for your use case - - `SYNC` for complete condition synchronization - - `ADD_ONLY` to preserve existing conditions while adding new ones - - `REPLACE` when you want to completely reset the condition structure - -## Summary - -The `ConditionManager` class is the sophisticated "brain" behind the Plugin Scheduler's condition evaluation system. By maintaining separate hierarchies for user and plugin conditions, it creates a powerful but flexible framework for defining when plugins should start and stop. Its integration with the RuneLite event system, watchdog capabilities, and hierarchical logical structure enable complex automation patterns that can adapt to the changing game state. diff --git a/docs/scheduler/conditions/location-conditions.md b/docs/scheduler/conditions/location-conditions.md deleted file mode 100644 index 301997926a4..00000000000 --- a/docs/scheduler/conditions/location-conditions.md +++ /dev/null @@ -1,136 +0,0 @@ -# Location Conditions - -Location conditions in the Plugin Scheduler system allow plugins to be controlled based on the player's position in the game world. - -## Overview - -Location conditions monitor the player's physical location in the game, enabling plugins to respond to position-based triggers. These conditions are useful for area-specific automation, region-based task scheduling, and creating location-aware plugin behaviors. - -## Available Location Conditions - -### PositionCondition - -The `PositionCondition` checks if the player is at a specific tile position in the game world. - -**Usage:** -```java -// Satisfied when the player is at the Grand Exchange center tile -PositionCondition condition = new PositionCondition( - new WorldPoint(3165, 3487, 0) // GE center coordinates -); -``` - -**Key features:** -- Checks for exact position matching -- Can include or ignore the plane/level coordinate -- Can specify a tolerance radius to create a small area around the target position -- Useful for precise location triggers - -### AreaCondition - -The `AreaCondition` checks if the player is within a defined area in the game world. - -**Usage:** -```java -// Satisfied when the player is in the Grand Exchange area -AreaCondition condition = new AreaCondition( - new WorldArea(3151, 3473, 30, 30, 0) // GE area -); -``` - -**Key features:** -- Defines rectangular areas using WorldArea -- Can cover multiple planes/levels -- Useful for monitoring presence in towns, dungeons, or training areas -- Can be inverted to check if player is outside an area - -## Common Features of Location Conditions - -All location conditions provide core functionality for position-based checks: - -- `isSatisfied()`: Determines if the player's current position satisfies the condition -- `getDescription()`: Returns a human-readable description of the location condition -- `reset()`: Refreshes any cached location data -- Various configuration options for making the check more or less strict - -## Using Location Conditions as Start Conditions - -When used as start conditions, location conditions can trigger plugins when the player enters specific areas: - -```java -PluginScheduleEntry entry = new PluginScheduleEntry("MyPlugin", true); - -// Start the plugin when the player enters the Wilderness -entry.addStartCondition(new AreaCondition( - new WorldArea(2944, 3520, 448, 448, 0) // Wilderness area -)); -``` - -## Using Location Conditions as Stop Conditions - -Location conditions are also useful as stop conditions to deactivate plugins when the player leaves or enters an area: - -```java -// Stop the plugin when the player returns to a safe area (e.g., Lumbridge) -entry.addStopCondition(new AreaCondition( - new WorldArea(3206, 3208, 30, 30, 0) // Lumbridge area -)); -``` - -## Combining with Logical Conditions - -Location conditions can be combined with logical conditions to create more complex location-based rules: - -```java -// Create a logical NOT condition -NotCondition notInSafeArea = new NotCondition( - new AreaCondition(new WorldArea(3206, 3208, 30, 30, 0)) // Lumbridge area -); - -// Create a logical AND condition -AndCondition dangerousCondition = new AndCondition(); - -// Require being in the wilderness -dangerousCondition.addCondition(new AreaCondition( - new WorldArea(2944, 3520, 448, 448, 0) // Wilderness area -)); - -// AND not being in a safe spot -dangerousCondition.addCondition(notInSafeArea); - -// Add this combined condition as a start condition -entry.addStartCondition(dangerousCondition); -``` - -## Creating Complex Area Monitoring - -For complex regions that cannot be represented by a single rectangle, multiple area conditions can be combined using logical OR: - -```java -// Create a logical OR condition for multiple areas -OrCondition bankingAreas = new OrCondition(); - -// Add various bank areas -bankingAreas.addCondition(new AreaCondition( - new WorldArea(3207, 3215, 10, 10, 0) // Lumbridge bank -)); -bankingAreas.addCondition(new AreaCondition( - new WorldArea(3180, 3433, 15, 15, 0) // Varrock West bank -)); -bankingAreas.addCondition(new AreaCondition( - new WorldArea(3251, 3420, 10, 10, 0) // Varrock East bank -)); -bankingAreas.addCondition(new AreaCondition( - new WorldArea(3088, 3240, 10, 10, 0) // Draynor bank -)); - -// Add this combined condition to stop a plugin when in any bank -entry.addStopCondition(bankingAreas); -``` - -## Event Integration - -Location conditions integrate with the RuneLite event system to track changes in real-time: - -- `GameTick`: Periodically checks the player's position against the condition -- Efficient position comparison to minimize performance impact \ No newline at end of file diff --git a/docs/scheduler/conditions/logical-conditions.md b/docs/scheduler/conditions/logical-conditions.md deleted file mode 100644 index 9f460131310..00000000000 --- a/docs/scheduler/conditions/logical-conditions.md +++ /dev/null @@ -1,192 +0,0 @@ -# Logical Conditions - -Logical conditions are a powerful feature of the Plugin Scheduler that enable the combination of other conditions using logical operators. - -## Overview - -Logical conditions allow for the creation of complex conditional expressions by combining simpler conditions. They are essential for creating sophisticated scheduling rules based on multiple factors. - -## Available Logical Conditions - -### AndCondition - -The `AndCondition` requires that all of its child conditions are satisfied for the condition itself to be satisfied. - -**Usage:** -```java -// Create an AND condition -AndCondition condition = new AndCondition(); - -// Add child conditions -condition.addCondition(new InventoryItemCountCondition(ItemID.COINS, 1000, )); -condition.addCondition(new SkillLevelCondition(Skill.ATTACK, 60, )); -``` - -**Key features:** -- Requires all child conditions to be satisfied -- Can contain any type of condition, including other logical conditions -- Returns the minimum progress percentage among child conditions - -### OrCondition - -The `OrCondition` is satisfied if any of its child conditions are satisfied. - -**Usage:** -```java -// Create an OR condition -OrCondition condition = new OrCondition(); - -// Add child conditions -condition.addCondition(new TimeWindowCondition(LocalTime.of(9, 0), LocalTime.of(12, 0))); -condition.addCondition(new TimeWindowCondition(LocalTime.of(14, 0), LocalTime.of(17, 0))); -``` - -**Key features:** -- Requires at least one child condition to be satisfied -- Useful for creating alternative paths to satisfy a condition -- Returns the maximum progress percentage among child conditions - -### NotCondition - -The `NotCondition` inverts the result of its child condition. - -**Usage:** -```java -// Create a NOT condition -NotCondition condition = new NotCondition( - new AreaCondition(new WorldArea(3200, 3200, 50, 50, 0)) -); -``` - -**Key features:** -- Inverts the satisfaction state of the wrapped condition -- Progress percentage is inverted (100 - child progress) -- Useful for creating negative conditions like "not in an area" or "no items in inventory" - -### LockCondition - -The `LockCondition` is a special logical condition that remains satisfied once it becomes satisfied, regardless of the subsequent state of its child condition. - -**Usage:** -```java -// Create a lock condition that stays satisfied once the player reaches level 70 -LockCondition condition = - new SkillLevelCondition(Skill.MINING, 70); -``` - -**Key features:** -- "Locks" to true once satisfied -- Useful for one-way transitions or milestone achievements -- Can be reset if needed - -### PredicateCondition - -The `PredicateCondition` is a versatile logical condition that evaluates a Java Predicate function against a game state. It combines a manual lock mechanism with dynamic state evaluation. - -**Usage:** -```java -// Create a predicate condition that checks if the player is in an agility course region -Predicate notInAgilityRegion = player -> { - if (player == null) return true; - int playerRegionId = player.getWorldLocation().getRegionID(); - return !courseRegionIds.contains(playerRegionId); -}; - -// Create the predicate condition with a descriptive reason -PredicateCondition condition = new PredicateCondition<>( - "Player is currently in an agility course", // reason for the lock - notInAgilityRegion, // the predicate to evaluate - () -> client.getLocalPlayer(), // supplier of the state to check - "Player is not in an agility course region" // description of the predicate -); -``` - -**Key features:** -- Combines a traditional lock mechanism with dynamic predicate evaluation -- Supports any type of game state through generic type parameter -- Only satisfied when both the lock is unlocked AND the predicate evaluates to true -- Extremely flexible for complex game state evaluation -- Perfect for state-driven plugin control logic - -## Common Features of Logical Conditions - -All logical conditions implement the `LogicalCondition` interface, which extends the base `Condition` interface and provides additional functionality for managing child conditions: - -- `addCondition(Condition)`: Adds a child condition -- `removeCondition(Condition)`: Removes a child condition -- `getConditions()`: Returns all child conditions -- `contains(Condition)`: Checks if a specific condition is contained in the logical structure - -## Using Logical Conditions as Start Conditions - -When used as start conditions, logical conditions provide complex rules for when a plugin should be activated: - -```java -PluginScheduleEntry entry = new PluginScheduleEntry("MyPlugin", true); - -// Create a logical structure for starting conditions -OrCondition startCondition = new OrCondition(); - -// Add multiple time windows -startCondition.addCondition(new TimeWindowCondition( - LocalTime.of(9, 0), - LocalTime.of(12, 0) -)); -startCondition.addCondition(new TimeWindowCondition( - LocalTime.of(14, 0), - LocalTime.of(17, 0) -)); - -entry.addStartCondition(startCondition); -``` - -## Using Logical Conditions as Stop Conditions - -When used as stop conditions, logical conditions define complex rules for when a plugin should be deactivated: - -```java -// Create a logical structure for stop conditions -AndCondition stopCondition = new AndCondition(); - -// Stop when inventory is full AND player is in a safe area -stopCondition.addCondition(new InventoryItemCountCondition( - ItemID.ANY, - 28, - -)); -stopCondition.addCondition(new AreaCondition( - new WorldArea(3200, 3200, 50, 50, 0) -)); - -entry.addStopCondition(stopCondition); -``` - -## Creating Complex Nested Logical Structures - -Logical conditions can be nested to create complex conditional expressions: - -```java -// Main structure is OR -OrCondition complexCondition = new OrCondition(); - -// First branch: AND condition -AndCondition firstBranch = new AndCondition(); -firstBranch.addCondition(new TimeWindowCondition(LocalTime.of(9, 0), LocalTime.of(17, 0))); -firstBranch.addCondition(new SkillLevelCondition(Skill.MINING, 60, )); - -// Second branch: AND condition with a NOT -AndCondition secondBranch = new AndCondition(); -secondBranch.addCondition(new TimeWindowCondition(LocalTime.of(20, 0), LocalTime.of(23, 59))); -secondBranch.addCondition(new NotCondition( - new AreaCondition(new WorldArea(3200, 3200, 50, 50, 0)) -)); - -// Add branches to main structure -complexCondition.addCondition(firstBranch); -complexCondition.addCondition(secondBranch); - -// Add to schedule entry -entry.addStartCondition(complexCondition); -``` - -This creates a structure that means: "Run the plugin if it's between 9 AM and 5 PM AND mining level is 60+, OR if it's between 8 PM and midnight AND the player is not in the specified area." \ No newline at end of file diff --git a/docs/scheduler/conditions/npc-conditions.md b/docs/scheduler/conditions/npc-conditions.md deleted file mode 100644 index 75ced92342d..00000000000 --- a/docs/scheduler/conditions/npc-conditions.md +++ /dev/null @@ -1,125 +0,0 @@ -# NPC Conditions - -NPC conditions in the Plugin Scheduler system allow plugins to be controlled based on NPC-related events and states in the game. - -## Overview - -NPC conditions monitor interactions with Non-Player Characters in the game, enabling plugins to respond to NPC presence, combat state, and other NPC-related factors. These conditions are particularly useful for combat automation, quest helpers, and NPC interaction scripts. - -## Available NPC Conditions - -### NpcCondition - -The `NpcCondition` monitors the presence, proximity, or state of specific NPCs in the game. - -**Usage:** -```java -// Satisfied when an NPC with ID 3080 (Moss giant) is within visibility range -NpcCondition condition = new NpcCondition( - 3080, // NPC ID to check - true // Whether the NPC must be present to satisfy the condition -); -``` - -**Key features:** -- Monitors for specific NPC IDs or name patterns -- Can check for NPC presence or absence -- Can validate distance to the NPC -- Supports combat state checking (in combat, health percentage) -- Updates dynamically as NPCs spawn, despawn, or change state - -## Common Features of NPC Conditions - -All NPC conditions provide core functionality for NPC-based checks: - -- `isSatisfied()`: Determines if the current NPC state satisfies the condition -- `getDescription()`: Returns a human-readable description of the NPC condition -- `reset()`: Refreshes any cached NPC data -- Various configuration options for specifying which NPCs to monitor and what states to check - -## Using NPC Conditions as Start Conditions - -When used as start conditions, NPC conditions can trigger plugins when specific NPCs appear or enter a certain state: - -```java -PluginScheduleEntry entry = new PluginScheduleEntry("MyPlugin", true); - -// Start the plugin when a Rock Crab appears -entry.addStartCondition(new NpcCondition( - "Rock Crab", // NPC name to check (can also use ID) - true, // NPC must be present - 15 // Within 15 tiles -)); -``` - -## Using NPC Conditions as Stop Conditions - -NPC conditions are also valuable as stop conditions to deactivate plugins based on NPC state: - -```java -// Stop the plugin when the target NPC dies or despawns -entry.addStopCondition(new NpcCondition( - 3080, // NPC ID (Moss giant) - false // NPC must NOT be present (i.e., has despawned) -)); - -// OR stop when the player is no longer in combat with any NPC -entry.addStopCondition(new NotCondition( - new NpcCondition().inCombat(true) -)); -``` - -## Combining with Logical Conditions - -NPC conditions can be combined with logical conditions to create more complex NPC-related rules: - -```java -// Create a logical AND condition -AndCondition combatCondition = new AndCondition(); - -// Require being in combat with an NPC -combatCondition.addCondition(new NpcCondition().inCombat(true)); - -// AND the NPC's health is below 25% -combatCondition.addCondition(new NpcCondition().healthPercentageLessThan(25)); - -// Add this combined condition as a start condition for a finishing move plugin -entry.addStartCondition(combatCondition); -``` - -## Advanced NPC Monitoring - -For more complex NPC monitoring scenarios, multiple conditions can be combined: - -```java -// Create a logical OR condition for multiple NPC types -OrCondition dragonTargets = new OrCondition(); - -// Add various dragon types -dragonTargets.addCondition(new NpcCondition( - "Blue dragon", true, 20 -)); -dragonTargets.addCondition(new NpcCondition( - "Red dragon", true, 20 -)); -dragonTargets.addCondition(new NpcCondition( - "Green dragon", true, 20 -)); -dragonTargets.addCondition(new NpcCondition( - "Black dragon", true, 20 -)); - -// Add this combined condition to start a dragon-fighting plugin -entry.addStartCondition(dragonTargets); -``` - -## Event Integration - -NPC conditions integrate with the RuneLite event system to track changes in real-time: - -- `NpcSpawned`: Detects when new NPCs appear in the game -- `NpcDespawned`: Detects when NPCs are removed from the game -- `NpcChanged`: Detects when NPC properties or appearance changes -- `InteractingChanged`: Detects when the player starts or stops interacting with an NPC -- `HitsplatApplied`: Monitors damage dealt to NPCs -- `GameTick`: Periodically validates NPC state \ No newline at end of file diff --git a/docs/scheduler/conditions/resource-conditions.md b/docs/scheduler/conditions/resource-conditions.md deleted file mode 100644 index 4ec13a828c9..00000000000 --- a/docs/scheduler/conditions/resource-conditions.md +++ /dev/null @@ -1,177 +0,0 @@ -# Resource Conditions - -Resource conditions in the Plugin Scheduler system allow plugins to be controlled based on the player's inventory, gathered resources, and item interactions. - -## Overview - -Resource conditions monitor various aspects of item and resource management in the game. They can track inventory contents, gathered resources, processed items, and loot drops, making them particularly useful for automation related to skilling, combat, and resource collection. - -## Available Resource Conditions - -### InventoryItemCountCondition - -The `InventoryItemCountCondition` monitors the quantity of specified items in the player's inventory. - -**Usage:** -```java -// Satisfied when inventory contains at least 1000 coins -InventoryItemCountCondition condition = new InventoryItemCountCondition( - ItemID.COINS, // Item ID to check - 1000, // Quantity - // Comparison operator -); -``` - -**Key features:** -- Monitors specific item IDs or any item (using ItemID.ANY) -- Supports various comparison types (equals, greater than, less than, etc.) -- Updates dynamically as inventory contents change -- Can track progress toward target quantities - -### GatheredResourceCondition - -The `GatheredResourceCondition` tracks resources that the player has gathered (like ore from mining or logs from woodcutting). - -**Usage:** -```java -// Satisfied when player has gathered 100 yew logs -GatheredResourceCondition condition = new GatheredResourceCondition( - ItemID.YEW_LOGS, // Resource item ID - 100, // Target quantity - -); -``` - -**Key features:** -- Tracks the total amount of a resource gathered over time -- Persists count even if items are banked, dropped, or used -- Useful for long-term gathering goals -- Can be reset if needed - -### ProcessItemCondition - -The `ProcessItemCondition` monitors items that have been processed (like ores smelted into bars or logs made into bows). - -**Usage:** -```java -// Satisfied when player has processed 50 yew logs into yew longbows -ProcessItemCondition condition = new ProcessItemCondition( - ItemID.YEW_LOGS, // Input item ID - ItemID.YEW_LONGBOW, // Output item ID - 50, // Target quantity - -); -``` - -**Key features:** -- Tracks the transformation of one item into another -- Monitors both input and output items -- Useful for crafting, smithing, and other processing skills -- Can be configured to track multiple possible outputs - -### LootItemCondition - -The `LootItemCondition` monitors items that have been looted from the ground. - -**Usage:** -```java -// Satisfied when player has looted 10 dragon bones -LootItemCondition condition = new LootItemCondition( - ItemID.DRAGON_BONES, // Item ID to track - 10, // Target quantity - -); -``` - -**Key features:** -- Specifically tracks items picked up from the ground -- Distinguishes between looted items and other inventory additions -- Useful for monster drop tracking -- Can be configured to track specific areas or with item filters - -## Common Features of Resource Conditions - -All resource conditions implement the `ResourceCondition` interface, which extends the base `Condition` interface and provides additional functionality: - -- `getProgressPercentage()`: Returns the progress toward the condition goal as a percentage -- `reset()`: Resets the tracking counters to zero -- `getTrackedQuantity()`: Returns the current tracked quantity -- `getTargetQuantity()`: Returns the target quantity needed to satisfy the condition - -## Using Resource Conditions as Start Conditions - -When used as start conditions, resource conditions can trigger plugins based on inventory state or gathered resources: - -```java -PluginScheduleEntry entry = new PluginScheduleEntry("MyPlugin", true); - -// Start the plugin when inventory contains at least 1000 coins -entry.addStartCondition(new InventoryItemCountCondition( - ItemID.COINS, - 1000, - -)); -``` - -## Using Resource Conditions as Stop Conditions - -Resource conditions are particularly powerful as stop conditions for plugins: - -```java -// Stop when inventory is full -entry.addStopCondition(new InventoryItemCountCondition( - ItemID.ANY, - 28, - -)); - -// OR stop when a specific goal is reached -entry.addStopCondition(new GatheredResourceCondition( - ItemID.YEW_LOGS, - 1000, - -)); -``` - -## Combining with Logical Conditions - -Resource conditions can be combined with logical conditions to create complex resource management rules: - -```java -// Create a logical OR condition -OrCondition stopConditions = new OrCondition(); - -// Stop when inventory is full -stopConditions.addCondition(new InventoryItemCountCondition( - ItemID.ANY, - 28, - -)); - -// OR when the player has gathered 1000 yew logs -stopConditions.addCondition(new GatheredResourceCondition( - ItemID.YEW_LOGS, - 1000, - -)); - -// OR when the player has crafted 500 yew longbows -stopConditions.addCondition(new ProcessItemCondition( - ItemID.YEW_LOGS, - ItemID.YEW_LONGBOW, - 500, - -)); - -// Add the combined stop conditions to the plugin schedule -entry.addStopCondition(stopConditions); -``` - -## Progress Tracking and Event Integration - -Resource conditions integrate with the RuneLite event system to track changes in real-time: - -- `ItemContainerChanged`: Updates inventory item counts -- `ItemSpawned`/`ItemDespawned`: Monitors ground items for looting -- `MenuOptionClicked`: Detects item processing actions -- `GameTick`: Periodically validates condition state \ No newline at end of file diff --git a/docs/scheduler/conditions/skill-conditions.md b/docs/scheduler/conditions/skill-conditions.md deleted file mode 100644 index 655f33ab53e..00000000000 --- a/docs/scheduler/conditions/skill-conditions.md +++ /dev/null @@ -1,230 +0,0 @@ -# Skill Conditions - -Skill conditions in the Plugin Scheduler system allow plugins to be controlled based on the player's skill levels and experience points. - -## Overview - -Skill conditions monitor the player's progress in various skills, allowing plugins to respond to skill-related milestones and achievements. These conditions can be used to automate skill training, set goals, and manage plugin schedules based on skill progress. - -## Available Skill Conditions - -### SkillLevelCondition - -The `SkillLevelCondition` monitors the player's actual level in a specific skill. - -**Usage:** -```java -// Satisfied when player has at least level 70 Mining -SkillLevelCondition condition = new SkillLevelCondition( - Skill.MINING, // The skill to monitor - 70 // Target level -); - -// Satisfied when player gains 5 levels in Attack (relative) -SkillLevelCondition relativeCondition = SkillLevelCondition.createRelative( - Skill.ATTACK, // The skill to monitor - 5 // Target level gain -); - -// Satisfied when player reaches a random level between 70-80 in Mining -SkillLevelCondition randomizedCondition = SkillLevelCondition.createRandomized( - Skill.MINING, // The skill to monitor - 70, // Minimum target level - 80 // Maximum target level -); -``` - -**Key features:** -- Monitors any skill in the game -- Can track total level using `Skill.OVERALL` -- Supports absolute level targets (reach a specific level) -- Supports relative level targets (gain X levels from current) -- Can use randomization within a min/max range -- Updates dynamically as skill levels change -- Provides progress tracking toward target levels - -### SkillXpCondition - -The `SkillXpCondition` monitors the player's experience points in a specific skill. - -**Usage:** -```java -// Absolute XP goal: Satisfied when player has at least 1,000,000 XP in Woodcutting -SkillXpCondition condition = new SkillXpCondition( - Skill.WOODCUTTING, // The skill to monitor - 1_000_000 // Target XP (absolute) -); - -// Relative XP goal: Satisfied when player gains 50,000 XP from the starting point -SkillXpCondition relativeCondition = SkillXpCondition.createRelative( - Skill.WOODCUTTING, // The skill to monitor - 50_000 // Target XP gain -); - -// Randomized XP goal: Satisfied when player reaches a random XP between 1M-1.5M -SkillXpCondition randomizedCondition = SkillXpCondition.createRandomized( - Skill.WOODCUTTING, // The skill to monitor - 1_000_000, // Minimum target XP - 1_500_000 // Maximum target XP -); - -// Randomized relative XP goal: Satisfied when player gains a random amount of XP -// between 50K-100K from starting point -SkillXpCondition relativeRandomCondition = SkillXpCondition.createRelativeRandomized( - Skill.WOODCUTTING, // The skill to monitor - 50_000, // Minimum XP gain - 100_000 // Maximum XP gain -); -``` - -**Key features:** -- Monitors precise XP values rather than levels -- Useful for tracking progress between levels -- Can be used to set specific XP goals -- Provides accurate progress percentage toward XP targets -- Supports both absolute XP targets (reach a specific XP amount) -- Supports relative XP targets (gain X XP from current) -- Can use randomization within a min/max range for both absolute and relative targets - -## Common Features of Skill Conditions - -All skill conditions implement the `SkillCondition` interface, which extends the base `Condition` interface and provides additional functionality: - -- `getProgressPercentage()`: Returns the progress toward the target level or XP as a percentage -- `reset()`: Resets any cached skill data -- `getSkill()`: Returns the skill being monitored -- `getTargetValue()`: Returns the target level or XP value - -## Using Skill Conditions as Start Conditions - -When used as start conditions, skill conditions can trigger plugins based on skill achievements: - -```java -PluginScheduleEntry entry = new PluginScheduleEntry("MyPlugin", true); - -// Start the plugin when the player reaches level 70 in Mining -entry.addStartCondition(new SkillLevelCondition( - Skill.MINING, - 70 -)); -``` - -## Using Skill Conditions as Stop Conditions - -Skill conditions can be used as stop conditions to end a plugin's execution when a skill goal is reached: - -```java -// Stop when the player reaches level 80 in Mining -entry.addStopCondition(new SkillLevelCondition( - Skill.MINING, - 80 -)); - -// OR stop when the player gains 100,000 XP in Mining -entry.addStopCondition(SkillXpCondition.createRelative( - Skill.MINING, - 100_000 -)); -``` - -## Tracking Relative Changes - -Both `SkillXpCondition` and `SkillLevelCondition` support tracking relative changes, which is useful for setting goals based on progress from the current state rather than absolute values: - -```java -// Satisfied when the player gains 50,000 XP in total from when the condition was created -SkillXpCondition condition = SkillXpCondition.createRelative( - Skill.OVERALL, // Track total XP across all skills - 50_000 // Target XP gain -); - -// Satisfied when the player gains 5 levels in Mining from when the condition was created -SkillLevelCondition levelCondition = SkillLevelCondition.createRelative( - Skill.MINING, // The skill to monitor - 5 // Target level gain -); -``` - -## Combining with Logical Conditions - -Skill conditions can be combined with logical conditions to create complex skill-based rules: - -```java -// Create a logical AND condition -AndCondition skillGoals = new AndCondition(); - -// Require level 70 in Mining -skillGoals.addCondition(new SkillLevelCondition( - Skill.MINING, - 70 -)); - -// AND level 70 in Smithing -skillGoals.addCondition(new SkillLevelCondition( - Skill.SMITHING, - 70 -)); - -// Add these combined requirements as a start condition -entry.addStartCondition(skillGoals); -``` - -## Multi-Skill Training Scenarios - -For multi-skill training scenarios, skill conditions can be configured to monitor several skills: - -```java -// Create a logical OR condition for alternative training paths -OrCondition trainingGoals = new OrCondition(); - -// Path 1: Mining to level 80 -trainingGoals.addCondition(new SkillLevelCondition( - Skill.MINING, - 80 -)); - -// Path 2: Fishing to level 80 -trainingGoals.addCondition(new SkillLevelCondition( - Skill.FISHING, - 80 -)); - -// Path 3: Woodcutting to level 80 -trainingGoals.addCondition(new SkillLevelCondition( - Skill.WOODCUTTING, - 80 -)); - -// Add these alternative goals as a stop condition -entry.addStopCondition(trainingGoals); -``` - -## Event Integration - -Skill conditions integrate with the RuneLite event system to track changes in real-time: - -- `StatChanged`: Updates skill levels and XP values when they change -- `GameTick`: Periodically validates condition state - -## Performance Optimizations - -The `SkillCondition` base class includes several optimizations for improved performance: - -- **Static Caching**: Skill levels and XP values are cached in static maps to minimize client thread calls -- **Throttled Updates**: Updates are throttled to prevent excessive client thread operations -- **Icon Caching**: Skill icons are cached to improve UI rendering performance -- **Single Source of Truth**: All skill-related conditions use the same cached skill data -- **Efficient Event Handling**: Only relevant skill updates trigger condition recalculation - -Example using the cached data: - -```java -// Get cached skill data without requiring client thread call -int currentLevel = SkillCondition.getSkillLevel(Skill.MINING); -long currentXp = SkillCondition.getSkillXp(Skill.MINING); -int totalLevel = SkillCondition.getTotalLevel(); -long totalXp = SkillCondition.getTotalXp(); - -// Force an update of all skill data (throttled to prevent performance issues) -SkillCondition.forceUpdate(); -``` \ No newline at end of file diff --git a/docs/scheduler/conditions/time-conditions.md b/docs/scheduler/conditions/time-conditions.md deleted file mode 100644 index 4520b6233b5..00000000000 --- a/docs/scheduler/conditions/time-conditions.md +++ /dev/null @@ -1,128 +0,0 @@ -# Time Conditions - -Time conditions are a fundamental part of the Plugin Scheduler system that allow plugins to be scheduled based on various time-related factors. - -## Overview - -Time conditions enable plugins to run at specific times, intervals, or within time windows. They are essential for automating plugin execution based on real-world time constraints. - -## Available Time Conditions - -### IntervalCondition - -The `IntervalCondition` allows plugins to run at regular time intervals. - -**Usage:** -```java -// Run every 30 minutes -IntervalCondition condition = new IntervalCondition(Duration.ofMinutes(30)); -``` - -**Key features:** -- Flexible interval specification using Java's `Duration` class -- Configurable randomization to add variation to the timing -- Reset capability to restart the interval countdown - -### SingleTriggerTimeCondition - -The `SingleTriggerTimeCondition` triggers once at a specific date and time. - -**Usage:** -```java -// Trigger at a specific date and time -ZonedDateTime triggerTime = ZonedDateTime.of(2025, 4, 18, 15, 0, 0, 0, ZoneId.systemDefault()); -SingleTriggerTimeCondition condition = new SingleTriggerTimeCondition(triggerTime); -``` - -**Key features:** -- One-time execution at a precise moment -- Cannot trigger again after the specified time has passed -- Progress tracking toward the trigger time - -### TimeWindowCondition - -The `TimeWindowCondition` allows plugins to run within specific time windows on a daily basis. - -**Usage:** -```java -// Run between 9 AM and 5 PM -TimeWindowCondition condition = new TimeWindowCondition( - LocalTime.of(9, 0), // Start time - LocalTime.of(17, 0) // End time -); -``` - -**Key features:** -- Daily recurring time windows -- Configurable start and end times -- Support for windows that span midnight - -### DayOfWeekCondition - -The `DayOfWeekCondition` restricts plugin execution to specific days of the week. - -**Usage:** -```java -// Run only on weekends -Set weekendDays = EnumSet.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY); -DayOfWeekCondition condition = new DayOfWeekCondition(weekendDays); -``` - -**Key features:** -- Selection of multiple days of the week -- Combines with other time conditions to create complex schedules - -## Common Features of Time Conditions - -All time conditions implement the `TimeCondition` interface, which extends the base `Condition` interface and provides additional time-specific functionality: - -- `getCurrentTriggerTime()`: Returns the next time this condition will be satisfied -- `getDurationUntilNextTrigger()`: Returns the time remaining until the next trigger -- `hasTriggered()`: Indicates if a one-time condition has already triggered -- `canTriggerAgain()`: Determines if the condition can trigger again in the future - -## Using Time Conditions as Start Conditions - -When used as start conditions, time conditions determine when a plugin should be activated: - -```java -PluginScheduleEntry entry = new PluginScheduleEntry("MyPlugin", true); -// Run every 2 hours -entry.addStartCondition(new IntervalCondition(Duration.ofHours(2))); -``` - -## Using Time Conditions as Stop Conditions - -When used as stop conditions, time conditions control when a plugin should be deactivated: - -```java -// Stop after running for 30 minutes -entry.addStopCondition(new SingleTriggerTimeCondition( - ZonedDateTime.now().plusMinutes(30) -)); -``` - -## Combining Time Conditions - -Time conditions can be combined with logical conditions to create complex scheduling rules: - -```java -// Create a logical AND condition -AndCondition timeRules = new AndCondition(); - -// Add time window (9 AM to 5 PM) -timeRules.addCondition(new TimeWindowCondition( - LocalTime.of(9, 0), - LocalTime.of(17, 0) -)); - -// Add day of week condition (weekdays only) -Set weekdays = EnumSet.of( - DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, DayOfWeek.FRIDAY -); -timeRules.addCondition(new DayOfWeekCondition(weekdays)); - -// Add the combined time rules as a start condition -entry.addStartCondition(timeRules); -``` \ No newline at end of file diff --git a/docs/scheduler/defining-conditions.md b/docs/scheduler/defining-conditions.md deleted file mode 100644 index 6e5e6f4eba4..00000000000 --- a/docs/scheduler/defining-conditions.md +++ /dev/null @@ -1,584 +0,0 @@ -# Defining Conditions in the Scheduler UI - -This guide provides detailed instructions on how to use the condition configuration panels in the Plugin Scheduler UI to define start and stop conditions for your plugins. - -## Understanding the Condition Panel -The condition configuration panel is the heart of the scheduler's power, allowing you to create sophisticated logic that determines when plugins start and stop. Think of it as programming your character's behavior without writing code. - -### Panel Components - -The panel consists of four main sections: - -1. **Condition Category Dropdown**: Select the type of condition you want to create - - Time: For scheduling based on real-world time - - Skill: For conditions based on your character's skills and XP - - Resource: For inventory, item collection, and processing conditions - - Location: For position-based conditions in the game world - - Varbit: For game state variables and collection log entries - -2. **Condition Type Selection**: Each category has several specific condition types - - Shows only relevant condition types based on your selected category - - Provides tooltips explaining each condition type's purpose - -3. **Parameter Configuration**: Customize the condition with specific values - - Different condition types have different parameter forms - - Includes helpful controls like time pickers, dropdown menus, and numeric inputs - -4. **Logical Structure Tree**: Visual representation of your condition structure - - Shows how conditions are combined with AND/OR logic - - Allows selecting, grouping, and organizing conditions - - Displays satisfaction status with visual indicators - -![Condition Panel Overview](../img/condition-panel-overview.png) - -### Creating Your First Condition - -1. Select the relevant tab (Start Conditions or Stop Conditions) -2. Choose a condition category from the dropdown -3. Select a specific condition type -4. Configure the parameters -5. Click "Add" to add it to your condition structure -6. The condition appears in the tree view, where you can organize it - -## Condition Categories and Types - -### Time Conditions - -Time conditions are among the most commonly used and versatile conditions in the scheduler. They allow you to control when plugins start and stop based on real-world time factors. - -#### Available Time Condition Types - -| Condition Type | Description | Real-World Examples | -|----------------|-------------|-------------| -| **Time Duration** | Runs for a specific amount of time | "Run for 3 hours then stop" (good as a stop condition) | -| **Time Window** | Runs during specific hours each day | "Only run between 8am-10pm when I'm at my computer" | -| **Not In Time Window** | Runs outside specific hours | "Don't run during work hours (9am-5pm)" | -| **Specific Time** | Runs at an exact date/time | "Run exactly at 8:00pm on Tuesdays" | -| **Day of Week** | Runs on specific days | "Run on weekends only" | -| **Interval** | Runs at regular time intervals | "Run every 2 hours" or "Take a 15-minute break every hour" | - -#### Practical Examples - -**Example 1: Setting Up a Time Window for Evening Play** - -This is perfect for limiting your botting to specific hours: - -1. Select "Time" from the Category dropdown -2. Select "Time Window" from the Type dropdown -3. Set the start time to 6:00 PM (when you get home) -4. Set the end time to 11:00 PM (before bed) -5. Optional: Enable randomization for more human-like timing -6. Click "Add" to add the condition - -Now your plugins will only run during your evening hours and automatically stop at night. - -**Example 2: Creating a Playtime Limit** - -To limit how long a plugin runs (great for stop conditions): - -1. Select "Time" from the Category dropdown -2. Select "Time Duration" from the Type dropdown -3. Set the duration to your desired play time (e.g., 2 hours) -4. Enable randomization for natural variation (e.g., 1h45m to 2h15m) -5. Click "Add" to add the condition - -This ensures your plugin won't run for too long, preventing detection and burnout. - -**Example 3: Setting Up Weekend-Only Gaming** - -For those who only want to bot on weekends: - -1. Select "Time" from the Category dropdown -2. Select "Day of Week" from the Type dropdown -3. Check only "Saturday" and "Sunday" in the day selector -4. Click "Add" to add the condition - -Combine this with other time conditions for even more control, like "Run on weekends between 10am and 8pm". - -### Skill Conditions - -Skill conditions allow you to create automation based on your character's skills and experience progress. They're perfect for training goals and level-based activities. - -#### Available Skill Condition Types - -| Condition Type | Description | Strategic Uses | -|----------------|-------------|---------------| -| Skill Level | Sets absolute or relative level targets | Stop when reaching a milestone level | -| Skill XP Goal | Sets XP targets rather than levels | Get precise control over training sessions | -| Skill Level Required | Sets minimum requirements | Safeguard activities that require certain levels | - -#### Practical Skill Condition Examples - -**Example 1: Setting a Level-Based Training Goal** - -To create a condition that stops a plugin when you reach a specific level: - -1. Select "Skill" from the Category dropdown -2. Select "Skill Level" from the Type dropdown -3. Choose your skill from the dropdown (e.g., "Fishing") -4. Select either: - - Absolute level: Specific level to reach (e.g., 70) - - Relative level: Levels to gain from current (e.g., +5 levels) -5. Optional: Enable randomization for the target level to vary behavior -6. Click "Add" to add the condition - -This creates a natural stopping point for your training session, perfect for goal-oriented skilling. - -**Example 2: Setting an XP-Based Training Session** - -For more precise control over training duration: - -1. Select "Skill" from the Category dropdown -2. Select "Skill XP Goal" from the Type dropdown -3. Choose your skill from the dropdown -4. Enter your XP target (e.g., 50,000 XP) -5. Select whether this is absolute or relative to current XP -6. Click "Add" to add the condition - -XP-based conditions give you finer control than level-based ones, especially at higher levels where levels take longer to achieve. - -**Example 3: Level Requirement Safety Check** - -To ensure you have the required level before attempting an activity: - -1. Select "Skill" from the Category dropdown -2. Select "Skill Level Required" from the Type dropdown -3. Choose the required skill (e.g., "Agility") -4. Set the minimum level needed (e.g., 60 for Seers Village course) -5. Click "Add" to add the condition - -This prevents your plugin from starting activities you don't have the levels for, avoiding failures and wasted time. - -**Pro Tip:** Combine skill conditions with time conditions to create balanced training sessions. For example: "Train Woodcutting until level 70 OR until 2 hours have passed." - -### Resource Conditions - -Resource conditions are essential for inventory management and gathering activities. They let you create rules based on items, resources, and inventory state. - -#### Available Resource Condition Types - -| Condition Type | Description | Common Use Cases | -|----------------|-------------|-----------------| -| Item Collection | Tracks collected items over time | Stop after gathering 1000 feathers | -| Process Items | Tracks items processed/created | Stop after smithing 500 cannonballs | -| Gather Resources | Tracks gathered raw resources | Stop after mining 200 iron ore | -| Inventory Item Count | Checks current inventory state | Start when inventory is empty, stop when full | -| Bank Item Count | Checks items in bank | Stop when bank has 10,000 vials | -| Loot Item | Checks for specific drops | Keep killing until specific drop obtained | -| Item Required | Checks for required items | Only start if you have antipoison | - -#### Practical Resource Condition Examples - -**Example 1: Setting Up an Efficient Woodcutting Session** - -To create a stop condition that triggers when you've cut 500 logs or your inventory is full: - -1. Select "Resource" from the Category dropdown -2. Select "Gather Resources" from the Type dropdown -3. Enter "Yew logs" in the item name field -4. Set the target amount to 500 -5. Click "Add" to add this condition -6. Add another condition for inventory: - - Select "Resource" category again - - Select "Inventory Item Count" type - - Enter "28" for full inventory - - Set the comparison to "greater than or equal to" - - Click "Add" -7. Ensure these are in an OR relationship in the condition tree - -Now your plugin will stop when either you've gathered 500 logs OR your inventory becomes full. - -**Example 2: Setting Up a Profitable Crafting Session** - -For a plugin that runs until you've crafted a specific number of items: - -1. Select "Resource" from the Category dropdown -2. Select "Process Items" from the Type dropdown -3. Enter "Gold bracelet" in the item name field -4. Set the target amount to 100 -5. Enable "Track profits" if you want to monitor profitability -6. Click "Add" to add the condition - -This will track how many items you've crafted and automatically stop once you reach your goal. - -**Example 3: Required Items Safety Check** - -To ensure you have the necessary items before starting a dangerous activity: - -1. Select "Resource" from the Category dropdown -2. Select "Item Required" from the Type dropdown -3. Enter "Super antipoison" in the item name field -4. Set the minimum amount to 2 -5. Click "Add" to add this condition -6. Add more required items as needed using AND logic - -This creates a safety check that prevents your plugin from starting unless you have all the required items. - -### Location Conditions - -Location conditions allow you to create rules based on where your character is in the game world. These are essential for region-specific activities and location-based task switching. - -#### Available Location Condition Types - -| Condition Type | Description | Strategic Uses | -|----------------|-------------|----------------| -| Position | Based on exact coordinates with radius | Create specific action spots | -| Area | Based on a rectangular area | Define larger working zones | -| Region | Based on game region IDs | Control activities by game area | -| Distance | Based on distance from a point | Create proximity-based triggers | -| Not In Area | Inverted area condition | Create exclusion zones | - -#### Practical Location Condition Examples - -**Example 1: Setting Up a Mining Location** - -To create a condition that only runs your plugin when you're at a specific mining site: - -1. Navigate to the desired mining location in-game -2. Select "Location" from the Category dropdown -3. Select "Area" from the Type dropdown -4. Click "Capture Current Area" to use your current position -5. Adjust the area size using the sliders (make it large enough to cover the entire mining area) -6. Click "Add" to add the condition - -This ensures your mining plugin only runs when you're actually at the mining site, preventing it from activating in inappropriate locations. - -**Example 2: Creating a Safe Zone** - -To create a condition that stops a plugin when you enter a dangerous area: - -1. Select "Location" from the Category dropdown -2. Select "Not In Area" from the Type dropdown -3. Define the area you consider safe (e.g., a bank or city) -4. Click "Add" to add the condition - -This works as a safety measure - the plugin will only run when you're in the defined safe area and automatically stop if you leave it. - -**Example 3: Bank-Proximity Condition** - -For creating a condition that activates when you're near a bank: - -1. Stand near your preferred bank -2. Select "Location" from the Category dropdown -3. Select "Position" from the Type dropdown -4. Click "Use Current Position" -5. Set an appropriate radius (e.g., 15 tiles to cover the bank area) -6. Click "Add" to add the condition - -This is perfect for banking plugins that should only activate when you're actually near a bank, preventing premature attempts to bank while still gathering resources. - -**Pro Tip:** Combine location conditions with resource conditions for complete automation cycles. For example: "Stop mining when inventory is full OR player is no longer in the mining area." - -### NPC Conditions - -NPC conditions allow you to create rules based on interactions with non-player characters in the game. They're particularly useful for combat activities, boss fights, and NPC-dependent tasks. - -#### Available NPC Condition Types - -| Condition Type | Description | Strategic Uses | Notes | -|----------------|-------------|----------------|-------| -| NPC Kill Count | Tracks how many of a specific NPC you've killed | Stop after killing 100 goblins | May have tracking issues in multi-combat areas | - -> **Note**: Currently, only the NPC Kill Count condition is fully implemented. Future versions may include NPC Presence and NPC Interaction conditions. - -#### Practical NPC Condition Examples - -**Example 1: Basic Kill Count Goal** - -For a plugin that runs until you've killed a certain number of NPCs: - -1. Select "NPC" from the Category dropdown -2. Select "Kill Count" from the Type dropdown -3. Enter the NPC name (e.g., "Goblin") -4. Set the target count (e.g., 50) -5. Click "Add" to add the condition - -The plugin will track NPC interactions and count kills until the target is reached. - -**Example 2: Multi-NPC Kill Goals with Logical Conditions** - -For more complex hunting goals that involve multiple NPC types: - -1. Create a kill count condition for the first NPC type -2. Create a kill count condition for the second NPC type -3. Use logical operators to combine them: - - AND: Must kill both target counts (e.g., "Kill 50 goblins AND 30 rats") - - OR: Killing either target count is sufficient (e.g., "Kill 50 goblins OR 30 rats") - -**Advanced Features:** - -- **Name Pattern Matching**: Use regular expressions to match similar NPCs (e.g., "Goblin.*" matches all goblin variants) -- **Randomized Goals**: Set min/max ranges for more varied play patterns -- **Progress Tracking**: View detailed statistics including kill rate and completion percentage -- **Interaction Detection**: Accurately tracks which NPCs you're engaged with - -### Varbit Conditions - -Varbit conditions relate to game state variables and track internal game values, allowing you to create rules based on quests, minigames, collection logs, and other game state information. - -#### Available Varbit Condition Types - -| Condition Type | Description | Use Case | -|----------------|-------------|----------| -| Collection Log - Bosses | Based on boss collection log entries | "Stop after collecting all GWD items" | -| Collection Log - Minigames | Based on minigame collection log entries | "Run until Tempoross log is complete" | -| Custom Varbit | Track a specific varbit or varp ID | "Wait until quest state changes" | - -#### Advanced Varbit Features - -- **Relative or Absolute Values**: Compare against absolute values or relative changes from the starting state -- **Comparison Operators**: Use equals, not equals, greater than, less than, etc. -- **Randomization**: Set min/max target ranges for more varied behavior - -#### Working with Varbit Conditions - -Varbit conditions provide powerful ways to track game progress but require some technical understanding: - -1. **Finding Varbit IDs**: Use developer tools to identify the relevant varbit ID for your condition -2. **Understanding Value Ranges**: Most varbits use values 0-1 for off/on states, but some track counts or progress -3. **Test Thoroughly**: Always test your varbit conditions before relying on them for automation - -## Creating Complex Logical Conditions - -The true power of the scheduler comes from creating sophisticated logic by combining multiple conditions. Think of this as building "if-then" statements that determine when your plugins run. - -### Understanding Logical Operators - -Before we dive in, let's understand the basic logical operators: - -- **AND**: All conditions must be true (like saying "I'll only go fishing IF I have bait AND I have a fishing rod") -- **OR**: Any condition can be true (like saying "I'll stop fishing IF my inventory is full OR it's been 2 hours") -- **NOT**: Inverts a condition (like saying "Run the plugin when I'm NOT in the Wilderness") -- **LOCK**: Prevents a plugin from stopping while the lock is active (critical for combat and dangerous activities) - -### Using AND Logic (All Conditions) - -Use AND logic when you want a plugin to start/stop only when ALL specified conditions are met: - -1. Add your first condition (e.g., "Time Window: 6PM-10PM") -2. In the condition tree, select the root node -3. Click "Convert to AND" (the icon changes to show AND logic) -4. Add your second condition (e.g., "Location: Mining Guild") -5. Add any additional conditions - -**Real-World Example: Safe Mining Training** -``` -AND -├── TimeWindow(6:00 PM to 10:00 PM) -├── Location(Mining Guild) -└── InventoryItemCount(Pickaxe) >= 1 -``` -This setup ensures your plugin only runs in the evening, when you're in the Mining Guild, and have a pickaxe. - -### Using OR Logic (Any Conditions) - -Use OR logic when you want a plugin to start/stop when ANY of the specified conditions are met: - -1. Add your first condition (e.g., "Inventory Full") -2. In the condition tree, ensure it's set to OR (this is the default) -3. Add your second condition (e.g., "Time Duration: 2 hours") -4. Add any additional conditions - -**Real-World Example: Smart Fishing Stop Condition** -``` -OR -├── InventoryItemCount(Raw fish) >= 28 -├── TimeDuration(2 hours) -└── PlayerHealth < 20% -``` -This will stop your fishing plugin when your inventory fills OR you've been fishing for 2 hours OR your health gets dangerously low. - -### Using NOT Logic (Negate Conditions) - -Use NOT logic when you want to invert a condition's result: - -1. Select the condition in the tree -2. Click the "Negate" button (usually shown as a "!" icon) -3. The condition will be inverted and shown with "NOT" in the description - -**Real-World Example: Avoid Dangerous Times** - -```text -NOT(TimeWindow(2:00 AM to 6:00 AM)) -``` - -This creates a condition that's true except during the specified hours, helping you avoid playing during suspicious times. - -### Creating Nested Conditions - -For more complex logic, you can create nested condition groups that combine AND and OR logic: - -1. Add several conditions -2. Select a subset of those conditions in the tree -3. Click "Group AND" or "Group OR" to create a sub-group -4. The sub-group will now behave as a single condition within the parent group - -**Real-World Example: Advanced Skilling Strategy** - -```text -AND -├── OR -│ ├── Location(Varrock Mine) -│ └── Location(Al Kharid Mine) -└── AND - ├── InventoryItemCount(Total) < 28 - └── NOT(NearbyPlayer > 5) -``` - -This complex condition translates to: "Run the plugin when I'm at either mining location AND my inventory isn't full AND there aren't too many players nearby." - -### Visual Indicators in the Logical Tree - -The condition tree provides visual feedback about your logical structure: - -- **Connecting lines**: Show the relationship between conditions -- **Icons**: Indicate AND/OR/NOT relationships -- **Checkmarks/X marks**: Show which conditions are currently satisfied -- **Highlighting**: Indicates which condition is selected for editing - -Remember that well-designed logical structures can create extremely sophisticated automation patterns without requiring any actual coding! - -## Understanding One-Time vs. Recurring Conditions - -When configuring conditions, it's important to understand which conditions trigger just once and which can trigger repeatedly. This affects how your plugins will behave over extended periods. - -### One-Time Conditions - -One-time conditions are triggers that once satisfied, will stay satisfied forever. They're perfect for milestone events and permanent changes. - -**Examples of one-time conditions:** - -- **Specific Time condition**: Triggers once exactly at 8:00 PM on June 15 -- **Skill Level condition**: Triggers once when Woodcutting reaches level 70 -- **Item Collection condition**: Triggers once after collecting 1000 feathers -- **Collection Log condition**: Triggers once after completing a boss collection - -**How to identify one-time conditions:** - -- Look for the "One-time" label in the schedule table -- These conditions use absolute values rather than ranges or states -- They typically represent achievement of a specific goal - -**Strategic use:** -One-time conditions are excellent for progression-based automation. For example, you might set up a series of plugins that activate as you reach certain milestones, automatically moving your character from one training method to the next as you level up. - -### Recurring Conditions - -Recurring conditions can trigger multiple times as their state changes between satisfied and unsatisfied. - -**Examples of recurring conditions:** - -- **Time Window condition**: Triggers daily during set hours (e.g., 6PM-10PM) -- **Interval condition**: Triggers repeatedly at set intervals (e.g., every 2 hours) -- **Inventory Item Count**: Can trigger repeatedly as inventory fills and empties -- **Location condition**: Triggers each time you enter or leave an area - -**How they work:** -These conditions can switch between active and inactive states multiple times. For example, a Time Window condition for 6PM-10PM will be: - -- Inactive from midnight until 6PM -- Active from 6PM to 10PM -- Inactive from 10PM to midnight -- Active again at 6PM the next day - -**Strategic use:** -Use recurring conditions to create cyclical behavior patterns. For example, combine an inventory condition with a location condition to create a gathering cycle: gather resources until inventory is full, bank when near a bank, repeat when inventory is empty. - -## Indicators and Visualization - -The condition panel provides visual feedback on condition status: - -- **Green checkmark (✓)**: Condition is currently satisfied -- **Red X (✗)**: Condition is not currently satisfied -- **Lightning bolt (⚡)**: Condition is currently relevant to the plugin's state - -The condition tree visualizes the logical structure of your conditions, allowing you to see at a glance how they're organized. - -## Tips for Effective Condition Configuration - -1. **Start simple**: Begin with just one or two conditions and add complexity gradually -2. **Test thoroughly**: Always test your conditions with controlled parameters first -3. **Use the condition tree**: View the logical structure to ensure it matches your intent -4. **Monitor condition status**: Watch the indicators to see if conditions are behaving as expected -5. **Combine time with other conditions**: For example, "Run for 2 hours OR until inventory is full" -6. **Use nested logical groups**: For complex scenarios like "Run when (at location A OR location B) AND (have antipoison)" - -## Common Issues and Solutions - -### Issue: Plugin never starts - -- **Possible cause**: Start conditions are too restrictive -- **Solution**: Check if all required conditions are being met, or switch from AND to OR logic - -### Issue: Plugin never stops - -- **Possible cause**: Stop conditions are never met or are incorrectly configured -- **Solution**: Add a time-based fallback stop condition - -### Issue: Plugin starts or stops at unexpected times - -- **Possible cause**: Logical structure (AND/OR) is not what you intended -- **Solution**: Review the condition tree and restructure as needed - -### Issue: Condition status indicators don't match expectations - -- **Possible cause**: The condition parameters don't match the current game state -- **Solution**: Verify the current game state and adjust the condition parameters - -## Advanced Condition Techniques - -### Using Lock Conditions - -Lock conditions prevent a plugin from stopping during critical operations: - -1. Add your regular stop conditions -2. Add a LockCondition for critical operations -3. In your plugin code, activate the lock when needed -4. The plugin won't stop while the lock is active, even if stop conditions are met - -### Time Randomization - -For more human-like behavior: - -1. When creating time conditions, use the randomization options -2. For intervals, set a min/max range instead of exact times -3. For time windows, consider adding random variations to start/end times - -### Progressive Conditions - -Create conditions that change as the plugin runs: - -1. Use the plugin's `onStopConditionCheck()` method -2. Modify conditions based on progress -3. This allows for dynamic behavior adaptation - -## Condition Reliability Disclaimer - -> **Important**: Not all conditions have been thoroughly tested in all scenarios. The following list indicates the confidence level in each condition type's reliability: -> -> ### High Confidence (Thoroughly Tested) -> - **Time Conditions**: Well-tested and reliable, though some features users want may be missing (feedback appreciated) -> - **Skill Conditions**: Thoroughly tested and reliable -> - **Location Conditions**: Well-tested in most common areas -> - **Logical Conditions**: Thoroughly tested (AND, OR, NOT operators) -> - **Lock Conditions**: Thoroughly tested -> - **Item Collection Conditions** (from Resource Conditions): Well-tested for most common items -> -> ### Medium Confidence (Tested but May Have Edge Cases) -> - **Varbit Conditions**: Tested for collection log entries and common game variables -> - **Bank Item Conditions**: May have edge cases with certain items -> - **Process Item Conditions**: May have tracking issues with certain processing methods -> -> ### Lower Confidence (Newer Implementations) -> - **NPC Conditions**: Implementation is still being refined, particularly for multi-combat areas -> - **NPC Kill Count Conditions**: May not track kills accurately in crowded areas or specific circumstances -> -> If you encounter any issues with condition reliability, please report them with specific details to help improve the system. For critical tasks, we recommend using the high confidence conditions or adding fallback conditions (such as time limits). - -## Conclusion - -Mastering the condition configuration system is key to effectively using the Plugin Scheduler. With proper condition setup, you can create sophisticated automation plans that respond intelligently to game states, time factors, and resource availability. - -For more information on how conditions fit into the overall Scheduler workflow, including default plugins, priorities, and the "Allow Continue" setting, see the [Plugin Scheduler User Guide](user-guide.md). - -For implementation details about specific condition types, see the [API documentation for conditions](api/conditions/). diff --git a/docs/scheduler/event/plugin-schedule-entry-finished-event.md b/docs/scheduler/event/plugin-schedule-entry-finished-event.md deleted file mode 100644 index 78cb19a01fa..00000000000 --- a/docs/scheduler/event/plugin-schedule-entry-finished-event.md +++ /dev/null @@ -1,175 +0,0 @@ -# Plugin Schedule Entry Finished Event - -## Overview - -The `PluginScheduleEntryMainTaskFinishedEvent` is a crucial component of the Plugin Scheduler system's inter-component communication mechanism. This event is fired when a plugin self-reports that it has completed its assigned task and is ready to be stopped by the scheduler. - -## Class Structure - -```java -@Getter -public class PluginScheduleEntryMainTaskFinishedEvent { - private final Plugin plugin; - private final ZonedDateTime finishDateTime; - private final String reason; - private final boolean success; - - // Constructor implementations -} -``` - -## Key Features - -### Plugin Identification - -The event carries a reference to the specific `Plugin` instance that has completed: - -```java -private final Plugin plugin; -``` - -This allows the scheduler to correctly identify which scheduled plugin has finished, even when multiple plugins might be active or scheduled. - -### Timestamp Tracking - -The event includes a precise timestamp of when the plugin completed its work: - -```java -private final ZonedDateTime finishDateTime; -``` - -This timestamp is used by the scheduler for logging, debugging, and calculating statistics about plugin execution times. - -### Success Reporting - -The event includes a boolean flag indicating whether the plugin completed successfully: - -```java -private final boolean success; -``` - -This allows the scheduler to distinguish between different types of completion: -- `true`: The plugin completed its task as expected and terminated normally -- `false`: The plugin encountered an issue that prevented it from completing its task but was still able to gracefully report its status - -### Reason Documentation - -The event provides a human-readable explanation of why the plugin finished: - -```java -private final String reason; -``` - -This reason string can be used for: -- User interface display -- Logging and diagnostics -- Decision-making about future scheduling attempts - -## Technical Details - -### Event Propagation - -This event is sent through the RuneLite EventBus system: - -```java -// Inside a plugin that wants to report completion -Microbot.getEventBus().post(new PluginScheduleEntryMainTaskFinishedEvent( - this, // The plugin itself - "Inventory full", // Reason for finishing - true // Was successful -)); -``` - -### Constructors - -The class provides two constructor options: - -```java -// Constructor with explicit timestamp -public PluginScheduleEntryMainTaskFinishedEvent( - Plugin plugin, - ZonedDateTime finishDateTime, - String reason, - boolean success -) - -// Constructor using current time as the finish time -public PluginScheduleEntryMainTaskFinishedEvent( - Plugin plugin, - String reason, - boolean success -) -``` - -The second constructor is a convenience method that automatically captures the current time. - -### Immutability - -All fields in the event are marked as `final`, ensuring the event is immutable once created. This prevents potential issues with event data being modified during propagation. - -## Usage Example - -### Inside a Plugin - -A plugin can report its completion using code like this: - -```java -// When the plugin has completed its task successfully -Microbot.getEventBus().post(new PluginScheduleEntryMainTaskFinishedEvent( - this, - "Target level reached", - true -)); - -// Or when the plugin needs to stop due to an issue -Microbot.getEventBus().post(new PluginScheduleEntryMainTaskFinishedEvent( - this, - "Unable to find target NPC", - false -)); -``` - -### Using the ConditionProvider Interface - -For plugins that implement the `ConditionProvider` interface, a convenience method is provided: - -```java -// Inside a plugin that implements ConditionProvider -@Override -public void onSkillLevelReached(int level) { - if (level >= targetLevel) { - reportFinished("Target level " + level + " reached", true); - } -} -``` - -The `reportFinished` method internally creates and posts the `PluginScheduleEntryMainTaskFinishedEvent`. - -### In the Scheduler - -The scheduler listens for these events and processes them: - -```java -@Subscribe -public void onPluginScheduleEntryMainTaskFinishedEvent(PluginScheduleEntryMainTaskFinishedEvent event) { - if (currentPlugin != null && currentPlugin.getPlugin() == event.getPlugin()) { - log.info("Plugin {} reported finished: {} (success={})", - currentPlugin.getName(), - event.getReason(), - event.isSuccess()); - - // Stop the plugin and record the finish reason - currentPlugin.setStopReason( - event.isSuccess() ? StopReason.PLUGIN_FINISHED : StopReason.ERROR); - forceStopCurrentPluginScheduleEntry(); - } -} -``` - -## Relationship to Other Components - -The `PluginScheduleEntryMainTaskFinishedEvent` works alongside other events in the scheduler system: - -- It differs from `PluginScheduleEntryPostScheduleTaskEvent`, which is sent by the scheduler to request that a plugin stop -- It is typically used after the plugin has responded to a soft stop request or has independently determined it should stop -- It provides feedback to the scheduler about the plugin's state at the end of execution \ No newline at end of file diff --git a/docs/scheduler/event/plugin-schedule-entry-soft-stop-event.md b/docs/scheduler/event/plugin-schedule-entry-soft-stop-event.md deleted file mode 100644 index 9d7706b7634..00000000000 --- a/docs/scheduler/event/plugin-schedule-entry-soft-stop-event.md +++ /dev/null @@ -1,153 +0,0 @@ -# Plugin Schedule Entry Soft Stop Event - -## Overview - -The `PluginScheduleEntryPostScheduleTaskEvent` signals the start of post-schedule tasks for a plugin. It is emitted when the scheduler transitions a schedule entry out of its main task phase (e.g., stop conditions met, user-initiated stop, or scheduler shutdown) so that coordinated cleanup/post actions can run. - -## Class Structure - -```java -@Getter -public class PluginScheduleEntryPostScheduleTaskEvent { - private final Plugin plugin; - private final ZonedDateTime stopDateTime; - - public PluginScheduleEntryPostScheduleTaskEvent(Plugin plugin, ZonedDateTime stopDateTime) { - this.plugin = plugin; - this.stopDateTime = stopDateTime; - } -} -``` - -## Key Features - -### Plugin Identification - -The event carries a reference to the specific `Plugin` instance that should stop: - -```java -private final Plugin plugin; -``` - -This allows the event to specifically target the plugin that needs to be stopped, enabling precise communication even in a multi-plugin environment. - -### Timestamp Tracking - -The event includes a timestamp indicating when the stop request was issued: - -```java -private final ZonedDateTime stopDateTime; -``` - -This timestamp serves several purposes: -- Records when the stop decision was made for logging and analytics -- Enables time-based escalation if the plugin doesn't respond promptly -- Provides context to the plugin about the timing of the stop request - -## Technical Details - -### Event Propagation - -This event is sent through the RuneLite EventBus system: - -```java -// Inside the scheduler when a plugin should be stopped -Microbot.getEventBus().post(new PluginScheduleEntryPostScheduleTaskEvent( - plugin, // The plugin to stop - ZonedDateTime.now() // Current time -)); -``` - -### Soft Stop Concept - -The term "soft stop" in the event name is significant: - -1. It indicates that this is a request for the plugin to stop gracefully, not an immediate termination command -2. It gives the plugin an opportunity to: - - Complete critical operations in progress - - Save any necessary state - - Clean up resources - - Reach a safe termination point - -### Immutability - -All fields in the event are marked as `final`, ensuring the event is immutable once created. This prevents potential issues with event data being modified during propagation. - -## Usage Example - -### In the Scheduler - -The scheduler creates and posts this event when a plugin's stop conditions are met: - -```java -private void softStopPlugin(PluginScheduleEntry entry) { - Plugin plugin = entry.getPlugin(); - if (plugin != null) { - log.debug("Sending soft stop request to plugin {}", entry.getName()); - - // Post the event to the EventBus - Microbot.getEventBus().post(new PluginScheduleEntryPostScheduleTaskEvent( - plugin, - ZonedDateTime.now() - )); - - // Set state to indicate stopping is in progress - entry.setStopReason(StopReason.CONDITIONS_MET); - currentState = SchedulerState.STOPPING; - - // Schedule hard stop fallback if plugin doesn't respond in time - if (entry.allowHardStop()) { - scheduleHardStopFallback(entry, Duration.ofSeconds(30)); - } - } -} -``` - -### In a Plugin Implementing ConditionProvider - -Plugins that implement the `ConditionProvider` interface can handle the event: - -```java -@Subscribe -@Override -public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - // Only respond if this event is targeted at this plugin - if (event.getPlugin() == this) { - log.info("Received soft stop request, cleaning up and stopping..."); - - // Perform necessary cleanup - saveCurrentProgress(); - closeOpenInterfaces(); - - // Actually stop the plugin - Microbot.stopPlugin(this); - } -} -``` - -## Relationship to Other Components - -The `PluginScheduleEntryPostScheduleTaskEvent` is part of a coordinated stop sequence: - -1. **Trigger**: Stop conditions are met, manual stop requested, or scheduler is shutting down -2. **Soft Stop**: The scheduler sends `PluginScheduleEntryPostScheduleTaskEvent` to request graceful termination -3. **Plugin Response**: The plugin performs cleanup and either: - - Stops itself directly using `Microbot.stopPlugin(this)` - - Posts a `PluginScheduleEntryMainTaskFinishedEvent` to report completion -4. **Hard Stop Fallback**: If the plugin doesn't respond within a timeout period and is marked as hard-stoppable, the scheduler may forcibly terminate it - -## Best Practices - -### For Plugin Developers - -1. **Implement the Event Handler**: Ensure your plugin properly handles the `PluginScheduleEntryPostScheduleTaskEvent` if it implements `ConditionProvider` -2. **Respond Promptly**: Complete cleanup operations quickly to avoid being hard-stopped -3. **Check Target Plugin**: Always verify that the event is targeting your plugin before processing it -4. **Report Completion**: Consider posting a `PluginScheduleEntryMainTaskFinishedEvent` to provide more context about the stop reason - -### For Scheduler Implementation - -1. **Timeout Management**: Set appropriate timeouts for plugins to respond to soft stop requests -2. **Escalation Path**: Define clear escalation when plugins don't respond to soft stops -3. **Stop Context**: Provide useful context in the event for why the plugin is being stopped -4. **Validation**: Verify that the plugin is actually running before sending a stop event \ No newline at end of file diff --git a/docs/scheduler/plugin-schedule-entry-merged.md b/docs/scheduler/plugin-schedule-entry-merged.md deleted file mode 100644 index 539dfd72472..00000000000 --- a/docs/scheduler/plugin-schedule-entry-merged.md +++ /dev/null @@ -1,753 +0,0 @@ -# Plugin Schedule Entry - -## Overview - -The `PluginScheduleEntry` class is the core component of the Plugin Scheduler system, serving as the central data structure that connects plugins with their scheduling configuration. Each entry represents a scheduled plugin and encapsulates all the information needed to determine when a plugin should start and stop, as well as tracking its execution state and history. - -This class acts as the bridge between the scheduler orchestrator (`SchedulerPlugin`), the plugin API (`SchedulablePlugin`), and the user interface, managing both user-defined conditions and plugin-provided conditions through separate `ConditionManager` instances. - -## Class Definition - -```java -@Data -@AllArgsConstructor -@Getter -@Slf4j -public class PluginScheduleEntry implements AutoCloseable { - // Plugin reference and metadata - private transient Plugin plugin; - private String name; - private boolean enabled; - private boolean allowContinue = true; - private boolean hasStarted = false; - - // Condition management - private ConditionManager startConditionManager; - private ConditionManager stopConditionManager; - - // Scheduling properties - private boolean allowRandomScheduling = true; - private int runCount = 0; - private int priority = 0; // Higher numbers = higher priority - private boolean isDefault = false; - - // State tracking - private String lastStopReason; - private boolean lastRunSuccessful; - private boolean onLastStopUserConditionsSatisfied = false; - private boolean onLastStopPluginConditionsSatisfied = false; - private StopReason lastStopReasonType = StopReason.NONE; - private Duration lastRunDuration = Duration.ZERO; - private ZonedDateTime lastRunStartTime; - private ZonedDateTime lastRunEndTime; - - // Stop reason enumeration - public enum StopReason { - NONE("None"), - MANUAL_STOP("Manually Stopped"), - PLUGIN_FINISHED("Plugin Finished"), - ERROR("Error"), - SCHEDULED_STOP("Scheduled Stop"), - INTERRUPTED("Interrupted"), - HARD_STOP("Hard Stop"); - - private final String description; - - // Implementation details... - } - - // Other implementation details... -} -``` - -## Key Features - -### Plugin Reference and Identification - -Each `PluginScheduleEntry` maintains a reference to the actual RuneLite `Plugin` instance it schedules, along with a name for display and identification purposes. The plugin reference is marked as `transient` to ensure it's not serialized when the schedule entry is saved to configuration. - -### Scheduling Properties - -The class includes several properties that directly affect how a plugin gets scheduled: - -#### Priority System - -```java -private int priority = 0; // Higher numbers = higher priority -``` - -The `priority` field is used to determine which plugins are considered first during scheduling: - -- Higher numbers indicate higher priority -- Plugins with the same priority are grouped together for scheduling decisions -- When multiple plugins are due to run at the same time, the highest priority plugins are considered first - -The scheduler will always prefer to run the highest priority plugins first. Only if there are multiple plugins with the same priority level will other factors like the default status, randomization, or weighted selection be considered. - -#### Default Plugin Status - -```java -private boolean isDefault = false; // Flag to indicate if this is a default plugin -``` - -The `isDefault` flag marks a plugin as a "default" plugin that can be preempted by non-default plugins: - -- Default plugins are lower-priority in the overall scheduling system -- Non-default plugins are always preferred over default plugins with the same priority -- The Scheduler can be configured to stop default plugins when a non-default plugin is about to run -- This allows users to run certain critical plugins at scheduled times without interference - -The scheduler configuration includes settings like `prioritizeNonDefaultPlugins` which can automatically stop default plugins when a non-default plugin is scheduled to run soon. - -#### Random Scheduling Support - -```java -private boolean allowRandomScheduling = true; // Whether this plugin can be randomly scheduled -``` - -The `allowRandomScheduling` flag controls whether a plugin participates in the weighted random selection process: - -- When `true` (the default), the plugin can be selected randomly among other plugins with the same priority -- When `false`, the plugin will be strictly scheduled based on its trigger time and priority -- Non-randomizable plugins are always prioritized over randomizable plugins with the same priority - -This property is particularly important for plugins that must run at exact times (like plugins that perform critical actions at specific game events), as setting `allowRandomScheduling = false` will ensure they run exactly when scheduled, without any randomization. - -#### Run Count Tracking - -```java -private int runCount = 0; // Track how many times this plugin has been run -``` - -The `runCount` property keeps track of how many times the plugin has been executed and is used for weighted selection: - -- Plugins that have run less frequently are given a higher weight in the weighted selection algorithm -- This ensures a balanced distribution of execution time among different plugins -- The weight calculation is: `weight = (maxRuns - runCount + 1)` where `maxRuns` is the highest run count among all plugins - -### Dual Condition Management System - -#### User Conditions vs. Plugin Conditions - -The class uses two distinct sets of conditions, each managed by separate `ConditionManager` instances: - -1. **User Conditions**: Defined through the UI by the end user. These are conditions the user configures to determine when a plugin should start or stop. - - Added via `addStartCondition()` and `addStopCondition()` methods - - Persist across client sessions - - Can be modified through the UI - -2. **Plugin Conditions**: Defined programmatically by implementing the `SchedulablePlugin` interface. These are conditions defined within the plugin's own code. - - Provided via `getStartCondition()` and `getStopCondition()` methods - - Typically define the plugin's own business logic around when it should run - - Cannot be modified through the UI - -Both types of conditions are evaluated separately with their own logical rules, then combined for the final decision: - -```java -// For start conditions -private boolean areUserStartConditionsMet() { - if (startConditionManager.getUserConditions().isEmpty()) { - return true; - } - return startConditionManager.areUserConditionsMet(); -} - -private boolean arePluginStartConditionsMet() { - if (startConditionManager.getPluginConditions().isEmpty()) { - return true; - } - return startConditionManager.arePluginConditionsMet(); -} - -// Both must be satisfied -public boolean isDueToRun() { - // ... other checks ... - if (areUserStartConditionsMet() && arePluginStartConditionsMet()) { - return true; - } - return false; -} -``` - -The same pattern applies to stop conditions. This dual approach provides flexibility while maintaining control - users can automate plugins according to their needs, but plugins can still enforce their own operational requirements. - -#### Condition Evaluation Logic - -The combination of user and plugin conditions follows these rules: - -- **Start Logic**: `(User Start Conditions AND Plugin Start Conditions)` - - Both sets must be satisfied for the plugin to start - - If either set is empty, it's treated as automatically satisfied - -- **Stop Logic**: `(User Stop Conditions OR Plugin Stop Conditions)` - - Either set being satisfied is sufficient to stop the plugin - - If both sets are empty, the plugin won't stop automatically - -This provides a balance of control between the user and the plugin developer. - -### Stop Mechanism - -The `PluginScheduleEntry` class supports several ways a plugin can be stopped, tracked through the `StopReason` enum: - -| Stop Reason | Description | Initiated By | -|-------------|-------------|------------| -| `NONE` | Plugin hasn't stopped (still running or never started) | - | -| `MANUAL_STOP` | The user manually stopped the plugin | User | -| `PLUGIN_FINISHED` | The plugin self-reported completion through `reportFinished()` | Plugin | -| `ERROR` | An error occurred during plugin execution | System | -| `SCHEDULED_STOP` | The plugin was stopped due to its scheduled stop conditions being met | Scheduler | -| `INTERRUPTED` | Plugin was interrupted (e.g., client shutdown) | System | -| `HARD_STOP` | Plugin was forcibly terminated after not responding to a soft stop | Scheduler | - -#### The Stopping Process - -The stopping process follows a sophisticated pattern: - -1. **Stop Initiation**: When conditions are met or user triggers a stop, the `stopInitiated` flag is set and the stop process begins -2. **Soft Stop**: A `PluginScheduleEntryPostScheduleTaskEvent` is sent to the plugin, allowing it to perform cleanup operations -3. **Grace Period**: The plugin gets time to clean up (based on `softStopRetryInterval`) -4. **Stop Monitoring**: A separate monitoring thread tracks the stopping process -5. **Hard Stop**: If the plugin doesn't respond within `hardStopTimeout`, a forced stop occurs - -```java -private void softStop(boolean successfulRun) { - // Set flags to track stop process - stopInitiated = true; - stopInitiatedTime = ZonedDateTime.now(); - lastStopAttemptTime = stopInitiatedTime; - - // Create and post the stop event - PluginScheduleEntryPostScheduleTaskEvent stopEvent = new PluginScheduleEntryPostScheduleTaskEvent( - this, - isRunning(), - areUserDefinedStopConditionsMet(), - arePluginStopConditionsMet(), - lastStopReasonType - ); - - // Post event to notify the plugin - Microbot.getEventBus().post(stopEvent); - - // Start monitoring thread to track stop progress - startStopMonitoringThread(successfulRun); -} -``` - -#### Stop Monitoring - -A dedicated monitoring thread watches the plugin during the stopping process: - -```java -private void startStopMonitoringThread(boolean successfulRun) { - if (isMonitoringStop) { - return; - } - - isMonitoringStop = true; - - stopMonitorThread = new Thread(() -> { - try { - // Keep checking until the stop completes or is abandoned - while (stopInitiated && isMonitoringStop) { - // Check if plugin has stopped running - if (!isRunning()) { - // Plugin has stopped, update state and exit loop - stopInitiated = false; - hasStarted = false; - // ...other cleanup... - break; - } - - // Check every 300ms to be responsive but not wasteful - Thread.sleep(300); - } - } catch (InterruptedException e) { - // Thread was interrupted, just exit - } finally { - isMonitoringStop = false; - } - }); - - stopMonitorThread.setName("StopMonitor-" + name); - stopMonitorThread.setDaemon(true); // Don't prevent JVM exit - stopMonitorThread.start(); -} -``` - -This ensures that plugins have a chance to clean up resources before being terminated while still preventing hung processes. - -### Condition Management Methods - -The class provides methods to manipulate its start and stop conditions: - -```java -// Add a start condition to determine when the plugin should be activated -public void addStartCondition(Condition condition) { - startConditionManager.addUserCondition(condition); -} - -// Add a stop condition to determine when the plugin should be deactivated -public void addStopCondition(Condition condition) { - stopConditionManager.addUserCondition(condition); -} -``` - -### Condition Watchdogs - -Condition watchdogs are scheduled tasks that periodically update conditions from the plugin. This is particularly useful for dynamic conditions that need to be re-evaluated regularly: - -```java -public boolean scheduleConditionWatchdogs(long checkIntervalMillis, UpdateOption updateOption) { - if (!(this.plugin instanceof SchedulablePlugin)) { - return false; - } - - SchedulablePlugin schedulablePlugin = (SchedulablePlugin) this.plugin; - - // Create suppliers that get the current plugin conditions - Supplier startConditionSupplier = - () -> schedulablePlugin.getStartCondition(); - - Supplier stopConditionSupplier = - () -> schedulablePlugin.getStopCondition(); - - // Schedule the watchdogs - startConditionWatchdogFuture = startConditionManager.scheduleConditionWatchdog( - startConditionSupplier, - checkIntervalMillis, - updateOption - ); - - stopConditionWatchdogFuture = stopConditionManager.scheduleConditionWatchdog( - stopConditionSupplier, - checkIntervalMillis, - updateOption - ); - - return true; -} -``` - -This mechanism allows plugins to provide fresh conditions at runtime, enabling adaptive behavior based on changing game states. - -### Plugin Lock Mechanism - -A sophisticated locking system allows plugins to temporarily prevent being stopped during critical operations: - -```java -public boolean isLocked() { - if (!(plugin instanceof SchedulablePlugin)) { - return false; - } - - SchedulablePlugin schedulablePlugin = (SchedulablePlugin) plugin; - return schedulablePlugin.isLocked(null); -} - -// SchedulablePlugin interface provides these methods: -// boolean lock(Condition stopCondition); -// boolean unlock(Condition stopCondition); -``` - -This is crucial for operations that should not be interrupted, such as trading, banking, or other sensitive activities. - -### Progress Tracking - -The class includes methods to track and report the progress of both start and stop conditions: - -```java -// Get progress percentage toward stop conditions being met -public double getStopConditionProgress() { - return stopConditionManager.getFullConditionProgress(); -} - -// Get progress percentage toward start conditions being met -public double getStartConditionProgress() { - return startConditionManager.getFullConditionProgress(); -} -``` - -### Timing Calculation - -Methods are provided to calculate: - -- When the plugin is next scheduled to run -- The duration until the next scheduled execution -- Whether the plugin is due to run based on its conditions -- Total and average runtime statistics -- Randomized intervals for more natural scheduling - -```java -// Get the next time this plugin is scheduled to run -public Optional getCurrentStartTriggerTime() { - return startConditionManager.getCurrentTriggerTime(); -} - -// Check if the plugin is due to run based on its conditions -public boolean isDueToRun() { - // Check basic preconditions - if (isRunning() || !hasAnyStartConditions()) { - return false; - } - - // For diagnostic purposes, we may log detailed condition information - if (Microbot.isDebug()) { - String diagnosticInfo = diagnoseStartConditions(); - log.debug("\n[isDueToRun] - \n"+diagnosticInfo); - } - - // Check if all start conditions are met (combining both user and plugin conditions) - return startConditionManager.areAllConditionsMet(); -} - -// Get a user-friendly string showing when the plugin will next run -public String getNextStartTriggerTimeString() { - Optional triggerTime = getCurrentStartTriggerTime(); - if (triggerTime.isPresent()) { - ZonedDateTime now = ZonedDateTime.now(); - Duration until = Duration.between(now, triggerTime.get()); - if (until.isNegative()) { - return "Due now"; - } - return formatDuration(until) + " from now"; - } - return "Not scheduled"; -} -``` - -## The Plugin Scheduling Algorithm - -The plugin scheduling process in the `SchedulerPlugin` uses several key properties from `PluginScheduleEntry` to determine which plugin to run next: - -1. **Basic Filters**: - - Only enabled plugins are considered - - Only plugins that are due to run (conditions satisfied) are considered - - Plugins with `stopInitiated` are excluded - -2. **Priority-Based Selection**: - - Plugins are first filtered by the highest `priority` value - - Among equally high priority plugins, non-default plugins (`isDefault = false`) are preferred - -3. **Random vs. Non-Random Selection**: - - Non-randomizable plugins (`allowRandomScheduling = false`) are always preferred over randomizable ones - - Non-randomizable plugins are strictly ordered by their trigger times (earliest first) - -4. **Weighted Selection for Randomizable Plugins**: - - For randomizable plugins with the same priority and default status, a weighted selection is applied - - Plugins with lower `runCount` values receive higher weights - - The weight formula: `weight = (maxRuns - plugin's runCount + 1)` - - This creates a balanced distribution, with less-frequently run plugins having a higher chance of selection - -5. **Final Selection**: - - If multiple plugins have the exact same trigger time and other factors, a stable sort by name and object identity is used - -### Non-Default Plugin Prioritization - -The scheduler can be configured to interrupt or prevent default plugins from running when a non-default plugin is scheduled soon: - -```java -// In SchedulerPlugin.java -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 the next due plugin is a default plugin, don't start it - if (nextDuePlugin.isPresent() && nextDuePlugin.get().isDefault()) { - log.info("Not starting default plugin '{}' because non-default plugin '{}' is scheduled within {} minutes", - nextDuePlugin.get().getCleanName(), - upcomingNonDefault.getCleanName(), - nonDefaultPluginLookAheadMinutes); - return; - } -} -``` - -This mechanism ensures that high-priority, non-default plugins can run as scheduled without being blocked by long-running default plugins. - -## Integration with SchedulablePlugin - -When a plugin implements the `SchedulablePlugin` interface, `PluginScheduleEntry` detects this and interacts with it differently: - -1. Retrieves plugin-defined start and stop conditions -2. Sets up condition watchdogs to periodically refresh those conditions -3. Routes stop events to the plugin so it can handle cleanup -4. Respects the plugin's lock status when stopping -5. Tracks whether the plugin self-reports completion - -This creates a strong relationship between the plugin and its schedule entry, allowing for sophisticated scheduling behaviors. - -```java -// Check if the plugin implements SchedulablePlugin -public boolean isSchedulablePlugin() { - return this.plugin instanceof SchedulablePlugin; -} - -// Register plugin conditions -private boolean registerPluginStartingConditions(UpdateOption updateOption) { - if (!(this.plugin instanceof SchedulablePlugin)) { - return false; - } - - SchedulablePlugin provider = (SchedulablePlugin) plugin; - LogicalCondition pluginLogic = provider.getStartCondition(); - - if (pluginLogic != null) { - return startConditionManager.updatePluginCondition(pluginLogic, updateOption); - } - - return false; -} -``` - -## Usage Example - -Here's an extended example showing how to create and configure a `PluginScheduleEntry` with scheduling properties: - -```java -// Create a high-priority, non-default schedule entry for a critical plugin -PluginScheduleEntry criticalEntry = new PluginScheduleEntry( - criticalPlugin, // Plugin instance - "Critical Task", // Display name - true // Enabled -); -criticalEntry.setPriority(10); // High priority (default is 0) -criticalEntry.setDefault(false); // Not a default plugin -criticalEntry.setAllowRandomScheduling(false); // Must run at exact scheduled time - -// Add start condition (when the plugin should activate) -criticalEntry.addStartCondition(new TimeWindowCondition( - LocalTime.of(12, 0), - LocalTime.of(12, 30) -)); - -// Create a low-priority default plugin that can be randomized -PluginScheduleEntry defaultEntry = new PluginScheduleEntry( - backgroundPlugin, // Plugin instance - "Background Task", // Display name - true // Enabled -); -defaultEntry.setPriority(0); // Low priority -defaultEntry.setDefault(true); // This is a default plugin -defaultEntry.setAllowRandomScheduling(true); // Can be randomly scheduled - -// Register with the scheduler -schedulerPlugin.addScheduledPlugin(criticalEntry); -schedulerPlugin.addScheduledPlugin(defaultEntry); -``` - -In this example, the critical task will always run at exactly 12:00-12:30, while the background task will run based on weighted selection when no critical tasks are scheduled. - -## Internal Lifecycle - -The internal plugin lifecycle follows this pattern: - -1. **Creation**: Plugin schedule entry is created and configured -2. **Registration**: Entry is registered with the `SchedulerPlugin` -3. **Start Check**: Periodically checks if start conditions are met -4. **Activation**: When conditions are met, plugin is started -5. **Monitoring**: While running, stop conditions are evaluated -6. **Deactivation**: When stop conditions are met, stopping process begins -7. **Cleanup**: Plugin gets opportunity to clean up before full stop -8. **Reset**: Conditions are reset for the next activation cycle - -## Key Methods - -### Starting and Stopping - -```java -// Start the plugin -public boolean start(boolean logConditions) { - if (getPlugin() == null || !this.isEnabled() || isRunning()) { - return false; - } - - // Log conditions if requested - if (logConditions) { - logStartConditionsWithDetails(); - logStopConditionsWithDetails(); - } - - // Reset conditions if needed - if (!this.allowContinue || lastStopReasonType != StopReason.INTERRUPTED) { - resetStopConditions(); - } - - // Set state and start the plugin - this.lastRunStartTime = ZonedDateTime.now(); - Microbot.startPlugin(plugin); - return true; -} - -// Check if stop conditions are met -public boolean shouldStop() { - if (!isRunning()) { - return false; - } - - // Check if the plugin is locked - if (isLocked()) { - return false; - } - - // Check both plugin and user conditions - return arePluginStopConditionsMet() && areUserDefinedStopConditionsMet(); -} -``` - -### Condition Evaluation - -```java -// Check if plugin-defined stop conditions are met -private boolean arePluginStopConditionsMet() { - if (stopConditionManager.getPluginConditions().isEmpty()) { - return true; - } - return stopConditionManager.arePluginConditionsMet(); -} - -// Check if user-defined stop conditions are met -private boolean areUserDefinedStopConditionsMet() { - if (stopConditionManager.getUserConditions().isEmpty()) { - return true; - } - return stopConditionManager.areUserConditionsMet(); -} -``` - -### Condition Validation and Optimization - -```java -// Validates stop conditions structure and logs issues -private void validateStopConditions() { - LogicalCondition stopLogical = getStopConditionManager().getFullLogicalCondition(); - if (stopLogical != null) { - List issues = stopLogical.validateStructure(); - if (!issues.isEmpty()) { - log.warn("Validation issues found in stop conditions for '{}':", name); - for (String issue : issues) { - log.warn(" - {}", issue); - } - } - } -} - -// Optimizes condition structures by flattening unnecessary nesting -private void optimizeConditionStructures() { - LogicalCondition startLogical = getStartConditionManager().getFullLogicalCondition(); - if (startLogical != null) { - boolean optimized = startLogical.optimizeStructure(); - if (optimized) { - log.debug("Optimized start condition structure for '{}'", name); - } - } - - LogicalCondition stopLogical = getStopConditionManager().getFullLogicalCondition(); - if (stopLogical != null) { - boolean optimized = stopLogical.optimizeStructure(); - if (optimized) { - log.debug("Optimized stop condition structure for '{}'", name); - } - } -} -``` - -## Stop Flags and Monitoring - -The `stopInitiated` flag is a crucial part of the stopping process, indicating that the plugin is in the process of being stopped. This flag is set in the `softStop()` method when the stop process begins and cleared when the plugin is fully stopped: - -```java -// Set when stop process begins -stopInitiated = true; -stopInitiatedTime = ZonedDateTime.now(); - -// Checked by monitoring thread -while (stopInitiated && isMonitoringStop) { - if (!isRunning()) { - // Plugin has stopped, clear the flag - stopInitiated = false; - break; - } - Thread.sleep(300); -} -``` - -The stop monitoring thread continuously checks if the plugin has stopped running and updates the state once the stop is complete. If the plugin doesn't stop within the configured timeout, a hard stop may be initiated. - -### Hard Stop Fallback - -If a plugin doesn't respond to a soft stop within the configured timeout, a hard stop is performed: - -```java -private void hardStop(boolean successfulRun) { - log.warn("Performing hard stop on plugin '{}'", name); - - // Force stop the plugin - Microbot.stopPlugin(plugin); - - // Update state - lastStopReasonType = StopReason.HARD_STOP; - lastStopReason = "Plugin did not respond to soft stop and was forcibly terminated"; - stopInitiated = false; - hasStarted = false; - - // ...additional cleanup... -} -``` - -## Best Practices - -When working with `PluginScheduleEntry`: - -1. Prefer setting both start and stop conditions for predictable behavior -2. Use plugin-defined conditions for essential business logic -3. Allow user-defined conditions for customization -4. Consider plugin safety with appropriate lock usage during critical operations -5. Use the soft-stop mechanism to ensure your plugin can clean up properly -6. Make good use of the `reportFinished()` method to signal natural completion -7. Set appropriate priority levels - use higher values only for truly critical plugins -8. Mark essential time-sensitive plugins as `allowRandomScheduling = false` -9. Use the `isDefault` flag for background plugins that can be interrupted - -## Advanced Features - -The class includes several advanced features: - -- Condition validation and optimization -- Detailed logging and diagnostics -- Time condition randomization for bot detection prevention -- Support for complex logical hierarchies through the `LogicalCondition` framework -- Serialization support for configuration persistence - -## Relationship with SchedulerPlugin - -The `PluginScheduleEntry` class works closely with the `SchedulerPlugin` class, which acts as the orchestrator for the entire scheduling system: - -1. **SchedulerPlugin** manages a collection of `PluginScheduleEntry` instances -2. It periodically checks each entry to determine if it should start or stop -3. It handles the scheduling of breaks between plugin executions -4. It manages the overall scheduler state (IDLE, EXECUTING, STOPPING, etc.) -5. It provides methods to add, remove, and configure scheduled plugins - -This separation of concerns allows the `PluginScheduleEntry` to focus on the state and management of an individual plugin while the `SchedulerPlugin` handles the higher-level orchestration. - -```java -// In SchedulerPlugin: -public void checkPluginsToStart() { - for (PluginScheduleEntry entry : scheduledPlugins) { - if (entry.isDueToRun()) { - entry.start(true); - } - } -} - -public void checkPluginsToStop() { - for (PluginScheduleEntry entry : getRunningScheduledPlugins()) { - if (entry.shouldStop()) { - entry.initiateStop(PluginScheduleEntry.StopReason.SCHEDULED_STOP, "Stop conditions met", true); - } - } -} -``` diff --git a/docs/scheduler/plugin-writers-guide.md b/docs/scheduler/plugin-writers-guide.md deleted file mode 100644 index e68e8bd842e..00000000000 --- a/docs/scheduler/plugin-writers-guide.md +++ /dev/null @@ -1,626 +0,0 @@ -# Plugin Writer's Guide for the Scheduler Infrastructure - -## Introduction - -This guide provides comprehensive information for plugin developers who want to make their plugins compatible with the Plugin Scheduler system. By implementing the `SchedulablePlugin` interface, your plugin can take advantage of sophisticated scheduling capabilities, including condition-based starting and stopping,5. **Document Conditions**: Make sure your condition implementations have clear descriptions that explain what they do. - -6. **Test Thoroughly**: Test your plugin with the scheduler under various scenarios to ensure it behaves as expected. - -7. **Use LockCondition for Critical Operations**: Always protect critical operations with a LockCondition, especially in combat contexts. See [Combat Lock Examples](combat-lock-examples.md) for detailed patterns used in bossing plugins. - -## Example Implementation: GotrPluginrity-based execution, and integration with the scheduler's user interface. - -## Understanding the SchedulablePlugin Interface - -The `SchedulablePlugin` interface is the cornerstone of the scheduler infrastructure. It defines the contract that plugins must follow to work with the scheduler system. - -### Core Methods - -```java -public interface SchedulablePlugin { - // Required methods - LogicalCondition getStartCondition(); - LogicalCondition getStopCondition(); - void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event); - - // Optional methods with default implementations - void onStopConditionCheck(); - void reportFinished(String reason, boolean success); - boolean allowHardStop(); - ConfigDescriptor getConfigDescriptor(); - // Lock-related methods - boolean isLocked(Condition stopConditions); - boolean lock(Condition stopConditions); - boolean unlock(Condition stopConditions); - Boolean toggleLock(Condition stopConditions); -} -``` - -Each method plays a specific role in how your plugin interacts with the scheduler: - -1. **getStartCondition()**: Defines when your plugin is eligible to start. -2. **getStopCondition()**: Defines when your plugin should terminate. -3. **onPluginScheduleEntryPostScheduleTaskEvent()**: Handles graceful shutdown requests from the scheduler. -4. **onStopConditionCheck()**: Hook for updating condition state before evaluation. -5. **reportFinished()**: Allows the plugin to self-report task completion. -6. **allowHardStop()**: Indicates if the plugin can be forcibly terminated. -7. **Lock methods**: Prevent the plugin from being stopped during critical operations. -8. **getConfigDescriptor()**: Provides the scheduler with configuration information. - -## Step-by-Step Implementation Guide - -### Step 1: Implement the SchedulablePlugin Interface - -```java -@PluginDescriptor( - name = "My Schedulable Plugin", - description = "A plugin that works with the scheduler", - tags = {"microbot", "scheduler"}, - enabledByDefault = false -) -@Slf4j -public class MyPlugin extends Plugin implements SchedulablePlugin { - // Plugin implementation... -} -``` - -### Step 2: Define Stop Conditions - -The stop condition determines when your plugin should terminate. This is a required implementation: - -```java -@Override -public LogicalCondition getStopCondition() { - // Create a logical condition structure for when the plugin should stop - OrCondition orCondition = new OrCondition(); - - // Create a lock condition to prevent stopping during critical operations , and the break handler for taking a break - LockCondition lockCondition = new LockCondition("Locked during critical operation", true); //ensure unlock on shutdown of the plugin ! - - // Add your specific conditions - orCondition.addCondition(new TimeCondition(30, TimeUnit.MINUTES)); - orCondition.addCondition(new InventoryFullCondition()); - - // Combine with lock condition using AND logic - AndCondition andCondition = new AndCondition(); - andCondition.addCondition(orCondition); - andCondition.addCondition(lockCondition); - - return andCondition; -} -``` - -Real-world example from GotrPlugin: - -```java -@Override -public LogicalCondition getStopCondition() { - if (this.stopCondition == null) { - this.stopCondition = createStopCondition(); - } - return this.stopCondition; -} - -private LogicalCondition createStopCondition() { - if (this.lockCondition == null) { - this.lockCondition = new LockCondition("Locked because the Plugin " + getName() + " is in a critical operation", true); //ensure unlock on shutdown of the plugin ! - } - - AndCondition andCondition = new AndCondition(); - andCondition.addCondition(lockCondition); - return andCondition; -} -``` - -The GotrPlugin example shows a minimal implementation that only uses a lock condition. This is because the Guardians of the Rift minigame has its own natural start and end points, and the plugin uses the lock condition to prevent the scheduler from stopping it during an active game. - -### Step 3: Define Start Conditions (Optional) - -If you want to restrict when your plugin can start, implement the `getStartCondition()` method: - -```java -@Override -public LogicalCondition getStartCondition() { - // Create a logical condition for start conditions - OrCondition startCondition = new OrCondition(); - - // Add conditions based on your requirements - startCondition.addCondition(new LocationCondition( - "Grand Exchange", - 20 - )); - - return startCondition; -} -``` - -If you don't need specific start conditions (the plugin can start anytime), you can use the default implementation which returns a simple `AndCondition`. - -### Step 4: Implement the Soft Stop Handler - -The soft stop handler is essential for graceful shutdown. It's triggered when the scheduler determines that your plugin should stop: - -```java -@Override -@Subscribe -public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - if (event.getPlugin() == this) { - log.info("Scheduler requesting plugin shutdown"); - - // Perform any necessary cleanup - saveState(); - - // Schedule the actual stop on the client thread - Microbot.getClientThread().invokeLater(() -> { - Microbot.stopPlugin(this); - return true; - }); - } -} -``` - -Real-world example from GotrPlugin: - -```java -@Subscribe -public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - if (event.getPlugin() == this) { - Microbot.log("Scheduler about to turn off Guardians of the Rift"); - - if (exitScheduledFuture != null) { - return; // Exit task is already scheduled - } - - exitScheduledFuture = exitExecutorService.scheduleWithFixedDelay(() -> { - try { - if (lockCondition != null && lockCondition.isLocked()) { - Microbot.log("Exiting GOTR - waiting for the game to end"); - sleep(10000); - return; - } - gotrScript.shutdown(); - sleepUntil(() -> !gotrScript.isRunning(), 10000); - GotrScript.leaveMinigame(); - - Microbot.log("Successfully exited GOTR - stopping plugin"); - Microbot.getClientThread().invokeLater(() -> { - Microbot.stopPlugin(this); - return true; - }); - } catch (Exception ex) { - Microbot.log("Error during safe exit: " + ex.getMessage()); - Microbot.getClientThread().invokeLater(() -> { - Microbot.stopPlugin(this); - return true; - }); - } - }, 0, 500, TimeUnit.SECONDS); - } -} -``` - -The GotrPlugin example demonstrates a more sophisticated approach: - -1. It schedules a periodic check to see if it's safe to exit -2. It respects the lock condition to avoid stopping during an active game -3. It performs proper cleanup with `gotrScript.shutdown()` -4. It actively leaves the minigame area before stopping -5. It handles exceptions gracefully - -### Step 5: Using the Lock Condition - -The lock condition is a powerful feature that prevents your plugin from being stopped during critical operations: - -```java -// Creating the lock condition -this.lockCondition = new LockCondition("Locked during critical operation", true; //ensure unlock on shutdown of the plugin ! - -// Locking before a critical operation -lockCondition.lock(); -try { - // Perform critical operation that shouldn't be interrupted - performBankTransaction(); -} finally { - // Always unlock when the critical operation is complete - lockCondition.unlock(); -} -``` - -Real-world example from GotrPlugin: - -```java -// During chat message handling -if (msg.contains("The rift becomes active!")) { - if (Microbot.isPluginEnabled(BreakHandlerPlugin.class)) { - BreakHandlerScript.setLockState(true); - } - GotrScript.nextGameStart = Optional.empty(); - GotrScript.timeSincePortal = Optional.of(Instant.now()); - GotrScript.isFirstPortal = true; - GotrScript.state = GotrState.ENTER_GAME; - if (lockCondition != null) { - lockCondition.lock(); - } -} -else if (msg.toLowerCase().contains("closed the rift!") || msg.toLowerCase().contains("The great guardian was defeated!")) { - if (Microbot.isPluginEnabled(BreakHandlerPlugin.class)) { - Global.sleep(Rs2Random.randomGaussian(2000, 300)); - BreakHandlerScript.setLockState(false); - } - if (lockCondition != null) { - lockCondition.unlock(); - } - GotrScript.shouldMineGuardianRemains = true; -} -``` - -The GotrPlugin example shows how to: - -1. Lock the plugin when the GOTR minigame starts -2. Unlock it when the minigame ends -3. Integrate with other systems like the break handler - -### Step 6: Reporting Task Completion - -If your plugin can determine when it has completed its task, it should report this to the scheduler: - -```java -// Report successful completion -reportFinished("Task completed successfully", true); - -// Report unsuccessful completion -reportFinished("Failed to complete task: insufficient resources", false); -``` - -The scheduler will handle this report and update the plugin's status accordingly. - -### Step 7: Providing Configuration Information - -If your plugin has configurable options that should be accessible from the scheduler interface, implement the `getConfigDescriptor()` method: - -```java -@Override -public ConfigDescriptor getConfigDescriptor() { - if (Microbot.getConfigManager() == null) { - return null; - } - MyPluginConfig conf = Microbot.getConfigManager().getConfig(MyPluginConfig.class); - return Microbot.getConfigManager().getConfigDescriptor(conf); -} -``` - -Real-world example from GotrPlugin: - -```java -@Override -public ConfigDescriptor getConfigDescriptor() { - if (Microbot.getConfigManager() == null) { - return null; - } - GotrConfig conf = Microbot.getConfigManager().getConfig(GotrConfig.class); - return Microbot.getConfigManager().getConfigDescriptor(conf); -} -``` - -## Condition Types and Logical Structures - -### Common Condition Types - -The scheduler provides various condition types for different scenarios: - -1. **Time Conditions**: - - `TimeCondition`: Stops after a specified duration - - `IntervalCondition`: Runs at specific intervals - - `TimeWindowCondition`: Runs within specific time windows - - `DayOfWeekCondition`: Runs on specific days of the week - - `SingleTriggerTimeCondition`: Runs once at a specific time - -2. **Resource Conditions**: - - `InventoryItemCountCondition`: Stops when inventory contains a specific number of items - - `BankItemCountCondition`: Stops based on bank contents - - `GatheredResourceCondition`: Tracks gathered resources - - `LootItemCondition`: Tracks looted items - - `ProcessItemCondition`: Tracks item processing (crafting, smithing, etc.) - -3. **Location Conditions**: - - `AreaCondition`: Checks if player is in a specific area - - `RegionCondition`: Checks if player is in a specific region - - `PositionCondition`: Checks if player is at a specific position - -4. **Skill Conditions**: - - `SkillLevelCondition`: Stops when a skill reaches a level - - `SkillXpCondition`: Stops after gaining a certain amount of XP - -5. **NPC Conditions**: - - `NpcKillCountCondition`: Stops after killing a number of NPCs - -6. **Varbit Conditions**: - - `VarbitCondition`: Tracks game state variables - -7. **Logical Conditions**: - - `AndCondition`: Combines conditions with AND logic - - `OrCondition`: Combines conditions with OR logic - - `NotCondition`: Inverts a condition - - `LockCondition`: Special condition for preventing plugin termination - -### Creating Logical Structures - -Conditions can be combined using logical operators: - -1. **AND Logic**: All conditions must be satisfied - -```java -AndCondition andCondition = new AndCondition(); -andCondition.addCondition(conditionA); -andCondition.addCondition(conditionB); -``` - -2. **OR Logic**: Any condition can be satisfied - -```java -OrCondition orCondition = new OrCondition(); -orCondition.addCondition(conditionA); -orCondition.addCondition(conditionB); -``` - -3. **NOT Logic**: Inverts a condition - -```java -NotCondition notCondition = new NotCondition(conditionA); -``` - -4. **Complex Logic**: Conditions can be nested - -```java -// (A OR B) AND C -AndCondition root = new AndCondition(); -OrCondition orGroup = new OrCondition(); - -orGroup.addCondition(conditionA); -orGroup.addCondition(conditionB); - -root.addCondition(orGroup); -root.addCondition(conditionC); -``` - -## Best Practices - -1. **Always Use Lock Conditions**: Include a lock condition in your stop condition structure to prevent your plugin from being stopped during critical operations. - -2. **Handle Soft Stops Gracefully**: Implement proper cleanup in your `onPluginScheduleEntryPostScheduleTaskEvent` method. - -3. **Use the Client Thread**: Always stop your plugin on the client thread to avoid synchronization issues. - -4. **Report Completion**: Use `reportFinished()` when your plugin completes its task, rather than letting it run until stop conditions are met. - -5. **Provide Clear Configuration**: Implement `getConfigDescriptor()` to make your plugin's configuration accessible from the scheduler. - -6. **Document Conditions**: Make sure your condition implementations have clear descriptions that explain what they do. - -7. **Test Thoroughly**: Test your plugin with the scheduler under various scenarios to ensure it behaves as expected. - -## Example Implementation: GotrPlugin - -The Guardians of the Rift plugin (GotrPlugin) is a real-world example of a schedulable plugin. It demonstrates several best practices: - -1. **Minimal Stop Condition**: Uses only a lock condition since the minigame has natural start and end points. - -2. **Lock Management**: Locks the plugin during active games and unlocks it when games end. - -3. **Safe Shutdown**: Implements a sophisticated shutdown procedure that: - - Checks if it's safe to exit - - Completes necessary cleanup - - Leaves the minigame area - - Handles exceptions - -4. **Integration with Other Systems**: Works with the break handler to coordinate breaks. - -5. **Config Descriptor**: Provides configuration information to the scheduler. - -By following these patterns, your plugin can work seamlessly with the scheduler system, providing a better user experience and more reliable operation. - -## Reference - -For more information, see the following resources: - -- [Scheduler User Guide](user-guide.md): How to use the scheduler from a user perspective -- [Defining Conditions](defining-conditions.md): Detailed information on condition types -- [API Documentation: SchedulablePlugin](api/schedulable-plugin.md): Full API reference for the SchedulablePlugin interface -- [Combat Lock Examples](combat-lock-examples.md): Examples of using LockCondition in combat and bossing plugins - -## Location-Based Conditions - -The scheduler provides robust location-based conditions that can be used to start or stop plugins based on the player's position in the game world. This section covers how to leverage these powerful tools. - -### Working with LocationCondition Utilities - -The `LocationCondition` abstract class provides several utility methods for creating location-based conditions: - -#### 1. Bank Location Conditions - -You can create conditions that trigger when the player is near a specific bank: - -```java -// Create a condition that is satisfied when the player is at the Grand Exchange -Condition atBankCondition = LocationCondition.atBank(BankLocation.GRAND_EXCHANGE, 20); - -// This condition will be satisfied when the player is within 20 tiles of the GE bank -``` - -#### 2. Working with Multiple Points - -Sometimes, you want a condition that triggers at any of several points: - -```java -// Define multiple points where the player may be -WorldPoint[] importantLocations = new WorldPoint[] { - new WorldPoint(3222, 3218, 0), // Lumbridge - new WorldPoint(3165, 3485, 0), // Grand Exchange - new WorldPoint(2964, 3378, 0) // Falador -}; - -// Create a condition that is satisfied at any of these points -Condition atAnyPointCondition = LocationCondition.atAnyPoint( - "At a major city", - importantLocations, - 10 // Within 10 tiles of any point -); -``` - -#### 3. Creating Area Conditions - -For rectangular areas, you can use the `createArea` utility method: - -```java -// Create a condition for a rectangular area centered around a point -WorldPoint center = new WorldPoint(3222, 3218, 0); // Lumbridge -AreaCondition lumbridgeAreaCondition = LocationCondition.createArea( - "Lumbridge Center", - center, - 20, // Width in tiles - 20 // Height in tiles -); -``` - -#### 4. Working with Multiple Areas - -You can also create conditions that check if the player is in any of several areas: - -```java -// Define multiple areas -WorldArea[] trainingAreas = new WorldArea[] { - new WorldArea(3207, 3206, 30, 30, 0), // Lumbridge cows - new WorldArea(3244, 3295, 20, 15, 0) // Varrock east mine -}; - -// Create a condition that is satisfied in any of these areas -Condition inAnyAreaCondition = LocationCondition.inAnyArea( - "At training location", - trainingAreas -); -``` - -Alternatively, you can define areas using coordinate arrays: - -```java -// Define areas using coordinate arrays [x1, y1, x2, y2, plane] -int[][] areaDefs = new int[][] { - {3207, 3206, 3237, 3236, 0}, // Lumbridge cows - {3244, 3295, 3264, 3310, 0} // Varrock east mine -}; - -// Create a condition that is satisfied in any of these areas -Condition inAnyAreaCondition = LocationCondition.inAnyArea( - "At training location", - areaDefs -); -``` - -### Practical Use Cases for Location Conditions - -Location conditions can serve various purposes: - -1. **Start Conditions**: Define where a plugin can start - ```java - @Override - public LogicalCondition getStartCondition() { - // Only start the woodcutting plugin when in a woodcutting area - return (LogicalCondition) LocationCondition.inAnyArea( - "At woodcutting location", - new int[][] { - {3163, 3415, 3173, 3425, 0}, // Varrock west trees - {3040, 3308, 3055, 3323, 0} // Falador trees - } - ); - } - ``` - -2. **Stop Conditions**: Define where a plugin should stop - ```java - OrCondition stopCondition = new OrCondition(); - - // Stop if inventory is full OR player leaves the mining area - stopCondition.addCondition(new InventoryFullCondition()); - - NotCondition notInMiningArea = new NotCondition( - LocationCondition.inAnyArea( - "Mining area", - new int[][] {{3027, 9733, 3055, 9747, 0}} // Mines - ) - ); - stopCondition.addCondition(notInMiningArea); - ``` - -3. **Safety Checks**: Prevent dangerous activities - ```java - // Don't allow stop in dangerous areas - LockCondition lockCondition = new LockCondition("In wilderness", true); //ensure unlock on shutdown of the plugin ! - - // Lock when entering wilderness - if (LocationCondition.inAnyArea( - "Wilderness", - new int[][] {{3008, 3525, 3071, 3589, 0}} - ).isSatisfied()) { - lockCondition.lock(); - } - ``` - - - -4. **Arceuus script**: using condition-based locking : -```java -private LogicalCondition createStopCondition() { - // Import required classes - import java.util.Arrays; - import java.util.List; - - - // Create location conditions for Dense Runestone and Blood Altar - // NOTE: Update these coordinates with the actual in-game coordinates - LocationCondition atDenseRunestone = new AreaCondition("At Dense Runestone", 1760, 3850, 1780, 3870, 0); - LogicalCondition notAtDenseRunestone = new NotCondition(atDenseRunestone); - LocationCondition atBloodAltar = new AreaCondition("At Blood Altar", 1710, 3820, 1730, 3840, 0); - - - - // Option 1: Using createAndCondition helper method for more readable code - // Create a list of items to check (both Dark essence types) - List darkEssenceItems = Arrays.asList("Dark essence fragments", "Dark essence block"); - - // Create an AND condition that checks if both items have count >=1 - // Each condition is satisfied when the item count is >=1 (using NOT to invert the default behavior item count <1) - LogicalCondition noDarkEssence = new AndCondition(); - for (String itemName : darkEssenceItems) { - NotCondition noItem = new NotCondition( - new InventoryItemCountCondition(itemName, 1, true) - ); - noDarkEssence.addCondition(noItem); - } - - // Option 2: Using a single regex pattern to match both item types (more efficient) - // This creates a condition that checks if there are any Dark essence items (fragments or blocks), with count >=1, count all matching items - InventoryItemCountCondition hasAnyDarkEssence = new InventoryItemCountCondition( - "Dark essence.*", 1, true); // Regex pattern to match both item types - - // Invert the condition to check if there are NO dark essence items in the inventory - NotCondition noDarkEssenceItems = new NotCondition(hasAnyDarkEssence); - - // Use an ANDCondition for being at the Blood Altar with any dark essence item - AndCondition atBloodAltarWithEssence = new AndCondition(); - atBloodAltarWithNoEssence.addCondition(atBloodAltar); - atBloodAltarWithNoEssence.addCondition(hasAnyDarkEssence); // Using Option 2: the regex pattern approach - // we can invert it, so the condition is true if we are not at the blood alter or we dont have any dark essence items - LogicalCondition notAtBloodAltarOrNoDarkEssenceItems = new NotCondition(atBloodAltarWithEssence); - // Alternatively, using Option 3: createOrCondition helper for multiple items with count>=1 - List darkEssenceTypes = Arrays.asList("Dark essence fragments", "Dark essence block"); - LogicalCondition hasDarkEssenceAlt = InventoryItemCountCondition.createOrCondition( - darkEssenceTypes, 1, 1, true); - - - // Create the stop condition, so we can stop when we are not at the runestone and (we are not at Blood Altar or we have no essences - LocationCondition logicalStopCondition = new AndCondition(); - logicalStopCondition.addCondition(notAtDenseRunestone); - logicalStopCondition.addCondition(notAtBloodAltarOrNoDarkEssenceItems); - return logicalStopCondition; -} -``` \ No newline at end of file diff --git a/docs/scheduler/predicate-condition-examples.md b/docs/scheduler/predicate-condition-examples.md deleted file mode 100644 index cf87564ff13..00000000000 --- a/docs/scheduler/predicate-condition-examples.md +++ /dev/null @@ -1,257 +0,0 @@ -# PredicateCondition Examples - -This document provides practical examples of how to use the `PredicateCondition` class in your plugins to create dynamic stop conditions based on the game state. - -## Overview - -The `PredicateCondition` is a powerful extension of `LockCondition` that combines: -1. A manual lock mechanism (inherited from `LockCondition`) -2. A Java Predicate for evaluating dynamic game states - -This makes it perfect for creating stop conditions that depend on the current state of the game rather than static values. - -## Basic Implementation Pattern - -The general pattern for implementing a `PredicateCondition` is: - -```java -// Create the predicate function that evaluates game state -Predicate myPredicate = gameState -> { - // Logic to evaluate the game state - return someBoolean; // true if condition is satisfied -}; - -// Create a supplier that provides the current state -Supplier stateSupplier = () -> { - // Return the current state to be evaluated - return currentState; -}; - -// Create the PredicateCondition -PredicateCondition condition = new PredicateCondition<>( - "Human-readable reason for locking", - true, // with break handler lock, so during the predicate is true we cane take a break - myPredicate, - stateSupplier, - "Description of what the predicate checks" -); -``` - -## Example 1: Region-Based Agility Plugin - -This example demonstrates how to use `PredicateCondition` to stop an agility plugin when the player leaves an agility course region: - -```java -public class MicroAgilityPlugin extends Plugin implements SchedulablePlugin { - private PredicateCondition notInCourseCondition; - private LockCondition lockCondition; - private LogicalCondition stopCondition = null; - private final Set courseRegionIds = new HashSet<>(); - - @Inject - private Client client; - - /** - * Initialize the list of region IDs where agility courses are located - */ - private void initializeCourseRegions() { - // Add region IDs for all agility courses - courseRegionIds.add(9781); // Gnome Stronghold - courseRegionIds.add(12338); // Draynor Village - courseRegionIds.add(13105); // Al Kharid - // ... other course regions - } - - /** - * Set up the predicate condition that will be used to determine if the player is in an agility course - */ - private void setupPredicateCondition() { - // This predicate checks if the player is in an agility course region - Predicate notInAgilityCourse = player -> { - if (player == null) return true; // If player is null, condition is satisfied (safer to stop) - - boolean playerPlayerIsNotInCourse = ((Microbot.getClient().getLocalPlayer()!=null && !Rs2Player.isInteracting()) && index == 0 && (AgilityPlugin.getMarksOfGrace() == null || AgilityPlugin.getMarksOfGrace().isEmpty())); - // Return true if player is NOT in a course (condition to stop is satisfied) - return playerPlayerIsNotInCourse; - }; - - // Create the predicate condition - notInCourseCondition = new PredicateCondition<>( - "Player is currently in an agility course", - true, // with break handler lock, so during the predicate is true we cane take a break - notInAgilityCourse, - () -> Rs2Player.getLocalPlayer(), // provider - "Player is not in an agility course region" - ); - } - - @Override - public LogicalCondition getStopCondition() { - if (this.stopCondition == null) { - this.stopCondition = createStopCondition(); - } - return this.stopCondition; - } - - private LogicalCondition createStopCondition() { - if (this.lockCondition == null) { - this.lockCondition = new LockCondition("Locked because the Agility Plugin is in a critical operation", true); //ensure unlock on shutdown of the plugin ! - } - - // Setup course regions if not already done - if (courseRegionIds.isEmpty()) { - initializeCourseRegions(); - } - - // Setup predicate condition if not already done - if (notInCourseCondition == null) { - setupPredicateCondition(); - } - - // Combine the lock condition and the predicate condition with AND logic - AndCondition andCondition = new AndCondition(); - andCondition.addCondition(lockCondition); - andCondition.addCondition(notInCourseCondition); - return andCondition; - } -} -``` - -## Example 2: Combat State Monitoring - -This example shows how to use `PredicateCondition` to track if a player is in combat: - -```java -public class CombatPluginExample extends Plugin implements SchedulablePlugin { - private PredicateCondition notInCombatCondition; - private LockCondition lockCondition; - - @Inject - private Client client; - - private void setupCombatCondition() { - Predicate notInCombat = player -> { - if (player == null) return true; - - // Check if the player is in combat - boolean inCombat = player.getInteracting() != null || - (player.getHealthScale() > 0 && - System.currentTimeMillis() - player.getLastCombatTime() < 5000); - - // Return true if NOT in combat (condition to stop is satisfied) - return !inCombat; - }; - - notInCombatCondition = new PredicateCondition<>( - "Player is currently in combat", - notInCombat, - () -> client.getLocalPlayer(), - "Player is not in combat" - ); - } - - @Override - public LogicalCondition getStopCondition() { - if (lockCondition == null) { - lockCondition = new LockCondition("Critical combat operation in progress", true); - } - - if (notInCombatCondition == null) { - setupCombatCondition(); - } - - AndCondition stopCondition = new AndCondition(); - stopCondition.addCondition(lockCondition); - stopCondition.addCondition(notInCombatCondition); - - return stopCondition; - } -} -``` - -## Example 3: Multiple State Conditions - -This example demonstrates combining multiple predicate conditions: - -```java -public class FishingPluginExample extends Plugin implements SchedulablePlugin { - private PredicateCondition notFishingCondition; - private PredicateCondition inventoryFullCondition; - private LockCondition lockCondition; - - @Inject - private Client client; - - private void setupConditions() { - // Check if the player is not fishing - Predicate notFishing = player -> { - if (player == null) return true; - return player.getAnimation() != FISHING_ANIMATION; - }; - - // Check if inventory is full - Predicate inventoryFull = player -> { - if (player == null) return false; - return client.getItemContainer(InventoryID.INVENTORY).size() >= 28; - }; - - notFishingCondition = new PredicateCondition<>( - "Player is actively fishing", - notFishing, - () -> client.getLocalPlayer(), - "Player is not currently fishing" - ); - - inventoryFullCondition = new PredicateCondition<>( - "Inventory has space", - inventoryFull, - () -> client.getLocalPlayer(), - "Inventory is full" - ); - } - - @Override - public LogicalCondition getStopCondition() { - if (lockCondition == null) { - lockCondition = new LockCondition("Critical fishing operation in progress", true); - } - - if (notFishingCondition == null || inventoryFullCondition == null) { - setupConditions(); - } - - // Create a structure: (Lock AND (NotFishing OR InventoryFull)) - OrCondition fishingOrInventoryCondition = new OrCondition(); - fishingOrInventoryCondition.addCondition(notFishingCondition); - fishingOrInventoryCondition.addCondition(inventoryFullCondition); - - AndCondition stopCondition = new AndCondition(); - stopCondition.addCondition(lockCondition); - stopCondition.addCondition(fishingOrInventoryCondition); - - return stopCondition; - } -} -``` - -## Best Practices - -When using `PredicateCondition`, follow these best practices: - -1. **Safety Checks**: Always handle null values in your predicates to prevent NullPointerExceptions. - -2. **Clear Descriptions**: Provide meaningful descriptions for your predicate conditions, as these will be shown in the UI. - -3. **Logical Grouping**: Use logical conditions (AND/OR) to group predicate conditions with other conditions in meaningful ways. - -4. **State Suppliers**: Create efficient state suppliers that provide only the necessary game state for evaluation. - -5. **Locking Logic**: Remember that the condition is only satisfied when both the lock is unlocked AND the predicate returns true. - -6. **Performance**: Keep predicate evaluation efficient as it may be called frequently. - -7. **Debugging**: Use `Microbot.log()` in your predicates during development to debug condition evaluation. - -## Conclusion - -The `PredicateCondition` class provides a powerful way to create dynamic stop conditions based on the current game state. By leveraging Java Predicates, you can create sophisticated conditions that respond to the game environment in real-time, making your plugins more intelligent and responsive. diff --git a/docs/scheduler/roadmap.md b/docs/scheduler/roadmap.md deleted file mode 100644 index 3e8d5c7344a..00000000000 --- a/docs/scheduler/roadmap.md +++ /dev/null @@ -1,152 +0,0 @@ -# SchedulerPlugin: Development Roadmap - -## Short-term Priorities - -- **Community-driven Fixes** - - Implement bug fixes based on community feedback - - Address stability issues reported by early adopters - - Improve error handling and diagnostic messaging - -- **Documentation Enhancements** - - Complete comprehensive guides for all condition types - - Create video tutorials demonstrating scheduler setup and usage - - Provide more code examples for common scheduling scenarios - - Add troubleshooting section to documentation - -- **Review Response** - - Address code review comments from community members - - Implement suggested API improvements for better integration with existing plugins - - Enhance testing coverage for edge cases and failure modes - -## Medium-term Plans - -### Utility Framework for Schedulable Plugins - -- **Uility and Base Class** - - Develop a utility base class - - Provide common lifecycle hooks and helper methods - - Include default implementations for common scheduling patterns - -- **Impelentation of Pre-Schedule Task Framework** - - **Resource Acquisition System** - - Automated Grand Exchange purchasing functionality - - Automated collection of items - - Automated shopping of items - - Bank item withdrawal/preparation based on configurable templates - - Tool and equipment verification and acquisition - - **Location Management** - - Travel to appropriate starting locations before main task execution - - Fallback handling when target locations are unreachable - - **Inventory Setup Automation** - - Configure and validate inventory setups before starting primary task - - Handle edge cases like missing items - -- **Post-Schedule Task Framework** - - **Resource Management** - - Automated selling of gathered resources on Grand Exchange - - Intelligent price determination based on market conditions - - Bank organization and item categorization - - **Cleanup Operations** - - Return to safe locations after task completion - - Store valuable equipment to prevent loss - - Log detailed statistics about completed operations - - **Notification System** - - Discord/Telegram integration for completion notifications - - Configurable alerts based on success/failure states - - Detailed reports on resources gathered, skills gained, etc. - -- **Graceful Stopping Support** - - **State Persistence Framework - Just an Idea** - - Save critical state during interruptions - - Resume capability from last known good state (location,...) - - Progress tracking with persistent checkpoints - - **Safety Mechanism Implementations** - - Transition to safe areas before stopping - - Complete partial operations before full shutdown - - Banking valuable items before exit - - -### Plugin Integration - -- **Existing Plugins** - - Convert popular plugins to use the scheduling framework - - Update woodcutting, mining, and fishing plugins with schedulable support - - Implement combat scripts with safety condition integration - -### UI Enhancements - -- **Schedule Management GUI** - - Visual timeline showing planned plugin execution - - Better Conflict detection and resolution for overlapping schedules - - Schedule templates and sharing capability - - Improve Condition visualization and editing tools - - -## Long-term Enhancements - -### Advanced Condition Framework - -- **Machine Learning Integration** - - Train models to recognize optimal stopping conditions - - Implement predictive scheduling based on historical performance - - Auto-adjust parameters based on success/failure patterns - -- **Extended Condition Types** - - **Market Conditions** - - Start/stop based on Grand Exchange prices - - Algorithmic trading strategies with configurable parameters - - Support for price trend detection and forecasting - - **Server Conditions** - - Player density monitoring to avoid crowded areas - - World-hopping integration based on optimal conditions - - Server performance metrics to avoid high-lag situations - - **Account Progression Conditions** - - Quest completion dependencies - - Achievement diary stage requirements - - Total skill level milestones - -- **Condition Chain System** - - Sequential condition evaluation with dependencies - - Milestone-based progression between different plugins - - Complex workflow orchestration across multiple plugins - -### Advanced GUI Features - -- **Data Visualization** - - Interactive charts showing resource collection rates - - Performance analytics across different schedules - - Heat maps of player activity and resource distribution - -- **Remote Management** - - Web interface for monitoring and controlling schedules - - Mobile companion app for notifications and basic control - - Cross-account scheduling and coordination - -- **Community Integration** - - Schedule sharing platform for common tasks - - Upvoting system for effective condition combinations - - Community benchmarks for plugin performance - -### Ecosystem Extensions - -- **Integration with External Tools** - - Prayer/HP monitoring via companion services - - Network condition monitoring for stability - - Anti-ban pattern enhancements through scheduling variability - -- **Environment-aware Scheduling** - - Adapt to in-game events and seasonal activities - - Dynamic resource targeting based on current game economy - - Account-specific optimization based on stats and equipment - -## Implementation Approach - -The development will follow an iterative approach, with regular releases that incrementally add functionality. Community feedback will be actively sought after each significant feature addition to ensure the system meets real-world needs. - -Priority will be given to features that: -1. Improve stability and reliability -2. Enhance user experience for non-technical players -3. Provide valuable automation to complex multi-step tasks -4. Support intelligent decision-making based on game state - -This roadmap is subject to change based on community feedback, game updates, and shifting priorities within the development team. \ No newline at end of file diff --git a/docs/scheduler/schedulable-example-plugin.md b/docs/scheduler/schedulable-example-plugin.md deleted file mode 100644 index 11b4ae2c096..00000000000 --- a/docs/scheduler/schedulable-example-plugin.md +++ /dev/null @@ -1,364 +0,0 @@ -# SchedulableExamplePlugin - -## Overview - -The `SchedulableExamplePlugin` is a reference implementation demonstrating how to create plugins that work with the Plugin Scheduler system. It showcases various types of conditions for both starting and stopping a plugin, as well as proper implementation of the `SchedulablePlugin` interface. - -## Key Features - -1. **Comprehensive Condition Examples**: Demonstrates all major condition types: - - Time-based conditions - - Resource gathering conditions - - Item looting conditions - - NPC kill count conditions - - Process item conditions (crafting/smithing) - - Location-based conditions - -2. **Manual Testing Capabilities**: Includes hotkeys to manually trigger events for testing: - - Finish plugin event trigger - - Lock condition toggling - - Custom area definition for location-based conditions - -3. **Start Condition Examples**: Shows how to restrict plugin activation to specific locations: - - Bank-based start conditions - - Custom area start conditions - -## Making Your Plugin Schedulable - -### Step 1: Plugin Declaration - -```java -@PluginDescriptor( - name = "Schedulable Example", - description = "Designed for use with the scheduler and testing its features", - tags = {"microbot", "woodcutting", "combat", "scheduler", "condition"}, - enabledByDefault = false -) -@Slf4j -public class SchedulableExamplePlugin extends Plugin implements SchedulablePlugin { - // Plugin implementation... -} -``` - -A plugin becomes schedulable by implementing the `SchedulablePlugin` interface. - -### Step 2: Implement SchedulablePlugin - -The `SchedulablePlugin` interface requires implementation of key methods: - -```java -public interface SchedulablePlugin { - LogicalCondition getStartCondition(); - LogicalCondition getStopCondition(); - void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event); - - // Optional methods with default implementations - void onStopConditionCheck(); - void reportFinished(String reason, boolean success); -} -``` - -### Step 3: Define Stop Conditions - -```java -@Override -public LogicalCondition getStopCondition() { - // Create an OR condition - stop when ANY of the enabled conditions are met - OrCondition orCondition = new OrCondition(); - - // Create a lock condition for manual prevention of stopping - this.lockCondition = new LockCondition("Locked because the Plugin is in a critical operation", true); - - // Add enabled conditions based on configuration - if (config.enableTimeCondition()) { - orCondition.addCondition(createTimeCondition()); - } - - if (config.enableLootItemCondition()) { - orCondition.addCondition(createLootItemCondition()); - } - - // Add more conditions... - - // Combine with lock condition using AND logic - AndCondition andCondition = new AndCondition(); - andCondition.addCondition(orCondition); - andCondition.addCondition(lockCondition); - return andCondition; -} -``` - -### Step 4: Define Start Conditions (Optional) - -```java -@Override -public LogicalCondition getStartCondition() { - // Only create start conditions if enabled - if (!config.enableLocationStartCondition()) { - return null; // null means plugin can start anytime - } - - // Create a logical condition for start conditions - LogicalCondition startCondition = new OrCondition(); - - // Add conditions based on configuration - if (config.locationStartType() == LocationStartType.BANK) { - // Bank-based start condition - BankLocation selectedBank = config.bankStartLocation(); - int distance = config.bankDistance(); - - // Create condition using bank location - Condition bankCondition = LocationCondition.atBank(selectedBank, distance); - ((OrCondition) startCondition).addCondition(bankCondition); - } - else if (config.locationStartType() == LocationStartType.CUSTOM_AREA) { - // Custom area start condition logic - // ... - } - - return startCondition; -} -``` - -### Step 5: Implement Soft Stop Handler - -```java -@Override -@Subscribe -public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - // Save state before stopping - if (event.getPlugin() == this) { - WorldPoint currentLocation = null; - if (Microbot.isLoggedIn()) { - currentLocation = Rs2Player.getWorldLocation(); - } - Microbot.getConfigManager().setConfiguration("SchedulableExample", "lastLocation", currentLocation); - log.info("Scheduling stop for plugin: {}", event.getPlugin().getClass().getSimpleName()); - - // Schedule the stop operation on the client thread - Microbot.getClientThread().invokeLater(() -> { - try { - Microbot.getPluginManager().setPluginEnabled(this, false); - Microbot.getPluginManager().stopPlugin(this); - } catch (Exception e) { - log.error("Error stopping plugin", e); - } - }); - } -} -``` - -The `onPluginScheduleEntryPostScheduleTaskEvent` method is triggered when the Plugin Scheduler determines that a plugin's stop conditions have been met and requests the plugin to gracefully shut down. This implementation follows best practices for safely stopping a plugin: - -1. **State Preservation**: First saves the current player location to configuration for later use. -2. **Thread Safety**: Uses `Microbot.getClientThread().invokeLater()` to ensure the plugin is stopped on the client thread, avoiding concurrency issues. -3. **Clean Shutdown**: Disables the plugin and then properly stops it using `Microbot.getPluginManager().stopPlugin(this)`. - -#### Alternative Stopping Methods - -You can also directly stop a plugin using: - -```java -// Direct method to stop a plugin -Microbot.stopPlugin(this); -``` - -This is useful in situations where you need an immediate shutdown response, but be careful to ensure you've performed any necessary cleanup operations first. - -### Understanding the Plugin Shutdown Process - -The scheduler-managed shutdown process follows this sequence: - -1. **Trigger**: Stop conditions are met or manual stop requested -2. **Soft Stop Request**: The scheduler sends `PluginScheduleEntryPostScheduleTaskEvent` to the plugin -3. **Plugin Cleanup**: The plugin performs necessary cleanup operations -4. **Graceful Termination**: The plugin stops itself using one of the following methods: - - `Microbot.getPluginManager().stopPlugin(this)` - - `Microbot.stopPlugin(this)` -5. **Completion Reporting**: Optionally, the plugin can report detailed completion status using: - ```java - reportFinished("Task completed successfully", true); - ``` - -This approach ensures that plugins can safely save their state and perform cleanup operations before being terminated, preserving data integrity and preventing issues that could arise from abrupt termination. - -## Understanding SchedulableExampleConfig - -The `SchedulableExampleConfig` interface provides a comprehensive configuration system for the example plugin, demonstrating how to create configurable conditions: - -### Config Sections - -The config is organized into logical sections: - -```java -@ConfigSection( - name = "Start Conditions", - description = "Conditions for when the plugin is allowed to start", - position = 0 -) -String startConditionSection = "startConditions"; - -@ConfigSection( - name = "Stop Conditions", - description = "Conditions for when the plugin should stop", - position = 101 -) -String stopSection = "stopConditions"; - -// More specific condition sections... -``` - -### Condition Configuration Groups - -Each condition type has its own configuration group: - -1. **Location Start Conditions**: Control where the plugin can start - - Bank location selection - - Custom area definition with hotkey - - Area radius configuration - -2. **Time Conditions**: Control duration-based stopping - - Min/max runtime settings - - Randomized intervals - -3. **Loot Item Conditions**: Stop based on collected items - - Item name patterns (supports regex) - - Min/max item count targets - - Logical operators (AND/OR) - - Note item inclusion settings - -4. **Resource Conditions**: Stop based on gathered resources - - Resource type patterns - - Min/max count settings - - Logical operators - -5. **Process Item Conditions**: Stop based on items processed - - Source/target item tracking - - Tracking mode selection - - Min/max processed items - -6. **NPC Conditions**: Stop based on NPC kill counts - - NPC name patterns - - Min/max kill targets - - Per-NPC or total counting options - -7. **Debug Options**: Test functionality without running the full plugin - - Manual finish hotkey - - Success reporting - - Lock condition toggle - -## Testing Conditions - -The `SchedulableExamplePlugin` includes features specifically designed for testing the condition system: - -### Manual Condition Triggers - -```java -// HotkeyListener for testing PluginScheduleEntryMainTaskFinishedEvent -private final HotkeyListener finishPluginHotkeyListener = new HotkeyListener(() -> config.finishPluginHotkey()) { - @Override - public void hotkeyPressed() { - String reason = config.finishReason(); - boolean success = config.reportSuccessful(); - log.info("Manually triggering plugin finish: reason='{}', success={}", reason, success); - reportFinished(reason, success); - } -}; - -// HotkeyListener for toggling the lock condition -private final HotkeyListener lockConditionHotkeyListener = new HotkeyListener(() -> config.lockConditionHotkey()) { - @Override - public void hotkeyPressed() { - boolean newState = toggleLock(currentCondition); - log.info("Lock condition toggled: {}", newState ? "LOCKED - " + config.lockDescription() : "UNLOCKED"); - } -}; -``` - -### Location-Based Start Conditions - -```java -// HotkeyListener for the area marking -private final HotkeyListener areaHotkeyListener = new HotkeyListener(() -> config.areaMarkHotkey()) { - @Override - public void hotkeyPressed() { - toggleCustomArea(); - } -}; - -private void toggleCustomArea() { - // Logic to mark the current player position as center of a custom area - // or to clear a previously defined area -} -``` - -## Understanding the LockCondition - -The `LockCondition` is a special condition used to prevent a plugin from being stopped during critical operations, even if other stop conditions are met. - -### How It Works - -```java -public class LockCondition implements Condition { - private final AtomicBoolean locked = new AtomicBoolean(false); - private final String reason; - - // Constructor and methods... - - @Override - public boolean isSatisfied() { - // If locked, the condition is NOT satisfied, which prevents stopping - return !isLocked(); - } -} -``` - -The lock condition works by returning `false` for `isSatisfied()` when locked, which prevents the stop condition from being met when used with AND logic. - -### Usage in the Example Plugin - -```java -// Create the lock condition -this.lockCondition = new LockCondition("Locked because the Plugin is in a critical operation", true); - -// Add it to the condition structure with AND logic -AndCondition andCondition = new AndCondition(); -andCondition.addCondition(orCondition); // Other stop conditions -andCondition.addCondition(lockCondition); // Lock condition -``` - -### When to Use Lock Condition - -The lock condition should be used during critical operations where stopping the plugin might leave game state inconsistent: - -- **Banking operations**: Prevent stopping mid-transaction -- **Trading**: Ensure trades complete fully -- **Complex combat sequences**: Avoid stopping during multi-step attacks -- **Item processing**: Complete crafting/smithing operations fully - -### Testing the Lock Condition - -The example plugin provides a hotkey to test locking and unlocking: - -```java -private final HotkeyListener lockConditionHotkeyListener = new HotkeyListener(() -> config.lockConditionHotkey()) { - @Override - public void hotkeyPressed() { - boolean newState = toggleLock(currentCondition); - log.info("Lock condition toggled: {}", newState ? "LOCKED - " + config.lockDescription() : "UNLOCKED"); - } -}; -``` - -## Summary - -The `SchedulableExamplePlugin` serves as a comprehensive demonstration of how to create plugins that work with the Plugin Scheduler system. The conditions it implements are designed for testing purposes but illustrate the patterns needed for real-world scheduling scenarios: - -- Time-based scheduling using intervals -- Resource gathering targets -- Item processing goals -- NPC kill counts -- Location-based activation -- Manual intervention capabilities - -By studying this example plugin, both plugin developers and script writers can understand how to make their own plugins schedulable, creating more sophisticated automation workflows that operate according to complex rules and conditions. \ No newline at end of file diff --git a/docs/scheduler/scheduler-plugin.md b/docs/scheduler/scheduler-plugin.md deleted file mode 100644 index efdddfa3944..00000000000 --- a/docs/scheduler/scheduler-plugin.md +++ /dev/null @@ -1,1009 +0,0 @@ -# Scheduler Plugin - -## Overview - -The `SchedulerPlugin` class is the central orchestrator of the Plugin Scheduler system. It manages the automated execution of plugins based on configurable conditions and schedules, providing a comprehensive framework for automating RuneLite plugins in a controlled, prioritized, and natural-appearing manner. - -## Component Relationships - -The scheduler system consists of three main components that work together: - -| Component | Role | Responsibility | -|-----------|------|----------------| -| `SchedulerPlugin` | **Orchestrator** | Coordinates the overall scheduling process, state transitions, and integration with other systems | -| `PluginScheduleEntry` | **Data Model** | Holds configuration and execution state for each scheduled plugin | -| `SchedulablePlugin` | **Interface** | Implemented by RuneLite plugins to define start/stop conditions and handle events | - -### Component Interaction Flow - -```ascii -┌────────────────┐ registers ┌──────────────────┐ -│ │◄──────────────────────â”Ī │ -│ │ │ │ -│ SchedulerPlugin schedules/evaluates │ PluginScheduleEntry │ -│ │─────────────────────â–ķ│ │ -│ │ │ │ -└────────┮───────┘ └────────┮─────────┘ - │ │ - │ manages │ references - │ │ - ▾ ▾ -┌────────────────┐ ┌──────────────────┐ -│ │ │ │ -│ Regular RuneLite│ implements │ SchedulablePlugin │ -│ Plugin │◄─────────────────────â”Ī (API) │ -│ │ │ │ -└────────────────┘ └──────────────────┘ -``` - -## Configuration Options - -The SchedulerPlugin offers extensive configuration options organized into four main sections: - -### Control Settings - -Controls the core behavior of the scheduler: - -| Setting | Description | Default | -|---------|-------------|---------| -| Soft Stop Retry (seconds) | Time between attempts to gracefully stop a plugin | 60 seconds | -| Enable Hard Stop | When enabled, forcibly stops plugins if they don't respond to soft stop | Disabled | -| Hard Stop Timeout (seconds) | Time to wait before forcing a hard stop | 0 seconds | -| Manual Start Threshold (minutes) | Minimum time until next scheduled plugin before manual start is allowed | 1 minute | -| Prioritize Non-Default Plugins | Stop default plugins when non-default plugins are due soon | Enabled | -| Non-Default Plugin Look-Ahead (minutes) | Time window to check for upcoming non-default plugins | 1 minute | -| Notifications On | Enable notifications for scheduler events | Disabled | - -### Conditions Settings - -Controls how the scheduler enforces conditions: - -| Setting | Description | Default | -|---------|-------------|---------| -| Enforce Stop Conditions | Prompt before running plugins without time-based stop conditions | Enabled | -| Dialog Timeout (seconds) | Time before the 'No Stop Conditions' dialog auto-closes | 30 seconds | -| Config Timeout (seconds) | Time to wait for user to add stop conditions before canceling | 60 seconds | - -### Log-In Settings - -Controls login behavior of scheduled plugins: - -| Setting | Description | Default | -|---------|-------------|---------| -| Enable Auto Log In | Enable auto-login before starting a plugin | Disabled | -| Auto Log In World | World to log into (0 for random) | 0 | -| World Type | Type of world to log into (0: F2P, 1: P2P, 2: Any) | 2 (Any) | -| Auto Log Out on Stop | Automatically log out when stopping the scheduler | Disabled | - -### Break Settings - -Controls break behavior between plugin executions: - -| Setting | Description | Default | -|---------|-------------|---------| -| BreakHandler on Start | Automatically enable BreakHandler when starting a plugin | Enabled | -| Break During Wait | Take breaks when waiting for the next scheduled plugin | Enabled | -| Min Break Duration (minutes) | Minimum duration of breaks between schedules | 2 minutes | -| Max Break Duration (minutes) | Maximum duration of breaks between schedules | 2 minutes | -| Log Out During A Break | Automatically log out during breaks | Disabled | -| Use Play Schedule | Enable play schedule to control when scheduler is active | Disabled | -| Play Schedule | Select pre-defined play schedule pattern | Medium Day | - -## User Interface - -The SchedulerPlugin provides a comprehensive user interface for managing scheduled plugins through several key components: - -### Main Scheduler Window - -The `SchedulerWindow` is the primary interface for managing the plugin scheduler. It contains: - -- **Schedule Tab**: Displays a table of all scheduled plugins and their status -- **Start Conditions Tab**: Configure when plugins should start running -- **Stop Conditions Tab**: Configure when running plugins should stop -- **Information Panel**: Shows real-time scheduler status and statistics - -### Schedule Table Panel - -The `ScheduleTablePanel` displays all scheduled plugins with the following information: - -- Plugin name -- Schedule type -- Next run time -- Start/Stop conditions -- Priority -- Enabled status -- Run count - -Special visual indicators help identify: -- Currently running plugin (purple highlight) -- Next scheduled plugin (amber highlight) -- Plugins with met/unmet conditions (green/red indicators) - -### Schedule Form Panel - -The `ScheduleFormPanel` allows users to add, edit, and remove scheduled plugins. It provides: - -- Plugin selection dropdown -- Time condition configuration -- Priority setting -- Randomization options -- Default plugin status toggle -- Allow continue after interruption option - -When editing an existing plugin, additional statistics are shown: -- Total runs -- Last run time -- Last run duration -- Last stop reason - -### Condition Configuration - -The condition configuration interface allows users to create complex logical conditions for starting and stopping plugins: - -- **Time Conditions**: Specific times, intervals, time windows, days of week -- **Game State Conditions**: Player status, inventory contents, skill levels -- **Logical Operators**: AND, OR, NOT for combining conditions -- **Lock Conditions**: Prevent plugins from stopping during critical operations - -## Plugin Schedule Entry Model - -The `PluginScheduleEntry` class is the core data model representing a scheduled plugin: - -### Key Properties - -| Property | Description | -|----------|-------------| -| `name` | Name of the plugin to schedule | -| `enabled` | Whether this schedule entry is active | -| `allowRandomScheduling` | Whether this plugin can be scheduled randomly | -| `isDefault` | Whether this is a default plugin (lower priority) | -| `priority` | Numeric priority (higher values = higher priority) | -| `allowContinue` | Whether to resume after interruption | -| `startConditions` | Logical conditions that determine when plugin starts | -| `stopConditions` | Logical conditions that determine when plugin stops | - -### Statistics Tracking - -Each entry tracks comprehensive statistics: -- Run count -- Last run time -- Last run duration -- Last stop reason -- Success/failure status - -### Stop Reason Types - -The system tracks why plugins stop: -- `NONE`: Not stopped yet -- `MANUAL_STOP`: User manually stopped the plugin -- `PLUGIN_FINISHED`: Plugin completed its task normally -- `ERROR`: Error occurred while running -- `SCHEDULED_STOP`: Stop conditions were met -- `INTERRUPTED`: Externally interrupted -- `HARD_STOP`: Forcibly stopped after timeout - -## Adding New Schedule Entries - -New schedule entries can be added through the `ScheduleFormPanel`: - -1. **Select Plugin**: Choose a plugin from the dropdown menu -2. **Set Priority**: Adjust the priority spinner (higher values = higher priority) -3. **Configure Time Conditions**: Choose from: - - Run Default: Use plugin's built-in schedule - - Run at Specific Time: Run at a particular time of day - - Run at Interval: Run at regular intervals - - Run in Time Window: Run during specific hours - - Run on Day of Week: Run on particular days -4. **Optional Settings**: - - Random Scheduling: Allow the scheduler to choose this plugin randomly - - Default Plugin: Set as a default (lower priority) plugin - - Time-based Stop: Configure automatic stop after running for some time - - Allow Continue: Resume after interruption -5. **Click Add**: Add the plugin to the schedule - -## State Management and Execution Flow - -The SchedulerPlugin implements a sophisticated state machine that manages the entire plugin scheduling life cycle through the `SchedulerState` enum. - -### State Categories and Relationships - -The scheduler's states can be organized into four functional categories: - -#### 1. Initialization States -- **UNINITIALIZED**: Initial state before the plugin is ready -- **INITIALIZING**: Loading required dependencies and preparing to run -- **READY**: Fully initialized and waiting for user activation - -#### 2. Active Scheduling States -- **SCHEDULING**: Actively monitoring schedules -- **STARTING_PLUGIN**: Beginning execution of a scheduled plugin -- **RUNNING_PLUGIN**: Plugin is currently executing -- **SOFT_STOPPING_PLUGIN**: Gracefully requesting a plugin to stop -- **HARD_STOPPING_PLUGIN**: Forcefully stopping a plugin - -#### 3. Waiting States -- **WAITING_FOR_LOGIN**: Waiting for user login before starting a plugin -- **LOGIN**: Currently in the process of logging in -- **WAITING_FOR_STOP_CONDITION**: Waiting for user to configure stop conditions -- **WAITING_FOR_SCHEDULE**: Waiting for the next scheduled plugin -- **BREAK**: Taking a configured break between plugin executions -- **PLAYSCHEDULE_BREAK**: Taking a break based on play schedule settings - -#### 4. Control States -- **HOLD**: Scheduler manually paused by user -- **ERROR**: An error occurred that prevents normal operation - -### State Transitions Diagram - -```ascii -┌───────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ INITIALIZATION STATES │ -├───────────────┐ ┌───────────────┐ ┌───────────────┐ │ -│ UNINITIALIZED │────────────────â–ķ│ INITIALIZING │────────────────â–ķ│ READY │ │ -└───────────────┘ └───────────────┘ └───────┮───────┘ │ - │ │ -┌──────────────────────────────────────────────────────────────────┐ │ │ -│ WAITING STATES │ │ │ -│ ┌────────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ -│ │WAITING_FOR_ │◀──────â–ķ│ LOGIN │──────â–ķ│WAITING_FOR_│ │ │ │ -│ │ LOGIN │ └────────────┘ │ STOP │ │ │ │ -│ └─────────┮──────┘ │ CONDITION │ │ │ │ -│ │ └─────┮──────┘ │ │ │ -│ ┌─────────▾──────┐ ┌────────────┐ │ │ │ │ -│ │WAITING_FOR_ │ │ BREAK │◀───────────┘ │ │ │ -│ │ SCHEDULE │◀────────â”Ī │ │ │ │ -│ └─────────┮──────┘ └─────┮──────┘ │ │ │ -│ │ │ │ │ │ -│ ┌─────────▾──────┐ ┌─────▾──────┐ │ │ │ -│ │PLAYSCHEDULE_ │ │ SCHEDULING │◀──────────────────────┾────────┘ │ -│ │ BREAK │◀────────â”Ī │ │ │ -│ └───────────────┮┘ └─────┮──────┘ │ │ -└───────────────────────────────────┘ │ │ - │ │ -┌───────────────────────────────────────────────────────┐ │ │ -│ ACTIVE SCHEDULING STATES │ │ │ -│ ┌────────────────┐ │ │ │ -│ │ STARTING_PLUGIN│◀───────────┾──────────┘ │ -│ └────────┮───────┘ │ │ -│ │ │ │ -│ ┌────────▾───────┐ │ ┌────────────────────────────────┐ -│ │ RUNNING_PLUGIN │ │ │ CONTROL STATES │ -│ └────────┮───────┘ │ │ ┌────────────┐ ┌──────────┐ │ -│ │ │ │ │ HOLD │ │ ERROR │ │ -│ ┌────────▾───────┐ │ │ └────────────┘ └──────────┘ │ -│ │SOFT_STOPPING_ │ │ └────────────────────────────────┘ -│ │ PLUGIN │ │ -│ └────────┮───────┘ │ -│ │ │ -│ ┌────────▾───────┐ │ -│ │HARD_STOPPING_ │ │ -│ │ PLUGIN │ │ -│ └────────────────┘ │ -└───────────────────────────────────────────────────────┘ -``` - -### State Descriptions and Transition Logic - -| State | Description | Entry Conditions | Exit Conditions | Helper Methods | -|-------|-------------|------------------|-----------------|----------------| -| UNINITIALIZED | Default state when plugin is loaded but not ready | Initial state | Transitions to INITIALIZING when plugin starts | isInitializing() | -| INITIALIZING | Loading required plugins and setting up | From UNINITIALIZED on startup | Transitions to READY when dependencies are loaded | isInitializing() | -| READY | Ready but not actively scheduling | After successful initialization | Transitions to SCHEDULING when user activates scheduler | !isSchedulerActive() | -| SCHEDULING | Actively monitoring for plugins to run | From READY when activated, or after breaks/waiting | To WAITING states or STARTING_PLUGIN | isSchedulerActive(), isWaiting() | -| STARTING_PLUGIN | In process of starting a plugin | When conditions are met to start a plugin | To RUNNING_PLUGIN when started successfully | isAboutStarting() | -| RUNNING_PLUGIN | A scheduled plugin is currently running | After plugin successfully starts | To SOFT_STOPPING_PLUGIN when stop conditions met | isActivelyRunning() | -| WAITING_FOR_LOGIN | Plugin needs login before running | When plugin requires login but user is not logged in | To LOGIN when login process begins | isAboutStarting() | -| LOGIN | Currently logging in | From WAITING_FOR_LOGIN | To STARTING_PLUGIN after successful login | N/A | -| WAITING_FOR_STOP_CONDITION | Awaiting user to add stop conditions | When plugin has no stop conditions | To STARTING_PLUGIN when conditions added | isAboutStarting() | -| SOFT_STOPPING_PLUGIN | Requesting plugin to stop gracefully | When stop conditions are met | To HARD_STOPPING_PLUGIN on timeout or successful stop | isStopping() | -| HARD_STOPPING_PLUGIN | Forcing plugin to stop | After soft stop timeout or when hard stop requested | To SCHEDULING or ERROR | isStopping() | -| BREAK | Taking scheduled break between plugins | After plugin completes and break is needed | To SCHEDULING when break duration completes | isWaiting(), isBreaking() | -| PLAYSCHEDULE_BREAK | Break due to play schedule settings | When outside allowed play hours | To SCHEDULING when entering allowed play hours | isWaiting(), isBreaking() | -| WAITING_FOR_SCHEDULE | Waiting for next scheduled plugin | When no plugins are ready to run currently | To SCHEDULING when schedule time approaches | isWaiting() | -| ERROR | Error occurred during scheduling | On exception or plugin error | After error is acknowledged | N/A | -| HOLD | Scheduler manually paused | User requested pause | When user resumes scheduling | N/A | - -### State Helper Methods - -The `SchedulerState` enum provides helper methods to classify states into functional groups, making it easier to check the scheduler's current status: - -```java -// Returns true if the scheduler is active (not in initialization, hold, or error states) -public boolean isSchedulerActive() { - return this != SchedulerState.UNINITIALIZED && - this != SchedulerState.INITIALIZING && - this != SchedulerState.ERROR && - this != SchedulerState.HOLD && - this != SchedulerState.READY; -} - -// Returns true if a plugin is currently running -public boolean isActivelyRunning() { - return isSchedulerActive() && - (this == SchedulerState.RUNNING_PLUGIN); -} - -// Returns true if scheduler is about to start a plugin -public boolean isAboutStarting() { - return this == SchedulerState.STARTING_PLUGIN || - this == SchedulerState.WAITING_FOR_STOP_CONDITION || - this == SchedulerState.WAITING_FOR_LOGIN; -} - -// Returns true if scheduler is waiting between plugin executions -public boolean isWaiting() { - return isSchedulerActive() && - (this == SchedulerState.SCHEDULING || - this == SchedulerState.WAITING_FOR_SCHEDULE || - this == SchedulerState.BREAK || - this == SchedulerState.PLAYSCHEDULE_BREAK); -} - -// Returns true if scheduler is in a break state -public boolean isBreaking() { - return (this == SchedulerState.BREAK || - this == SchedulerState.PLAYSCHEDULE_BREAK); -} - -// Returns true if scheduler is stopping a plugin -public boolean isStopping() { - return this == SchedulerState.SOFT_STOPPING_PLUGIN || - this == SchedulerState.HARD_STOPPING_PLUGIN; -} - -// Returns true if scheduler is initializing -public boolean isInitializing() { - return this == SchedulerState.INITIALIZING || - this == SchedulerState.UNINITIALIZED; -} -``` - -## Class Structure - -```java -@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 { - // Dependencies, state variables, configuration - // Methods for plugin lifecycle management and scheduling -} -``` - -## Key Features - -### Plugin Lifecycle Management - -The `SchedulerPlugin` manages the complete lifecycle of scheduled plugins: - -- **Registration**: Allows plugins to be registered with the scheduler through `PluginScheduleEntry` objects -- **Activation**: Starts plugins when their start conditions are met, respecting priority and scheduling properties -- **Monitoring**: Tracks running plugins and continuously evaluates their stop conditions -- **Deactivation**: Implements both soft and hard stop mechanisms when stop conditions are met -- **Persistence**: Saves and loads scheduled plugin configurations across client sessions - -### Advanced Scheduling Algorithm - -The scheduler implements a sophisticated algorithm to determine which plugins to run and prioritize: - -```java -/** - * Schedules the next plugin based on priority and timing rules - */ -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("Not starting default plugin '{}' because non-default plugin '{}' is scheduled within {} 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 - if (isOnBreak()) { - log.info("Interrupting active break to start scheduled plugin: {}", selected.get().getCleanName()); - interruptBreak(); - } - - // Start the selected plugin - log.info("Starting scheduled plugin: {}", selected.get().getCleanName()); - startPluginScheduleEntry(selected.get()); -} -``` - -The plugin selection algorithm incorporates several factors and methods: - -1. **Priority**: Higher priority plugins are always considered first -2. **Default Status**: Non-default plugins can be prioritized over default plugins -3. **Time Window Forecasting**: The scheduler looks ahead for upcoming non-default plugins -4. **Run Count Balance**: For randomizable plugins, uses weighted selection based on run counts - -```java -/** - * Selects a plugin using weighted random selection. - * Plugins with lower run counts have higher probability of being selected. - */ -private PluginScheduleEntry selectPluginWeighted(List plugins) { - // Return the only plugin if there's just one - if (plugins.size() == 1) { - return plugins.get(0); - } - - // Calculate weights - plugins with lower run counts get higher weights - // Find the maximum run count - int maxRuns = plugins.stream() - .mapToInt(PluginScheduleEntry::getRunCount) - .max() - .orElse(0); - - // Add 1 to avoid division by zero and ensure all plugins have some chance - maxRuns = maxRuns + 1; - - // Calculate weights - double[] weights = new double[plugins.size()]; - double totalWeight = 0; - - for (int i = 0; i < plugins.size(); i++) { - weights[i] = maxRuns - plugins.get(i).getRunCount() + 1; - totalWeight += weights[i]; - } - - // Select based on weighted probability - double randomValue = Math.random() * totalWeight; - double weightSum = 0; - for (int i = 0; i < plugins.size(); i++) { - weightSum += weights[i]; - if (randomValue < weightSum) { - return plugins.get(i); - } - } - - // Fallback - return plugins.get(0); -} -``` - -### State-Based Decision Making - -The scheduler uses the state machine to make intelligent decisions about plugin execution, with different behavior based on the current state: - -#### 1. State-Dependent UI Updates -The UI reflects the current state with appropriate colors and messages: -- Active states (RUNNING_PLUGIN) show green indicators -- Warning states (STOPPING_PLUGIN) show orange indicators -- Error states show red indicators -- Break states show blue indicators - -#### 2. State-Based Priority Handling -The scheduler prioritizes actions differently based on the current state: -- During SCHEDULING, it evaluates which plugin should run next -- During BREAK states, it calculates appropriate break durations -- During WAITING states, it monitors for conditions to transition - -#### 3. State Transition Guards -Transitions between states have guards that ensure proper flow: -- Cannot transition directly from ERROR to RUNNING_PLUGIN -- HARD_STOPPING_PLUGIN only follows SOFT_STOPPING_PLUGIN -- STARTING_PLUGIN must precede RUNNING_PLUGIN - -This state-based design creates a robust system that can handle complex scheduling scenarios while maintaining proper execution flow. - -### Seamless Integration with Core Systems - -The scheduler deeply integrates with other Microbot systems to create a cohesive automation experience: - -#### Break Handler Integration - -The scheduler integrates with the BreakHandler plugin in several ways: - -```java -/** - * Starts a short break until the next plugin is scheduled to run - */ -private boolean startBreakBetweenSchedules(boolean logout, - int minBreakDurationMinutes, int maxBreakDurationMinutes) { - if (!isBreakHandlerEnabled()) { - return false; - } - if (BreakHandlerScript.isLockState()) - BreakHandlerScript.setLockState(false); - - // Check if we're outside play schedule - if (config.usePlaySchedule() && config.playSchedule().isOutsideSchedule()) { - Duration untilNextSchedule = config.playSchedule().timeUntilNextSchedule(); - log.info("Outside play schedule. Next schedule in: {}", 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); - - // Wait for break to become active - sleepUntil(() -> BreakHandlerScript.isBreakActive(), 1000); - - setState(SchedulerState.PLAYSCHEDULE_BREAK); - return true; - } - - // Configure duration for regular break - // Determine break duration and start break handler - - return true; -} -``` - -#### Auto Login Integration - -The scheduler manages the login process through a dedicated monitoring thread: - -```java -/** - * Starts a thread to monitor login state and process login when needed - */ -private void startLoginMonitoringThread() { - if (loginMonitor != null && loginMonitor.isAlive()) { - return; - } - - setState(SchedulerState.WAITING_FOR_LOGIN); - - loginMonitor = new Thread(() -> { - try { - // Don't continue if auto login is disabled - if (!config.autoLogIn()) { - log.info("Auto login is disabled"); - setState(SchedulerState.SCHEDULING); - return; - } - - // Wait a moment for the game client to be ready - sleep(1000); - - // Set state to indicate login attempt is in progress - setState(SchedulerState.LOGIN); - - // Determine world selection logic - int worldNumber = config.autoLogInWorld(); - int worldType = config.worldType(); - - // Attempt login with configured settings - AutoLoginPlugin autoLogin = injector.getInstance(AutoLoginPlugin.class); - autoLogin.requestLogin(); - - // Wait for login to complete - boolean loggedIn = sleepUntil(() -> Login.isLoggedIn(), 60000); - - if (loggedIn) { - setState(SchedulerState.SCHEDULING); - } else { - setState(SchedulerState.ERROR); - } - } catch (Exception e) { - log.error("Error in login monitor thread", e); - setState(SchedulerState.ERROR); - } - }); - - loginMonitor.setName("Login-Monitor"); - loginMonitor.setDaemon(true); - loginMonitor.start(); -} - -### Two-Tiered Plugin Stop Mechanism - -The scheduler implements a sophisticated stop mechanism with both soft and hard stop options: - -```java -/** - * Initiates a forced stop process for the current plugin. - * Used when a plugin needs to be stopped immediately or when soft stop fails. - * - * @param successful Whether the plugin completed successfully before stopping - */ -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); - } - 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(); -} -``` - -The actual stop monitoring logic is implemented in the `PluginScheduleEntry` class: - -```java -/** - * Starts a monitor thread that oversees the plugin stopping process - * - * @param successfulRun Whether the plugin run was successful - */ -private void startStopMonitoringThread(boolean successfulRun) { - // Don't start a new thread if one is already running - if (isMonitoringStop) { - return; - } - - isMonitoringStop = true; - - stopMonitorThread = new Thread(() -> { - log.info("Stop monitoring thread started for plugin '" + name + "'"); - - try { - // Keep checking until the stop completes or is abandoned - while (stopInitiated && isMonitoringStop) { - // Check if plugin has stopped running - if (!isRunning()) { - // Plugin has stopped successfully - if (scheduleEntryConfigManager != null) { - scheduleEntryConfigManager.setScheduleMode(false); - } - - // Update conditions for next run - if (successfulRun) { - resetStartConditions(); - } else { - setEnabled(false); - } - - // Reset stop state - stopInitiated = false; - hasStarted = false; - break; - } - - Thread.sleep(300); // Check every 300ms - } - } catch (InterruptedException e) { - // Thread was interrupted, just exit - } finally { - isMonitoringStop = false; - } - }); - - stopMonitorThread.setName("StopMonitor-" + name); - stopMonitorThread.setDaemon(true); // Use daemon thread to not prevent JVM exit - stopMonitorThread.start(); -} -``` - -This approach allows plugins to clean up resources and save state during a soft stop, while ensuring they eventually stop even if unresponsive. - -### Comprehensive UI and User Experience - -The scheduler provides an intuitive interface for managing scheduled plugins: - -```java -@Inject -private ClientToolbar clientToolbar; - -private NavigationButton navButton; -private SchedulerPanel panel; -private SchedulerWindow schedulerWindow; - -/** - * Initializes the scheduler UI components and navigation - */ -private void initializeUI() { - 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); - - // Initialize the main scheduler window - schedulerWindow = new SchedulerWindow(this); -} -``` - -The UI allows users to: - -- Add, edit, and remove scheduled plugins -- Configure start and stop conditions through an intuitive condition builder -- View detailed plugin execution history and statistics -- Manually control plugin execution with start, stop, and pause options - -## Plugin Selection and Scheduling Algorithm - -The scheduler implements a sophisticated multi-factor algorithm to determine which plugins to run and when. - -### Plugin Selection Process - -1. **Scheduling Cycle**: - - The scheduler runs a periodic check approximately every second via `checkSchedule()` - - During each cycle, it evaluates if the current plugin should be stopped with `checkCurrentPlugin()` - - If no plugin is running, it selects the next plugin to run with `scheduleNextPlugin()` - -2. **Selection Algorithm**: - ```text - START - Check if any non-default plugins are scheduled soon (within lookup window) - If a non-default plugin is upcoming and current plugin is default: - Don't start any default plugin, wait for the non-default plugin - Get next plugin that's due to run via getNextScheduledPluginEntry() - If on a break, interrupt it to start the selected plugin - Start the selected plugin with startPluginScheduleEntry() - END - ``` - -3. **Selection Factors** (in order of precedence): - - **Plugin Priority**: Higher priority plugins are always evaluated first - - **Plugin Type**: Non-default plugins take precedence over default plugins - - **Start Conditions**: Plugins with met start conditions are selected first - - **Randomization**: For equal priority plugins, weighted random selection - - **Run Balance**: Plugins run less frequently get higher weighting - -### Condition Evaluation Model - -The scheduler uses a sophisticated condition evaluation model with different rules for start vs. stop: - -```text -For Starting a Plugin: -Plugin Start Conditions AND User Start Conditions must ALL be true - -For Stopping a Plugin: -Plugin Stop Conditions OR User Stop Conditions - ANY being true triggers stop -``` - -This condition model gives both plugins and users appropriate control: - -- Plugins cannot start unless both their requirements and user preferences allow -- Either the plugin or user can trigger a stop when needed - -## Break System and Play Schedule - -The scheduler implements two complementary break systems to create natural-appearing behavior patterns. - -### Core Break System - -The break system controls automated breaks between plugin executions: - -1. **Break Duration Calculation**: - - Minimum break duration from config (default: 2 minutes) - - Maximum break duration from config (default: 2 minutes, configurable up to 60) - - The `startBreakBetweenSchedules` method handles break initialization - -2. **Break Triggers**: - - After plugin completion - - When no plugins are running and no plugins are due to run soon - - When outside allowed play schedule hours - - Based on configured break frequency - -3. **Break Management**: - - **Break Initiation**: Uses `BreakHandlerScript.breakDuration` to set break length - - **Break Interruption**: Can interrupt breaks for high-priority plugins using `interruptBreak()` - - **Break State Tracking**: Uses dedicated states (BREAK, PLAYSCHEDULE_BREAK) - -### Play Schedule System - -The play schedule controls when the scheduler is allowed to run plugins: - -1. **Schedule Configuration**: - - Each day can have different allowed play hours - - Multiple time windows can be configured per day - - Randomization can be applied to window boundaries - -2. **Schedule Behavior**: - - Outside allowed hours: Scheduler enters PLAYSCHEDULE_BREAK state - - Approaching end of window: Current plugin may be stopped - - Beginning of window: Scheduler resumes normal operation - - Window transitions: Can trigger login/logout actions - -### Integration with BreakHandler Plugin - -The scheduler integrates with the standalone BreakHandler plugin: - -1. **Coordination Mechanism**: - - BreakHandler signals when breaks begin/end - - Scheduler respects BreakHandler's break state - - Login/logout settings synchronized between systems - - Break statistics shared for consistent behavior - -2. **Break Handler Settings Management**: - - ```java - /** - * Saves current BreakHandler settings before modifying them - * for scheduler operation, and restores original settings - * when scheduler is disabled. - */ - private void syncBreakHandlerSettings() { - // Save original settings - savedBreakHandlerLogoutSetting = getBreakHandlerSetting("logout"); - - // Apply scheduler settings - if (config.breakHandlerForceLogout()) { - setBreakHandlerSetting("logout", true); - } - } - ``` - -## Implementation Strategies - -### Main Schedule Check Logic - -The scheduler uses a main scheduling check that runs approximately every second: - -```java -private void checkSchedule() { - // Skip checking if in certain states - if (SchedulerState.LOGIN == currentState || - SchedulerState.WAITING_FOR_LOGIN == currentState || - SchedulerState.HARD_STOPPING_PLUGIN == currentState || - SchedulerState.SOFT_STOPPING_PLUGIN == currentState || - currentState == SchedulerState.HOLD) { - 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()) { - // Check if login is needed - PluginScheduleEntry nextPluginWith = null; - PluginScheduleEntry nextPluginPossible = getNextScheduledPluginEntry(false, null).orElse(null); - - // Skip breaks if min break duration is 0 - int minBreakDuration = config.minBreakDuration(); - if (minBreakDuration == 0) { - minBreakDuration = 1; - nextPluginWith = getNextScheduledPluginEntry(true, null).orElse(null); - } else { - minBreakDuration = Math.max(1, minBreakDuration); - // Get the next scheduled plugin within minBreakDuration - nextPluginWith = getNextScheduledPluginWithinTime( - Duration.ofMinutes(minBreakDuration)); - } - - // Handle login requirements - if (nextPluginWith == null && - nextPluginPossible != null && - !nextPluginPossible.hasOnlyTimeConditions() - && !isOnBreak() && !Microbot.isLoggedIn()) { - startLoginMonitoringThread(); - return; - } - - // Determine if we should schedule next plugin or take a break - if (nextPluginWith != null && canRunNow()) { - scheduleNextPlugin(); - } else { - // Take a break if nothing is running - startBreak(); - } - } -} -``` - -### Plugin Management - -The actual management of plugin lifecycle is handled through: - -1. **Starting Plugins**: `startPluginScheduleEntry(PluginScheduleEntry entry)` -2. **Stopping Plugins**: `forceStopCurrentPluginScheduleEntry(boolean successful)` -3. **Checking Conditions**: `checkCurrentPlugin()` evaluates stop conditions - -### User Interface Functions - -The scheduler provides UI controls through: - -- **Force Start**: `forceStartPluginScheduleEntry(PluginScheduleEntry entry)` -- **Stop**: `forceStopCurrentPluginScheduleEntry(boolean successful)` -- **Pause/Resume**: `setSchedulerState(SchedulerState.HOLD)` and `startScheduler()` - -## Integration with Other Systems - -The SchedulerPlugin integrates with several other plugins and systems to provide a complete automation solution: - -### BreakHandler Integration - -The scheduler works closely with the BreakHandler plugin to: -- Respect global break settings -- Coordinate break times across plugins -- Share break statistics and patterns -- Manage login/logout during breaks - -Configuration options allow you to control: -- Whether to use BreakHandler for scheduled plugins -- Whether to log out during breaks -- Break duration settings - -### AutoLogin Integration - -The scheduler integrates with the AutoLogin plugin to: -- Automatically log in before starting plugins when needed -- Handle world selection based on configuration -- Manage disconnections and reconnections -- Support play schedule login/logout requirements - -### Antiban Features - -The scheduler implements various antiban measures: -- Randomized break patterns -- Natural play schedule enforcement -- Varied execution timing -- Random plugin selection within priority groups - -## Best Practices - -For optimal use of the Plugin Scheduler: - -1. **Set Clear Priorities**: - - High priority (8-10): Critical tasks - - Medium priority (4-7): Regular tasks - - Low priority (1-3): Background tasks - -2. **Use Appropriate Stop Conditions**: - - Always include a time-based stop condition as a fallback - - Use game-state conditions for more precise control - - Test conditions thoroughly before long-term use - -3. **Balance Random vs. Fixed Scheduling**: - - Use randomization for most plugins - - Reserve fixed schedules for time-sensitive tasks - - Mix default and non-default plugins for natural patterns - -4. **Configure Integration Features**: - - Enable BreakHandler integration for more natural patterns - - Use AutoLogin when running unattended - - Set up Play Schedules that match realistic play patterns diff --git a/docs/scheduler/tasks/README.md b/docs/scheduler/tasks/README.md deleted file mode 100644 index 5e106c4e045..00000000000 --- a/docs/scheduler/tasks/README.md +++ /dev/null @@ -1,123 +0,0 @@ -# Pre/Post Schedule Tasks System Documentation - -## Overview - -The Pre/Post Schedule Tasks system transforms plugin development by introducing automated preparation and cleanup capabilities into the Plugin Scheduler. This evolutionary enhancement shifts plugin architecture from manual resource management to declarative requirement specification, creating more reliable and user-friendly automation. - -### The Transformation - -Traditional plugin development required manual handling of equipment setup, inventory management, location positioning, and resource cleanup. The task system automates these operations through a declarative approach where plugins specify requirements rather than implementation details. - -### System Architecture - -The task system operates through three interconnected layers: - -**Infrastructure Layer**: The [`AbstractPrePostScheduleTasks`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/AbstractPrePostScheduleTasks.java) provides thread-safe execution services, lifecycle management, and error handling foundations. - -**Requirements Layer**: The [`PrePostScheduleRequirements`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/PrePostScheduleRequirements.java) framework enables declarative specification of plugin needs through a comprehensive requirement type system. - -**Integration Layer**: The enhanced [`SchedulablePlugin`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/api/SchedulablePlugin.java) interface connects plugins seamlessly with the scheduler ecosystem. - -## Evolution Through Three Commits - -The system development followed three strategic phases: - -### Phase 1: Enhanced Requirements Framework - -Introduced flexible requirement definitions with sophisticated logical operations, enabling complex requirement combinations through OR operation modes and intelligent planning algorithms. - -### Phase 2: Robust Infrastructure - -Enhanced task lifecycle management with comprehensive cancellation support, sophisticated error handling, and rich user interface components for real-time monitoring. - -### Phase 3: Production Integration - -Established core infrastructure with the complete requirement type ecosystem, demonstrating real-world implementation through the GOTR plugin integration. - -## Core Concepts - -### Declarative Requirements - -The system's power emerges from its declarative nature. Instead of writing procedural code to acquire items or navigate to locations, plugins declare requirements through specialized classes. The [`RequirementRegistry`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/registry/RequirementRegistry.java) manages these declarations and coordinates their fulfillment. - -### Intelligent Fulfillment - -The [`RequirementSolver`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSolver.java) analyzes requirements and determines optimal fulfillment strategies, considering factors like resource availability, player location, and requirement priorities. - -### Context-Aware Execution - -Requirements operate within specific contexts (PRE_SCHEDULE, POST_SCHEDULE, BOTH) allowing the system to coordinate when different preparations occur during the plugin lifecycle. - -## Documentation Structure - -### Foundation Documentation - -- **[Task Management System](task-management-system.md)** — Comprehensive guide to the infrastructure layer, covering execution services, lifecycle management, and error handling patterns -- **[Requirements System](requirements-system.md)** — Complete exploration of the requirement type ecosystem, fulfillment strategies, and advanced logical combinations -- **[Enhanced SchedulablePlugin API](enhanced-schedulable-plugin-api.md)** — Advanced integration patterns for scheduler connectivity and lifecycle management - -### Implementation Resources - -- **[Plugin Writer's Guide](plugin-writers-guide.md)** - Practical step-by-step implementation guidance with decision trees and best practices -- **[Requirements Integration](requirements-integration.md)** - Strategies for using requirements beyond scheduler context, enabling code reuse across different plugin architectures - -### Reference Implementations - -Study these production codebases for practical implementation insights: - -- **GOTR Integration**: Examine [GOTR Plugin Implementation](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/runecrafting/gotr/) demonstrating sophisticated minigame automation with comprehensive preparation requirements -- **Example Plugin**: Review [SchedulableExample Implementation](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/) showcasing complete system integration patterns - -## Implementation Approach - -### Understanding Before Implementation - -The task system's effectiveness depends on understanding its conceptual framework before diving into implementation specifics. Focus on grasping how declarative requirements translate into automated behaviors. - -### Three-Component Integration - -Every implementation involves three fundamental components working in coordination: - -**Requirements Definition**: Create a class extending [`PrePostScheduleRequirements`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/PrePostScheduleRequirements.java) that declares what your plugin needs to operate successfully. - -**Task Management**: Develop a class extending [`AbstractPrePostScheduleTasks`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/AbstractPrePostScheduleTasks.java) that handles both requirement fulfillment and custom plugin-specific preparation or cleanup operations. - -**Plugin Integration**: Modify your plugin class to implement the enhanced [`SchedulablePlugin`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/api/SchedulablePlugin.java) interface, establishing the connection with the scheduler system. - -### Development Philosophy - -Embrace the separation between "what" and "how" - specify what your plugin needs rather than how to obtain it. This approach creates more maintainable, reliable, and adaptable automation systems. - -## System Benefits - -### Transformation for Developers - -The system revolutionizes plugin development by eliminating repetitive resource management code, providing standardized error handling patterns, and enabling focus on core plugin functionality rather than setup procedures. - -### Enhanced User Experience - -Users benefit from consistent preparation procedures across all plugins, automatic resource acquisition, intelligent optimization of preparation paths, and comprehensive progress feedback through rich UI components. - -### Ecosystem Advantages - -The framework promotes code reuse through shared requirement implementations, ensures quality consistency across plugin behaviors, optimizes performance through centralized resource management, and simplifies maintenance through standardized patterns. - -## Technical Foundation - -### Key Components - -Explore the complete implementation through the [Task System Directory](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/) which contains all system components including requirement types, management infrastructure, UI components, and example implementations. - -### Extension Points - -The system design emphasizes extensibility through well-defined interfaces, allowing custom requirement types, specialized fulfillment strategies, and plugin-specific preparation procedures while maintaining integration with the broader framework. - -## Next Steps for Implementation - -1. **Conceptual Understanding**: Begin with the [Task Management System](task-management-system.md) documentation to understand infrastructure concepts -2. **Requirement Design**: Study the [Requirements System](requirements-system.md) to plan your plugin's requirement specification -3. **Practical Implementation**: Follow the [Plugin Writer's Guide](plugin-writers-guide.md) for step-by-step integration instructions -4. **Reference Study**: Examine the [GOTR implementation](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/runecrafting/gotr/) for production patterns -5. **Advanced Integration**: Explore [Requirements Integration](requirements-integration.md) for sophisticated usage patterns beyond basic scheduler integration - -The Pre/Post Schedule Tasks system represents a paradigm shift toward intelligent, declarative automation that handles the complexity of game state management while maintaining the flexibility needed for diverse plugin requirements. diff --git a/docs/scheduler/tasks/enhanced-schedulable-plugin-api.md b/docs/scheduler/tasks/enhanced-schedulable-plugin-api.md deleted file mode 100644 index acbf560ce07..00000000000 --- a/docs/scheduler/tasks/enhanced-schedulable-plugin-api.md +++ /dev/null @@ -1,237 +0,0 @@ -# Enhanced SchedulablePlugin API - -## Overview - -The Enhanced SchedulablePlugin API represents an evolution from the original SchedulablePlugin interface, introducing comprehensive task management capabilities that standardize plugin automation while maintaining flexibility for diverse implementation needs. This API transforms plugin development from manual resource coordination to declarative requirement specification. - -## Architectural Evolution - -### From Manual to Declarative - -The enhanced API shifts plugin development philosophy from imperative resource management to declarative requirement specification. Instead of implementing complex resource acquisition logic within your plugin, you describe what your plugin needs, and the task system handles the execution details. - -### Integration Philosophy - -The API is designed around the principle of seamless integration - plugins enhanced with task management capabilities should function identically whether used standalone or within the scheduler environment. This dual-mode operation ensures backward compatibility while enabling advanced automation features. - -### System Coordination - -Enhanced plugins participate in a broader ecosystem where resource coordination, conflict resolution, and user experience standardization are handled at the system level rather than requiring individual plugin implementations. - -## Core Interface Components - -### Enhanced SchedulablePlugin Interface - -The primary interface extends the basic SchedulablePlugin with task management capabilities: - -**Task Management Integration**: Methods for coordinating with the task execution system, enabling plugins to participate in comprehensive automation workflows. - -**Lifecycle Coordination**: Enhanced lifecycle methods that integrate with the scheduler's execution model while maintaining plugin autonomy. - -**Event Handling**: Standardized event handling for scheduler interactions, allowing plugins to respond appropriately to system coordination events. - -Reference the complete interface definition in [`SchedulablePlugin`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/api/SchedulablePlugin.java) for detailed method specifications. - -### Task Provider Integration - -Enhanced plugins can provide task management capabilities through the system's provider pattern: - -**Task Manager Provision**: Plugins supply task manager instances that handle resource preparation and cleanup operations. - -**Requirement Specification**: Declarative requirement definition that describes plugin resource needs without implementing acquisition logic. - -**Configuration Integration**: Integration with the scheduler's configuration system for coordinated user experience across plugin and task management. - -## Implementation Architecture - -### Dual-Mode Operation - -Enhanced plugins must support both standalone and scheduler-managed operation: - -**Standalone Functionality**: Complete plugin functionality when operating independently, maintaining existing user workflows and expectations. - -**Scheduler Integration**: Enhanced capabilities when operating within the scheduler environment, leveraging centralized resource management and coordination. - -**Detection Logic**: Intelligent detection of the operational context to enable appropriate behavior in each mode. - -### Resource Coordination - -The API facilitates sophisticated resource coordination: - -**Shared Resource Management**: Coordination with other system components for shared resources like banking, equipment management, and location positioning. - -**Conflict Resolution**: Participation in system-level conflict resolution when multiple plugins or operations compete for limited resources. - -**State Synchronization**: Coordination with the scheduler's state management to ensure consistent operation across complex automation workflows. - -### Configuration Architecture - -Enhanced plugins integrate with the scheduler's configuration system: - -**Unified Configuration**: Seamless integration between plugin-specific configuration and task management settings. - -**User Experience Consistency**: Standardized configuration patterns that provide consistent user experience across different plugin types. - -**Dynamic Configuration**: Support for runtime configuration changes that affect both plugin behavior and task management strategies. - -## Advanced Integration Patterns - -### Complex Workflow Coordination - -For sophisticated automation scenarios: - -**Multi-Phase Operations**: Support for complex operations that span multiple game activities or require sophisticated state transitions. - -**Conditional Execution**: Dynamic adaptation based on game state, resource availability, or user preferences. - -**Cross-Plugin Coordination**: Integration with other enhanced plugins for coordinated automation workflows. - -### Error Handling and Recovery - -Robust error handling strategies for enhanced plugins: - -**Graceful Degradation**: Intelligent fallback strategies when optimal conditions cannot be achieved. - -**State Recovery**: Restoration of proper state when operations are interrupted or fail. - -**User Communication**: Clear communication of status, issues, and recovery actions to maintain user awareness and control. - -### Performance and Responsiveness - -Design considerations for maintaining optimal performance: - -**Asynchronous Operations**: Integration with asynchronous execution patterns to maintain system responsiveness. - -**Resource Efficiency**: Efficient resource utilization that minimizes impact on game performance and user experience. - -**Scalability**: Design patterns that support multiple concurrent enhanced plugins without degrading system performance. - -## Implementation Guidelines - -### Interface Implementation Strategy - -Approach enhanced plugin implementation systematically: - -**Incremental Enhancement**: Add enhanced capabilities to existing plugins without disrupting core functionality. - -**Testing Strategy**: Comprehensive testing in both standalone and scheduler-managed modes to ensure reliable operation. - -**Compatibility Maintenance**: Preserve existing plugin behavior while adding enhanced capabilities. - -### Best Practices - -Follow established patterns for reliable enhanced plugin development: - -**Clear Separation**: Maintain clear separation between core plugin logic and enhanced task management capabilities. - -**Robust Integration**: Design integration points that handle edge cases gracefully and provide appropriate fallback behavior. - -**User Experience**: Prioritize user experience consistency across different operational modes. - -### Common Integration Challenges - -Understand and address typical implementation challenges: - -**State Management**: Coordinate plugin state with task execution state to prevent conflicts or inconsistencies. - -**Resource Competition**: Handle scenarios where plugin needs conflict with system resource management. - -**Timing Coordination**: Ensure proper timing between plugin operations and task execution phases. - -## Example Implementation Analysis - -### Production Examples - -Study these production implementations for guidance: - -**GOTR Integration**: Examine the [GOTR enhanced plugin](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/runecrafting/gotr/) implementation for sophisticated minigame automation with comprehensive task integration. - -**Example Implementation**: Review the [complete example plugin](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/) that demonstrates all aspects of enhanced plugin development. - -### Pattern Analysis - -Learn from established implementation patterns: - -**Interface Implementation**: Study how production plugins implement the enhanced interface while maintaining backward compatibility. - -**Resource Coordination**: Examine real-world approaches to resource coordination and conflict resolution. - -**Error Handling**: Understand proven strategies for robust error handling and recovery in enhanced plugins. - -## Testing and Validation - -### Comprehensive Testing Strategy - -Ensure reliable enhanced plugin operation through thorough testing: - -**Dual-Mode Testing**: Validate functionality in both standalone and scheduler-managed operation modes. - -**Integration Testing**: Test coordination with task management system and other system components. - -**Edge Case Testing**: Verify behavior under unusual conditions, resource constraints, and error scenarios. - -### Performance Validation - -Ensure enhanced plugins maintain acceptable performance: - -**Resource Usage**: Monitor resource consumption in enhanced mode to prevent performance degradation. - -**Responsiveness**: Validate that enhanced capabilities don't negatively impact user interface responsiveness. - -**Scalability**: Test behavior when multiple enhanced plugins operate concurrently. - -### User Experience Testing - -Validate user experience across operational modes: - -**Consistency**: Ensure user experience remains consistent between standalone and enhanced operation. - -**Feedback**: Verify that users receive appropriate feedback about enhanced plugin status and operations. - -**Control**: Ensure users maintain appropriate control over enhanced plugin behavior. - -## Migration and Upgrade Strategies - -### Existing Plugin Enhancement - -Transform existing plugins to support enhanced capabilities: - -**Gradual Enhancement**: Add enhanced capabilities incrementally without disrupting existing functionality. - -**Compatibility Preservation**: Maintain backward compatibility throughout the enhancement process. - -**Testing Integration**: Integrate enhanced testing strategies into existing plugin testing workflows. - -### Version Management - -Manage enhanced plugin versions effectively: - -**Feature Gating**: Use feature flags to control enhanced capability availability during development and deployment. - -**Configuration Migration**: Provide smooth migration paths for existing plugin configurations. - -**Documentation Updates**: Maintain current documentation that covers both basic and enhanced plugin capabilities. - -## Future Evolution - -### API Evolution Path - -Understand the planned evolution of enhanced plugin capabilities: - -**Feature Expansion**: Anticipated additions to enhanced plugin capabilities and coordination features. - -**Performance Improvements**: Ongoing optimization of enhanced plugin execution patterns and resource management. - -**Integration Enhancements**: Planned improvements to cross-plugin coordination and ecosystem integration. - -### Development Considerations - -Plan enhanced plugin development with future evolution in mind: - -**Extensible Architecture**: Design enhanced plugins to accommodate future API enhancements without requiring major refactoring. - -**Configuration Flexibility**: Implement configuration patterns that can evolve with API capabilities. - -**Testing Framework**: Establish testing frameworks that can adapt to API evolution while maintaining comprehensive coverage. - -The Enhanced SchedulablePlugin API represents a significant evolution in plugin automation capabilities. Success with this API requires understanding both the technical implementation details and the broader architectural philosophy that guides the system design. Focus on the declarative approach, emphasize resource coordination, and prioritize user experience consistency across all operational modes. diff --git a/docs/scheduler/tasks/plugin-writers-guide.md b/docs/scheduler/tasks/plugin-writers-guide.md deleted file mode 100644 index 87d45ce9d66..00000000000 --- a/docs/scheduler/tasks/plugin-writers-guide.md +++ /dev/null @@ -1,225 +0,0 @@ -# Plugin Writer's Guide - -## Overview - -This guide provides practical, step-by-step instructions for integrating the Pre/Post Schedule Tasks system into your plugin. Rather than learning abstract concepts, this guide focuses on the concrete implementation steps needed to transform your plugin from manual resource management to declarative requirement specification. - -## Understanding the Integration Process - -### The Transformation Journey - -Integrating the task system involves transforming your plugin from imperative resource management to declarative requirement specification. This transformation typically improves plugin reliability, reduces user setup burden, and standardizes behavior across the plugin ecosystem. - -### Integration Complexity Levels - -**Basic Integration**: Simple requirement definition with minimal custom logic, suitable for straightforward skilling or combat plugins. - -**Intermediate Integration**: Multiple requirement types with some custom preparation logic, typical for specialized activities or minigames. - -**Advanced Integration**: Complex requirement combinations, conditional logic, and sophisticated custom task implementations for high-end automation systems. - -## Prerequisites and Planning - -### Evaluation Phase - -Before beginning integration, evaluate your plugin's current resource management patterns: - -**Manual Operations**: Identify operations users currently perform manually before starting your plugin - these become candidates for requirement automation. - -**Resource Dependencies**: Catalog items, equipment, locations, and game state conditions your plugin needs to function effectively. - -**Failure Points**: Examine common failure modes in your current plugin and determine which could be prevented through proper requirement fulfillment. - -### Architecture Planning - -Plan your integration approach based on your plugin's complexity: - -**Simple Plugins**: Focus primarily on requirement definition with minimal custom task logic. - -**Complex Plugins**: Design custom task implementations for plugin-specific operations that cannot be expressed as standard requirements. - -**Ecosystem Plugins**: Consider integration with other scheduler-aware plugins and shared resource coordination. - -## Step-by-Step Implementation - -### Step 1: Requirements Analysis and Design - -Begin by analyzing your plugin's resource needs and designing appropriate requirements: - -**Equipment Analysis**: Determine optimal equipment for your plugin's activities, considering alternatives and upgrade paths. - -**Location Strategy**: Identify key locations your plugin operates in and determine positioning requirements. - -**Resource Planning**: Catalog consumables, tools, and other items needed for successful plugin execution. - -**State Dependencies**: Identify spellbook requirements, quest prerequisites, and other game state conditions. - -Study the [`ExamplePrePostScheduleRequirements`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleRequirements.java) implementation to understand requirement definition patterns. - -### Step 2: Requirements Implementation - -Create your requirements class extending [`PrePostScheduleRequirements`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/PrePostScheduleRequirements.java): - -**Class Structure**: Implement the abstract `initializeRequirements()` method to define all requirements your plugin needs. - -**Registry Usage**: Use the provided registry to register requirements with appropriate priorities and contexts. - -**Collection Integration**: Leverage [`ItemRequirementCollection`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/data/ItemRequirementCollection.java) for standard equipment sets rather than defining individual items. - -**Context Assignment**: Assign requirements to appropriate contexts (PRE_SCHEDULE, POST_SCHEDULE, BOTH) based on when they should be fulfilled. - -### Step 3: Task Manager Implementation - -Create your task manager extending [`AbstractPrePostScheduleTasks`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/AbstractPrePostScheduleTasks.java): - -**Core Methods**: Implement the required abstract methods, focusing on custom logic that cannot be expressed as requirements. - -**Error Handling**: Design robust error handling strategies for custom operations, considering both recoverable and non-recoverable failures. - -**Resource Management**: Ensure proper cleanup in custom task implementations, following the established patterns for resource lifecycle management. - -**Integration Hooks**: Implement scheduler detection logic to ensure task execution only occurs when appropriate. - -Reference the [`ExamplePrePostScheduleTasks`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleTasks.java) for implementation patterns and best practices. - -### Step 4: Plugin Integration - -Modify your main plugin class to integrate with the task system: - -**Interface Implementation**: Implement the enhanced [`SchedulablePlugin`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/api/SchedulablePlugin.java) interface to enable scheduler integration. - -**Lifecycle Modification**: Modify startup and shutdown procedures to execute tasks when appropriate while maintaining backward compatibility for non-scheduler usage. - -**Event Handling**: Implement scheduler event handlers to respond to stop requests and other scheduler coordination events. - -**State Management**: Ensure proper coordination between task execution and main plugin logic to prevent interference. - -### Step 5: Testing and Validation - -Develop comprehensive testing strategies to ensure reliable integration: - -**Requirement Testing**: Validate that all requirements can be fulfilled under various game conditions and resource availability scenarios. - -**Integration Testing**: Test the complete lifecycle from requirement fulfillment through main plugin execution to cleanup operations. - -**Edge Case Testing**: Verify behavior when requirements cannot be fulfilled, when operations time out, and when emergency cancellation is triggered. - -**Performance Testing**: Ensure task execution doesn't negatively impact main plugin performance or introduce unacceptable delays. - -## Advanced Implementation Patterns - -### Complex Requirement Combinations - -For activities requiring sophisticated resource coordination: - -**OR Requirements**: Use logical requirements to specify alternatives when multiple valid approaches exist. - -**Conditional Requirements**: Implement state-dependent requirements that adapt to current game conditions. - -**Hierarchical Planning**: Design requirement hierarchies for complex activities with multiple phases or optional enhancements. - -### Custom Task Implementation - -For plugin-specific operations beyond standard requirements: - -**Preparation Logic**: Implement custom pre-schedule tasks for plugin initialization, state validation, or specialized setup procedures. - -**Cleanup Operations**: Design custom post-schedule tasks for data persistence, state restoration, or plugin-specific resource management. - -**Integration Coordination**: Coordinate with other systems or plugins through custom task implementations when standard requirements are insufficient. - -### Performance Optimization - -Optimize task performance for smooth user experience: - -**Asynchronous Operations**: Design tasks to utilize asynchronous patterns where appropriate to maintain responsiveness. - -**Resource Efficiency**: Minimize resource usage during task execution to prevent interference with main plugin operations. - -**Caching Strategies**: Implement intelligent caching for expensive operations while maintaining accuracy through proper invalidation. - -## Common Implementation Challenges - -### Requirement Conflicts - -Address potential conflicts between requirements: - -**Resource Competition**: Handle scenarios where multiple requirements compete for limited resources. - -**Timing Conflicts**: Resolve conflicts between requirements that must be fulfilled in specific orders. - -**State Inconsistencies**: Design strategies for handling inconsistent game state during requirement fulfillment. - -### Error Recovery - -Implement robust error recovery strategies: - -**Graceful Degradation**: Design fallback strategies when optimal requirements cannot be fulfilled. - -**User Communication**: Provide clear feedback about requirement fulfillment status and any issues encountered. - -**Retry Logic**: Implement intelligent retry strategies for transient failures while avoiding infinite loops. - -### Integration Complexity - -Manage complexity in sophisticated integrations: - -**Code Organization**: Structure your implementation to maintain clear separation between requirement definition, task management, and main plugin logic. - -**Configuration Management**: Design configuration interfaces that expose appropriate task system options to users. - -**Debugging Support**: Implement comprehensive logging and debugging support to aid in troubleshooting integration issues. - -## Real-World Examples - -### Study Production Implementations - -Examine these production examples for implementation insights: - -**GOTR Integration**: Study the [GOTR plugin directory](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/runecrafting/gotr/) for a sophisticated minigame integration with comprehensive requirement management. - -**Example Plugin**: Review the [SchedulableExample implementation](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/) for a complete demonstration of all system features. - -### Implementation Patterns - -Learn from established patterns: - -**Requirement Organization**: Study how production implementations organize and structure their requirements for maintainability. - -**Error Handling**: Examine real-world error handling strategies and recovery mechanisms. - -**User Experience**: Understand how production plugins balance automation with user control and feedback. - -## Best Practices and Guidelines - -### Code Quality - -Maintain high code quality in your integration: - -**Clear Separation**: Keep requirement definition, task implementation, and main plugin logic clearly separated. - -**Comprehensive Testing**: Implement thorough testing for all aspects of your integration. - -**Documentation**: Document your requirements and custom task logic for future maintenance. - -### User Experience - -Design your integration with user experience in mind: - -**Predictable Behavior**: Ensure task execution is predictable and provides appropriate feedback. - -**Graceful Failures**: Handle failures gracefully with clear error messages and recovery suggestions. - -**Performance Impact**: Minimize the performance impact of task execution on overall plugin responsiveness. - -### Maintenance and Evolution - -Design for long-term maintenance: - -**Extensible Architecture**: Structure your implementation to accommodate future enhancements. - -**Version Compatibility**: Ensure your integration remains compatible with system updates. - -**Documentation Updates**: Keep documentation current as your implementation evolves. - -The integration process requires careful planning and attention to detail, but the result is more reliable, maintainable, and user-friendly plugin automation. Focus on understanding the concepts before implementing, and don't hesitate to study the example implementations for guidance on best practices and common patterns. diff --git a/docs/scheduler/tasks/requirements-integration.md b/docs/scheduler/tasks/requirements-integration.md deleted file mode 100644 index a29732437e1..00000000000 --- a/docs/scheduler/tasks/requirements-integration.md +++ /dev/null @@ -1,259 +0,0 @@ -# Requirements Integration - -## Overview - -Requirements integration represents the bridge between high-level plugin needs and concrete automation actions. This system transforms abstract requirements like "equipped for combat" or "positioned for mining" into specific, executable operations that prepare the game environment for plugin operation. - -## Integration Philosophy - -### Declarative Resource Management - -The requirements integration system embodies a declarative approach to resource management. Rather than implementing complex resource acquisition logic within each plugin, developers describe their needs using standardized requirement types, and the integration system handles the execution details. - -### Contextual Execution - -Requirements are fulfilled within specific contexts that determine when and how they should be executed. This contextual framework ensures requirements are fulfilled at appropriate times without interfering with plugin operation or creating user experience disruptions. - -### Extensible Foundation - -The integration architecture is designed for extensibility, allowing new requirement types to be added seamlessly while maintaining compatibility with existing implementations. This extensibility ensures the system can evolve to support new gameplay patterns and automation needs. - -## Integration Architecture - -### Registry-Based Management - -The integration system uses a registry-based architecture for requirement management: - -**Centralized Registration**: All requirements are registered through the [`RequirementRegistry`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/registry/RequirementRegistry.java), providing centralized management and coordination. - -**Type System**: Requirements are organized by type, allowing for specialized handling of different resource categories like equipment, inventory, location, and game state. - -**Priority Management**: The registry manages requirement priorities, ensuring critical requirements are fulfilled before optional enhancements. - -**Conflict Resolution**: Intelligent conflict resolution handles scenarios where requirements conflict or compete for limited resources. - -### Execution Framework - -Requirements are executed through a sophisticated framework that coordinates timing, resource management, and error handling: - -**Context-Aware Execution**: Requirements execute within appropriate contexts (PRE_SCHEDULE, POST_SCHEDULE, BOTH) based on their nature and timing requirements. - -**Resource Coordination**: The execution framework coordinates access to shared resources like banking, equipment management, and location positioning. - -**Error Handling**: Comprehensive error handling ensures failed requirements don't prevent other operations and provide appropriate user feedback. - -**Performance Optimization**: Intelligent execution patterns minimize overhead while maintaining reliability and user experience quality. - -## Core Integration Components - -### Requirement Type Ecosystem - -The system includes a comprehensive ecosystem of requirement types: - -**Equipment Requirements**: Managed through [`ItemRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/ItemRequirement.java) for automated equipment optimization and management. - -**Inventory Requirements**: Handled by [`ItemRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/ItemRequirement.java) for complex inventory preparation and item management. - -**Location Requirements**: Coordinated through [`LocationRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/LocationRequirement.java) for precise positioning and area preparation. - -**Game State Requirements**: Various specialized requirements for spellbooks, prayers, and other game state conditions. - -### Collection-Based Organization - -Related requirements are organized into collections for easier management: - -**Equipment Collections**: [`ItemRequirementCollection`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/data/ItemRequirementCollection.java) provides standardized equipment sets for common activities. - -**Activity-Specific Collections**: Specialized collections for specific activities like combat, skilling, or minigames that combine multiple requirement types. - -**User-Customizable Collections**: Support for user-defined requirement collections that can be shared across multiple plugins or activities. - -## Advanced Integration Patterns - -### Conditional Requirements - -The system supports sophisticated conditional requirement patterns: - -**State-Dependent Requirements**: Requirements that adapt based on current game state, player progress, or environmental conditions. - -**Logical Combinations**: OR requirements that specify alternatives when multiple valid approaches exist for achieving the same goal. - -**Hierarchical Dependencies**: Complex requirement hierarchies where fulfilling one requirement enables or modifies others. - -**Dynamic Adaptation**: Requirements that modify their behavior based on execution context or previous fulfillment attempts. - -### Resource Optimization - -Intelligent resource optimization ensures efficient requirement fulfillment: - -**Shared Resource Detection**: Automatic detection of shared resource needs across multiple requirements to optimize fulfillment order. - -**Cost-Benefit Analysis**: Intelligent analysis of fulfillment costs to choose optimal approaches when multiple options exist. - -**Resource Caching**: Strategic caching of expensive operations to improve performance across multiple requirement executions. - -**Batch Processing**: Optimization of related requirements to minimize repeated operations and resource access. - -### Error Recovery and Resilience - -Comprehensive error recovery ensures robust requirement integration: - -**Graceful Degradation**: Intelligent fallback strategies when optimal requirements cannot be fulfilled. - -**Partial Fulfillment**: Support for partial requirement fulfillment when complete fulfillment is not possible. - -**Retry Strategies**: Sophisticated retry logic for transient failures with appropriate backoff and timeout handling. - -**User Feedback**: Clear communication of requirement status, issues, and recovery actions to maintain user awareness. - -## Implementation Strategies - -### Basic Integration Approach - -For straightforward plugin integration: - -**Requirement Identification**: Systematically identify plugin resource needs and map them to appropriate requirement types. - -**Registry Configuration**: Configure the requirement registry with appropriate requirements, priorities, and contexts. - -**Testing Validation**: Comprehensive testing to ensure requirement fulfillment works reliably under various game conditions. - -Study the [`ExamplePrePostScheduleRequirements`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleRequirements.java) for basic integration patterns. - -### Advanced Integration Techniques - -For complex plugin requirements: - -**Custom Requirement Types**: Development of plugin-specific requirement types for specialized resource needs. - -**Conditional Logic**: Implementation of sophisticated conditional requirements that adapt to game state and context. - -**Cross-Plugin Coordination**: Integration with other plugins' requirements for coordinated resource management. - -**Performance Optimization**: Advanced optimization techniques for high-performance requirement fulfillment. - -### Integration Testing - -Comprehensive testing strategies for requirement integration: - -**Individual Requirement Testing**: Validation of each requirement type under various conditions and resource states. - -**Integration Testing**: Testing complete requirement fulfillment workflows from registration through execution. - -**Edge Case Validation**: Testing behavior under unusual conditions, resource constraints, and error scenarios. - -**Performance Testing**: Validation that requirement fulfillment doesn't negatively impact plugin or system performance. - -## Common Integration Challenges - -### Resource Competition - -Address scenarios where multiple requirements compete for limited resources: - -**Priority Resolution**: Use requirement priorities to resolve conflicts between competing requirements. - -**Resource Sharing**: Design requirements to share resources efficiently when possible. - -**Conflict Detection**: Implement detection of resource conflicts before they cause fulfillment failures. - -**Alternative Strategies**: Develop alternative fulfillment approaches when primary strategies conflict with other requirements. - -### Timing Coordination - -Manage complex timing relationships between requirements: - -**Execution Order**: Ensure requirements execute in appropriate order based on dependencies and priorities. - -**Context Coordination**: Coordinate requirement execution across different contexts to prevent interference. - -**Synchronization**: Synchronize requirement fulfillment with plugin lifecycle and game state changes. - -**Timeout Management**: Implement appropriate timeouts for requirement fulfillment to prevent indefinite delays. - -### State Management - -Handle complex game state interactions: - -**State Validation**: Ensure game state is appropriate for requirement fulfillment before execution. - -**State Preservation**: Preserve important game state during requirement fulfillment when necessary. - -**State Recovery**: Implement recovery strategies when requirement fulfillment disrupts expected game state. - -**State Monitoring**: Monitor game state changes that might affect requirement validity or fulfillment strategies. - -## Real-World Integration Examples - -### Production Implementations - -Study these production examples for integration insights: - -**GOTR Requirements**: Examine the [GOTR requirements implementation](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/runecrafting/gotr/) for sophisticated minigame requirement coordination. - -**Example Integration**: Review the [complete example](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/) for comprehensive requirement integration patterns. - -### Integration Patterns - -Learn from established integration approaches: - -**Requirement Organization**: Study how production implementations organize requirements for maintainability and clarity. - -**Error Handling**: Examine real-world error handling and recovery strategies in complex requirement scenarios. - -**Performance Optimization**: Understand proven techniques for optimizing requirement fulfillment performance. - -## Future Integration Evolution - -### System Enhancement - -Planned improvements to the requirements integration system: - -**New Requirement Types**: Development of additional requirement types to support emerging gameplay patterns and automation needs. - -**Performance Improvements**: Ongoing optimization of requirement fulfillment algorithms and resource management. - -**Integration Enhancements**: Improved coordination between requirements and enhanced plugin ecosystem integration. - -### Development Considerations - -Plan integration implementations with future evolution in mind: - -**Extensible Design**: Design requirement integrations to accommodate new requirement types and capabilities. - -**Backward Compatibility**: Ensure integration approaches remain compatible with system evolution. - -**Configuration Flexibility**: Implement configuration patterns that can evolve with system capabilities. - -## Best Practices for Integration - -### Design Principles - -Follow established principles for reliable requirement integration: - -**Clear Separation**: Maintain clear separation between requirement definition and fulfillment implementation. - -**Robust Error Handling**: Implement comprehensive error handling for all requirement fulfillment scenarios. - -**Performance Awareness**: Design integrations with performance impact awareness and optimization. - -### User Experience - -Prioritize user experience in requirement integration: - -**Predictable Behavior**: Ensure requirement fulfillment is predictable and provides appropriate feedback. - -**Graceful Failures**: Handle fulfillment failures gracefully with clear error messages and recovery suggestions. - -**Minimal Disruption**: Minimize disruption to user gameplay during requirement fulfillment. - -### Code Quality - -Maintain high code quality in integration implementations: - -**Comprehensive Testing**: Implement thorough testing for all aspects of requirement integration. - -**Clear Documentation**: Document requirement integration approaches for future maintenance and enhancement. - -**Consistent Patterns**: Follow established patterns and conventions for integration implementation. - -The requirements integration system represents a sophisticated approach to automated resource management that transforms plugin development from manual coordination to declarative specification. Success with this system requires understanding both the technical implementation details and the broader architectural philosophy that guides requirement fulfillment and resource coordination. diff --git a/docs/scheduler/tasks/requirements-system.md b/docs/scheduler/tasks/requirements-system.md deleted file mode 100644 index 872af97355b..00000000000 --- a/docs/scheduler/tasks/requirements-system.md +++ /dev/null @@ -1,197 +0,0 @@ -# Requirements System - -## Overview - -The Requirements System transforms plugin development by introducing a declarative approach to resource management and game state preparation. Built around the [`PrePostScheduleRequirements`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/PrePostScheduleRequirements.java) framework and powered by the [`RequirementRegistry`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/registry/RequirementRegistry.java), this system enables plugins to specify what they need rather than how to obtain it. - -## Declarative Philosophy - -### Paradigm Shift - -Traditional plugin development required imperative programming for resource acquisition - writing step-by-step procedures to obtain items, navigate to locations, and configure game state. The requirements system introduces a declarative paradigm where plugins specify desired outcomes and delegate implementation details to intelligent fulfillment algorithms. - -### Separation of Concerns - -The system achieves clean separation between requirement specification and fulfillment implementation. Plugin developers focus on defining requirements using specialized requirement types, while the fulfillment engine handles the complex logistics of actually meeting those requirements. - -### Intelligent Fulfillment - -The [`RequirementSolver`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSolver.java) analyzes requirement sets and determines optimal fulfillment strategies, considering factors like resource availability, player location, priority levels, and interdependencies between requirements. - -## Requirement Type Ecosystem - -### Item Requirements - -The [`ItemRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/ItemRequirement.java) system handles equipment and inventory management with sophisticated features: - -**Equipment Management**: Automatic detection of optimal equipment for activity requirements, with support for equipment slots, stat requirements, and compatibility checking. - -**Inventory Planning**: Intelligent inventory space management that considers item stacking, quantities needed, and space optimization for different activities. - -**Collection Integration**: The [`ItemRequirementCollection`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/data/ItemRequirementCollection.java) provides pre-configured equipment sets for common activities like combat, skilling, and specialized minigames. - -### Location Requirements - -The [`LocationRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/LocationRequirement.java) system manages player positioning and navigation: - -**Smart Navigation**: Integration with the walking system to automatically navigate to required locations using optimal pathfinding algorithms. - -**Proximity Handling**: Flexible proximity requirements that can specify exact positioning or acceptable ranges depending on activity needs. - -**Resource Location Integration**: The [`ResourceLocationOption`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/ResourceLocationOption.java) connects location requirements with resource availability, choosing optimal locations based on current game state. - -### Spellbook Requirements - -The [`SpellbookRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/SpellbookRequirement.java) manages magic system configuration: - -**Automatic Switching**: Seamless spellbook changes with restoration to original state during cleanup phases. - -**Context Awareness**: Different spellbook requirements for different phases of execution, enabling complex spell usage patterns. - -**State Preservation**: Careful tracking of original spellbook state to ensure proper restoration after task completion. - -### Shop Requirements - -The [`ShopRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/) system handles Grand Exchange and NPC shop interactions: - -**Multi-Item Support**: The [`MultiItemConfig`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/MultiItemConfig.java) enables complex purchasing strategies for items that come in sets or have alternatives. - -**World Hopping Integration**: The [`WorldHoppingConfig`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/WorldHoppingConfig.java) provides intelligent world selection for optimal shop availability. - -**Transaction Management**: Sophisticated handling of offer states, cancellations, and retry logic through the [`CancelledOfferState`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/CancelledOfferState.java) system. - -### Logical Requirements - -The [`LogicalRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/logical/) system enables complex requirement combinations: - -**OR Operations**: The [`OrRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/logical/OrRequirement.java) allows specification of alternative requirements, with intelligent selection based on the [`OrRequirementMode`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/OrRequirementMode.java) configuration. - -**Conditional Logic**: Advanced requirement combinations that adapt to current game state and player capabilities. - -**Hierarchical Planning**: Complex requirement trees that enable sophisticated preparation strategies for advanced activities. - -### Conditional Requirements - -The [`ConditionalRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/conditional/) system provides state-dependent requirement activation: - -**Dynamic Requirements**: Requirements that activate or deactivate based on current game conditions, player state, or plugin configuration. - -**Ordered Execution**: The [`OrderedRequirement`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/conditional/OrderedRequirement.java) enables sequential requirement fulfillment when order matters. - -**State-Driven Logic**: Requirements that adapt their behavior based on complex state evaluation using the [`ConditionalRequirementBuilder`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/ConditionalRequirementBuilder.java). - -## Registry Architecture - -### Centralized Management - -The [`RequirementRegistry`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/registry/RequirementRegistry.java) serves as the central coordination point for all requirements within a plugin, providing: - -**Uniqueness Enforcement**: Automatic prevention of duplicate requirements while allowing updates and refinements to existing requirements. - -**Type-Safe Access**: Efficient retrieval of requirements by type, context, and priority level with compile-time safety guarantees. - -**Consistency Guarantees**: Validation of requirement combinations to prevent conflicting or impossible requirement sets. - -### Context Management - -The [`TaskContext`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/TaskContext.java) system enables requirements to specify when they should be fulfilled: - -**PRE_SCHEDULE**: Requirements fulfilled before main plugin execution begins, ensuring all prerequisites are met. - -**POST_SCHEDULE**: Requirements for cleanup and restoration operations after plugin completion. - -**BOTH**: Requirements that must be fulfilled immediately when encountered, for urgent or time-sensitive operations. - -### Priority Framework - -The [`RequirementPriority`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementPriority.java) system enables intelligent requirement prioritization: - -**MANDATORY**: Requirements that must be fulfilled for plugin execution to proceed. - -**RECOMMENDED**: Requirements that significantly improve plugin performance but aren't essential. - -## Advanced Features - -### OR Requirement Modes - -The [`OrRequirementMode`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/OrRequirementMode.java) system provides sophisticated strategies for handling alternative requirements: - - **ANY_COMBINATION**: The total amount can be fulfilled by any combination of items in the OR requirement. For example, if 5 food items are needed, you could have 2 lobsters + 3 swordfish. - **SINGLE_TYPE**: Must fulfill the entire amount with exactly one type of item from the OR requirement. For example, if 5 food items are needed, you must have exactly 5 lobsters OR 5 swordfish, but not a combination. -### Requirement Selection - -The [`RequirementSelector`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSelector.java) provides sophisticated algorithms for choosing optimal requirement combinations when multiple options are available. - -### Collection Systems - -Pre-configured requirement collections eliminate repetitive specification of common requirement patterns: - -**Skill Outfits**: Complete equipment sets for various skills including woodcutting, mining, fishing, and combat activities. - -**Combat Configurations**: Comprehensive combat setups including weapons, armor, consumables, and utility items. - -**Utility Collections**: Common requirement patterns for activities like banking, transportation, and resource gathering. - -## Implementation Patterns - -### Basic Requirements Definition - -Plugin requirements are defined by extending [`PrePostScheduleRequirements`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/PrePostScheduleRequirements.java) and implementing the `initializeRequirements()` method. This method uses the registry to declare all requirements the plugin needs for successful operation. - -### Advanced Requirement Combinations - -Complex activities often require sophisticated requirement combinations that adapt to current game state. The logical requirement system enables these advanced patterns through OR operations, conditional requirements, and hierarchical planning structures. - -### Integration with Task Management - -Requirements integrate seamlessly with the task management system through automatic fulfillment during pre-schedule phases and cleanup during post-schedule phases. This integration ensures that plugins always start with their requirements satisfied and clean up properly when execution completes. - -## Performance and Optimization - -### Efficient Fulfillment - -The requirement fulfillment engine optimizes execution by: - -**Dependency Analysis**: Understanding relationships between requirements to optimize fulfillment order. - -**Resource Sharing**: Identifying opportunities to fulfill multiple requirements with single operations. - -**Path Optimization**: Coordinating location-based requirements to minimize unnecessary travel. - -### Caching and State Management - -The system employs intelligent caching strategies to avoid redundant validation and fulfillment operations, while maintaining accuracy through proper cache invalidation when game state changes. - -### Asynchronous Operations - -Where appropriate, the system utilizes asynchronous fulfillment patterns to improve responsiveness and allow for parallel requirement processing when operations don't interfere with each other. - -## Extension and Customization - -### Custom Requirement Types - -The system is designed for extensibility, enabling plugins to create custom requirement types for specialized needs while integrating with the existing fulfillment infrastructure. - -### Specialized Fulfillment Strategies - -Plugins can provide custom fulfillment strategies for complex or highly specialized requirements that cannot be handled by the standard fulfillment algorithms. - -### Integration Hooks - -Multiple integration points enable custom UI components, specialized validation logic, and plugin-specific optimization strategies while maintaining compatibility with the broader system. - -## Real-World Usage - -### Example Implementations - -Study the [`ExamplePrePostScheduleRequirements`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleRequirements.java) for comprehensive implementation patterns, or examine the GOTR integration in the [GOTR plugin directory](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/runecrafting/gotr/) for production usage. - -### Integration Patterns - -The requirements system integrates with various aspects of the plugin ecosystem, from simple equipment setups to complex multi-phase activities requiring sophisticated resource coordination. - -### Evolution and Enhancement - -The requirement system continues to evolve based on real-world usage patterns, with new requirement types and fulfillment strategies added to address emerging plugin needs while maintaining backward compatibility. - -The Requirements System represents a fundamental advancement in plugin automation, shifting from imperative resource management to declarative requirement specification. This transformation enables more reliable, maintainable, and user-friendly automation while providing the flexibility needed for diverse plugin requirements. diff --git a/docs/scheduler/tasks/task-management-system.md b/docs/scheduler/tasks/task-management-system.md deleted file mode 100644 index af262888588..00000000000 --- a/docs/scheduler/tasks/task-management-system.md +++ /dev/null @@ -1,218 +0,0 @@ -# Task Management System - -## Overview - -The Task Management System forms the infrastructure foundation of the Pre/Post Schedule Tasks architecture. Built around the [`AbstractPrePostScheduleTasks`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/AbstractPrePostScheduleTasks.java) base class, this system provides thread-safe execution services, lifecycle management, and comprehensive error handling for plugin preparation and cleanup operations. - -## Architectural Foundation - -### Design Principles - -The task management infrastructure operates on several core principles that ensure reliable and maintainable plugin automation: - -**Thread Safety**: All operations utilize dedicated executor services to prevent interference with main plugin execution and ensure consistent behavior across concurrent operations. - -**Lifecycle Management**: The system provides clear separation between preparation (pre-schedule) and cleanup (post-schedule) phases, with well-defined hooks for custom plugin-specific operations. - -**Resource Cleanup**: Implements AutoCloseable patterns with guaranteed resource cleanup, preventing memory leaks and ensuring proper shutdown procedures. - -**Error Resilience**: Comprehensive error handling with timeout support, cancellation capabilities, and graceful degradation when operations cannot complete successfully. - -### Infrastructure Components - -The task management system consists of several interconnected components: - -**Executor Services**: Separate thread pools for pre-schedule and post-schedule operations, ensuring isolated execution environments and preventing cross-contamination of operations. - -**Future Management**: CompletableFuture-based task coordination enabling asynchronous execution with proper timeout handling and cancellation support. - -**State Tracking**: The [`TaskExecutionState`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/state/TaskExecutionState.java) system provides comprehensive monitoring of task progress and status information for UI components. - -**Emergency Controls**: Built-in cancellation support through hotkey integration (Ctrl+C) allowing users to abort problematic operations safely. - -## Implementation Architecture - -### Base Class Structure - -The [`AbstractPrePostScheduleTasks`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/AbstractPrePostScheduleTasks.java) provides the foundation that all plugin task managers extend. This base class handles the complex infrastructure concerns while exposing simple extension points for plugin-specific logic. - -Key extension points include: - -- `getPrePostScheduleRequirements()`: Returns the requirements definition for the plugin -- `executeCustomPreScheduleTask()`: Plugin-specific preparation logic beyond requirement fulfillment -- `executeCustomPostScheduleTask()`: Plugin-specific cleanup logic beyond resource management -- `isScheduleMode()`: Detection logic for determining if the plugin is running under scheduler control - -### Execution Flow - -The task execution follows a predictable pattern that ensures consistent behavior across all plugins: - -**Pre-Schedule Phase**: The system first fulfills all requirements defined in the plugin's requirements specification, then executes any custom pre-schedule tasks. This ensures that all necessary resources, equipment, and game state conditions are met before the main plugin logic begins. - -**Main Execution**: After successful preparation, the system invokes the provided callback to start the main plugin execution. The plugin runs with the confidence that all prerequisites have been satisfied. - -**Post-Schedule Phase**: When the plugin completes or is stopped, the system executes custom cleanup tasks followed by requirement-based cleanup operations, ensuring proper resource management and state restoration. - -### Thread Management - -The system utilizes separate thread pools for different phases of execution to ensure proper isolation and prevent interference: - -**Pre-Schedule Executor**: Dedicated to requirement fulfillment and preparation tasks, with appropriate timeout handling to prevent hanging operations. - -**Post-Schedule Executor**: Focused on cleanup and resource management operations, designed to complete even when main execution has failed or been cancelled. - -**Main Thread Integration**: Careful coordination with the main plugin thread to ensure UI updates and game interactions occur safely. - -## Key Features - -### Cancellation Support - -The system provides multiple levels of cancellation support to handle different failure scenarios: - -**User-Initiated Cancellation**: Emergency hotkey support (Ctrl+C) allows users to abort operations that are taking too long or behaving unexpectedly. - -**Timeout-Based Cancellation**: Configurable timeouts prevent operations from hanging indefinitely, with graceful degradation when operations cannot complete within reasonable time limits. - -**Cascade Cancellation**: When one operation fails or is cancelled, the system intelligently determines whether to abort dependent operations or continue with available resources. - -### Error Handling Strategy - -The error handling approach balances robustness with user feedback: - -**Graceful Degradation**: Optional requirements that cannot be fulfilled don't prevent plugin execution, allowing partial preparation when full requirements cannot be met. - -**Detailed Logging**: Comprehensive logging provides developers with detailed information about failure causes while maintaining appropriate log levels for different scenarios. - -**User Communication**: Integration with the UI system ensures users receive clear feedback about preparation status, failures, and recovery options. - -### State Management - -The [`TaskExecutionState`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/state/TaskExecutionState.java) system tracks execution progress and provides information for UI components: - -**Progress Tracking**: Real-time monitoring of requirement fulfillment progress, including individual requirement status and overall completion percentage. - -**Status Information**: Current operation details, time elapsed, estimated completion time, and success/failure indicators. - -**UI Integration**: Seamless integration with overlay components and status panels for rich user feedback. - -**State Reset Capabilities**: The state tracking system provides intelligent reset functionality for handling interruptions and preparing for subsequent task executions, ensuring clean state transitions. - -### Game State Awareness and Initialization - -The task management system requires careful coordination with game state to ensure proper initialization timing: - -**Why Initialization Timing Matters**: Many requirements need access to live game information such as current player location for shop proximity calculations, world position data for location requirements, equipment state for validation, and banking locations for optimal routing strategies. - -**Proper Initialization Pattern**: The system should initialize when `GameState.LOGGED_IN` is reached to ensure all game information is available: - -```java -@Subscribe -public void onGameStateChanged(GameStateChanged event) { - if (event.getGameState() == GameState.LOGGED_IN) { - // Initialize when game information is available - getPrePostScheduleTasks(); - - // Auto-start if in scheduler mode - if (prePostScheduleTasks != null && - prePostScheduleTasks.isScheduleMode() && - !prePostScheduleTasks.isPreTaskComplete()) { - runPreScheduleTasks(); - } - } else if (event.getGameState() == GameState.LOGIN_SCREEN) { - // Reset on logout for fresh initialization - prePostScheduleRequirements = null; - prePostScheduleTasks = null; - } -} -``` - -**Initialization Validation**: The system validates successful initialization before allowing task execution: - -```java -@Override -public AbstractPrePostScheduleTasks getPrePostScheduleTasks() { - if (prePostScheduleRequirements == null || prePostScheduleTasks == null) { - if(Microbot.getClient().getGameState() != GameState.LOGGED_IN) { - log.debug("My Plugin - Cannot return pre/post schedule tasks - not logged in"); - return null; // Return null if not logged in - } - this.prePostScheduleRequirements = new MyPluginRequirements(config); - this.prePostScheduleTasks = new MyPluginTasks(this, keyManager, prePostScheduleRequirements); - if (prePostScheduleRequirements.isInitialized()){log.info("My Plugin PrePostScheduleRequirements initialized:\n{}", prePostScheduleRequirements.getDetailedDisplay());} - } - - // Critical: Validate initialization success - if (!prePostScheduleRequirements.isInitialized()) { - log.error("Failed to initialize requirements system!"); - this.prePostScheduleRequirements = null; - this.prePostScheduleTasks = null; - return null; // Prevent task execution with failed requirements - } - - return this.prePostScheduleTasks; -} -``` - -**Wait-for-Initialization Pattern**: Before executing tasks, plugins should verify that requirements are properly initialized, as some requirements need game data that's only available after login. - -## Integration Patterns - -### Plugin Implementation - -Plugins integrate with the task management system by extending the base class and implementing the required abstract methods. The implementation focuses on plugin-specific logic while delegating infrastructure concerns to the base class. - -Examine the [`ExamplePrePostScheduleTasks`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleTasks.java) for a comprehensive implementation example, or study the GOTR integration in the [GOTR plugin directory](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/runecrafting/gotr/) for production usage patterns. - -### Scheduler Integration - -The task management system integrates seamlessly with the broader scheduler architecture through the [`SchedulablePlugin`](../../../runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/api/SchedulablePlugin.java) interface, providing automatic task execution when plugins are managed by the scheduler. - -### Resource Coordination - -The system coordinates with the requirements framework to ensure efficient resource utilization, preventing conflicts between concurrent operations and optimizing the order of requirement fulfillment. - -## Performance Considerations - -### Resource Efficiency - -The task management system is designed for efficient resource utilization: - -**Lazy Initialization**: Executor services and resources are created only when needed, reducing memory footprint for plugins that don't use task functionality. - -**Resource Pooling**: Thread pools are managed efficiently to prevent resource exhaustion while maintaining responsive behavior. - -**Cleanup Guarantees**: AutoCloseable implementation ensures resources are properly released even when exceptions occur. - -### Scalability - -The architecture supports multiple concurrent task managers without interference, enabling complex plugin ecosystems where multiple automation systems operate simultaneously. - -## Extension and Customization - -### Custom Task Types - -While the base system handles standard preparation and cleanup operations, plugins can extend the functionality through custom task implementations that integrate with the existing infrastructure. - -### Integration Hooks - -The system provides multiple integration points for advanced customization, including custom requirement types, specialized fulfillment strategies, and plugin-specific UI components. - -### Future Enhancements - -The architecture is designed for extensibility, with clear separation of concerns enabling future enhancements such as task prioritization, dependency management, and performance optimization without breaking existing implementations. - -## Implementation Guidelines - -### Development Approach - -When implementing task managers, focus on clear separation between requirement definition and custom logic. Use the requirements system for standard operations like equipment setup and inventory management, reserving custom tasks for plugin-specific operations that cannot be expressed as requirements. - -### Error Handling - -Implement robust error handling in custom task methods, providing clear error messages and appropriate recovery strategies. The base infrastructure handles many error scenarios, but plugin-specific operations require careful consideration of failure modes. - -### Testing and Validation - -The task management system provides multiple hooks for testing and validation, enabling comprehensive testing of both requirement fulfillment and custom task operations in isolation. - -The Task Management System provides the reliable foundation necessary for sophisticated plugin automation while maintaining the flexibility needed for diverse plugin requirements. By handling the complex infrastructure concerns, it enables plugin developers to focus on their core functionality while benefiting from standardized, robust preparation and cleanup procedures. diff --git a/docs/scheduler/user-guide.md b/docs/scheduler/user-guide.md deleted file mode 100644 index eceed835689..00000000000 --- a/docs/scheduler/user-guide.md +++ /dev/null @@ -1,469 +0,0 @@ -# Scheduler Plugin User Guide - -## Introduction - -The Plugin Scheduler is a sophisticated system that allows you to automatically schedule and manage plugins based on various conditions. This guide focuses on how to use the scheduler's user interface to set up plugin scheduling plans, including defining start and stop conditions, understanding plugin priorities, and configuring plugin behavior. - -## The Scheduler Interface - -The scheduler interface is divided into several main components: - -1. **Schedule Table**: Displays all scheduled plugins with their current status, configurations, and next run times -2. **Schedule Form**: Allows you to add new scheduled plugins -3. **Properties Panel**: Lets you modify settings for existing scheduled plugins -4. **Start Conditions Panel**: Configures when a plugin should start running -5. **Stop Conditions Panel**: Configures when a plugin should stop running - -![Scheduler Window Overview](../img/scheduler-overview.png) - -## Adding a New Plugin Schedule - -To create a new plugin schedule: - -1. Navigate to the "New Schedule" tab in the scheduler window -2. Select a plugin from the dropdown menu -3. Choose a scheduling method from the "Time Condition" dropdown: - - **Run Default**: Makes the plugin a default option (priority 0) - - **Run at Specific Time**: Runs the plugin once at a specific date/time - - **Run at Interval**: Runs the plugin at regular intervals - - **Run in Time Window**: Runs the plugin during specific hours of the day - - **Run on Day of Week**: Runs the plugin on specific days of the week -4. Configure the time condition options based on your selection -5. Set additional options: - - **Allow Random Scheduling**: Lets the scheduler randomly select this plugin when multiple are due - - **Requires time-based stop condition**: Requires you to add a time condition to stop the plugin - - **Allow continue**: Allows the plugin to resume automatically after being interrupted, maintaining progress - - **Priority**: Sets the plugin's priority level (0 = default plugin) -6. Click "Add Schedule" to create the schedule - -## Understanding Default vs. Non-Default Plugins - -### Default Plugins - -Default plugins serve as your "background" or "fallback" activities that run when nothing else is scheduled. Think of them as what your character does during downtime. - -**Characteristics:** -- Always have **priority 0** (automatically enforced by the scheduler) -- Run only when no other plugins are scheduled or eligible to run -- Typically use a very short interval check (often 1 second) to quickly start when needed -- Are displayed with a teal background color and ⭐ star icon in the schedule table - -**To mark a plugin as default:** -- Check the "Set as default plugin" checkbox in the schedule form, or -- Set its priority to 0 (the system will recognize it as a default plugin) - -**Example use cases:** -- AFK training activities (e.g., NMZ, splashing) -- Simple repetitive tasks that should run when nothing more important is active -- "Maintenance" plugins that handle routine tasks during downtime - -### Non-Default Plugins - -Non-default plugins are your primary scheduled activities that run according to specific conditions. - -**Characteristics:** -- Have a priority of 1 or higher (lower number = higher priority) -- Run according to their specific start conditions -- Will always take precedence over default plugins when their conditions are met -- Can interrupt default plugins (unless the default plugin has "Allow Continue" enabled) - -**Example use cases:** -- Your main skilling activities (fishing, mining, etc.) -- Time-specific activities (e.g., farm runs, daily challenges) -- Condition-based activities (e.g., "Bank items when inventory is full") - -## Defining Start and Stop Conditions - -The power of the Plugin Scheduler comes from its ability to create sophisticated start and stop conditions for your plugins. This allows for highly automated and intelligent plugin scheduling. - -For detailed instructions on creating and configuring various condition types (Time, Skill, Resource, Location), see the [Defining Conditions guide](defining-conditions.md). - -### Start Conditions - -Start conditions determine when a plugin is eligible to begin running. A plugin will start when all its start conditions are met (or any, depending on your logical configuration). - -**Basic Configuration:** - -1. Select your plugin in the schedule table -2. Navigate to the "Start Conditions" tab -3. Click "Add New Condition" -4. Select a condition category (Time, Skill, Resource, Location, etc.) -5. Choose a specific condition type within that category -6. Configure the parameters for your chosen condition -7. Click "Add" to add the condition to your logical structure - -**Example Start Conditions:** - -- **Time-based:** "Start at 8:00 PM every day" -- **Resource-based:** "Start when inventory is empty" -- **Location-based:** "Start when player is at the Grand Exchange" -- **Skill-based:** "Start when Mining reaches level 70" - -### Stop Conditions - -Stop conditions determine when a running plugin should stop. They're evaluated continuously while the plugin is running. - -**Basic Configuration:** - -1. Select your plugin in the schedule table -2. Navigate to the "Stop Conditions" tab -3. Click "Add New Condition" -4. Select a condition category -5. Choose a specific condition type -6. Configure the parameters -7. Click "Add" to add the condition - -**Example Stop Conditions:** - -- **Time-based:** "Stop after 2 hours of running" -- **Resource-based:** "Stop when inventory is full" or "Stop after collecting 1000 items" -- **Location-based:** "Stop when reaching Lumbridge" -- **Skill-based:** "Stop when gaining 50,000 XP in Fishing" - -### Building Complex Logical Structures - -You can combine multiple conditions using logical operators to create sophisticated rules: - -1. **AND Logic (ALL conditions must be met):** - - Click on the root condition node in the tree view - - Click "Convert to AND" - - All child conditions must be satisfied for the overall condition to be met - -2. **OR Logic (ANY condition can be met):** - - Click on the root condition node - - Click "Convert to OR" (default) - - Any child condition being satisfied will satisfy the overall condition - -3. **Nesting AND/OR Groups:** - - Select multiple conditions in the tree - - Click "Group AND" or "Group OR" - - This creates a sub-group with its own logical relationship - -4. **NOT Logic:** - - Select a condition - - Click "Negate" - - The condition's result will be inverted - -**Example Complex Condition:** -"Start the plugin when (it's between 8 PM and midnight) AND (player is in the Mining Guild OR at Al Kharid mines) AND (inventory is empty)" - -## The "Allow Continue" Setting Explained - -The "Allow Continue" option determines what happens when a plugin is interrupted by a higher-priority plugin and later has a chance to resume: - -- **When enabled**: The interrupted plugin will continue running immediately after the higher-priority plugin finishes, without resetting its start and stop conditions. This preserves all progress made toward stop conditions and doesn't require start conditions to be re-evaluated. -- **When disabled**: The interrupted plugin will not automatically resume after being interrupted. It will need to be triggered again from scratch, with all start and stop conditions reset. - -**How "Allow Continue" Works in Detail**: - -1. **State Preservation**: - - With "Allow Continue" enabled, a plugin that gets interrupted preserves its full state, including: - - Progress toward any stop conditions (e.g., items collected, time elapsed) - - Internal state variables and script position - - UI components and overlays - - This means the plugin can pick up exactly where it left off, which is ideal for complex, stateful plugins - -2. **Resumption Logic**: - - When a higher-priority plugin finishes: - - If the interrupted plugin had "Allow Continue" enabled, it immediately resumes - - If the interrupted plugin had "Allow Continue" disabled, it goes back into the pool of eligible plugins - - For default plugins with "Allow Continue" disabled, they must compete again with other default plugins based on run count - -3. **Default Plugin Considerations**: - - "Allow Continue" is especially important for default plugins (priority 0) that you want to resume predictably - - Without "Allow Continue", your default plugin might not be the one selected after an interruption - - This can lead to unpredictable switching between default plugins, which might not be desirable - -For a comprehensive explanation of this feature, see the [Allow Continue Feature Guide](allow-continue-feature.md). - -**Example Scenarios:** - -1. **Allow Continue = ON** - - You're running a Woodcutting plugin (priority 0) - - A Banking plugin (priority 2) interrupts it - - After banking is done, the Woodcutting plugin automatically resumes with all progress intact - - Perfect for plugins that should pick up where they left off after being interrupted - -2. **Allow Continue = OFF** - - You're running a Fishing plugin (priority 0) - - A Cooking plugin (priority 2) interrupts it - - After cooking is done, the Fishing plugin does not automatically resume - - The scheduler will select among eligible default plugins based on run count - - Best for tasks that should fully restart from the beginning when interrupted - -## What Happens When a Plugin Has No Stop Conditions - -When a schedulable plugin has no plugin-based stop conditions: - -1. The plugin will run indefinitely until: - - You manually stop it through the UI - - A higher-priority plugin interrupts it (after which it may resume if "Allow Continue" is enabled) - - The plugin's internal logic determines it should stop - -2. The scheduler UI will display a warning icon next to plugins without stop conditions, reminding you that they may run indefinitely. - -3. If "Requires time-based stop condition" is checked in the settings, the scheduler will display a prompt when you try to add a schedule without stop conditions. - -**Best Practices:** - -- **Always include at least one stop condition** for every plugin schedule -- **Add a time-based safety condition** (e.g., "stop after 2 hours") even if you have other stop conditions -- **Use LockCondition for critical operations** to prevent interruption during important tasks -- **Combine multiple stop conditions** for more sophisticated control, such as: - - "Stop after collecting 1000 items OR after 3 hours" (whichever comes first) - - "Stop when inventory is full AND we're in a safe area" - -**Safety Mechanism:** -The scheduler includes a built-in watchdog that monitors plugins for signs they might be stuck. If a plugin runs for an extended period without showing progress, the system can detect this and provide warnings. - -## Working with the Schedule Table - -The schedule table displays all configured plugins and their current status: - -- **Plugin**: The name of the plugin (⭐ indicates default plugins, â–ķ indicates currently running) -- **Schedule**: When the plugin is scheduled to run -- **Next Run**: When the plugin will run next -- **Start Conditions**: Summary of conditions that trigger the plugin to start -- **Stop Conditions**: Summary of conditions that will stop the plugin -- **Priority**: The plugin's priority level (0 = default) -- **Enabled**: Whether the plugin is enabled in the scheduler -- **Runs**: How many times the plugin has been executed - -### Row Colors and Indicators - -- **Purple background**: Currently running plugin -- **Amber background**: Next plugin scheduled to run -- **Teal background**: Default plugin -- **Green indicators**: Conditions are satisfied -- **Red indicators**: Conditions are not satisfied -- **Strikethrough text**: Plugin is disabled - -## Managing Existing Schedules - -To modify an existing schedule: - -1. Click on it in the schedule table -2. Use the "Plugin Properties" tab to adjust: - - Enabled status - - Default plugin setting - - Priority - - Random scheduling - - Time-based stop condition requirement - - Allow continue setting -3. Use the "Start Conditions" and "Stop Conditions" tabs to modify conditions -4. Click "Save Changes" to apply your modifications - -## Tips and Best Practices - -1. **Set appropriate priorities**: - - Higher priority plugins (higher numbers) run before lower priority ones - - Use priorities to establish a clear hierarchy of tasks - -2. **Use time windows effectively**: - - Consider using time windows to run different plugins at different times of day - - Time windows can help avoid detection by making your play patterns more natural - -3. **Combine condition types**: - - Mix time, resource, and location conditions for sophisticated automation - - Example: Run a mining plugin only when inventory is empty AND at certain times of day - -4. **Plan for interruptions**: - - Enable "Allow Continue" for tasks that should automatically resume after being interrupted - - Create robust stop conditions that handle unexpected situations - -5. **Monitor run statistics**: - - Use the statistics in the Properties panel to track plugin performance - - Look for unusually short runs that might indicate problems - -## Common Scenarios and Real-World Examples - -Here are some practical examples of how to set up the scheduler for common gameplay situations. Use these as templates for your own scheduling plans. - -### Scenario 1: AFK Training with Default Plugin - -**Goal**: Train a skill passively when you're not doing anything else - -**Solution**: -1. Add your AFK training plugin (e.g., NMZ, splashing) with "Run Default" time condition -2. Ensure "Set as default plugin" is checked (priority will be set to 0) -3. Add a safety stop condition like "Stop after 5 hours" to prevent running indefinitely -4. Consider adding "Stop when XP gained reaches 100,000" as an additional stop condition - -**Example Setup:** -- Plugin: AFKCombatPlugin -- Priority: 0 (Default) -- Start Conditions: None (runs as default) -- Stop Conditions: OR(TimeCondition(5 hours), SkillXPCondition(100,000 XP)) - -### Scenario 2: Resource Gathering and Processing Cycle - -**Goal**: Alternate between gathering resources and processing them (e.g., fishing and cooking) - -**Solution**: -1. Create a schedule for fishing plugin: - - Priority: 2 - - Start Condition: Inventory is empty OR time since last run > 15 minutes - - Stop Condition: Inventory is full (28 items) - -2. Create a schedule for cooking plugin: - - Priority: 1 (higher priority than fishing) - - Start Condition: Inventory has raw fish - - Stop Condition: No raw fish in inventory - -This creates a natural cycle: When inventory fills with fish, the cooking plugin (higher priority) takes over. When cooking is done, the fishing plugin starts again. - -### Scenario 3: Time-Limited Daily Activities - -**Goal**: Run specific plugins at certain times of day, like farm runs every few hours - -**Solution**: -1. Create a schedule with "Run in Time Window" or "Run at Interval" - - For farm runs: "Run at Interval" of 80 minutes - - For daily tasks: "Run in Time Window" (e.g., 6:00 PM to 7:00 PM) - -2. Add specific stop conditions: - - Time-based: "Stop after 15 minutes" (prevents running too long) - - Completion-based: "Stop when all farming patches are harvested" - -3. Enable "Allow Continue" if this activity should be able to resume after interruptions - -**Example Setup:** -- Plugin: FarmRunPlugin -- Priority: 1 -- Start Conditions: TimeCondition(Interval: 80 minutes) -- Stop Conditions: OR(TimeCondition(15 minutes), CompletionCondition(all patches harvested)) - -### Scenario 4: Location-Based Task Switching - -**Goal**: Run different plugins based on player location - -**Solution**: -1. Create a mining plugin: - - Priority: 2 - - Start Condition: In mining area AND inventory not full - - Stop Condition: Inventory full OR not in mining area - -2. Create a banking plugin: - - Priority: 1 (higher priority) - - Start Condition: Inventory full AND near bank - - Stop Condition: Inventory empty - -This creates a smart workflow where your character will mine until inventory is full, then prioritize banking when near a bank. - -### Scenario 5: Skill Goal Achievement - -**Goal**: Train a skill until reaching a specific level, then switch to another activity - -**Solution**: -1. Create a woodcutting plugin: - - Priority: 2 - - Start Condition: Woodcutting level < 70 - - Stop Condition: Woodcutting level reaches 70 - -2. Create a fishing plugin: - - Priority: 2 - - Start Condition: Woodcutting level >= 70 AND Fishing level < 70 - - Stop Condition: Fishing level reaches 70 - -This creates a progression plan where the scheduler automatically moves from one skill goal to the next. - -### Scenario 6: Setting Up a Cycle of Default Plugins - -**Goal**: Create a cycle where multiple default plugins (priority 0) take turns running, ensuring each runs once before cycling through again. - -**Solution**: - -1. Set up multiple plugins with priority 0 (default plugins): - - Plugin 1: Priority 0, with user-defined stop condition - - Plugin 2: Priority 0, with user-defined stop condition - - Plugin 3: Priority 0, with user-defined stop condition - -2. Ensure each plugin has a proper user stop condition: - - Time-based: "Run for 30-35 minutes" - - Skill XP-based: "Stop after gaining X experience" - - Resource-based: "Stop after collecting Y items" - -3. Enable "Allow Random Scheduling" for all plugins. - -When all plugins are set to priority 0 and multiple ones are due to run at the same time, the scheduler will automatically select the one with the **lowest run count**. This ensures each plugin gets a fair chance to run, and the system will cycle through all plugins before repeating. - -**Example Setup - Cycling through 3 Default Plugins:** - -- Plugin 1: Woodcutting - - Priority: 0 (Default) - - User Start Condition: default (interval condition 1sec) or if you want to delay any other time condition - - User Stop Condition: TimeCondition(30-35 minutes) - - Allow Random Scheduling: Enabled - -- Plugin 2: Fishing - - Priority: 0 (Default) - - User Start Condition: default (interval condition 1sec) or if you want to delay any other time condition - - User Stop Condition: TimeCondition(30-35 minutes) - - Allow Random Scheduling: Enabled - -- Plugin 3: Mining - - Priority: 0 (Default) - - User Start Condition: default (interval condition 1sec) or if you want to delay any other time condition - - User Stop Condition: TimeCondition(30-35 minutes) - - Allow Random Scheduling: Enabled - -**Higher Priority Tasks:** - -- Plugin 4: Birdhouse Run - - Priority: 1 - - User Start Condition: TimeCondition(Interval: 50 minutes) - - User Stop Condition: CompletionCondition(all birdhouses checked) - -- Plugin 5: Herb Run - - Priority: 1 - - Start Condition: TimeCondition(Interval: 80 minutes) - - Stop Condition: CompletionCondition(all herbs harvested) - -In this setup: - -- Plugins 4 and 5 will always interrupt and take precedence over default plugins when their conditions are met -- When no higher-priority plugin is running, the scheduler will cycle through Plugins 1, 2, and 3, always selecting the one with the lowest run count -- Each default plugin will run once before any of them runs a second time - -### Scenario 7: Advanced Priority-Based Task Chain with Herb Running - -**Goal**: Set up a hierarchy of tasks with herb running as a priority task that interrupts default activities. - -**Solution**: - -1. Create a Herb Runner plugin schedule: - - Priority: 10 (high priority) - - Start Condition: TimeCondition(Interval: 80 minutes) - - Stop Condition: None (uses reportFinished to signal completion) - - Allow Continue: Not needed (runs completely each time) - -2. Create a Tree Runner plugin schedule: - - Priority: 5 (middle priority) - - Start Condition: TimeCondition(Interval: 8 hours) - - Stop Condition: None (uses reportFinished to signal completion) - - Allow Continue: Not needed (runs completely each time) - -3. Create a default NMZ plugin schedule: - - Priority: 0 (default plugin) - - Start Condition: TimeWindowCondition(10PM-8AM or your preferred hours) - - Stop Condition: Optional TimeCondition(run for maximum 6 hours) - - Allow Continue: Enabled (important for preserving state when interrupted) - -This setup creates a priority-based chain where: - -- The Herb Runner has highest priority and will interrupt any other active plugin every 80 minutes -- The Tree Runner has middle priority and will interrupt only the NMZ plugin (but not Herb Runner) -- The NMZ plugin serves as the default activity that runs during the specified time window -- When the Herb Runner or Tree Runner completes its task, NMZ will automatically resume with its state preserved because "Allow Continue" is enabled - -## The "Allow Continue" Setting: Deep Dive - -The "Allow Continue" option is particularly important when using default plugins in a cycle or when higher-priority plugins might interrupt your default activities. Let's look more deeply at this feature: - -- **Behavior with Default Plugins:** - - Default plugins are meant to run in the background with no specific start conditions. - - If a default plugin is interrupted by a higher-priority plugin and "Allow Continue" is ON, it will resume immediately after the higher-priority plugin finishes. - - This is useful for preserving the state of long-running default activities. - -- **Behavior with Non-Default Plugins:** - - For non-default plugins, "Allow Continue" works similarly but consider diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java index bf15d0c83d8..25132fbb46a 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java @@ -32,13 +32,9 @@ import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginInstantiationException; import net.runelite.client.plugins.PluginManager; -import net.runelite.client.plugins.loottracker.LootTrackerItem; -import net.runelite.client.plugins.loottracker.LootTrackerPlugin; import net.runelite.client.plugins.loottracker.LootTrackerRecord; import net.runelite.client.plugins.microbot.configs.SpecialAttackConfigs; import net.runelite.client.plugins.microbot.pouch.PouchScript; -import net.runelite.client.plugins.microbot.util.cache.Rs2VarPlayerCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2VarbitCache; import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; import net.runelite.client.plugins.microbot.util.item.Rs2ItemManager; import net.runelite.client.plugins.microbot.util.menu.NewMenuEntry; @@ -192,10 +188,6 @@ public class Microbot { @Getter private static Rs2ItemManager rs2ItemManager = new Rs2ItemManager(); - @Setter - @Getter - public static boolean isRs2CacheEnabled = false; - @Inject @Getter private static Rs2PlayerCache rs2PlayerCache; @@ -223,16 +215,10 @@ public static boolean isDebug() { } public static int getVarbitValue(@Varbit int varbit) { - if (isRs2CacheEnabled()) { - return Rs2VarbitCache.getVarbitValue(varbit); - } return rs2PlayerCache.getVarbitValue(varbit); } public static int getVarbitPlayerValue(@Varp int varpId) { - if (isRs2CacheEnabled()) { - return Rs2VarPlayerCache.getVarPlayerValue(varpId); - } return rs2PlayerCache.getVarpValue(varpId); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotConfig.java index 28cdf25561a..fba8721ef0c 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotConfig.java @@ -124,18 +124,6 @@ default boolean enableMenuEntryLogging() { return false; } - String keyEnableCache = "enableRs2Cache"; - @ConfigItem( - keyName = keyEnableCache, - name = "Enable Microbot Cache", - description = "This will cache ingame entities (npcs, objects,...) to improve performance", - position = 0, - section = cacheSection - ) - default boolean isRs2CacheEnabled() { - return false; - } - @AllArgsConstructor enum GameChatLogLevel { ERROR("Error", Level.ERROR), diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotOverlay.java index a124c51c711..8a3be400409 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotOverlay.java @@ -5,22 +5,11 @@ import net.runelite.api.Point; import net.runelite.api.coords.LocalPoint; import net.runelite.api.coords.WorldPoint; -import net.runelite.api.gameval.InterfaceID; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.cache.Rs2CacheManager; -import net.runelite.client.plugins.microbot.util.cache.Rs2ObjectCache; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2CacheLoggingUtils; -import net.runelite.client.plugins.microbot.util.grandexchange.Rs2GrandExchange; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.tile.Rs2Tile; -import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; -import net.runelite.client.ui.FontManager; import net.runelite.client.ui.overlay.OverlayPanel; import net.runelite.client.ui.overlay.OverlayPosition; import net.runelite.client.ui.overlay.OverlayUtil; -import net.runelite.client.ui.overlay.components.ButtonComponent; -import net.runelite.client.ui.overlay.components.LineComponent; -import net.runelite.client.ui.overlay.components.TitleComponent; import javax.annotation.Nullable; import javax.inject.Inject; @@ -30,8 +19,6 @@ public class MicrobotOverlay extends OverlayPanel { MicrobotPlugin plugin; MicrobotConfig config; - public final ButtonComponent cacheButton; - public final ButtonComponent logCacheButton; @Inject MicrobotOverlay(MicrobotPlugin plugin, MicrobotConfig config) { @@ -39,42 +26,6 @@ public class MicrobotOverlay extends OverlayPanel { setPosition(OverlayPosition.TOP_RIGHT); this.plugin = plugin; this.config = config; - - // Initialize cache management button - cacheButton = new ButtonComponent("Clear Caches"); - cacheButton.setPreferredSize(new Dimension(120, 25)); - cacheButton.setParentOverlay(this); - cacheButton.setFont(FontManager.getRunescapeBoldFont()); - cacheButton.setOnClick(() -> { - try { - Rs2CacheManager.invalidateAllCaches(false); - Rs2CacheManager.triggerSceneScansForAllCaches(); // Repopulate caches immediately - //Microbot.getClientThread().runOnClientThreadOptional( - // ()-> {Microbot.openPopUp("Cache Manager", String.format("Cleared Cache:

%s", "All caches have been invalidated!")); - // return true;} - //); // causes Exception java.lang.IllegalStateException: component does not exist - } catch (Exception e) { - Microbot.log("Cache Manager: All caches have been invalidated and repopulated!"); - } - }); - - // Initialize cache logging button - logCacheButton = new ButtonComponent("Log to Files"); - logCacheButton.setPreferredSize(new Dimension(60, 25)); - logCacheButton.setParentOverlay(this); - logCacheButton.setFont(FontManager.getRunescapeBoldFont()); - logCacheButton.setOnClick(() -> { - // Run on separate thread to avoid blocking UI - new Thread(() -> { - try { - Rs2CacheLoggingUtils.logAllCachesToFiles(); - //Microbot.openPopUp("Cache Logger", String.format("Cache Logging:

%s", "All cache states dumped to files successfully!"));// causes Exception - } catch (Exception e) { - // Fallback to console if popup fails - Microbot.log(" \"All cache states dumped to files successfully!\"" + e.getMessage()); - } - }).start(); - }); } @Override @@ -84,247 +35,10 @@ public Dimension render(Graphics2D graphics) { for (Map.Entry dangerousTile : Rs2Tile.getDangerousGraphicsObjectTiles().entrySet()) { drawTile(graphics, dangerousTile.getKey(), Color.RED, dangerousTile.getValue().toString()); } - - // Show cache information if enabled in config and not hidden by overlapping widgets - boolean shouldShowCache = config.showCacheInfo(); - - // Always check for actual widget overlap with our render position - if (shouldShowCache) { - Rectangle estimatedCacheBounds = estimateCacheInfoBounds(); - if (estimatedCacheBounds != null) { - // Convert estimatedCacheBounds (panel/canvas) to canvas coordinates if needed - // Pass as canvas coordinates to plugin.hasWidgetOverlapWithBounds - shouldShowCache = !plugin.hasWidgetOverlapWithBounds(estimatedCacheBounds); - } - shouldShowCache = shouldShowCache && Microbot.isLoggedIn() && !Rs2Bank.isOpen() && !Rs2Widget.isWidgetVisible(InterfaceID.Worldmap.CLOSE) && !Rs2GrandExchange.isOpen(); - } - - if (shouldShowCache) { - panelComponent.getChildren().add(TitleComponent.builder() - .text("Cache Manager") - .color(Color.CYAN) - .build()); - - panelComponent.getChildren().add(LineComponent.builder().build()); - - panelComponent.getChildren().add(cacheButton); - panelComponent.getChildren().add(logCacheButton); - - // Only hook mouse listeners if visible - cacheButton.hookMouseListener(); - logCacheButton.hookMouseListener(); - // Render cache statistics tooltip when hovering over the clear cache button - if (cacheButton.isHovered()) { - renderCacheStatsTooltip(graphics); - } - - // Render logging info tooltip when hovering over the log cache button - if (logCacheButton.isHovered()) { - renderLogCacheTooltip(graphics); - } - } else { - // Unhook mouse listeners if not visible - cacheButton.unhookMouseListener(); - logCacheButton.unhookMouseListener(); - } - return super.render(graphics); } - /** - * Estimates the bounds where cache information would be rendered - * This helps determine potential overlap before actually rendering - */ - private Rectangle estimateCacheInfoBounds() { - try { - // Get the current overlay position and size - Rectangle currentBounds = this.getBounds(); - - // If we don't have bounds yet, use preferred location and calculate estimated size - if (currentBounds == null || (currentBounds.width == 0 && currentBounds.height == 0)) { - java.awt.Point preferredLocation = this.getPreferredLocation(); - if (preferredLocation == null) { - // Use default DYNAMIC position (top-left area) - preferredLocation = new java.awt.Point(10, 10); - } - - // Estimate cache info panel size based on typical components - // Title: ~150x20, Button: ~120x25, spacing: ~5-10px - int estimatedWidth = 200; // panelComponent preferred width - int estimatedHeight = 85; // Title + separator + 2 buttons + padding - - return new Rectangle(preferredLocation.x, preferredLocation.y, - estimatedWidth, estimatedHeight); - } - - // If we have existing bounds, estimate where cache info would appear within them - // Cache info appears after dangerous tiles, so add some offset - int cacheStartY = currentBounds.y + 10; // Some offset for dangerous tiles rendering - int cacheHeight = 60; // Estimated height for cache components - - return new Rectangle(currentBounds.x, cacheStartY, - currentBounds.width, cacheHeight); - - } catch (Exception e) { - // Fallback: return a small default area - return new Rectangle(10, 10, 200, 60); - } - } - - /** - * Renders cache statistics as a tooltip box when hovering over the button - */ - private void renderCacheStatsTooltip(Graphics2D graphics) { - try { - // Set smaller font for tooltip - Font originalFont = graphics.getFont(); - Font tooltipFont = new Font(Font.SANS_SERIF, Font.PLAIN, 10); - graphics.setFont(tooltipFont); - - // Get cache statistics - String cacheStats = Rs2CacheManager.getAllCacheStatisticsString(); - - // Get object type statistics - String objectTypeStats = ""; - try { - objectTypeStats = Rs2ObjectCache.getObjectTypeStatistics(); - } catch (Exception e) { - objectTypeStats = "Object stats unavailable"; - } - - // Combine cache stats with object type stats - String[] cacheLines = cacheStats.split("\n"); - String[] allLines = new String[cacheLines.length + 1]; - System.arraycopy(cacheLines, 0, allLines, 0, cacheLines.length); - allLines[cacheLines.length] = objectTypeStats; - - // Calculate tooltip position (next to the button) - Rectangle buttonBounds = cacheButton.getBounds(); - int tooltipX = buttonBounds.x + buttonBounds.width + 10; - int tooltipY = buttonBounds.y; - - // Calculate tooltip dimensions - FontMetrics metrics = graphics.getFontMetrics(); - int maxWidth = 0; - int totalHeight = 0; - - for (String line : allLines) { - if (!line.trim().isEmpty()) { - int lineWidth = metrics.stringWidth(line.trim()); - maxWidth = Math.max(maxWidth, lineWidth); - totalHeight += metrics.getHeight(); - } - } - - int padding = 6; - int backgroundWidth = maxWidth + (padding * 2); - int backgroundHeight = totalHeight + (padding * 2); - - // Draw semi-transparent background - Color backgroundColor = new Color(0, 0, 0, 180); - graphics.setColor(backgroundColor); - graphics.fillRect(tooltipX, tooltipY, backgroundWidth, backgroundHeight); - - // Draw border - graphics.setColor(Color.CYAN); - graphics.drawRect(tooltipX, tooltipY, backgroundWidth, backgroundHeight); - - // Render cache statistics text - graphics.setColor(Color.WHITE); - int lineY = tooltipY + metrics.getAscent() + padding; - - for (String line : allLines) { - if (!line.trim().isEmpty()) { - graphics.drawString(line.trim(), tooltipX + padding, lineY); - lineY += metrics.getHeight(); - } - } - - // Restore original font - graphics.setFont(originalFont); - - } catch (Exception e) { - // Silent fail for overlay rendering - } - } - - /** - * Renders logging information as a tooltip box when hovering over the log cache button - */ - private void renderLogCacheTooltip(Graphics2D graphics) { - try { - // Set smaller font for tooltip - Font originalFont = graphics.getFont(); - Font tooltipFont = new Font(Font.SANS_SERIF, Font.PLAIN, 10); - graphics.setFont(tooltipFont); - - // Define tooltip content - String[] lines = { - "Log Cache States to Files", - "", - "â€Ē Saves all cache data to log files", - "â€Ē Files saved in ~/.runelite/microbot-plugins/cache/", - "â€Ē Includes: NPCs, Objects, Ground Items,", - " Skills, Varbits, VarPlayers, Quests", - "â€Ē No console output (file only)", - "â€Ē Useful for analysis and debugging" - }; - - // Calculate tooltip position (next to the button) - Rectangle buttonBounds = logCacheButton.getBounds(); - int tooltipX = buttonBounds.x + buttonBounds.width + 10; - int tooltipY = buttonBounds.y; - - // Calculate tooltip dimensions - FontMetrics metrics = graphics.getFontMetrics(); - int maxWidth = 0; - int totalHeight = 0; - - for (String line : lines) { - if (!line.trim().isEmpty()) { - int lineWidth = metrics.stringWidth(line.trim()); - maxWidth = Math.max(maxWidth, lineWidth); - totalHeight += metrics.getHeight(); - } else { - totalHeight += metrics.getHeight() / 2; // Half height for empty lines - } - } - - int padding = 6; - int backgroundWidth = maxWidth + (padding * 2); - int backgroundHeight = totalHeight + (padding * 2); - - // Draw semi-transparent background - Color backgroundColor = new Color(0, 0, 0, 180); - graphics.setColor(backgroundColor); - graphics.fillRect(tooltipX, tooltipY, backgroundWidth, backgroundHeight); - - // Draw border - graphics.setColor(Color.GREEN); - graphics.drawRect(tooltipX, tooltipY, backgroundWidth, backgroundHeight); - - // Render tooltip text - graphics.setColor(Color.WHITE); - int lineY = tooltipY + metrics.getAscent() + padding; - - for (String line : lines) { - if (!line.trim().isEmpty()) { - graphics.drawString(line.trim(), tooltipX + padding, lineY); - lineY += metrics.getHeight(); - } else { - lineY += metrics.getHeight() / 2; // Half height for empty lines - } - } - - // Restore original font - graphics.setFont(originalFont); - - } catch (Exception e) { - // Silent fail for overlay rendering - } - } - private void drawTile(Graphics2D graphics, WorldPoint point, Color color, @Nullable String label) { WorldPoint playerLocation = Rs2Player.getWorldLocation(); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java index f3901214b88..06341e932b5 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java @@ -16,12 +16,12 @@ import net.runelite.client.events.RuneScapeProfileChanged; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.plugins.microbot.api.grounditem.Rs2GroundItemCache; import net.runelite.client.plugins.microbot.pouch.PouchOverlay; import net.runelite.client.plugins.microbot.ui.MicrobotPluginConfigurationDescriptor; import net.runelite.client.plugins.microbot.ui.MicrobotPluginListPanel; import net.runelite.client.plugins.microbot.ui.MicrobotTopLevelConfigPanel; import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.cache.*; import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; import net.runelite.client.plugins.microbot.util.inventory.Rs2Gembag; import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; @@ -147,8 +147,6 @@ protected void startUp() throws AWTException microbotConfig.onlyMicrobotLogging() ); - Microbot.setRs2CacheEnabled(microbotConfig.isRs2CacheEnabled()); - Microbot.pauseAllScripts.set(false); MicrobotPluginListPanel pluginListPanel = pluginListPanelProvider.get(); @@ -176,17 +174,13 @@ protected void startUp() throws AWTException Microbot.getPouchScript().startUp(); - // Initialize the cache system - if (microbotConfig.isRs2CacheEnabled()) { - initializeCacheSystem(); - } + Rs2GroundItemCache.registerEventBus(); if (overlayManager != null) { overlayManager.add(microbotOverlay); overlayManager.add(gembagOverlay); overlayManager.add(pouchOverlay); - microbotOverlay.cacheButton.hookMouseListener(); } } @@ -195,13 +189,9 @@ protected void shutDown() overlayManager.remove(microbotOverlay); overlayManager.remove(gembagOverlay); overlayManager.remove(pouchOverlay); - microbotOverlay.cacheButton.unhookMouseListener(); clientToolbar.removeNavigation(navButton); if (gameChatAppender.isStarted()) gameChatAppender.stop(); microbotVersionChecker.shutdown(); - - // Shutdown the cache system - shutdownCacheSystem(); } @@ -221,12 +211,6 @@ public void onRuneScapeProfileChanged(RuneScapeProfileChanged event) ) { log.info("\nReceived RuneScape profile change event from '{}' to '{}'", oldProfile, newProfile); - if (microbotConfig.isRs2CacheEnabled()) { - // Use async profile change to avoid blocking client thread - Rs2CacheManager.handleProfileChange(newProfile, oldProfile); - log.info("Initiated async profile change from '{}' to '{}'", oldProfile, newProfile); - } - return; } } @@ -235,11 +219,7 @@ public void onRuneScapeProfileChanged(RuneScapeProfileChanged event) public void onItemContainerChanged(ItemContainerChanged event) { Microbot.getPouchScript().onItemContainerChanged(event); - if (event.getContainerId() == InventoryID.BANK) - { - Rs2Bank.updateLocalBank(event); - } - else if (event.getContainerId() == InventoryID.INV) + if (event.getContainerId() == InventoryID.INV) { Rs2Inventory.storeInventoryItemsInMemory(event); } @@ -307,9 +287,6 @@ public void onGameStateChanged(GameStateChanged gameStateChanged) if (!wasLoggedIn) { LoginManager.markLoggedIn(); Rs2RunePouch.fullUpdate(); - if (microbotConfig.isRs2CacheEnabled()) { - Rs2CacheManager.registerEventHandlers(); - } } if (currentRegions != null) { Microbot.setLastKnownRegions(currentRegions.clone()); @@ -323,9 +300,6 @@ public void onGameStateChanged(GameStateChanged gameStateChanged) //Rs2CacheManager.emptyCacheState(); // should not be nessary here, handled in ClientShutdown event, // and we also handle correct cache loading in onRuneScapeProfileChanged event LoginManager.markLoggedOut(); - if (microbotConfig.isRs2CacheEnabled()) { - Rs2CacheManager.unregisterEventHandlers(); - } Microbot.setLastKnownRegions(null); } } @@ -434,10 +408,6 @@ public void onConfigChanged(ConfigChanged ev) gameChatAppender.stop(); } break; - case MicrobotConfig.keyEnableCache: - // Handle dynamic cache system initialization/shutdown - handleCacheConfigChange(ev.getNewValue()); - break; default: break; } @@ -536,115 +506,9 @@ public void onGameTick(GameTick event) @Subscribe(priority = 100) private void onClientShutdown(ClientShutdown e) { - // Save all caches through Rs2CacheManager using async operations - if (microbotConfig.isRs2CacheEnabled()) { - try { - // Use async save but wait for completion during shutdown - Rs2CacheManager.savePersistentCachesAsync().get(30, java.util.concurrent.TimeUnit.SECONDS); - log.info("Successfully saved all caches asynchronously during shutdown"); - } catch (Exception ex) { - log.error("Failed to save caches during shutdown: {}", ex.getMessage(), ex); - // Fallback to synchronous save if async fails - Rs2CacheManager.savePersistentCaches(); - } - Rs2CacheManager.getInstance().close(); - } - } - - /** - * Initializes the cache system and registers all caches with the EventBus. - * Cache loading from configuration will happen later during game events when the RS profile is available. - */ - private void initializeCacheSystem() { - try { - // Check if already initialized - if (Rs2CacheManager.isEventHandlersRegistered()) { - log.debug("Cache system already initialized, skipping"); - return; - } - - // Get the cache manager instance - Rs2CacheManager cacheManager = Rs2CacheManager.getInstance(); - - // Set the EventBus for cache event handling (without loading caches yet) - Rs2CacheManager.setEventBus(eventBus); - - // Register event handlers - Rs2CacheManager.registerEventHandlers(); - - // Keep deprecated EntityCache for backward compatibility (for now) - //Rs2EntityCache.getInstance(); - - log.info("Cache system initialized successfully with specialized caches"); - log.info("Cache persistence will be loaded when RS profile becomes available"); - log.debug("Cache statistics: {}", cacheManager.getCacheStatistics()); - - } catch (Exception e) { - log.error("Failed to initialize cache system: {}", e.getMessage(), e); - } - } - - /** - * Shuts down the cache system and cleans up resources. - */ - private void shutdownCacheSystem() { - try { - // Check if already shutdown - if (!Rs2CacheManager.isEventHandlersRegistered()) { - log.debug("Cache system already shutdown, skipping"); - return; - } - - Rs2CacheManager cacheManager = Rs2CacheManager.getInstance(); - - log.debug("Final cache statistics before shutdown: {}", cacheManager.getCacheStatistics()); - - // Unregister event handlers first - Rs2CacheManager.unregisterEventHandlers(); - - // Close the cache manager and all caches - cacheManager.close(); - - // Reset singleton instances for clean shutdown - Rs2CacheManager.resetInstance(); - Rs2VarbitCache.resetInstance(); - Rs2SkillCache.resetInstance(); - Rs2QuestCache.resetInstance(); - - // Reset specialized entity cache instances - Rs2NpcCache.resetInstance(); - Rs2GroundItemCache.resetInstance(); - Rs2ObjectCache.resetInstance(); - - // Reset deprecated EntityCache - //Rs2EntityCache.resetInstance(); - - log.info("Cache system shutdown completed"); - - } catch (Exception e) { - log.error("Error during cache system shutdown: {}", e.getMessage(), e); - } - } - - /** - * Handles cache configuration changes dynamically without requiring client restart. - * This method is called when the user changes the "Enable Microbot Cache" config option. - * - * @param newValue The new value of the cache enable config ("true" or "false") - */ - private void handleCacheConfigChange(String newValue) { - boolean enableCache = Objects.equals(newValue, "true"); - - if (enableCache) { - log.info("Cache system enabled via config change - initializing..."); - initializeCacheSystem(); - Microbot.showMessage("Cache system enabled successfully"); - } else { - log.info("Cache system disabled via config change - shutting down..."); - shutdownCacheSystem(); - Microbot.showMessage("Cache system disabled successfully"); - } + } + /** * Dynamically checks if any visible widget overlaps with the specified bounds * @param overlayBoundsCanvas The bounds to check for widget overlap diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Script.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Script.java index 243cf3fb144..443dccdbf63 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Script.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Script.java @@ -5,7 +5,6 @@ import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.shortestpath.ShortestPathPlugin; import net.runelite.client.plugins.microbot.util.Global; -import net.runelite.client.plugins.microbot.util.cache.Rs2CacheManager; import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; @@ -69,11 +68,6 @@ public boolean run() { return false; if (Thread.currentThread().isInterrupted()) return false; - //when we log in, we must wait for the cache to be loaded before doing anything - if (Microbot.isLoggedIn() && Microbot.isRs2CacheEnabled() && !Rs2CacheManager.isCacheDataValid()) { - log.debug("Cache data is not valid, waiting..."); - return false; - } if (Microbot.isLoggedIn()) { boolean hasRunEnergy = Microbot.getClient().getEnergy() > Microbot.runEnergyThreshold; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/GroundItemFilterPreset.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/GroundItemFilterPreset.java deleted file mode 100644 index 71c881d6c82..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/GroundItemFilterPreset.java +++ /dev/null @@ -1,141 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.rs2cachedebugger; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; - -/** - * Preset filters for Ground Items in the Game Information overlay system. - * Provides common filtering options for different item types and values. - */ -@Getter -@RequiredArgsConstructor -public enum GroundItemFilterPreset { - ALL("All Items", "Show all ground items"), - HIGH_VALUE("High Value (10k+)", "Show items worth 10,000+ coins"), - MEDIUM_VALUE("Medium Value (1k-10k)", "Show items worth 1,000-10,000 coins"), - LOW_VALUE("Low Value (<1k)", "Show items worth less than 1,000 coins"), - VALUABLE_ONLY("Valuable Only (50k+)", "Show items worth 50,000+ coins"), - RARE_ITEMS("Rare Items", "Show rare drops and unique items"), - STACKABLE("Stackable", "Show only stackable items"), - NON_STACKABLE("Non-Stackable", "Show only non-stackable items"), - TRADEABLE("Tradeable", "Show only tradeable items"), - UNTRADEABLE("Untradeable", "Show only untradeable items"), - EQUIPMENT("Equipment", "Show weapons, armor, and accessories"), - CONSUMABLES("Consumables", "Show food, potions, and consumable items"), - RESOURCES("Resources", "Show raw materials and resources"), - COINS("Coins", "Show coin drops only"), - RECENTLY_SPAWNED("Recently Spawned", "Show items spawned in last 10 ticks"), - OWNED_ITEMS("Owned Items", "Show items that belong to the player"), - PUBLIC_ITEMS("Public Items", "Show items visible to all players"), - WITHIN_5_TILES("Within 5 Tiles", "Show items within 5 tiles"), - WITHIN_10_TILES("Within 10 Tiles", "Show items within 10 tiles"), - CUSTOM("Custom", "Use custom filter criteria"); - - private final String displayName; - private final String description; - - @Override - public String toString() { - return displayName; - } - - /** - * Test if a ground item matches this filter preset. - * - * @param item The ground item to test - * @return true if the item matches the filter criteria - */ - public boolean test(Rs2GroundItemModel item) { - if (item == null) { - return false; - } - - switch (this) { - case ALL: - return true; - - case HIGH_VALUE: - return item.getValue() >= 10000; - - case MEDIUM_VALUE: - int value = item.getValue(); - return value >= 1000 && value < 10000; - - case LOW_VALUE: - return item.getValue() < 1000; - - case VALUABLE_ONLY: - return item.getValue() >= 50000; - - case RARE_ITEMS: - // Basic check - could be enhanced with specific rare item IDs - return item.getValue() >= 100000; - - case STACKABLE: - return item.isStackable(); - - case NON_STACKABLE: - return !item.isStackable(); - - case TRADEABLE: - return item.isTradeable(); - - case UNTRADEABLE: - return !item.isTradeable(); - - case EQUIPMENT: - // Basic check for equipment - could be enhanced with item categories - String name = item.getName(); - if (name == null) return false; - return name.toLowerCase().contains("sword") || - name.toLowerCase().contains("bow") || - name.toLowerCase().contains("armor") || - name.toLowerCase().contains("helm") || - name.toLowerCase().contains("boots") || - name.toLowerCase().contains("gloves"); - - case CONSUMABLES: - String consumableName = item.getName(); - if (consumableName == null) return false; - return consumableName.toLowerCase().contains("potion") || - consumableName.toLowerCase().contains("food") || - consumableName.toLowerCase().contains("cake") || - consumableName.toLowerCase().contains("brew"); - - case RESOURCES: - String resourceName = item.getName(); - if (resourceName == null) return false; - return resourceName.toLowerCase().contains("ore") || - resourceName.toLowerCase().contains("log") || - resourceName.toLowerCase().contains("fish") || - resourceName.toLowerCase().contains("bone"); - - case COINS: - return item.getId() == 995; // Coins item ID - - case RECENTLY_SPAWNED: - // For now, just return true - proper implementation would need cache timing - return true; - - case OWNED_ITEMS: - return item.isOwned(); - - case PUBLIC_ITEMS: - return !item.isOwned(); - - case WITHIN_5_TILES: - return item.getDistanceFromPlayer() <= 5; - - case WITHIN_10_TILES: - return item.getDistanceFromPlayer() <= 10; - - case CUSTOM: - // Custom filtering should be handled by the plugin logic - return true; - - default: - return true; - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/NpcFilterPreset.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/NpcFilterPreset.java deleted file mode 100644 index 21a5b3a310e..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/NpcFilterPreset.java +++ /dev/null @@ -1,111 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.rs2cachedebugger; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; - -/** - * Preset filters for NPCs in the Game Information overlay system. - * Provides common filtering options that can be selected from config. - */ -@Getter -@RequiredArgsConstructor -public enum NpcFilterPreset { - ALL("All NPCs", "Show all NPCs"), - ATTACKABLE("Attackable", "Show only attackable NPCs"), - NON_ATTACKABLE("Non-Attackable", "Show only non-attackable NPCs"), - COMBAT_LEVEL_HIGH("High Combat (100+)", "Show NPCs with combat level 100 or higher"), - COMBAT_LEVEL_MID("Mid Combat (50-99)", "Show NPCs with combat level 50-99"), - COMBAT_LEVEL_LOW("Low Combat (1-49)", "Show NPCs with combat level 1-49"), - INTERACTABLE("Interactable", "Show only interactable NPCs (shops, banks, etc.)"), - AGRESSIVE("Aggressive", "Show only aggressive NPCs"), - BOSSES("Bosses", "Show only boss NPCs"), - SLAYER_MONSTERS("Slayer Monsters", "Show only slayer task monsters"), - RECENTLY_SPAWNED("Recently Spawned", "Show NPCs spawned in last 10 ticks"), - NAMED_ONLY("Named Only", "Show only NPCs with custom names"), - WITHIN_5_TILES("Within 5 Tiles", "Show NPCs within 5 tiles"), - WITHIN_10_TILES("Within 10 Tiles", "Show NPCs within 10 tiles"), - CUSTOM("Custom", "Use custom filter criteria"); - - private final String displayName; - private final String description; - - @Override - public String toString() { - return displayName; - } - - /** - * Test if an NPC matches this filter preset. - * - * @param npc The NPC to test - * @return true if the NPC matches the filter criteria - */ - public boolean test(Rs2NpcModel npc) { - if (npc == null) { - return false; - } - - switch (this) { - case ALL: - return true; - - case ATTACKABLE: - return npc.getCombatLevel() > 0; - - case NON_ATTACKABLE: - return npc.getCombatLevel() <= 0; - - case COMBAT_LEVEL_HIGH: - return npc.getCombatLevel() >= 100; - - case COMBAT_LEVEL_MID: - return npc.getCombatLevel() >= 50 && npc.getCombatLevel() <= 99; - - case COMBAT_LEVEL_LOW: - return npc.getCombatLevel() >= 1 && npc.getCombatLevel() <= 49; - - case INTERACTABLE: - // Basic check for common interactable NPCs - String name = npc.getName(); - if (name == null) return false; - return name.toLowerCase().contains("banker") || - name.toLowerCase().contains("shop") || - name.toLowerCase().contains("clerk") || - name.toLowerCase().contains("trader"); - - case AGRESSIVE: - // This would require more complex logic to determine aggression - return npc.getCombatLevel() > 0; - - case BOSSES: - // Basic boss detection - could be enhanced with specific boss IDs - return npc.getCombatLevel() >= 200; - - case SLAYER_MONSTERS: - // This would require slayer task checking - placeholder for now - return npc.getCombatLevel() > 0; - - case RECENTLY_SPAWNED: - // For now, just return true - proper implementation would need cache timing - return true; - - case NAMED_ONLY: - String npcName = npc.getName(); - return npcName != null && !npcName.trim().isEmpty(); - - case WITHIN_5_TILES: - return npc.getDistanceFromPlayer() <= 5; - - case WITHIN_10_TILES: - return npc.getDistanceFromPlayer() <= 10; - - case CUSTOM: - // Custom filtering should be handled by the plugin logic - return true; - - default: - return true; - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/ObjectFilterPreset.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/ObjectFilterPreset.java deleted file mode 100644 index fb063fd2ccc..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/ObjectFilterPreset.java +++ /dev/null @@ -1,159 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.rs2cachedebugger; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel; - -/** - * Preset filters for Objects in the Game Information overlay system. - * Provides common filtering options for different object types. - */ -@Getter -@RequiredArgsConstructor -public enum ObjectFilterPreset { - ALL("All Objects", "Show all objects"), - INTERACTABLE("Interactable", "Show only interactable objects"), - BANKS("Banks", "Show bank booths and chests"), - DOORS("Doors", "Show doors and gates"), - STAIRS("Stairs", "Show stairs and ladders"), - TREES("Trees", "Show all trees"), - ROCKS("Rocks", "Show mining rocks"), - FISHING_SPOTS("Fishing Spots", "Show fishing spots"), - ALTARS("Altars", "Show prayer and magic altars"), - FURNACES("Furnaces", "Show furnaces and forges"), - ANVILS("Anvils", "Show anvils and smithing objects"), - COOKING("Cooking", "Show cooking ranges and fires"), - DECORATIVE("Decorative", "Show decorative objects only"), - WALLS("Walls", "Show wall objects"), - GROUND_OBJECTS("Ground Objects", "Show ground-level objects"), - RECENTLY_SPAWNED("Recently Spawned", "Show objects spawned in last 10 ticks"), - WITHIN_5_TILES("Within 5 Tiles", "Show objects within 5 tiles"), - WITHIN_10_TILES("Within 10 Tiles", "Show objects within 10 tiles"), - HIGH_VALUE("High Value", "Show valuable interaction objects"), - CUSTOM("Custom", "Use custom filter criteria"); - - private final String displayName; - private final String description; - - @Override - public String toString() { - return displayName; - } - - /** - * Test if an object matches this filter preset. - * - * @param object The object to test - * @return true if the object matches the filter criteria - */ - public boolean test(Rs2ObjectModel object) { - if (object == null) { - return false; - } - - switch (this) { - case ALL: - return true; - - case INTERACTABLE: - // Check if object has any actions - String[] actions = object.getActions(); - return actions != null && actions.length > 0; - - case BANKS: - String name = object.getName(); - if (name == null) return false; - return name.toLowerCase().contains("bank") || - name.toLowerCase().contains("chest") || - name.toLowerCase().contains("deposit"); - - case DOORS: - String doorName = object.getName(); - if (doorName == null) return false; - return doorName.toLowerCase().contains("door") || - doorName.toLowerCase().contains("gate"); - - case STAIRS: - String stairName = object.getName(); - if (stairName == null) return false; - return stairName.toLowerCase().contains("stair") || - stairName.toLowerCase().contains("ladder"); - - case TREES: - String treeName = object.getName(); - if (treeName == null) return false; - return treeName.toLowerCase().contains("tree") || - treeName.toLowerCase().contains("log"); - - case ROCKS: - String rockName = object.getName(); - if (rockName == null) return false; - return rockName.toLowerCase().contains("rock") || - rockName.toLowerCase().contains("ore") || - rockName.toLowerCase().contains("vein"); - - case FISHING_SPOTS: - String fishName = object.getName(); - if (fishName == null) return false; - return fishName.toLowerCase().contains("fishing") || - fishName.toLowerCase().contains("spot"); - - case ALTARS: - String altarName = object.getName(); - if (altarName == null) return false; - return altarName.toLowerCase().contains("altar"); - - case FURNACES: - String furnaceName = object.getName(); - if (furnaceName == null) return false; - return furnaceName.toLowerCase().contains("furnace") || - furnaceName.toLowerCase().contains("forge"); - - case ANVILS: - String anvilName = object.getName(); - if (anvilName == null) return false; - return anvilName.toLowerCase().contains("anvil"); - - case COOKING: - String cookName = object.getName(); - if (cookName == null) return false; - return cookName.toLowerCase().contains("range") || - cookName.toLowerCase().contains("fire") || - cookName.toLowerCase().contains("stove"); - - case DECORATIVE: - return object.getObjectType() == Rs2ObjectModel.ObjectType.DECORATIVE_OBJECT; - - case WALLS: - return object.getObjectType() == Rs2ObjectModel.ObjectType.WALL_OBJECT; - - case GROUND_OBJECTS: - return object.getObjectType() == Rs2ObjectModel.ObjectType.GROUND_OBJECT; - - case RECENTLY_SPAWNED: - // For now, just return true - proper implementation would need cache timing - return true; - - case WITHIN_5_TILES: - return object.getDistanceFromPlayer() <= 5; - - case WITHIN_10_TILES: - return object.getDistanceFromPlayer() <= 10; - - case HIGH_VALUE: - // Basic check for commonly valuable objects - String valueName = object.getName(); - if (valueName == null) return false; - return valueName.toLowerCase().contains("bank") || - valueName.toLowerCase().contains("shop") || - valueName.toLowerCase().contains("altar"); - - case CUSTOM: - // Custom filtering should be handled by the plugin logic - return true; - - default: - return true; - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/RenderStyle.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/RenderStyle.java deleted file mode 100644 index 6cd611e9f2e..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/RenderStyle.java +++ /dev/null @@ -1,32 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.rs2cachedebugger; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -/** - * Different rendering styles for entity overlays. - * Based on RuneLite's NPC and Object indicator patterns. - */ -@Getter -@RequiredArgsConstructor -public enum RenderStyle { - HULL("Hull", "Render convex hull outline"), - TILE("Tile", "Render tile area"), - TRUE_TILE("True Tile", "Render true tile (centered)"), - SW_TILE("SW Tile", "Render southwest tile"), - CLICKBOX("Clickbox", "Render clickbox area"), - OUTLINE("Outline", "Render model outline"), - NAME("Name", "Show entity name"), - MIXED("Mixed", "Combination of hull + name"), - MINIMAL("Minimal", "Just a small indicator"), - DETAILED("Detailed", "Full information display"), - BOTH("Both", "Render both hull and tile"); - - private final String displayName; - private final String description; - - @Override - public String toString() { - return displayName; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerConfig.java deleted file mode 100644 index ec63365ed34..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerConfig.java +++ /dev/null @@ -1,908 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.rs2cachedebugger; - -import net.runelite.client.config.*; - -import java.awt.Color; -import java.awt.event.KeyEvent; - -/** - * Configuration for the Rs2 Cache Debugger Plugin. - * Provides comprehensive cache debugging, overlay and filtering options for NPCs, Objects, and Ground Items. - */ -@ConfigGroup("rs2cachedebugger") -public interface Rs2CacheDebuggerConfig extends Config { - - // ===== CONFIG SECTIONS ===== - - @ConfigSection( - name = "General Settings", - description = "General plugin settings and hotkeys", - position = 0 - ) - String generalSection = "general"; - - @ConfigSection( - name = "NPC Settings", - description = "Configure NPC overlay appearance and filtering", - position = 1 - ) - String npcSection = "npc"; - - @ConfigSection( - name = "Object Settings", - description = "Configure object overlay appearance and filtering", - position = 2 - ) - String objectSection = "object"; - - @ConfigSection( - name = "Ground Item Settings", - description = "Configure ground item overlay appearance and filtering", - position = 3 - ) - String groundItemSection = "groundItem"; - - @ConfigSection( - name = "Info Panel Settings", - description = "Configure the information panel display", - position = 4 - ) - String infoPanelSection = "infoPanel"; - - - - // ===== GENERAL SETTINGS ===== - - @ConfigItem( - keyName = "enablePlugin", - name = "Enable Plugin", - description = "Master toggle for the entire plugin", - position = 0, - section = generalSection - ) - default boolean enablePlugin() { - return true; - } - - @ConfigItem( - keyName = "maxRenderDistance", - name = "Max Render Distance", - description = "Maximum distance to render entities (in tiles)", - position = 1, - section = generalSection - ) - @Range(min = 5, max = 50) - default int maxRenderDistance() { - return 15; - } - - @ConfigItem( - keyName = "verboseLogging", - name = "Verbose Logging", - description = "Enable detailed logging for debugging", - position = 2, - section = generalSection - ) - default boolean verboseLogging() { - return false; - } - - // Hotkeys - @ConfigItem( - keyName = "toggleNpcOverlayHotkey", - name = "Toggle NPC Overlay", - description = "Hotkey to toggle NPC overlay on/off", - position = 10, - section = generalSection - ) - default Keybind toggleNpcOverlayHotkey() { - return new Keybind(KeyEvent.VK_F1,0); - } - - @ConfigItem( - keyName = "toggleObjectOverlayHotkey", - name = "Toggle Object Overlay", - description = "Hotkey to toggle object overlay on/off", - position = 11, - section = generalSection - ) - default Keybind toggleObjectOverlayHotkey() { - return new Keybind(KeyEvent.VK_F2,0); - } - - @ConfigItem( - keyName = "toggleGroundItemOverlayHotkey", - name = "Toggle Ground Item Overlay", - description = "Hotkey to toggle ground item overlay on/off", - position = 12, - section = generalSection - ) - default Keybind toggleGroundItemOverlayHotkey() { - return new Keybind(KeyEvent.VK_F3,0); - } - - @ConfigItem( - keyName = "toggleInfoPanelHotkey", - name = "Toggle Info Panel", - description = "Hotkey to toggle information panel on/off", - position = 13, - section = generalSection - ) - default Keybind toggleInfoPanelHotkey() { - return new Keybind(KeyEvent.VK_F4,0); - } - - @ConfigItem( - keyName = "logCacheInfoHotkey", - name = "Log Cache Info", - description = "Hotkey to log cache information to console", - position = 14, - section = generalSection - ) - default Keybind logCacheInfoHotkey() { - return new Keybind(KeyEvent.VK_F5,0); - } - - @ConfigItem( - keyName = "showCachePerformanceMetrics", - name = "Show Cache Performance", - description = "Show cache hit/miss ratio and performance metrics", - position = 15, - section = generalSection - ) - default boolean showCachePerformanceMetrics() { - return false; - } - - @ConfigItem( - keyName = "logCacheStatsTicks", - name = "Cache Stats Log Interval", - description = "Log cache statistics every X ticks (0 = disabled)", - position = 16, - section = generalSection - ) - @Range(min = 0, max = 100) - default int logCacheStatsTicks() { - return 0; - } - - // ===== NPC SETTINGS ===== - - @ConfigItem( - keyName = "enableNpcOverlay", - name = "Enable NPC Overlay", - description = "Show NPC overlays", - position = 0, - section = npcSection - ) - default boolean enableNpcOverlay() { - return false; - } - - @ConfigItem( - keyName = "npcRenderStyle", - name = "NPC Render Style", - description = "How to render NPC overlays", - position = 1, - section = npcSection - ) - default RenderStyle npcRenderStyle() { - return RenderStyle.HULL; - } - - @ConfigItem( - keyName = "npcFilterPreset", - name = "NPC Filter Preset", - description = "Preset filter for NPCs to display", - position = 2, - section = npcSection - ) - default NpcFilterPreset npcFilterPreset() { - return NpcFilterPreset.ALL; - } - - @ConfigItem( - keyName = "npcBorderColor", - name = "NPC Border Color", - description = "Color for NPC overlay borders", - position = 3, - section = npcSection - ) - default Color npcBorderColor() { - return Color.ORANGE; - } - - @ConfigItem( - keyName = "npcFillColor", - name = "NPC Fill Color", - description = "Color for NPC overlay fill", - position = 4, - section = npcSection - ) - @Alpha - default Color npcFillColor() { - return new Color(255, 165, 0, 50); - } - - @ConfigItem( - keyName = "npcBorderWidth", - name = "NPC Border Width", - description = "Width of NPC overlay borders", - position = 5, - section = npcSection - ) - @Range(min = 1, max = 10) - default int npcBorderWidth() { - return 2; - } - - @ConfigItem( - keyName = "npcShowNames", - name = "Show NPC Names", - description = "Display NPC names above them", - position = 6, - section = npcSection - ) - default boolean npcShowNames() { - return false; - } - - @ConfigItem( - keyName = "npcShowCombatLevel", - name = "Show Combat Level", - description = "Display NPC combat levels", - position = 7, - section = npcSection - ) - default boolean npcShowCombatLevel() { - return false; - } - - @ConfigItem( - keyName = "npcShowId", - name = "Show NPC ID", - description = "Display NPC game IDs", - position = 8, - section = npcSection - ) - default boolean npcShowId() { - return false; - } - - @ConfigItem( - keyName = "npcShowCoordinates", - name = "Show World Coordinates", - description = "Display world coordinates for NPCs", - position = 9, - section = npcSection - ) - default boolean npcShowCoordinates() { - return false; - } - - @ConfigItem( - keyName = "npcInteractingColor", - name = "NPC Interacting Color", - description = "Color for NPCs interacting with player", - position = 10, - section = npcSection - ) - default Color npcInteractingColor() { - return Color.RED; - } - - @ConfigItem( - keyName = "npcShowDistance", - name = "Show Distance", - description = "Display distance to NPCs", - position = 9, - section = npcSection - ) - default boolean npcShowDistance() { - return false; - } - - @ConfigItem( - keyName = "npcCustomFilter", - name = "Custom NPC Filter", - description = "Custom text filter for NPC names (leave empty to disable)", - position = 10, - section = npcSection - ) - default String npcCustomFilter() { - return ""; - } - - @ConfigItem( - keyName = "npcMaxDistance", - name = "NPC Max Distance", - description = "Maximum distance to show NPCs (in tiles)", - position = 11, - section = npcSection - ) - @Range(min = 3, max = 30) - default int npcMaxDistance() { - return 15; - } - - // ===== OBJECT SETTINGS ===== - - @ConfigItem( - keyName = "enableObjectOverlay", - name = "Enable Object Overlay", - description = "Show object overlays", - position = 0, - section = objectSection - ) - default boolean enableObjectOverlay() { - return false; - } - - @ConfigItem( - keyName = "objectRenderStyle", - name = "Object Render Style", - description = "How to render object overlays", - position = 1, - section = objectSection - ) - default RenderStyle objectRenderStyle() { - return RenderStyle.HULL; - } - - @ConfigItem( - keyName = "objectFilterPreset", - name = "Object Filter Preset", - description = "Preset filter for objects to display", - position = 2, - section = objectSection - ) - default ObjectFilterPreset objectFilterPreset() { - return ObjectFilterPreset.INTERACTABLE; - } - - @ConfigItem( - keyName = "objectBorderColor", - name = "Default Object Border Color", - description = "Color for object overlay borders", - position = 3, - section = objectSection - ) - default Color objectBorderColor() { - return Color.BLUE; - } - - @ConfigItem( - keyName = "objectFillColor", - name = "Object Fill Color", - description = "Color for object overlay fill", - position = 4, - section = objectSection - ) - @Alpha - default Color objectFillColor() { - return new Color(0, 0, 255, 50); - } - - @ConfigItem( - keyName = "objectBorderWidth", - name = "Object Border Width", - description = "Width of object overlay borders", - position = 5, - section = objectSection - ) - @Range(min = 1, max = 10) - default int objectBorderWidth() { - return 2; - } - - @ConfigItem( - keyName = "objectShowNames", - name = "Show Object Names", - description = "Display object names", - position = 6, - section = objectSection - ) - default boolean objectShowNames() { - return false; - } - - @ConfigItem( - keyName = "objectShowId", - name = "Show Object ID", - description = "Display object game IDs and type", - position = 7, - section = objectSection - ) - default boolean objectShowId() { - return false; - } - - @ConfigItem( - keyName = "objectShowCoordinates", - name = "Show World Coordinates", - description = "Display world coordinates for objects", - position = 8, - section = objectSection - ) - default boolean objectShowCoordinates() { - return false; - } - - @ConfigItem( - keyName = "objectMaxDistance", - name = "Object Max Distance", - description = "Maximum distance to show objects (in tiles)", - position = 7, - section = objectSection - ) - @Range(min = 3, max = 30) - default int objectMaxDistance() { - return 15; - } - - @ConfigItem( - keyName = "objectCustomFilter", - name = "Custom Object Filter", - description = "Custom text filter for object names (leave empty to disable)", - position = 8, - section = objectSection - ) - default String objectCustomFilter() { - return ""; - } - - // Different object type colors - @ConfigItem( - keyName = "bankColor", - name = "Bank Color", - description = "Color for bank objects", - position = 10, - section = objectSection - ) - default Color bankColor() { - return Color.GREEN; - } - - @ConfigItem( - keyName = "altarColor", - name = "Altar Color", - description = "Color for altar objects", - position = 11, - section = objectSection - ) - default Color altarColor() { - return Color.CYAN; - } - - @ConfigItem( - keyName = "resourceColor", - name = "Resource Color", - description = "Color for resource objects (trees, rocks)", - position = 12, - section = objectSection - ) - default Color resourceColor() { - return Color.YELLOW; - } - - @ConfigItem( - keyName = "gameObjectColor", - name = "GameObject Color", - description = "Color for regular GameObjects", - position = 13, - section = objectSection - ) - default Color gameObjectColor() { - return Color.BLUE; - } - - @ConfigItem( - keyName = "wallObjectColor", - name = "WallObject Color", - description = "Color for wall objects", - position = 14, - section = objectSection - ) - default Color wallObjectColor() { - return new Color(0, 0, 139); // Dark blue - } - - @ConfigItem( - keyName = "decorativeObjectColor", - name = "DecorativeObject Color", - description = "Color for decorative objects", - position = 15, - section = objectSection - ) - default Color decorativeObjectColor() { - return new Color(173, 216, 230); // Light blue - } - - @ConfigItem( - keyName = "groundObjectColor", - name = "GroundObject Color", - description = "Color for ground objects", - position = 16, - section = objectSection - ) - default Color groundObjectColor() { - return new Color(0, 128, 0); // Dark green - } - - @ConfigItem( - keyName = "enableObjectTypeColoring", - name = "Enable Object Type Coloring", - description = "Use different colors for different object types", - position = 17, - section = objectSection - ) - default boolean enableObjectTypeColoring() { - return true; - } - - @ConfigItem( - keyName = "enableObjectCategoryColoring", - name = "Enable Object Category Coloring", - description = "Use different colors for object categories (bank, altar, resource)", - position = 18, - section = objectSection - ) - default boolean enableObjectCategoryColoring() { - return true; - } - - // Object type toggles - @ConfigItem( - keyName = "showGameObjects", - name = "Show GameObjects", - description = "Show regular game objects (type 10)", - position = 19, - section = objectSection - ) - default boolean showGameObjects() { - return true; - } - - @ConfigItem( - keyName = "showWallObjects", - name = "Show WallObjects", - description = "Show wall objects (type 1)", - position = 20, - section = objectSection - ) - default boolean showWallObjects() { - return true; - } - - @ConfigItem( - keyName = "showDecorativeObjects", - name = "Show DecorativeObjects", - description = "Show decorative objects (type 3)", - position = 21, - section = objectSection - ) - default boolean showDecorativeObjects() { - return true; - } - - @ConfigItem( - keyName = "showGroundObjects", - name = "Show GroundObjects", - description = "Show ground objects (type 2)", - position = 22, - section = objectSection - ) - default boolean showGroundObjects() { - return true; - } - - // ===== GROUND ITEM SETTINGS ===== - - @ConfigItem( - keyName = "enableGroundItemOverlay", - name = "Enable Ground Item Overlay", - description = "Show ground item overlays", - position = 0, - section = groundItemSection - ) - default boolean enableGroundItemOverlay() { - return false; - } - - @ConfigItem( - keyName = "groundItemRenderStyle", - name = "Ground Item Render Style", - description = "How to render ground item overlays", - position = 1, - section = groundItemSection - ) - default RenderStyle groundItemRenderStyle() { - return RenderStyle.TILE; - } - - @ConfigItem( - keyName = "groundItemFilterPreset", - name = "Ground Item Filter Preset", - description = "Preset filter for ground items to display", - position = 2, - section = groundItemSection - ) - default GroundItemFilterPreset groundItemFilterPreset() { - return GroundItemFilterPreset.HIGH_VALUE; - } - - @ConfigItem( - keyName = "groundItemBorderColor", - name = "Ground Item Border Color", - description = "Color for ground item overlay borders", - position = 3, - section = groundItemSection - ) - default Color groundItemBorderColor() { - return Color.GREEN; - } - - @ConfigItem( - keyName = "groundItemFillColor", - name = "Ground Item Fill Color", - description = "Color for ground item overlay fill", - position = 4, - section = groundItemSection - ) - @Alpha - default Color groundItemFillColor() { - return new Color(0, 255, 0, 50); - } - - @ConfigItem( - keyName = "groundItemBorderWidth", - name = "Ground Item Border Width", - description = "Width of ground item overlay borders", - position = 5, - section = groundItemSection - ) - @Range(min = 1, max = 10) - default int groundItemBorderWidth() { - return 2; - } - - @ConfigItem( - keyName = "groundItemShowNames", - name = "Show Item Names", - description = "Display ground item names", - position = 6, - section = groundItemSection - ) - default boolean groundItemShowNames() { - return true; - } - - @ConfigItem( - keyName = "groundItemShowValues", - name = "Show Item Values", - description = "Display ground item values", - position = 7, - section = groundItemSection - ) - default boolean groundItemShowValues() { - return true; - } - - @ConfigItem( - keyName = "groundItemShowId", - name = "Show Item ID", - description = "Display ground item game IDs", - position = 8, - section = groundItemSection - ) - default boolean groundItemShowId() { - return false; - } - - @ConfigItem( - keyName = "groundItemShowCoordinates", - name = "Show World Coordinates", - description = "Display world coordinates for ground items", - position = 9, - section = groundItemSection - ) - default boolean groundItemShowCoordinates() { - return false; - } - - @ConfigItem( - keyName = "groundItemMaxDistance", - name = "Ground Item Max Distance", - description = "Maximum distance to show ground items (in tiles)", - position = 8, - section = groundItemSection - ) - @Range(min = 3, max = 30) - default int groundItemMaxDistance() { - return 15; - } - - @ConfigItem( - keyName = "groundItemCustomFilter", - name = "Custom Ground Item Filter", - description = "Custom text filter for ground item names (leave empty to disable)", - position = 9, - section = groundItemSection - ) - default String groundItemCustomFilter() { - return ""; - } - - @ConfigItem( - keyName = "minimumItemValue", - name = "Minimum Item Value", - description = "Minimum value to show ground items (in coins)", - position = 9, - section = groundItemSection - ) - default int minimumItemValue() { - return 1000; - } - - @ConfigItem( - keyName = "groundItemShowQuantity", - name = "Show Item Quantity", - description = "Display quantity for stackable ground items", - position = 12, - section = groundItemSection - ) - default boolean groundItemShowQuantity() { - return true; - } - - @ConfigItem( - keyName = "groundItemShowDespawnTimer", - name = "Show Despawn Timer", - description = "Display countdown timer until item despawns", - position = 13, - section = groundItemSection - ) - default boolean groundItemShowDespawnTimer() { - return false; - } - - @ConfigItem( - keyName = "groundItemShowOwnership", - name = "Show Ownership Indicator", - description = "Display ownership status for ground items", - position = 14, - section = groundItemSection - ) - default boolean groundItemShowOwnership() { - return false; - } - - @ConfigItem( - keyName = "groundItemValueBasedColors", - name = "Value-Based Colors", - description = "Use different colors based on item value", - position = 15, - section = groundItemSection - ) - default boolean groundItemValueBasedColors() { - return false; - } - - @ConfigItem( - keyName = "groundItemLowValueThreshold", - name = "Low Value Threshold", - description = "Threshold for low value items (in GP)", - position = 16, - section = groundItemSection - ) - @Range(min = 1, max = 1000000) - default int groundItemLowValueThreshold() { - return 1000; - } - - @ConfigItem( - keyName = "groundItemMediumValueThreshold", - name = "Medium Value Threshold", - description = "Threshold for medium value items (in GP)", - position = 17, - section = groundItemSection - ) - @Range(min = 1000, max = 10000000) - default int groundItemMediumValueThreshold() { - return 10000; - } - - @ConfigItem( - keyName = "groundItemHighValueThreshold", - name = "High Value Threshold", - description = "Threshold for high value items (in GP)", - position = 18, - section = groundItemSection - ) - @Range(min = 10000, max = 100000000) - default int groundItemHighValueThreshold() { - return 100000; - } - - // ===== INFO PANEL SETTINGS ===== - - @ConfigItem( - keyName = "enableInfoPanel", - name = "Enable Info Panel", - description = "Enable the information panel", - position = 0, - section = infoPanelSection - ) - default boolean enableInfoPanel() { - return false; - } - - @ConfigItem( - keyName = "showInfoPanel", - name = "Show Info Panel", - description = "Display information panel with cache statistics", - position = 1, - section = infoPanelSection - ) - default boolean showInfoPanel() { - return false; - } - - @ConfigItem( - keyName = "infoPanelShowNpcs", - name = "Show NPC Info", - description = "Show NPC information in panel", - position = 1, - section = infoPanelSection - ) - default boolean infoPanelShowNpcs() { - return true; - } - - @ConfigItem( - keyName = "infoPanelShowObjects", - name = "Show Object Info", - description = "Show object information in panel", - position = 2, - section = infoPanelSection - ) - default boolean infoPanelShowObjects() { - return true; - } - - @ConfigItem( - keyName = "infoPanelShowGroundItems", - name = "Show Ground Item Info", - description = "Show ground item information in panel", - position = 3, - section = infoPanelSection - ) - default boolean infoPanelShowGroundItems() { - return true; - } - - @ConfigItem( - keyName = "infoPanelShowCacheStats", - name = "Show Cache Statistics", - description = "Show cache hit/miss statistics", - position = 4, - section = infoPanelSection - ) - default boolean infoPanelShowCacheStats() { - return true; - } - - @ConfigItem( - keyName = "infoPanelRefreshRate", - name = "Panel Refresh Rate", - description = "How often to refresh the info panel (in ticks)", - position = 5, - section = infoPanelSection - ) - @Range(min = 1, max = 10) - default int infoPanelRefreshRate() { - return 5; - } - - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerGroundItemOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerGroundItemOverlay.java deleted file mode 100644 index 915929e14d5..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerGroundItemOverlay.java +++ /dev/null @@ -1,157 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.rs2cachedebugger; - -import net.runelite.api.*; -import net.runelite.client.plugins.microbot.util.cache.overlay.Rs2GroundItemCacheOverlay; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; -import net.runelite.client.ui.overlay.outline.ModelOutlineRenderer; - -import java.awt.*; -import java.util.function.Predicate; - -import javax.inject.Inject; - -/** - * Rs2 Cache Debugger Ground Item Overlay with configurable rendering and filtering. - * Extends the base Ground Item cache overlay with config-driven customization. - * - * @author Vox - * @version 3.0 - Cache Debugging Focus - */ -public class Rs2CacheDebuggerGroundItemOverlay extends Rs2GroundItemCacheOverlay { - - private Rs2CacheDebuggerConfig config; - private Predicate renderFilter; - @Inject - public Rs2CacheDebuggerGroundItemOverlay(Client client, ModelOutlineRenderer modelOutlineRenderer) { - super(client, modelOutlineRenderer); - } - - /** - * Set the configuration for this overlay - */ - public void setConfig(Rs2CacheDebuggerConfig config) { - this.config = config; - updateRenderingOptions(); - } - - /** - * Set the render filter for Ground Items - */ - public Rs2CacheDebuggerGroundItemOverlay setRenderFilter(Predicate filter) { - this.renderFilter = filter; - return this; - } - - /** - * Update rendering options based on config - */ - private void updateRenderingOptions() { - if (config == null) return; - - // Update rendering options based on config - setRenderTile(config.groundItemRenderStyle() == RenderStyle.TILE || - config.groundItemRenderStyle() == RenderStyle.BOTH); - setRenderText(config.groundItemShowNames()); - - // New configuration options - setRenderItemInfo(config.groundItemShowId()); - setRenderWorldCoordinates(config.groundItemShowCoordinates()); - - // Advanced rendering features from Rs2GroundItemCacheOverlay - setRenderQuantity(config.groundItemShowQuantity()); - //setRenderValue(config.groundItemShowValues()); - setRenderDespawnTimer(config.groundItemShowDespawnTimer()); - setRenderOwnershipIndicator(config.groundItemShowOwnership()); - - // Set value thresholds for color coding - setValueThresholds( - config.groundItemLowValueThreshold(), - config.groundItemMediumValueThreshold(), - config.groundItemHighValueThreshold() - ); - } - - @Override - protected Color getDefaultBorderColor() { - if (config != null) { - return config.groundItemBorderColor(); - } - return super.getDefaultBorderColor(); - } - - @Override - protected Color getDefaultFillColor() { - if (config != null) { - // Create fill color with alpha from border color - Color borderColor = config.groundItemBorderColor(); - return new Color(borderColor.getRed(), borderColor.getGreen(), borderColor.getBlue(), 50); - } - return super.getDefaultFillColor(); - } - - /** - * Gets the border color for a specific ground item based on value and configuration. - * - * @param itemModel The ground item model - * @return The border color for this item - */ - @Override - protected Color getBorderColorForItem(Rs2GroundItemModel itemModel) { - if (config != null && config.groundItemShowValues()) { - return getItemValueColor(itemModel); - } - return getDefaultBorderColor(); - } - - /** - * Gets the fill color for a specific ground item based on value and configuration. - * - * @param itemModel The ground item model - * @return The fill color for this item - */ - @Override - protected Color getFillColorForItem(Rs2GroundItemModel itemModel) { - if (config != null && config.groundItemShowValues()) { - Color borderColor = getItemValueColor(itemModel); - return new Color(borderColor.getRed(), borderColor.getGreen(), borderColor.getBlue(), 50); - } - return getDefaultFillColor(); - } - - /** - * Gets the color for a ground item based on its value. - * - * @param itemModel The ground item model - * @return The value-based color - */ - private Color getItemValueColor(Rs2GroundItemModel itemModel) { - int totalValue = itemModel.getTotalValue(); - - if (totalValue >= config.groundItemHighValueThreshold()) { - return Color.RED; // High value items in red - } else if (totalValue >= config.groundItemMediumValueThreshold()) { - return Color.ORANGE; // Medium value items in orange - } else if (totalValue >= config.groundItemLowValueThreshold()) { - return Color.YELLOW; // Low value items in yellow - } else { - return getDefaultBorderColor(); // Default color for very low value items - } - } - - @Override - public Dimension render(Graphics2D graphics) { - if (config == null || !config.enableGroundItemOverlay()) { - return null; - } - - // Update configuration - updateRenderingOptions(); - - // Apply filter if set - if (renderFilter != null) { - setRenderFilter(renderFilter); - } - - return super.render(graphics); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerInfoPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerInfoPanel.java deleted file mode 100644 index dd4995230d4..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerInfoPanel.java +++ /dev/null @@ -1,326 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.rs2cachedebugger; - -import net.runelite.api.Client; -import net.runelite.client.plugins.microbot.util.cache.Rs2GroundItemCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2NpcCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2ObjectCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2Cache; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2NpcCacheUtils; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2ObjectCacheUtils; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2GroundItemCacheUtils; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel; -import net.runelite.client.ui.overlay.OverlayLayer; -import net.runelite.client.ui.overlay.OverlayPosition; -import net.runelite.client.ui.overlay.components.LineComponent; -import net.runelite.client.ui.overlay.components.PanelComponent; -import net.runelite.client.ui.overlay.components.TitleComponent; -import net.runelite.client.ui.overlay.Overlay; - -import java.awt.*; -import java.util.List; -import java.util.stream.Collectors; - -import javax.inject.Inject; - -/** - * Rs2 Cache Debugger Info Panel showing detailed cache information and viewport entities. - * Displays comprehensive information about NPCs, objects, and ground items in the current viewport - * with focus on cache performance and debugging statistics. - * - * @author Vox - * @version 3.0 - Cache Debugging Focus - */ -public class Rs2CacheDebuggerInfoPanel extends Overlay { - - private Rs2CacheDebuggerConfig config; - private final PanelComponent panelComponent = new PanelComponent(); - @Inject - public Rs2CacheDebuggerInfoPanel(Client client) { - setPosition(OverlayPosition.TOP_LEFT); - setLayer(OverlayLayer.ABOVE_WIDGETS); - } - - /** - * Set the configuration for this info panel - */ - public void setConfig(Rs2CacheDebuggerConfig config) { - this.config = config; - } - - @Override - public Dimension render(Graphics2D graphics) { - if (config == null || !config.enableInfoPanel() || !config.showInfoPanel()) { - return null; - } - - panelComponent.getChildren().clear(); - - // Panel title - panelComponent.getChildren().add(TitleComponent.builder() - .text("Rs2 Cache Debugger") - .color(Color.CYAN) - .build()); - - // Cache statistics - addCacheStatistics(); - - // Viewport entities - if (config.infoPanelShowNpcs() || config.infoPanelShowObjects() || config.infoPanelShowGroundItems()) { - panelComponent.getChildren().add(LineComponent.builder() - .left("").right("").build()); // Spacer - - panelComponent.getChildren().add(LineComponent.builder() - .left("Viewport Entities:") - .leftColor(Color.WHITE) - .build()); - } - - // NPCs in viewport - if (config.infoPanelShowNpcs()) { - addNpcInfo(); - } - - // Objects in viewport - if (config.infoPanelShowObjects()) { - addObjectInfo(); - } - - // Ground items in viewport - if (config.infoPanelShowGroundItems()) { - addGroundItemInfo(); - } - - return panelComponent.render(graphics); - } - - /** - * Add cache statistics to the panel - */ - private void addCacheStatistics() { - Rs2Cache npcCache = Rs2NpcCache.getInstance(); - Rs2Cache objectCache = Rs2ObjectCache.getInstance(); - Rs2Cache groundItemCache = Rs2GroundItemCache.getInstance(); - - panelComponent.getChildren().add(LineComponent.builder() - .left("Cache Statistics:") - .leftColor(Color.WHITE) - .build()); - - panelComponent.getChildren().add(LineComponent.builder() - .left("NPCs:") - .right(String.valueOf(npcCache.size())) - .leftColor(Color.ORANGE) - .rightColor(Color.WHITE) - .build()); - - panelComponent.getChildren().add(LineComponent.builder() - .left("Objects:") - .right(String.valueOf(objectCache.size())) - .leftColor(Color.BLUE) - .rightColor(Color.WHITE) - .build()); - - panelComponent.getChildren().add(LineComponent.builder() - .left("Ground Items:") - .right(String.valueOf(groundItemCache.size())) - .leftColor(Color.GREEN) - .rightColor(Color.WHITE) - .build()); - } - - /** - * Add NPC information to the panel - */ - private void addNpcInfo() { - List visibleNpcs = Rs2NpcCacheUtils.getAllInViewport() - .limit(5) // Limit to first 5 for display - .collect(Collectors.toList()); - - if (!visibleNpcs.isEmpty()) { - panelComponent.getChildren().add(LineComponent.builder() - .left("NPCs (" + visibleNpcs.size() + "):") - .leftColor(Color.ORANGE) - .build()); - - for (Rs2NpcModel npc : visibleNpcs) { - String npcInfo = buildNpcInfo(npc); - panelComponent.getChildren().add(LineComponent.builder() - .left(" " + npcInfo) - .leftColor(Color.LIGHT_GRAY) - .build()); - } - } - } - - /** - * Add object information to the panel - */ - private void addObjectInfo() { - List visibleObjects = Rs2ObjectCacheUtils.getAllInViewport() - .limit(5) // Limit to first 5 for display - .collect(Collectors.toList()); - - if (!visibleObjects.isEmpty()) { - panelComponent.getChildren().add(LineComponent.builder() - .left("Objects (" + visibleObjects.size() + "):") - .leftColor(Color.BLUE) - .build()); - - for (Rs2ObjectModel obj : visibleObjects) { - String objInfo = buildObjectInfo(obj); - panelComponent.getChildren().add(LineComponent.builder() - .left(" " + objInfo) - .leftColor(Color.LIGHT_GRAY) - .build()); - } - } - } - - /** - * Add ground item information to the panel - */ - private void addGroundItemInfo() { - List visibleItems = Rs2GroundItemCacheUtils.getAllInViewport() - .limit(5) // Limit to first 5 for display - .collect(Collectors.toList()); - - if (!visibleItems.isEmpty()) { - panelComponent.getChildren().add(LineComponent.builder() - .left("Ground Items (" + visibleItems.size() + "):") - .leftColor(Color.GREEN) - .build()); - - for (Rs2GroundItemModel item : visibleItems) { - String itemInfo = buildGroundItemInfo(item); - panelComponent.getChildren().add(LineComponent.builder() - .left(" " + itemInfo) - .leftColor(Color.LIGHT_GRAY) - .build()); - } - } - } - - /** - * Build info string for an NPC - */ - private String buildNpcInfo(Rs2NpcModel npc) { - StringBuilder info = new StringBuilder(); - - String name = npc.getName(); - if (name != null) { - info.append(name); - } else { - info.append("Unknown"); - } - - info.append(" [ID:").append(npc.getId()).append("]"); - - if (npc.getCombatLevel() > 0) { - info.append(" (CB: ").append(npc.getCombatLevel()).append(")"); - } - - info.append(" (").append(npc.getDistanceFromPlayer()).append("t)"); - - // Add interaction status - if (npc.getInteracting() != null) { - info.append(" [INTERACTING]"); - } - - // Add coordinates if config allows - if (config != null && config.npcShowCoordinates()) { - info.append(" @(").append(npc.getWorldLocation().getX()) - .append(",").append(npc.getWorldLocation().getY()).append(")"); - } - - return info.toString(); - } - - /** - * Build info string for an object - */ - private String buildObjectInfo(Rs2ObjectModel obj) { - StringBuilder info = new StringBuilder(); - - String name = obj.getName(); - if (name != null && !name.trim().isEmpty()) { - info.append(name); - } else { - info.append("Object"); - } - - info.append(" [ID:").append(obj.getId()).append("]"); - - // Add object type abbreviation - String typeAbbr = getObjectTypeAbbreviation(obj.getObjectType()); - info.append(" (").append(typeAbbr).append(")"); - - info.append(" (").append(obj.getDistanceFromPlayer()).append("t)"); - - // Add coordinates if config allows - if (config != null && config.objectShowCoordinates()) { - info.append(" @(").append(obj.getLocation().getX()) - .append(",").append(obj.getLocation().getY()).append(")"); - } - - return info.toString(); - } - - /** - * Get object type abbreviation for display - */ - private String getObjectTypeAbbreviation(Rs2ObjectModel.ObjectType objectType) { - switch (objectType) { - case GAME_OBJECT: - return "G"; - case WALL_OBJECT: - return "W"; - case DECORATIVE_OBJECT: - return "D"; - case GROUND_OBJECT: - return "Gnd"; - default: - return "?"; - } - } - - /** - * Build info string for a ground item - */ - private String buildGroundItemInfo(Rs2GroundItemModel item) { - StringBuilder info = new StringBuilder(); - - String name = item.getName(); - if (name != null) { - info.append(name); - } else { - info.append("Unknown Item"); - } - - info.append(" [ID:").append(item.getId()).append("]"); - - if (item.getQuantity() > 1) { - info.append(" x").append(item.getQuantity()); - } - - // Add value information - if (item.getValue() > 0) { - info.append(" (").append(item.getValue()).append("gp)"); - } - - info.append(" (").append(item.getDistanceFromPlayer()).append("t)"); - - if (item.isOwned()) { - info.append(" [OWNED]"); - } - - // Add coordinates if config allows - if (config != null && config.groundItemShowCoordinates()) { - info.append(" @(").append(item.getLocation().getX()) - .append(",").append(item.getLocation().getY()).append(")"); - } - - return info.toString(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerNpcOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerNpcOverlay.java deleted file mode 100644 index 03d5eee32b8..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerNpcOverlay.java +++ /dev/null @@ -1,168 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.rs2cachedebugger; - -import net.runelite.api.*; -import net.runelite.client.plugins.microbot.util.cache.overlay.Rs2NpcCacheOverlay; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; -import net.runelite.client.ui.overlay.outline.ModelOutlineRenderer; - -import java.awt.*; -import java.util.function.Predicate; - -import javax.inject.Inject; - -/** - * Rs2 Cache Debugger NPC Overlay with configurable rendering and filtering. - * Extends the base NPC cache overlay with config-driven customization. - * - * @author Vox - * @version 3.0 - Cache Debugging Focus - */ -public class Rs2CacheDebuggerNpcOverlay extends Rs2NpcCacheOverlay { - - private Rs2CacheDebuggerConfig config; - @Inject - public Rs2CacheDebuggerNpcOverlay(Client client, ModelOutlineRenderer modelOutlineRenderer) { - super(client, modelOutlineRenderer); - } - - /** - * Set the configuration for this overlay - */ - public void setConfig(Rs2CacheDebuggerConfig config) { - this.config = config; - updateRenderingOptions(); - } - - /** - * Set the render filter for NPCs - */ - public Rs2CacheDebuggerNpcOverlay setRenderFilter(Predicate filter) { - // Apply the filter to the parent class - super.setRenderFilter(filter); - return this; - } - - /** - * Update rendering options based on config - */ - private void updateRenderingOptions() { - if (config == null) return; - - // Update rendering options based on config - setRenderHull(config.npcRenderStyle() == RenderStyle.HULL || - config.npcRenderStyle() == RenderStyle.BOTH); - setRenderTile(config.npcRenderStyle() == RenderStyle.TILE || - config.npcRenderStyle() == RenderStyle.BOTH); - setRenderName(config.npcShowNames()); - setRenderOutline(config.npcRenderStyle() == RenderStyle.OUTLINE); - - // New configuration options - setRenderNpcInfo(config.npcShowId()); - setRenderWorldCoordinates(config.npcShowCoordinates()); - setRenderCombatLevel(config.npcShowCombatLevel()); - setRenderDistance(config.npcShowDistance()); - } - - @Override - protected Color getBorderColorForNpc(Rs2NpcModel npcModel) { - if (config == null) { - return super.getBorderColorForNpc(npcModel); - } - - // Check for specific NPC categories first - Color categoryColor = getNpcCategoryColor(npcModel); - if (categoryColor != null) { - return categoryColor; - } - - // Fallback to config default - return config.npcBorderColor(); - } - - @Override - protected Color getFillColorForNpc(Rs2NpcModel npcModel) { - Color borderColor = getBorderColorForNpc(npcModel); - return new Color(borderColor.getRed(), borderColor.getGreen(), borderColor.getBlue(), 50); - } - - /** - * Gets color based on NPC category (bank, shop, combat, etc.) - * - * @param npcModel The NPC model - * @return Category-specific color or null if no category match - */ - private Color getNpcCategoryColor(Rs2NpcModel npcModel) { - String name = npcModel.getName().toLowerCase(); - - // Get actions from the NPC composition - NPCComposition composition = npcModel.getTransformedComposition(); - String[] actions = null; - if (composition != null) { - actions = composition.getActions(); - } - - // Bank NPCs - if (hasAction(actions, "bank") || name.contains("banker")) { - return Color.GREEN; - } - - // Shop NPCs - if (hasAction(actions, "trade") || hasAction(actions, "shop") || name.contains("shop")) { - return Color.CYAN; - } - - // Combat NPCs (aggressive or high level) - if (npcModel.getCombatLevel() > 100) { - return Color.RED; - } - - // Training NPCs (low level combat) - if (npcModel.getCombatLevel() > 0 && npcModel.getCombatLevel() <= 100) { - return Color.ORANGE; - } - - return null; // No category match - } - - /** - * Checks if an action array contains a specific action - * - * @param actions The actions array - * @param action The action to look for - * @return true if the action exists - */ - private boolean hasAction(String[] actions, String action) { - if (actions == null || action == null) { - return false; - } - - for (String a : actions) { - if (a != null && a.toLowerCase().contains(action.toLowerCase())) { - return true; - } - } - return false; - } - - /** - * Get the interacting color for NPCs targeting the player - */ - public Color getInteractingColor() { - if (config != null) { - return config.npcInteractingColor(); - } - return Color.RED; // Default fallback - } - - @Override - public Dimension render(Graphics2D graphics) { - if (config == null || !config.enableNpcOverlay()) { - return null; - } - - // Update configuration - updateRenderingOptions(); - - return super.render(graphics); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerObjectOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerObjectOverlay.java deleted file mode 100644 index c7804609964..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerObjectOverlay.java +++ /dev/null @@ -1,231 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.rs2cachedebugger; - -import net.runelite.api.*; -import net.runelite.client.plugins.microbot.util.cache.overlay.Rs2ObjectCacheOverlay; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel; -import net.runelite.client.ui.overlay.outline.ModelOutlineRenderer; -import java.awt.*; -import java.util.function.Predicate; - -import javax.inject.Inject; - -import lombok.extern.slf4j.Slf4j; - -/** - * Rs2 Cache Debugger Object Overlay with configurable rendering and filtering. - * Extends the base Object cache overlay with config-driven customization. - * - * @author Vox - * @version 3.0 - Cache Debugging Focus - */ -@Slf4j -public class Rs2CacheDebuggerObjectOverlay extends Rs2ObjectCacheOverlay { - - private Rs2CacheDebuggerConfig config; - @Inject - public Rs2CacheDebuggerObjectOverlay(Client client, ModelOutlineRenderer modelOutlineRenderer) { - super(client, modelOutlineRenderer); - } - - /** - * Set the configuration for this overlay - */ - public void setConfig(Rs2CacheDebuggerConfig config) { - this.config = config; - updateRenderingOptions(); - } - - /** - * Set the render filter for Objects - */ - public Rs2CacheDebuggerObjectOverlay setRenderFilter(Predicate filter) { - // Apply the filter to the parent class - super.setRenderFilter(filter); - return this; - } - - /** - * Update rendering options based on config - */ - private void updateRenderingOptions() { - if (config == null) return; - - // Update rendering options based on config - setRenderHull(config.objectRenderStyle() == RenderStyle.HULL || - config.objectRenderStyle() == RenderStyle.BOTH); - setRenderTile(config.objectRenderStyle() == RenderStyle.TILE || - config.objectRenderStyle() == RenderStyle.BOTH); - setRenderClickbox(config.objectRenderStyle() == RenderStyle.CLICKBOX); - setRenderOutline(config.objectRenderStyle() == RenderStyle.OUTLINE); - - // New configuration options - setRenderObjectInfo(config.objectShowId()); - setRenderObjectName(config.objectShowNames()); - setRenderWorldCoordinates(config.objectShowCoordinates()); - setOnlyShowTextOnHover(true); // Always show text only on hover for better UX - - // Enable/disable object types based on config - setEnableGameObjects(config.showGameObjects()); - setEnableWallObjects(config.showWallObjects()); - setEnableDecorativeObjects(config.showDecorativeObjects()); - setEnableGroundObjects(config.showGroundObjects()); - - // Configure text rendering style for debugging - } - - @Override - protected Color getBorderColorForObject(Rs2ObjectModel objectModel) { - if (config == null) { - if (log.isDebugEnabled()) { - log.debug("Config is null, using super.getBorderColorForObject for object {}", objectModel.getId()); - } - return super.getBorderColorForObject(objectModel); - } - - - - // Check for object category-based coloring first (if enabled) - if (config.enableObjectCategoryColoring()) { - Color categoryColor = getObjectCategoryColor(objectModel); - if (categoryColor != null) { - - return categoryColor; - } - } - - // Check for object type-based coloring (if enabled) - if (config.enableObjectTypeColoring()) { - Color typeColor = getBorderColorForObjectType(objectModel.getObjectType()); - if (typeColor != null) { - return typeColor; - } - } - // Fallback to config default - Color defaultColor = config.objectBorderColor(); - return defaultColor; - } - - @Override - protected Color getBorderColorForObjectType(Rs2ObjectModel.ObjectType objectType) { - if (config == null || !config.enableObjectTypeColoring()) { - return super.getBorderColorForObjectType(objectType); - } - - // Use the new config options for different object types - switch (objectType) { - case GAME_OBJECT: - return config.gameObjectColor(); - case WALL_OBJECT: - return config.wallObjectColor(); - case DECORATIVE_OBJECT: - return config.decorativeObjectColor(); - case GROUND_OBJECT: - return config.groundObjectColor(); - case TILE_OBJECT: - return new Color(255, 165, 0); // Orange - same as TILE_OBJECT_COLOR - default: - return config.objectBorderColor(); - } - } - - @Override - protected Color getFillColorForObject(Rs2ObjectModel objectModel) { - Color borderColor = getBorderColorForObject(objectModel); - return new Color(borderColor.getRed(), borderColor.getGreen(), borderColor.getBlue(), 50); - } - - /** - * Gets color based on object category (bank, altar, resource, etc.) - * - * @param objectModel The object model - * @return Category-specific color or null if no category match - */ - private Color getObjectCategoryColor(Rs2ObjectModel objectModel) { - String name = objectModel.getName().toLowerCase(); - String[] actions = objectModel.getActions(); - - // Bank objects - if (name.contains("bank") || hasAction(actions, "bank") || hasAction(actions, "collect")) { - return config.bankColor(); - } - - // Altar objects - if (name.contains("altar") || hasAction(actions, "pray-at") || hasAction(actions, "pray")) { - return config.altarColor(); - } - - // Resource objects (trees, rocks, fishing spots, etc.) - if (isResourceObject(name, actions)) { - return config.resourceColor(); - } - - return null; // No category match - } - - /** - * Checks if an object is a resource object (trees, rocks, fishing spots, etc.) - * - * @param name The object name (lowercase) - * @param actions The object actions - * @return true if this is a resource object - */ - private boolean isResourceObject(String name, String[] actions) { - // Trees - if (name.contains("tree") || name.contains("log") || hasAction(actions, "chop")) { - return true; - } - - // Rocks and mining - if (name.contains("rock") || name.contains("ore") || hasAction(actions, "mine")) { - return true; - } - - // Fishing spots - if (name.contains("fishing") || name.contains("pool") || hasAction(actions, "fish")) { - return true; - } - - // Other resources - if (hasAction(actions, "pick") || hasAction(actions, "harvest") || hasAction(actions, "gather")) { - return true; - } - - return false; - } - - /** - * Checks if an action array contains a specific action - * - * @param actions The actions array - * @param action The action to look for - * @return true if the action exists - */ - private boolean hasAction(String[] actions, String action) { - if (actions == null || action == null) { - return false; - } - - for (String a : actions) { - if (a != null && a.toLowerCase().contains(action.toLowerCase())) { - return true; - } - } - return false; - } - - @Override - public Dimension render(Graphics2D graphics) { - if (config == null || !config.enableObjectOverlay()) { - return null; - } - try{ - // Update configuration - updateRenderingOptions(); - - return super.render(graphics); - } catch (Exception e) { - log.error("Error rendering Rs2CacheDebuggerObjectOverlay: {}", e.getMessage(), e); - return null; - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerPlugin.java deleted file mode 100644 index 34213f8da1a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/rs2cachedebugger/Rs2CacheDebuggerPlugin.java +++ /dev/null @@ -1,637 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.rs2cachedebugger; - -import com.google.inject.Provides; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.events.GameTick; -import net.runelite.client.config.ConfigManager; -import net.runelite.client.events.ConfigChanged; -import net.runelite.client.input.KeyManager; -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.util.cache.*; -import net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel; -import net.runelite.client.ui.overlay.OverlayManager; -import net.runelite.client.util.HotkeyListener; -import net.runelite.client.eventbus.Subscribe; - -import javax.inject.Inject; - -/** - * Rs2 Cache Debugger Plugin providing comprehensive cache debugging and entity information overlays. - * Features configurable overlays for NPCs, objects, and ground items with preset filters, - * custom colors, render styles, detailed cache statistics, and performance monitoring. - * - * This plugin focuses on debugging and visualizing the Rs2Cache system for NPCs, Objects, and Ground Items. - * - * @author Vox - * @version 3.0 - Cache Debugging Focus - */ -@PluginDescriptor( - name = "Rs2 Cache Debugger", - description = "Debug and visualize Rs2Cache system with configurable overlays, cache statistics, and performance monitoring", - tags = {"cache", "debug", "overlay", "npc", "object", "ground items", "performance", "vox"}, - enabledByDefault = false, - disableOnStartUp = true -) -@Slf4j -public class Rs2CacheDebuggerPlugin extends Plugin { - - @Inject - private Rs2CacheDebuggerConfig config; - - @Inject - private OverlayManager overlayManager; - - @Inject - private KeyManager keyManager; - - @Inject - private Rs2CacheDebuggerNpcOverlay npcOverlay; - - @Inject - private Rs2CacheDebuggerObjectOverlay objectOverlay; - - @Inject - private Rs2CacheDebuggerGroundItemOverlay groundItemOverlay; - - @Inject - private Rs2CacheDebuggerInfoPanel infoPanel; - - // State tracking - private boolean npcOverlayEnabled = false; - private boolean objectOverlayEnabled = false; - private boolean groundItemOverlayEnabled = false; - private boolean infoPanelEnabled = false; - - - - // Performance tracking - private int tickCounter = 0; - private long lastCacheStatsLogTime = 0; - - // Hotkey listeners - private final HotkeyListener toggleNpcOverlayListener = new HotkeyListener(() -> config.toggleNpcOverlayHotkey()) { - @Override - public void hotkeyPressed() { - toggleNpcOverlay(); - } - }; - - private final HotkeyListener toggleObjectOverlayListener = new HotkeyListener(() -> config.toggleObjectOverlayHotkey()) { - @Override - public void hotkeyPressed() { - toggleObjectOverlay(); - } - }; - - private final HotkeyListener toggleGroundItemOverlayListener = new HotkeyListener(() -> config.toggleGroundItemOverlayHotkey()) { - @Override - public void hotkeyPressed() { - toggleGroundItemOverlay(); - } - }; - - private final HotkeyListener logCacheInfoListener = new HotkeyListener(() -> config.logCacheInfoHotkey()) { - @Override - public void hotkeyPressed() { - Microbot.getClientThread().runOnSeperateThread(()-> {logDetailedCacheInfo(); return null;}); - } - }; - - private final HotkeyListener toggleInfoPanelListener = new HotkeyListener(() -> config.toggleInfoPanelHotkey()) { - @Override - public void hotkeyPressed() { - toggleInfoPanel(); - } - }; - - @Provides - Rs2CacheDebuggerConfig provideConfig(ConfigManager configManager) { - return configManager.getConfig(Rs2CacheDebuggerConfig.class); - } - - @Override - protected void startUp() throws Exception { - log.info("Starting Rs2 Cache Debugger Plugin"); - - - - // Configure overlays with config - configureOverlays(); - - // Register hotkey listeners - keyManager.registerKeyListener(toggleNpcOverlayListener); - keyManager.registerKeyListener(toggleObjectOverlayListener); - keyManager.registerKeyListener(toggleGroundItemOverlayListener); - keyManager.registerKeyListener(logCacheInfoListener); - keyManager.registerKeyListener(toggleInfoPanelListener); - - // Enable overlays based on config - if (config.enableNpcOverlay()) { - enableNpcOverlay(); - } - - if (config.enableObjectOverlay()) { - enableObjectOverlay(); - } - - if (config.enableGroundItemOverlay()) { - enableGroundItemOverlay(); - } - - if (config.enableInfoPanel()) { - enableInfoPanel(); - } - - // Reset performance counters - tickCounter = 0; - lastCacheStatsLogTime = System.currentTimeMillis(); - - log.info("Rs2 Cache Debugger Plugin started successfully"); - } - - @Override - protected void shutDown() throws Exception { - log.info("Shutting down Rs2 Cache Debugger Plugin"); - - // Unregister hotkey listeners - keyManager.unregisterKeyListener(toggleNpcOverlayListener); - keyManager.unregisterKeyListener(toggleObjectOverlayListener); - keyManager.unregisterKeyListener(toggleGroundItemOverlayListener); - keyManager.unregisterKeyListener(logCacheInfoListener); - keyManager.unregisterKeyListener(toggleInfoPanelListener); - - // Disable all overlays - disableNpcOverlay(); - disableObjectOverlay(); - disableGroundItemOverlay(); - disableInfoPanel(); - - log.info("Rs2 Cache Debugger Plugin shut down successfully"); - } - - /** - * Configure overlays with current config settings - */ - private void configureOverlays() { - // Configure NPC overlay - npcOverlay.setConfig(config); - npcOverlay.setRenderFilter(createNpcFilter()); - - // Configure Object overlay - objectOverlay.setConfig(config); - objectOverlay.setRenderFilter(createObjectFilter()); - - // Configure Ground Item overlay - groundItemOverlay.setConfig(config); - groundItemOverlay.setRenderFilter(createGroundItemFilter()); - - // Configure Info Panel - infoPanel.setConfig(config); - } - - /** - * Create NPC filter based on config - */ - private java.util.function.Predicate createNpcFilter() { - NpcFilterPreset preset = config.npcFilterPreset(); - String customFilter = config.npcCustomFilter(); - - return npcModel -> { - if (npcModel == null) { - return false; - } - - // Apply preset filter - if (!preset.test(npcModel)) { - return false; - } - - // Apply custom filter if specified - if (customFilter != null && !customFilter.trim().isEmpty()) { - String npcName = npcModel.getName(); - if (npcName == null) { - return false; - } - return npcName.toLowerCase().contains(customFilter.toLowerCase()); - } - - return true; - }; - } - - /** - * Create Object filter based on config - */ - private java.util.function.Predicate createObjectFilter() { - ObjectFilterPreset preset = config.objectFilterPreset(); - String customFilter = config.objectCustomFilter(); - - return objectModel -> { - if (objectModel == null || objectModel.getTileObject() == null) { - return false; - } - - // Check object type visibility settings - switch (objectModel.getObjectType()) { - case GAME_OBJECT: - if (!config.showGameObjects()) { - return false; - } - break; - case WALL_OBJECT: - if (!config.showWallObjects()) { - return false; - } - break; - case DECORATIVE_OBJECT: - if (!config.showDecorativeObjects()) { - return false; - } - break; - case GROUND_OBJECT: - if (!config.showGroundObjects()) { - return false; - } - break; - default: - return false; // Unknown object type - } - - // Apply distance filtering - int maxDistance = config.objectMaxDistance(); - if (objectModel.getDistanceFromPlayer() > maxDistance) { - return false; - } - - // Apply preset filter - if (!preset.test(objectModel)) { - return false; - } - - // Apply custom filter if specified - if (customFilter != null && !customFilter.trim().isEmpty()) { - // Check object name or ID - String objectName = objectModel.getName(); - if (objectName == null) { - return false; - } - return objectName.toLowerCase().contains(customFilter.toLowerCase()); - } - - return true; - }; - } - - /** - * Create Ground Item filter based on config - */ - private java.util.function.Predicate createGroundItemFilter() { - GroundItemFilterPreset preset = config.groundItemFilterPreset(); - String customFilter = config.groundItemCustomFilter(); - - return itemModel -> { - if (itemModel == null) { - return false; - } - - // Apply preset filter - if (!preset.test(itemModel)) { - return false; - } - - // Apply custom filter if specified - if (customFilter != null && !customFilter.trim().isEmpty()) { - String itemName = itemModel.getName(); - if (itemName == null) { - return false; - } - return itemName.toLowerCase().contains(customFilter.toLowerCase()); - } - - return true; - }; - } - - // Overlay control methods - public void toggleNpcOverlay() { - if (npcOverlayEnabled) { - disableNpcOverlay(); - } else { - enableNpcOverlay(); - } - } - - public void enableNpcOverlay() { - if (!npcOverlayEnabled) { - overlayManager.add(npcOverlay); - npcOverlayEnabled = true; - log.debug("NPC cache overlay enabled"); - } - } - - public void disableNpcOverlay() { - if (npcOverlayEnabled) { - overlayManager.remove(npcOverlay); - npcOverlayEnabled = false; - log.debug("NPC cache overlay disabled"); - } - } - - public void toggleObjectOverlay() { - if (objectOverlayEnabled) { - disableObjectOverlay(); - } else { - enableObjectOverlay(); - } - } - - public void enableObjectOverlay() { - if (!objectOverlayEnabled) { - overlayManager.add(objectOverlay); - objectOverlayEnabled = true; - log.debug("Object cache overlay enabled"); - } - } - - public void disableObjectOverlay() { - if (objectOverlayEnabled) { - overlayManager.remove(objectOverlay); - objectOverlayEnabled = false; - log.debug("Object cache overlay disabled"); - } - } - - public void toggleGroundItemOverlay() { - if (groundItemOverlayEnabled) { - disableGroundItemOverlay(); - } else { - enableGroundItemOverlay(); - } - } - - public void enableGroundItemOverlay() { - if (!groundItemOverlayEnabled) { - overlayManager.add(groundItemOverlay); - groundItemOverlayEnabled = true; - log.debug("Ground item cache overlay enabled"); - } - } - - public void disableGroundItemOverlay() { - if (groundItemOverlayEnabled) { - overlayManager.remove(groundItemOverlay); - groundItemOverlayEnabled = false; - log.debug("Ground item cache overlay disabled"); - } - } - - public void toggleInfoPanel() { - if (infoPanelEnabled) { - disableInfoPanel(); - } else { - enableInfoPanel(); - } - } - - public void enableInfoPanel() { - if (!infoPanelEnabled) { - overlayManager.add(infoPanel); - infoPanelEnabled = true; - log.debug("Cache info panel enabled"); - } - } - - public void disableInfoPanel() { - if (infoPanelEnabled) { - overlayManager.remove(infoPanel); - infoPanelEnabled = false; - log.debug("Cache info panel disabled"); - } - } - - /** - * Log detailed cache information using the new unified logging system - */ - public void logDetailedCacheInfo() { - log.info("=== Rs2 Cache Debugger - Detailed Cache Analysis ==="); - - boolean dumpToFile = config.showCachePerformanceMetrics(); // Use performance metrics config to determine file dumping - LogOutputMode outputMode = - dumpToFile ?LogOutputMode.BOTH - : LogOutputMode.CONSOLE_ONLY; - // Use new unified logging for all caches - log.info("Generating detailed cache state reports..."); - - // NPC Cache State - if (Rs2NpcCache.getInstance() != null) { - log.info("--- NPC Cache Analysis ---"); - // Use new LogOutputMode for better control - - Rs2NpcCache.logState(outputMode); - } - // Quest Cache State - if (Rs2QuestCache.getInstance() != null) { - log.info("--- Quest Cache Analysis ---"); - Rs2QuestCache.logState(outputMode); - } - - // Object Cache State - if (Rs2ObjectCache.getInstance() != null) { - log.info("--- Object Cache Analysis ---"); - Rs2ObjectCache.logState(outputMode); - } - - // Ground Item Cache State - if (Rs2GroundItemCache.getInstance() != null) { - log.info("--- Ground Item Cache Analysis ---"); - Rs2GroundItemCache.logState(outputMode); - } - - // Skill Cache State - if (Rs2SkillCache.getInstance() != null) { - log.info("--- Skill Cache Analysis ---"); - Rs2SkillCache.logState(outputMode); - } - - // Varbit Cache State - if (Rs2VarbitCache.getInstance() != null) { - log.info("--- Varbit Cache Analysis ---"); - Rs2VarbitCache.logState(outputMode); - } - - // VarPlayer Cache State - if (Rs2VarPlayerCache.getInstance() != null) { - log.info("--- VarPlayer Cache Analysis ---"); - Rs2VarPlayerCache.logState(outputMode); - } - - // Quest Cache State - if (Rs2QuestCache.getInstance() != null) { - log.info("--- Quest Cache Analysis ---"); - Rs2QuestCache.logState(outputMode); - } - - // Plugin State Summary - log.info("--- Plugin State Summary ---"); - log.info("Active Overlays - NPC: {}, Object: {}, Ground Items: {}, Info Panel: {}", - npcOverlayEnabled, objectOverlayEnabled, groundItemOverlayEnabled, infoPanelEnabled); - - if (dumpToFile) { - log.info("Cache state files written to: ~/.runelite/microbot-plugins/cache/"); - } - - log.info("=== End Cache Analysis ==="); - } - - - - @Subscribe - public void onGameTick(GameTick gameTick) { - tickCounter++; - - // Update overlay configurations if they changed - configureOverlays(); - - // Log cache statistics at configured interval - if (config.logCacheStatsTicks() > 0 && tickCounter % config.logCacheStatsTicks() == 0) { - logPeriodicCacheStats(); - } - } - - /** - * Log periodic cache statistics - */ - private void logPeriodicCacheStats() { - if (!config.verboseLogging()) { - return; - } - - long currentTime = System.currentTimeMillis(); - long timeSinceLastLog = currentTime - lastCacheStatsLogTime; - - log.debug("Periodic Cache Stats ({}ms interval):", timeSinceLastLog); - - if (Rs2NpcCache.getInstance() != null) { - var npcStats = Rs2NpcCache.getInstance().getStatistics(); - log.debug(" NPC Cache: {} entries, {} hits, {} misses", - npcStats.currentSize, npcStats.cacheHits, npcStats.cacheMisses); - } - - if (Rs2ObjectCache.getInstance() != null) { - var objectStats = Rs2ObjectCache.getInstance().getStatistics(); - log.debug(" Object Cache: {} entries, {} hits, {} misses", - objectStats.currentSize, objectStats.cacheHits, objectStats.cacheMisses); - } - - if (Rs2GroundItemCache.getInstance() != null) { - var groundItemStats = Rs2GroundItemCache.getInstance().getStatistics(); - log.debug(" Ground Item Cache: {} entries, {} hits, {} misses", - groundItemStats.currentSize, groundItemStats.cacheHits, groundItemStats.cacheMisses); - } - - lastCacheStatsLogTime = currentTime; - } - - @Subscribe - public void onConfigChanged(ConfigChanged configChanged) { - if (!configChanged.getGroup().equals("rs2cachedebugger")) { - return; - } - - String key = configChanged.getKey(); - - // Handle overlay enable/disable toggles - switch (key) { - case "enableNpcOverlay": - if (config.enableNpcOverlay()) { - enableNpcOverlay(); - } else { - disableNpcOverlay(); - } - break; - - case "enableObjectOverlay": - if (config.enableObjectOverlay()) { - enableObjectOverlay(); - } else { - disableObjectOverlay(); - } - break; - - case "enableGroundItemOverlay": - if (config.enableGroundItemOverlay()) { - enableGroundItemOverlay(); - } else { - disableGroundItemOverlay(); - } - break; - - case "enableInfoPanel": - if (config.enableInfoPanel()) { - enableInfoPanel(); - } else { - disableInfoPanel(); - } - break; - - // Handle filter changes that require reconfiguration - case "npcFilterPreset": - case "npcCustomFilter": - case "npcRenderStyle": - case "npcBorderColor": - case "objectFilterPreset": - case "objectCustomFilter": - case "objectRenderStyle": - case "objectBorderColor": - case "objectShowId": - case "objectShowCoordinates": - case "objectMaxDistance": - case "showGameObjects": - case "showWallObjects": - case "showDecorativeObjects": - case "showGroundObjects": - case "enableObjectTypeColoring": - case "enableObjectCategoryColoring": - case "gameObjectColor": - case "wallObjectColor": - case "decorativeObjectColor": - case "groundObjectColor": - case "bankColor": - case "altarColor": - case "resourceColor": - case "groundItemFilterPreset": - case "groundItemCustomFilter": - case "groundItemRenderStyle": - case "groundItemBorderColor": - configureOverlays(); - break; - } - } - - // Getters for overlay state - public boolean isNpcOverlayEnabled() { - return npcOverlayEnabled; - } - - public boolean isObjectOverlayEnabled() { - return objectOverlayEnabled; - } - - public boolean isGroundItemOverlayEnabled() { - return groundItemOverlayEnabled; - } - - public boolean isInfoPanelEnabled() { - return infoPanelEnabled; - } - - // Getters for performance monitoring - public int getTickCounter() { - return tickCounter; - } - - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/LocationStartNotificationOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/LocationStartNotificationOverlay.java deleted file mode 100644 index fc89dbd585d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/LocationStartNotificationOverlay.java +++ /dev/null @@ -1,145 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example; - -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -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.OverlayPriority; -import net.runelite.client.ui.overlay.components.LineComponent; -import net.runelite.client.ui.overlay.components.PanelComponent; -import net.runelite.client.ui.overlay.components.TitleComponent; - -import java.awt.*; - -/** - * Displays information about location-based start conditions - */ -public class LocationStartNotificationOverlay extends Overlay { - private final SchedulableExamplePlugin plugin; - private final SchedulableExampleConfig config; - private final PanelComponent panelComponent = new PanelComponent(); - - public LocationStartNotificationOverlay(SchedulableExamplePlugin plugin, SchedulableExampleConfig config) { - super(plugin); - setPosition(OverlayPosition.TOP_LEFT); - setLayer(OverlayLayer.ABOVE_SCENE); - setPriority(OverlayPriority.MED); - this.plugin = plugin; - this.config = config; - } - - @Override - public Dimension render(Graphics2D graphics) { - if (!Microbot.isLoggedIn() || !config.enableLocationStartCondition()) { - return null; - } - - panelComponent.getChildren().clear(); - - // Show title - panelComponent.getChildren().add(TitleComponent.builder() - .text("Location Conditions") - .color(Color.WHITE) - .build()); - - if (config.locationStartType() == SchedulableExampleConfig.LocationStartType.BANK) { - // Bank location information - panelComponent.getChildren().add(LineComponent.builder() - .left("Type:") - .right("Bank Location") - .build()); - - panelComponent.getChildren().add(LineComponent.builder() - .left("Target:") - .right(config.bankStartLocation().name()) - .build()); - - panelComponent.getChildren().add(LineComponent.builder() - .left("Distance:") - .right(config.bankDistance() + " tiles") - .build()); - - // Check and show if condition is met - boolean inRange = isNearBank(); - Color statusColor = inRange ? Color.GREEN : Color.RED; - panelComponent.getChildren().add(LineComponent.builder() - .left("Status:") - .right(inRange ? "In Range" : "Out of Range") - .rightColor(statusColor) - .build()); - - } else if (config.locationStartType() == SchedulableExampleConfig.LocationStartType.CUSTOM_AREA) { - // Custom area information - panelComponent.getChildren().add(LineComponent.builder() - .left("Type:") - .right("Custom Area") - .build()); - - if (config.customAreaActive() && config.customAreaCenter() != null) { - WorldPoint center = config.customAreaCenter(); - panelComponent.getChildren().add(LineComponent.builder() - .left("Center:") - .right(center.getX() + ", " + center.getY() + ", " + center.getPlane()) - .build()); - - panelComponent.getChildren().add(LineComponent.builder() - .left("Radius:") - .right(config.customAreaRadius() + " tiles") - .build()); - - // Check and show if condition is met - boolean inArea = plugin.isPlayerInCustomArea(); - Color statusColor = inArea ? Color.GREEN : Color.RED; - panelComponent.getChildren().add(LineComponent.builder() - .left("Status:") - .right(inArea ? "In Area" : "Out of Area") - .rightColor(statusColor) - .build()); - - // Show distance to center if not in area - if (!inArea) { - - WorldPoint playerPos = Rs2Player.getWorldLocation(); - if (playerPos != null) { - int distance = playerPos.distanceTo(center); - panelComponent.getChildren().add(LineComponent.builder() - .left("Distance:") - .right(distance + " tiles away") - .build()); - } - } - } else { - panelComponent.getChildren().add(LineComponent.builder() - .left("Status:") - .right("No Area Defined") - .rightColor(Color.YELLOW) - .build()); - - panelComponent.getChildren().add(LineComponent.builder() - .left("Help:") - .right("Press hotkey to mark area") - .build()); - } - } - - return panelComponent.render(graphics); - } - - /** - * Checks if the player is near the configured bank - */ - private boolean isNearBank() { - WorldPoint playerPos = Rs2Player.getWorldLocation(); - if (playerPos == null) { - return false; - } - - WorldPoint bankPos = config.bankStartLocation().getWorldPoint(); - int maxDistance = config.bankDistance(); - - return (playerPos.getPlane() == bankPos.getPlane() && - playerPos.distanceTo(bankPos) <= maxDistance); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/README.md b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/README.md deleted file mode 100644 index a367c3a8b72..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/README.md +++ /dev/null @@ -1,499 +0,0 @@ -# SchedulableExamplePlugin Documentation - -## Overview -The `SchedulableExamplePlugin` demonstrates how to create a plugin compatible with Microbot's scheduler system. It implements the `ConditionProvider` interface to define configurable conditions for when the plugin should automatically start and stop based on various in-game criteria. This plugin serves as a comprehensive example for developers wanting to create scripts that can be managed by the scheduler framework, providing a template for implementing different types of conditions and state management approaches. - -The plugin provides a practical implementation of conditional logic that can be used to automate tasks in a controlled manner. By integrating with the scheduler, it enables users to create complex automation workflows where multiple plugins can work together in a sequenced manner, each starting and stopping based on specific game conditions. - -## Key Features -- **Seamless integration** with the Microbot scheduler framework -- **Highly configurable** start and stop conditions -- **Location-based start conditions**: - - Bank locations with configurable distance - - Custom areas with adjustable radius -- **Multiple stop condition types**: - - **Time-based**: Run for specified duration - - **Resource collection**: Stop after gathering specific items - - **Loot collection**: Stop after collecting specific drops - - **Item processing**: Stop after crafting/converting items - - **NPC kill counting**: Stop after killing a specific number of NPCs - -## Architecture -The plugin consists of four primary components that work together to provide a complete implementation of a scheduler-compatible plugin: - -1. **SchedulableExamplePlugin** (`SchedulableExamplePlugin.java`) - - Main plugin class implementing `ConditionProvider` and `KeyListener` - - Manages the plugin lifecycle and condition creation - - Handles hotkey inputs for custom area marking - - Serves as the central orchestrator that connects configuration, script execution, and condition evaluation - - Implements the scheduler integration points through the `ConditionProvider` methods - - Maintains state between sessions by saving and loading world locations - -2. **SchedulableExampleConfig** (`SchedulableExampleConfig.java`) - - Configuration interface with `@ConfigGroup` and `@ConfigItem` annotations - - Defines all configurable parameters for the plugin - - Organizes settings into logical sections using `@ConfigSection` annotations - - Provides default values for configuration options - - Includes setter methods for mutable state (like custom area coordinates) - - Uses enums to define valid option sets (like `LocationStartType` and `ProcessTrackingMode`) - - Implements hidden configuration items for internal state persistence - - Creates a hierarchical organization of settings with sections that can be collapsed by default - -3. **LocationStartNotificationOverlay** (`LocationStartNotificationOverlay.java`) - - Visual overlay displaying location-based start condition information - - Provides real-time feedback on condition status - - Uses RuneLite's overlay system to render information on the game screen - - Dynamically updates to show relevant details based on the current configuration - - Shows information about bank locations or custom areas depending on the active configuration - - Implements color-coded status indicators (green for met conditions, red for unmet) - - Displays distance measurements to target locations when relevant - - Updates in real-time as the player moves around the game world - - Provides helpful guidance messages for setting up custom areas - -## Condition Provider Implementation -The `ConditionProvider` interface is the key integration point between the plugin and the scheduler system. By implementing this interface, the plugin can define under what circumstances it should be automatically started or stopped by the scheduler. This provides a powerful abstraction that allows the scheduler to manage multiple plugins without needing to understand their specific functionality. - -### Stop Conditions -The `getStopCondition()` method returns a logical condition determining when the plugin should automatically stop. This method is called by the scheduler to evaluate whether the plugin should be terminated. The implementation combines multiple condition types into a single logical expression that can be evaluated to determine if stopping criteria have been met: - -```java -@Override -public LogicalCondition getStopCondition() { - // Create an OR condition - we'll stop when ANY of the enabled conditions are met - OrCondition orCondition = new OrCondition(); - - // Add enabled conditions based on configuration - if (config.enableTimeCondition()) { - orCondition.addCondition(createTimeCondition()); - } - - if (config.enableLootItemCondition()) { - orCondition.addCondition(createLootItemCondition()); - } - - // Add more conditions... - - // If no conditions were added, add a fallback time condition - if (orCondition.getConditions().isEmpty()) { - orCondition.addCondition(IntervalCondition.createRandomized(Duration.ofMinutes(5), Duration.ofMinutes(5))); - } - - return orCondition; -} -``` - -### Start Conditions -The `getStartCondition()` method returns a logical condition determining when the plugin is allowed to start. The scheduler uses this to determine if the plugin should be automatically started when it's scheduled to run. Unlike stop conditions which should always return a value, start conditions can return `null` to indicate that the plugin can start without any preconditions: - -```java -@Override -public LogicalCondition getStartCondition() { - // Default to no start conditions (always allowed to start) - if (!config.enableLocationStartCondition()) { - return null; - } - - // Create a logical condition for start conditions - LogicalCondition startCondition = null; - - // Create location-based condition based on selected type - if (config.locationStartType() == SchedulableExampleConfig.LocationStartType.BANK) { - // Bank-based start condition - // ... - } else if (config.locationStartType() == SchedulableExampleConfig.LocationStartType.CUSTOM_AREA) { - // Custom area start condition - // ... - } - - return startCondition; -} -``` - -## Detailed Condition Types -The plugin implements several types of conditions that can be used to control when it should start or stop. Each condition type is implemented as a separate method that creates and configures a condition object based on the current plugin configuration. - -### Time Condition -The time condition is the simplest form of stop condition, which will trigger after a specified duration has elapsed. This is useful for limiting the runtime of a plugin to prevent excessive resource usage or to simulate human-like play patterns with regular breaks: -```java -private Condition createTimeCondition() { - int minMinutes = config.minRuntime(); - int maxMinutes = config.maxRuntime(); - - return IntervalCondition.createRandomized( - Duration.ofMinutes(minMinutes), - Duration.ofMinutes(maxMinutes) - ); -} -``` - -### Loot Item Condition -The loot item condition is used to stop the plugin after collecting a specific number of items. This is particularly useful for gathering activities where you want to collect a certain amount of a resource before stopping. The condition supports both AND and OR logical operations, allowing for complex item collection goals: - -```java -private LogicalCondition createLootItemCondition() { - // Parse the comma-separated list of items - List lootItemsList = parseItemList(config.lootItems()); - - boolean andLogical = config.itemsToLootLogical(); - int minLootItems = config.minItems(); - int maxLootItems = config.maxItems(); - - // Create randomized targets for each item - List minLootItemPerPattern = new ArrayList<>(); - List maxLootItemPerPattern = new ArrayList<>(); - - // Generate target counts... - - // Create the appropriate logical condition based on config - if (andLogical) { - return LootItemCondition.createAndCondition( - lootItemsList, - minLootItemPerPattern, - maxLootItemPerPattern, - includeNoted, - allowNoneOwner - ); - } else { - return LootItemCondition.createOrCondition( - lootItemsList, - minLootItemPerPattern, - maxLootItemPerPattern, - includeNoted, - allowNoneOwner - ); - } -} -``` - -### Gathered Resource Condition -The gathered resource condition is similar to the loot item condition but is specifically designed for tracking resources gathered through skilling activities (mining, fishing, woodcutting, etc.). This allows for more precise tracking of gathering activities and can differentiate between items obtained through different methods: - -```java -private LogicalCondition createGatheredResourceCondition() { - // Parse the comma-separated list of resources - List resourcesList = parseItemList(config.gatheredResources()); - - boolean andLogical = config.resourcesLogical(); - int minResources = config.minResources(); - int maxResources = config.maxResources(); - boolean includeNoted = config.includeResourceNoted(); - - // Create target lists with randomized counts for each resource - List minResourcesPerItem = new ArrayList<>(); - List maxResourcesPerItem = new ArrayList<>(); - - for (String resource : resourcesList) { - int minCount = Rs2Random.between(minResources, maxResources); - int maxCount = Rs2Random.between(minCount, maxResources); - - minResourcesPerItem.add(minCount); - maxResourcesPerItem.add(maxCount); - } - - // Create the appropriate logical condition based on configuration - if (andLogical) { - return GatheredResourceCondition.createAndCondition( - resourcesList, - minResourcesPerItem, - maxResourcesPerItem, - includeNoted - ); - } else { - return GatheredResourceCondition.createOrCondition( - resourcesList, - minResourcesPerItem, - maxResourcesPerItem, - includeNoted - ); - } -} - -### Process Item Condition -The process item condition is designed to track item transformation operations such as crafting, smithing, cooking, and other production skills. It can monitor the consumption of source items, the production of target items, or both, making it versatile for various crafting and production tasks: - -```java -private Condition createProcessItemCondition() { - ProcessItemCondition.TrackingMode trackingMode; - - // Map config enum to condition enum - switch (config.trackingMode()) { - case SOURCE_CONSUMPTION: - trackingMode = ProcessItemCondition.TrackingMode.SOURCE_CONSUMPTION; - break; - // Other modes... - } - - // Create the appropriate process item condition based on tracking mode - if (trackingMode == ProcessItemCondition.TrackingMode.SOURCE_CONSUMPTION) { - // If tracking source consumption - // ... - } - // Other tracking modes... -} -``` - -### NPC Kill Count Condition -The NPC kill count condition monitors the number of NPCs killed during the plugin's execution. It supports pattern matching for NPC names and can be configured to track kills per NPC type or the total kill count across all specified NPCs. This is particularly useful for combat training and slayer task automation: - -```java -private LogicalCondition createNpcKillCountCondition() { - // Parse the comma-separated list of NPC names - List npcNamesList = parseItemList(config.npcNames()); - - boolean andLogical = config.npcLogical(); - int minKills = config.minKills(); - int maxKills = config.maxKills(); - boolean killsPerType = config.killsPerType(); - - // If we're counting per NPC type vs. total kills... -} -``` - -## Custom Area Management -The custom area feature allows users to define a specific area in the game world where the plugin should operate. This is implemented through a combination of configuration settings, hotkey handling, and visual overlay feedback. The custom area is defined as a circle with a configurable radius centered on the player's position when the area is created: - -```java -private void toggleCustomArea() { - if (!Microbot.isLoggedIn()) { - log.info("Cannot toggle custom area: Not logged in"); - return; - } - - boolean isActive = config.customAreaActive(); - - if (isActive) { - // Clear the custom area - config.setCustomAreaActive(false); - config.setCustomAreaCenter(null); - log.info("Custom area removed"); - } else { - // Create new custom area at current position - WorldPoint currentPos = null; - if (Microbot.isLoggedIn()){ - currentPos = Rs2Player.getWorldLocation(); - } - if (currentPos != null) { - config.setCustomAreaCenter(currentPos); - config.setCustomAreaActive(true); - log.info("Custom area created at: " + currentPos.toString() + " with radius: " + config.customAreaRadius()); - } - } -} -``` - -## Integration with Scheduler Events -The plugin integrates with the scheduler system by responding to events dispatched by the scheduler. The most important of these is the `PluginScheduleEntry`, which is triggered when the scheduler determines that a plugin should be stopped based on its stop conditions. The plugin handles this event by performing cleanup operations and then requesting that it be disabled: - -```java -@Override -@Subscribe -public void onPluginScheduleEntry(PluginScheduleEntry event) { - // Save location before stopping - if (event.getPlugin() == this) { - config.setLastLocation(Rs2Player.getWorldLocation()); - log.info("Scheduling stop for plugin: {}", event.getPlugin().getClass().getSimpleName()); - - // Schedule the stop operation on the client thread - Microbot.getClientThread().invokeLater(() -> { - try { - Microbot.getPluginManager().setPluginEnabled(this, false); - Microbot.getPluginManager().stopPlugin(this); - } catch (Exception e) { - log.error("Error stopping plugin", e); - } - }); - } -} -``` - -## Helper Methods -The plugin includes several helper methods that provide utility functionality for various aspects of its operation. These methods encapsulate common operations and logic to improve code readability and maintainability: - -```java -private List parseItemList(String itemsString) { - List itemsList = new ArrayList<>(); - if (itemsString != null && !itemsString.isEmpty()) { - String[] itemsArray = itemsString.split(","); - for (String item : itemsArray) { - String trimmedItem = item.trim(); - try { - // Validate regex pattern - java.util.regex.Pattern.compile(trimmedItem); - itemsList.add(trimmedItem); - log.debug("Valid item pattern found: {}", trimmedItem); - } catch (java.util.regex.PatternSyntaxException e) { - log.warn("Invalid regex pattern: '{}' - {}", trimmedItem, e.getMessage()); - } - } - } - return itemsList; -} -``` - -## Usage Guide - -### Setting Up the Plugin - -1. **Enable the plugin** through RuneLite's plugin manager - - Navigate to the plugin list and locate "Schedulable Example" - - Check the checkbox to enable it - - Note that the plugin can also be enabled by the scheduler when appropriate - -2. **Configure desired start/stop conditions** in the plugin's configuration panel: - - Click the configuration icon next to the plugin name - - Expand the various sections to access different types of conditions - - Configure at least one stop condition to ensure the plugin doesn't run indefinitely - - Common configurations include: - - Set time limits (minimum and maximum runtime) - - Define item collection targets (specific items and quantities) - - Configure NPC kill counts for combat activities - - Set up resource gathering goals for skilling activities - - Define item processing targets for crafting and production - -3. **Set up location-based start conditions** if desired: - - Enable the location start condition option - - Choose between bank location or custom area: - - **Bank Location**: Select a predefined bank location and set the maximum distance - - **Custom Area**: Position your character in the desired location and press the configured area marking hotkey - - The location overlay will show you when you're in a valid start position - - For custom areas, you can adjust the radius to control the size of the valid area - -4. **Start the plugin** in one of two ways: - - Manually start it through the plugin manager - - Let the scheduler start it automatically when scheduled and when start conditions are met - -5. **Monitor the plugin's operation**: - - Watch the status messages in the Microbot status area - - Check the overlay for location-based information - - The plugin will update its progress tracking as it runs - -6. **The plugin will automatically stop** when any of the following occurs: - - Any of the enabled stop conditions are satisfied - - The scheduler sends a stop event - - The plugin is manually disabled through the plugin manager - -## Example Configuration - -This configuration would make the plugin: -- Only start when the player is at the Grand Exchange -- Stop after running for 30-45 minutes OR after collecting 100-200 oak logs (whichever happens first) - -``` -enableLocationStartCondition: true -locationStartType: BANK -bankStartLocation: GRAND_EXCHANGE -bankDistance: 5 - -enableTimeCondition: true -minRuntime: 30 -maxRuntime: 45 - -enableLootItemCondition: true -lootItems: "Oak logs" -minItems: 100 -maxItems: 200 -``` - -## Technical Implementation Notes - -### Core Design Patterns and Principles - -1. **Thread Safety** - - The plugin uses `Microbot.getClientThread().invokeLater()` to ensure operations run on the client thread - - This is critical for preventing race conditions and ensuring proper interaction with the game client - - All UI updates and game state modifications should be performed on the client thread - -2. **State Persistence** - - Configuration state is saved between sessions using RuneLite's ConfigManager - - The plugin maintains state across sessions by saving: - - Last known player location - - Custom area definitions - - Configuration parameters - - This allows seamless continuation of tasks even after client restarts - -3. **Random Variance** - - Stop conditions use randomized ranges to add human-like variability - - The `Rs2Random.between()` utility is used to generate random values within configured ranges - - This prevents predictable patterns that might appear bot-like - - Different randomization approaches are used for different types of conditions - -4. **Pattern Matching** - - Item and NPC name matching supports regular expressions for flexibility - - This allows for powerful pattern matching capabilities like: - - Wildcards (e.g., ".*bones.*" to match any item containing "bones") - - Character classes (e.g., "[A-Za-z]+ logs" to match any type of logs) - - Alternations (e.g., "goblin|rat|spider" to match multiple NPC types) - - Regular expression patterns are validated before use to prevent runtime errors - -5. **Logical Composition** - - Conditions can be combined with AND/OR logic for complex triggering - - The `LogicalCondition` interface and its implementations (`AndCondition`, `OrCondition`) provide a composable framework - - This allows for arbitrarily complex condition trees to be constructed - - Each logical condition can contain any mix of primitive conditions or nested logical conditions - -6. **State Machine Pattern** - - The `SchedulableExampleScript` uses a state machine to manage its operation - - Different states handle different aspects of the script's functionality - - Transitions between states occur based on in-game conditions - - This provides a clear, maintainable structure for complex bot logic - -7. **Event-Driven Architecture** - - The plugin responds to events from the scheduler and game client - - Events trigger state changes and condition evaluations - - This decouples the plugin's logic from the specific timing of game updates - -## Extending the Plugin - -### Adding New Condition Types - -To extend the plugin with new types of conditions: - -1. **Create a new condition class** implementing the `Condition` interface - - Define the logic for when the condition is satisfied - - Implement the `reset()` method to reinitialize the condition's state - - Consider extending existing base classes like `ResourceCondition` if appropriate - -2. **Add configuration options** to `SchedulableExampleConfig` - - Create a new configuration section with `@ConfigSection` if needed - - Add configuration items with `@ConfigItem` annotations - - Define appropriate default values and descriptions - - Consider using enums for options with a fixed set of valid values - -3. **Implement a creation method** in `SchedulableExamplePlugin` - - Create a method that constructs and configures your new condition - - Add appropriate logic to handle configuration options - - Include randomization if appropriate for human-like behavior - - Handle edge cases and provide fallback values - -4. **Add the condition** to the appropriate logical group in `getStopCondition()` - - Check if the condition is enabled in the configuration - - Add it to the existing logical condition structure (typically an `OrCondition`) - - Consider how it interacts with other existing conditions - -### Implementing New Features - -To add entirely new functionality to the plugin: - -1. **Extend the script class** with new methods and state management - - Add new states to the state machine if needed - - Implement the logic for the new functionality - - Update the main loop to handle the new states and operations - -2. **Update the configuration interface** with options for the new features - - Group related settings into logical sections - - Provide clear descriptions and default values - - Add validation where appropriate - -3. **Enhance the overlay** if visual feedback is needed - - Add new information to the overlay rendering - - Consider color coding or other visual cues for status - - Ensure the overlay remains uncluttered and informative - -4. **Add new condition types** if needed for the new functionality - - Follow the steps outlined above for adding conditions - - Ensure the conditions properly integrate with the new features - -5. **Update documentation** to reflect the new capabilities - - Document configuration options - - Explain new condition types - - Provide usage examples \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleConfig.java deleted file mode 100644 index ed6b4c9bb49..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleConfig.java +++ /dev/null @@ -1,889 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example; - -import net.runelite.api.coords.WorldPoint; -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.Keybind; -import net.runelite.client.config.Range; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; -import net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example.enums.UnifiedLocation; -import net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example.enums.SpellbookOption; - -import java.awt.event.KeyEvent; - -@ConfigGroup("SchedulableExample") -public interface SchedulableExampleConfig extends Config { - @ConfigSection( - name = "Start Conditions", - description = "Conditions for when the plugin is allowed to start", - position = 0 - ) - String startConditionSection = "startConditions"; - - @ConfigSection( - name = "Location Start Conditions", - description = "Location-based conditions for when the plugin is allowed to start", - position = 1, - closedByDefault = false - ) - String locationStartConditionSection = "locationStartConditions"; - - // Location Start Condition Settings - @ConfigItem( - keyName = "enableLocationStartCondition", - name = "Enable Location Start Condition", - description = "Enable location-based start condition", - position = 0, - section = locationStartConditionSection - ) - default boolean enableLocationStartCondition() { - return false; - } - - @ConfigItem( - keyName = "locationStartType", - name = "Location Type", - description = "Type of location condition to use for starting the plugin", - position = 1, - section = locationStartConditionSection - ) - default LocationStartType locationStartType() { - return LocationStartType.BANK; - } - - @ConfigItem( - keyName = "bankStartLocation", - name = "Bank Location", - description = "Bank location where the plugin should start", - position = 2, - section = locationStartConditionSection - ) - default BankLocation bankStartLocation() { - return BankLocation.GRAND_EXCHANGE; - } - @Range( - min = 10, - max = 100 - ) - @ConfigItem( - keyName = "bankDistance", - name = "Bank Distance (tiles)", - description = "Maximum distance from bank to start the plugin", - position = 3, - section = locationStartConditionSection - ) - default int bankDistance() { - return 20; - } - - @ConfigItem( - keyName = "customAreaActive", - name = "Custom Area Active", - description = "Whether a custom area has been defined using the hotkey", - position = 4, - section = locationStartConditionSection, - hidden = true - ) - default boolean customAreaActive() { - return false; - } - - void setCustomAreaActive(boolean active); - - @ConfigItem( - keyName = "areaMarkHotkey", - name = "Area Mark Hotkey", - description = "Hotkey to mark current position as center of custom area (press again to clear)", - position = 5, - section = locationStartConditionSection - ) - default Keybind areaMarkHotkey() { - return Keybind.NOT_SET; - } - - @ConfigItem( - keyName = "customAreaRadius", - name = "Custom Area Radius (tiles)", - description = "Radius of the custom area around the marked position", - position = 6, - section = locationStartConditionSection - ) - default int customAreaRadius() { - return 10; - } - - @ConfigItem( - keyName = "customAreaCenter", - name = "Custom Area Center", - description = "Center point of the custom area", - position = 7, - section = locationStartConditionSection, - hidden = true - ) - default WorldPoint customAreaCenter() { - return null; - } - - void setCustomAreaCenter(WorldPoint center); - - // Enum for location start types - enum LocationStartType { - BANK("Bank Location"), - CUSTOM_AREA("Custom Area"); - - private final String name; - - LocationStartType(String name) { - this.name = name; - } - - @Override - public String toString() { - return name; - } - } - - enum ProcessTrackingMode { - SOURCE_CONSUMPTION("Source Consumption"), - TARGET_PRODUCTION("Target Production"), - EITHER("Either"), - BOTH("Both"); - - private final String name; - - ProcessTrackingMode(String name) { - this.name = name; - } - - @Override - public String toString() { - return name; - } - } - - - @ConfigSection( - name = "Stop Conditions", - description = "Conditions for when the plugin should stop", - position = 101 - ) - String stopSection = "stopConditions"; - - @ConfigSection( - name = "Time Conditions", - description = "Time-based conditions for stopping the plugin", - position = 102, - closedByDefault = false - ) - String timeConditionSection = "timeConditions"; - - @ConfigSection( - name = "Loot Item Conditions", - description = "Conditions related to looted items", - position = 103, - closedByDefault = false - ) - String lootItemConditionSection = "lootItemConditions"; - - @ConfigSection( - name = "Gathered Resource Conditions", - description = "Conditions related to gathered resources (mining, fishing, etc.)", - position = 104, - closedByDefault = false - ) - String gatheredResourceConditionSection = "gatheredResourceConditions"; - - @ConfigSection( - name = "Process Item Conditions", - description = "Conditions related to processed items (crafting, smithing, etc.)", - position = 105, - closedByDefault = false - ) - String processItemConditionSection = "processItemConditions"; - @ConfigSection( - name = "NPC Conditions", - description = "Conditions related to NPCs", - position = 106, - closedByDefault = false - ) - String npcConditionSection = "npcConditions"; - - @ConfigSection( - name = "Pre/Post Schedule Requirements", - description = "Configure requirements for pre and post schedule tasks", - position = 107, - closedByDefault = false - ) - String prePostScheduleRequirementsSection = "prePostScheduleRequirements"; - - // Time Condition Settings - @ConfigItem( - keyName = "enableTimeCondition", - name = "Enable Time Condition", - description = "Enable time-based stop condition", - position = 0, - section = timeConditionSection - ) - default boolean enableTimeCondition() { - return true; - } - - @ConfigItem( - keyName = "minRuntime", - name = "Minimum Runtime (minutes)", - description = "Minimum time to run before stopping", - position = 1, - section = timeConditionSection - ) - default int minRuntime() { - return 1; - } - - @ConfigItem( - keyName = "maxRuntime", - name = "Maximum Runtime (minutes)", - description = "Maximum time to run before stopping", - position = 2, - section = timeConditionSection - ) - default int maxRuntime() { - return 2; - } - - // Loot Item Condition Settings - @ConfigItem( - keyName = "enableLootItemCondition", - name = "Enable Loot Item Condition", - description = "Enable condition to stop based on looted items", - position = 0, - section = lootItemConditionSection - ) - default boolean enableLootItemCondition() { - return true; - } - - @ConfigItem( - keyName = "lootItems", - name = "Loot Items to Track", - description = "Comma separated list of items. Supports regex patterns (.*bones.*)", - position = 1, - section = lootItemConditionSection - ) - default String lootItems() { - return "Logs"; - } - - @ConfigItem( - keyName = "itemsToLootLogical", - name = "Or(False)/And(True)", - description = "Logical operator for items to loot: False=OR, True=AND", - position = 2, - section = lootItemConditionSection - ) - default boolean itemsToLootLogical() { - return false; - } - - @ConfigItem( - keyName = "minItems", - name = "Minimum Items", - description = "Minimum number of items to loot before stopping", - position = 3, - section = lootItemConditionSection - ) - default int minItems() { - return 5; - } - - @ConfigItem( - keyName = "maxItems", - name = "Maximum Items", - description = "Maximum number of items to loot before stopping", - position = 4, - section = lootItemConditionSection - ) - default int maxItems() { - return 10; - } - - @ConfigItem( - keyName = "includeNoted", - name = "Include Noted Items", - description = "Include noted items in loot tracking", - position = 5, - section = lootItemConditionSection - ) - default boolean includeNoted() { - return false; - } - - @ConfigItem( - keyName = "allowNoneOwner", - name = "Allow None Owner", - description = "Allow items not owned by the player (e.g. items which are spawned)", - position = 6, - section = lootItemConditionSection - ) - default boolean allowNoneOwner() { - return false; - } - - // Gathered Resource Condition Settings - @ConfigItem( - keyName = "enableGatheredResourceCondition", - name = "Enable Gathered Resource Condition", - description = "Enable condition to stop based on gathered resources", - position = 0, - section = gatheredResourceConditionSection - ) - default boolean enableGatheredResourceCondition() { - return false; - } - - @ConfigItem( - keyName = "gatheredResources", - name = "Resources to Track", - description = "Comma separated list of resources to track (e.g. logs,ore,fish)", - position = 1, - section = gatheredResourceConditionSection - ) - default String gatheredResources() { - return "logs"; - } - - @ConfigItem( - keyName = "resourcesLogical", - name = "Or(False)/And(True)", - description = "Logical operator for resources: False=OR, True=AND", - position = 2, - section = gatheredResourceConditionSection - ) - default boolean resourcesLogical() { - return false; - } - - @ConfigItem( - keyName = "minResources", - name = "Minimum Resources", - description = "Minimum number of resources to gather before stopping", - position = 3, - section = gatheredResourceConditionSection - ) - default int minResources() { - return 10; - } - - @ConfigItem( - keyName = "maxResources", - name = "Maximum Resources", - description = "Maximum number of resources to gather before stopping", - position = 4, - section = gatheredResourceConditionSection - ) - default int maxResources() { - return 15; - } - - @ConfigItem( - keyName = "includeResourceNoted", - name = "Include Noted Resources", - description = "Include noted resources in tracking", - position = 5, - section = gatheredResourceConditionSection - ) - default boolean includeResourceNoted() { - return false; - } - - // Process Item Condition Settings - @ConfigItem( - keyName = "enableProcessItemCondition", - name = "Enable Process Item Condition", - description = "Enable condition to stop based on processed items", - position = 0, - section = processItemConditionSection - ) - default boolean enableProcessItemCondition() { - return false; - } - - @ConfigItem( - keyName = "trackingMode", - name = "Tracking Mode", - description = "How to track item processing (source items consumed or target items produced)", - position = 1, - section = processItemConditionSection - ) - default ProcessTrackingMode trackingMode() { - return ProcessTrackingMode.SOURCE_CONSUMPTION; - } - - @ConfigItem( - keyName = "sourceItems", - name = "Source Items", - description = "Comma separated list of source items (e.g. logs,ore)", - position = 2, - section = processItemConditionSection - ) - default String sourceItems() { - return "logs"; - } - - @ConfigItem( - keyName = "targetItems", - name = "Target Items", - description = "Comma separated list of target items (e.g. bow,shield)", - position = 3, - section = processItemConditionSection - ) - default String targetItems() { - return "bow"; - } - - @ConfigItem( - keyName = "minProcessedItems", - name = "Minimum Processed Items", - description = "Minimum number of items to process before stopping", - position = 4, - section = processItemConditionSection - ) - default int minProcessedItems() { - return 5; - } - - @ConfigItem( - keyName = "maxProcessedItems", - name = "Maximum Processed Items", - description = "Maximum number of items to process before stopping", - position = 5, - section = processItemConditionSection - ) - default int maxProcessedItems() { - return 10; - } - // NPC Kill Count Condition Settings - @ConfigItem( - keyName = "enableNpcKillCountCondition", - name = "Enable NPC Kill Count Condition", - description = "Enable condition to stop based on NPC kill count", - position = 0, - section = npcConditionSection - ) - default boolean enableNpcKillCountCondition() { - return false; - } - @ConfigItem( - keyName = "npcNames", - name = "NPCs to Track", - description = "Comma separated list of NPC names to track kills for. Supports regex patterns.", - position = 1, - section = npcConditionSection - ) - default String npcNames() { - return "goblin"; - } - - @ConfigItem( - keyName = "npcLogical", - name = "Or(False)/And(True)", - description = "Logical operator for NPCs: False=OR (any NPC satisfies), True=AND (all NPCs must be killed)", - position = 2, - section = npcConditionSection - ) - default boolean npcLogical() { - return false; - } - - @ConfigItem( - keyName = "minKills", - name = "Minimum Kills", - description = "Minimum number of NPCs to kill before stopping", - position = 3, - section = npcConditionSection - ) - default int minKills() { - return 5; - } - - @ConfigItem( - keyName = "maxKills", - name = "Maximum Kills", - description = "Maximum number of NPCs to kill before stopping", - position = 4, - section = npcConditionSection - ) - default int maxKills() { - return 10; - } - - @ConfigItem( - keyName = "killsPerType", - name = "Count Per NPC Type", - description = "If true, need to kill the specified count of EACH NPC type. If false, count total kills across all types.", - position = 5, - section = npcConditionSection - ) - default boolean killsPerType() { - return true; - } - - - // Location tracking - @ConfigItem( - keyName = "lastLocation", - name = "Last Location", - description = "Last tracked location", - hidden = true - ) - default WorldPoint lastLocation() { - return null; - } - default void setLastLocation(WorldPoint location){ - if (location != null) { - if (Microbot.getConfigManager() != null) { - Microbot.getConfigManager().setConfiguration("SchedulableExample", "lastLocation", location); - } - } - - } - - // Pre/Post Schedule Requirements Configuration - @ConfigItem( - keyName = "enablePrePostRequirements", - name = "Enable Pre/Post Requirements", - description = "Enable pre and post schedule requirements and tasks", - position = 0, - section = prePostScheduleRequirementsSection - ) - default boolean enablePrePostRequirements() { - return false; - } - - @ConfigItem( - keyName = "preScheduleSpellbook", - name = "Pre-Schedule Spellbook", - description = "Spellbook required before starting the plugin (None = no switching)", - position = 1, - section = prePostScheduleRequirementsSection - ) - default SpellbookOption preScheduleSpellbook() { - return SpellbookOption.NONE; - } - - @ConfigItem( - keyName = "postScheduleSpellbook", - name = "Post-Schedule Spellbook", - description = "Spellbook to switch to after plugin completion (None = no switching)", - position = 2, - section = prePostScheduleRequirementsSection - ) - default SpellbookOption postScheduleSpellbook() { - return SpellbookOption.NONE; - } - - @ConfigItem( - keyName = "preScheduleLocation", - name = "Pre-Schedule Location", - description = "Location required before starting the plugin (None = no location requirement)", - position = 3, - section = prePostScheduleRequirementsSection - ) - default UnifiedLocation preScheduleLocation() { - return UnifiedLocation.NONE; - } - - @ConfigItem( - keyName = "postScheduleLocation", - name = "Post-Schedule Location", - description = "Location to move to after plugin completion (None = no location requirement)", - position = 4, - section = prePostScheduleRequirementsSection - ) - default UnifiedLocation postScheduleLocation() { - return UnifiedLocation.NONE; - } - - - - @ConfigItem( - keyName = "enableConditionalItemRequirement", - name = "Enable Alch Conditional Requirement based on Fire Staff/Rune", - description = "Enable the fire staff/fire rune conditional requirement for alching in pre-schedule tasks.", - position = 5, - section = prePostScheduleRequirementsSection - ) - default boolean enableConditionalItemRequirement() { - return false; - } - - - @ConfigItem( - keyName = "enableEquipmentRequirement", - name = "Enable Equipment Requirement", - description = "Enable equipment requirement", - position = 6, - section = prePostScheduleRequirementsSection - ) - default boolean enableEquipmentRequirement() { - return false; - } - - @ConfigItem( - keyName = "enableInventoryRequirement", - name = "Enable Inventory Requirement", - description = "Enable inventory requirement", - position = 7, - section = prePostScheduleRequirementsSection - ) - default boolean enableInventoryRequirement() { - return false; - } - @ConfigItem( - keyName = "enableLootRequirement", - name = "Enable Loot Requirement", - description = "Enable loot requirement for coins near Lumbridge", - position = 8, - section = prePostScheduleRequirementsSection - ) - default boolean enableLootRequirement() { - return false; - } - @ConfigItem( - keyName = "enableShopRequirement", - name = "Enable Shop Requirement", - description = "Enable shop maple longbow, buy from grand exchange as pre-schedule and sell at store on post-schedule", - position = 9, - section = prePostScheduleRequirementsSection - ) - default boolean enableShopRequirement() { - return false; - } - - @ConfigItem( - keyName = "externalRequirements", - name = "Enable External Requirements", - description = "Enable external requirements test for pre and post schedule tasks", - position = 10, - section = prePostScheduleRequirementsSection - ) - default boolean externalRequirements() { - return false; - } - - - - @ConfigSection( - name = "Antiban Testing", - description = "Antiban system testing and configuration", - position = 199, - closedByDefault = true - ) - String antibanTestSection = "antibanTestSection"; - - @ConfigItem( - keyName = "enableAntibanTesting", - name = "Enable Antiban Testing", - description = "Enable antiban features testing including micro breaks", - position = 0, - section = antibanTestSection - ) - default boolean enableAntibanTesting() { - return false; - } - - @ConfigItem( - keyName = "enableMicroBreaks", - name = "Enable Micro Breaks", - description = "Enable micro breaks during plugin execution", - position = 1, - section = antibanTestSection - ) - default boolean enableMicroBreaks() { - return false; - } - - @ConfigItem( - keyName = "microBreakChance", - name = "Micro Break Chance", - description = "Chance (0.0-1.0) of taking a micro break per check", - position = 2, - section = antibanTestSection - ) - @Range(min = 0, max = 100) - default int microBreakChancePercent() { - return 10; // 10% default - } - - @ConfigItem( - keyName = "microBreakDurationMin", - name = "Micro Break Min Duration (minutes)", - description = "Minimum duration for micro breaks in minutes", - position = 3, - section = antibanTestSection - ) - @Range(min = 1, max = 30) - default int microBreakDurationMin() { - return 3; - } - - @ConfigItem( - keyName = "microBreakDurationMax", - name = "Micro Break Max Duration (minutes)", - description = "Maximum duration for micro breaks in minutes", - position = 4, - section = antibanTestSection - ) - @Range(min = 1, max = 60) - default int microBreakDurationMax() { - return 15; - } - - @ConfigItem( - keyName = "statusReportInterval", - name = "Status Report Interval (seconds)", - description = "How often to report break status (0 = disable reporting)", - position = 5, - section = antibanTestSection - ) - @Range(min = 0, max = 300) - default int statusReportInterval() { - return 30; // Report every 30 seconds - } - - @ConfigItem( - keyName = "enableActionCooldowns", - name = "Enable Action Cooldowns", - description = "Enable action cooldown testing", - position = 6, - section = antibanTestSection - ) - default boolean enableActionCooldowns() { - return false; - } - - @ConfigItem( - keyName = "moveMouseOffScreen", - name = "Move Mouse Off-Screen", - description = "Move mouse off-screen during breaks", - position = 7, - section = antibanTestSection - ) - default boolean moveMouseOffScreen() { - return false; - } - - @ConfigSection( - name = "Debug Options", - description = "Options for testing and debugging", - position = 200, - closedByDefault = true - ) - String debugSection = "debugSection"; - - @ConfigItem( - keyName = "aliveReportTimeout", - name = "Alive Report Timeout (sec)", - description = "Time in seconds before script reports it's alive", - position = 0, - section = debugSection - ) - @Range( - min = 10, - max = 100 - ) - default int aliveReportTimeout() { - return 10; - } - - @ConfigItem( - keyName = "finishPluginNotSuccessfulHotkey", - name = "Finish Plugin Not-Successful Hotkey", - description = "Press this hotkey to manually trigger the PluginScheduleEntryMainTaskFinishedEvent for testing not successful completion", - position = 1, - section = debugSection - ) - default Keybind finishPluginNotSuccessfulHotkey() { - return new Keybind(KeyEvent.VK_F2, 0); - } - - @ConfigItem( - keyName = "finishPluginSuccessfulHotkey", - name = "Finish Plugin Hotkey", - description = "Press this hotkey to manually trigger the PluginScheduleEntryMainTaskFinishedEvent for testing successful completion", - position = 2, - section = debugSection - ) - default Keybind finishPluginSuccessfulHotkey() { - return new Keybind(KeyEvent.VK_F3, 0); - } - - - @ConfigItem( - keyName = "finishReason", - name = "Finish Reason", - description = "The reason to report when finishing the plugin", - position = 3, - section = debugSection - ) - default String finishReason() { - return "Task completed successfully"; - } - - @ConfigItem( - keyName = "lockConditionHotkey", - name = "Lock Condition Hotkey", - description = "Press this hotkey to toggle the lock condition (prevents plugin from being stopped)", - position = 4, - section = debugSection - ) - default Keybind lockConditionHotkey() { - return Keybind.NOT_SET; - } - - @ConfigItem( - keyName = "lockDescription", - name = "Lock Reason", - description = "Description of why the plugin is locked", - position = 5, - section = debugSection - ) - default String lockDescription() { - return "Plugin in critical state - do not stop"; - } - - @ConfigItem( - keyName = "testPreScheduleTasksHotkey", - name = "Test Pre-Schedule Tasks Hotkey", - description = "Press this hotkey to test pre-schedule tasks functionality (equipment, spellbook, location setup)", - position = 6, - section = debugSection - ) - default Keybind testPreScheduleTasksHotkey() { - return Keybind.NOT_SET; - } - - @ConfigItem( - keyName = "testPostScheduleTasksHotkey", - name = "Test Post-Schedule Tasks Hotkey", - description = "Press this hotkey to test post-schedule tasks functionality (cleanup, banking, spellbook restoration)", - position = 7, - section = debugSection - ) - default Keybind testPostScheduleTasksHotkey() { - return Keybind.NOT_SET; - } - - @ConfigItem( - keyName = "cancelTasksHotkey", - name = "Cancel & Reset Tasks Hotkey", - description = "Press this hotkey to cancel any running pre/post schedule tasks and reset execution state", - position = 8, - section = debugSection - ) - default Keybind cancelTasksHotkey() { - return Keybind.NOT_SET; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleOverlay.java deleted file mode 100644 index cd5ad772a10..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleOverlay.java +++ /dev/null @@ -1,139 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example; - -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.state.TaskExecutionState; -import net.runelite.client.ui.overlay.OverlayLayer; -import net.runelite.client.ui.overlay.OverlayPanel; -import net.runelite.client.ui.overlay.OverlayPosition; -import net.runelite.client.ui.overlay.components.ComponentConstants; - -import javax.inject.Inject; -import java.awt.*; - -/** - * Overlay for the SchedulableExample plugin that displays the current state of - * pre/post schedule requirements and task execution. - * - * This overlay demonstrates how to integrate the RequirementOverlayComponentFactory - * to provide real-time feedback about requirement fulfillment progress. - */ -public class SchedulableExampleOverlay extends OverlayPanel { - - private final SchedulableExamplePlugin plugin; - - @Inject - public SchedulableExampleOverlay(SchedulableExamplePlugin plugin) { - super(plugin); - this.plugin = plugin; - setPosition(OverlayPosition.TOP_LEFT); - setPreferredSize(new Dimension(ComponentConstants.STANDARD_WIDTH, 200)); - setNaughty(); - setDragTargetable(true); - setLayer(OverlayLayer.UNDER_WIDGETS); - } - - @Override - public Dimension render(Graphics2D graphics) { - // Clear previous components - panelComponent.getChildren().clear(); - - // Get the task manager and requirements - SchedulableExamplePrePostScheduleTasks tasks = (SchedulableExamplePrePostScheduleTasks)plugin.getPrePostScheduleTasks(); - - // Only show overlay if pre/post requirements are enabled or tasks are running - if (!plugin.getConfig().enablePrePostRequirements() && - (tasks == null || !tasks.isExecuting())) { - return null; // Don't show overlay when not needed - } - - try { - // Show concise information only - boolean isExecuting = tasks != null && tasks.isExecuting(); - boolean hasPrePostRequirements = plugin.getConfig().enablePrePostRequirements(); - - // Main title with status indication - String titleText = "SchedulableExample"; - Color titleColor = Color.CYAN; - - if (isExecuting) { - TaskExecutionState executionState = tasks.getExecutionState(); - if (executionState.isInErrorState()) { - titleText += " (ERROR)"; - titleColor = Color.RED; - } else { - titleText += " (ACTIVE)"; - titleColor = Color.YELLOW; - } - } else if (hasPrePostRequirements) { - titleText += " (READY)"; - titleColor = Color.CYAN; - } - - panelComponent.getChildren().add(net.runelite.client.ui.overlay.components.TitleComponent.builder() - .text(titleText) - .color(titleColor) - .build()); - - // Show current status - if (isExecuting) { - TaskExecutionState executionState = tasks.getExecutionState(); - String phase = executionState.getCurrentPhase() != null ? - executionState.getCurrentPhase().toString() : "EXECUTING"; - int progress = executionState.getProgressPercentage(); - String statusText = progress > 0 ? phase + " (" + progress + "%)" : phase; - - panelComponent.getChildren().add(net.runelite.client.ui.overlay.components.LineComponent.builder() - .left("Phase:") - .right(statusText) - .leftColor(Color.WHITE) - .rightColor(Color.YELLOW) - .build()); - - // Show detailed status if available and short enough - String detailedStatus = executionState.getDetailedStatus(); - if (detailedStatus != null && !detailedStatus.isEmpty() && detailedStatus.length() <= 25) { - panelComponent.getChildren().add(net.runelite.client.ui.overlay.components.LineComponent.builder() - .left("Status:") - .right(detailedStatus) - .leftColor(Color.WHITE) - .rightColor(Color.CYAN) - .build()); - } - } else { - // Show requirements status when not executing - String requirementsText = hasPrePostRequirements ? "ENABLED" : "DISABLED"; - Color requirementsColor = hasPrePostRequirements ? Color.GREEN : Color.GRAY; - - panelComponent.getChildren().add(net.runelite.client.ui.overlay.components.LineComponent.builder() - .left("Pre/Post:") - .right(requirementsText) - .leftColor(Color.WHITE) - .rightColor(requirementsColor) - .build()); - } - - // Show essential controls hint - panelComponent.getChildren().add(net.runelite.client.ui.overlay.components.LineComponent.builder() - .left("Hotkeys:") - .right("See config") - .leftColor(Color.WHITE) - .rightColor(Color.LIGHT_GRAY) - .build()); - - } catch (Exception e) { - // Show error in overlay - panelComponent.getChildren().add(net.runelite.client.ui.overlay.components.TitleComponent.builder() - .text("SchedulableExample - ERROR") - .color(Color.RED) - .build()); - - panelComponent.getChildren().add(net.runelite.client.ui.overlay.components.LineComponent.builder() - .left("Error:") - .right(e.getMessage() != null ? e.getMessage() : "Unknown error") - .leftColor(Color.WHITE) - .rightColor(Color.RED) - .build()); - } - - return super.render(graphics); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePlugin.java deleted file mode 100644 index 7fa73314123..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePlugin.java +++ /dev/null @@ -1,978 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example; - - -import java.awt.event.KeyEvent; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; - -import com.google.inject.Provides; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Client; -import net.runelite.api.GameState; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.GameStateChanged; -import net.runelite.client.config.ConfigDescriptor; -import net.runelite.client.config.ConfigManager; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.events.ConfigChanged; -import net.runelite.client.input.KeyListener; -import net.runelite.client.input.KeyManager; -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.pluginscheduler.api.SchedulablePlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.AreaCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.LocationCondition; -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.condition.logical.OrCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.npc.NpcKillCountCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.GatheredResourceCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.LootItemCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ProcessItemCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.event.PluginScheduleEntryPostScheduleTaskEvent; -import net.runelite.client.plugins.microbot.pluginscheduler.event.PluginScheduleEntryPreScheduleTaskEvent; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.AbstractPrePostScheduleTasks; -import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -import net.runelite.client.ui.overlay.OverlayManager; -import net.runelite.client.util.HotkeyListener; - -@PluginDescriptor( - name = "Schedulable Example", - description = "Designed for use with the scheduler and testing its features", - tags = {"microbot", "woodcutting", "combat", "scheduler", "condition"}, - enabledByDefault = false -) -@Slf4j -public class SchedulableExamplePlugin extends Plugin implements SchedulablePlugin, KeyListener { - - - - @Inject - private SchedulableExampleConfig config; - - @Inject - private Client client; - - @Inject - private KeyManager keyManager; - - @Inject - private OverlayManager overlayManager; - - @Inject - private SchedulableExampleOverlay overlay; - - @Provides - SchedulableExampleConfig provideConfig(ConfigManager configManager) { - if (configManager == null) { - log.warn("ConfigManager is null, cannot provide SchedulableExampleConfig"); - return null; - } - return configManager.getConfig(SchedulableExampleConfig.class); - } - - /** - * Gets the plugin configuration. - * - * @return The SchedulableExampleConfig instance - */ - public SchedulableExampleConfig getConfig() { - return config; - } - - private SchedulableExampleScript script; - private WorldPoint lastLocation = null; - private int itemsCollected = 0; - - private LockCondition lockCondition; - private LogicalCondition startCondition = null; - private LogicalCondition stopCondition = null; - - - // Pre/Post Schedule Tasks and Requirements - private SchedulableExamplePrePostScheduleRequirements prePostScheduleRequirements = null; - private SchedulableExamplePrePostScheduleTasks prePostScheduleTasks = null; - - // HotkeyListener for the area marking - private final HotkeyListener areaHotkeyListener = new HotkeyListener(() -> config.areaMarkHotkey()) { - @Override - public void hotkeyPressed() { - toggleCustomArea(); - } - }; - - // HotkeyListener for testing PluginScheduleEntryMainTaskFinishedEvent - private final HotkeyListener finishPluginSuccessHotkeyListener = new HotkeyListener(() -> config.finishPluginSuccessfulHotkey()) { - @Override - public void hotkeyPressed() { - String reason = config.finishReason() + " (success)"; - boolean success = true; - log.info("\nManually triggering plugin finish: reason='{}', success={}", reason, success); - Microbot.getClientThread().invokeLater( () -> {reportFinished(reason, success); return true;}); - } - }; - // HotkeyListener for testing PluginScheduleEntryMainTaskFinishedEvent - private final HotkeyListener finishPluginNotSuccessHotkeyListener = new HotkeyListener(() -> config.finishPluginNotSuccessfulHotkey()) { - @Override - public void hotkeyPressed() { - String reason = config.finishReason()+ " (not success)"; - boolean success = false; - log.info("\nManually triggering plugin finish: reason='{}', success={}", reason, success); - Microbot.getClientThread().invokeLater( () -> {reportFinished(reason, success); return true;}); - } - }; - - // HotkeyListener for toggling the lock condition - private final HotkeyListener lockConditionHotkeyListener = new HotkeyListener(() -> config.lockConditionHotkey()) { - @Override - public void hotkeyPressed() { - log.info("Toggling lock condition for plugin: {}", getName()); - if (stopCondition == null || stopCondition.getConditions().isEmpty()) { - log.warn("Stop condition is not initialized. Cannot toggle lock condition."); - return; - } - boolean newState = toggleLock((Condition)(stopCondition)); - log.info("\n\tLock condition toggled: {}", newState ? "LOCKED - " + config.lockDescription() : "UNLOCKED"); - } - }; - - // HotkeyListener for testing pre-schedule tasks - private final HotkeyListener testPreScheduleTasksHotkeyListener = new HotkeyListener(() -> config.testPreScheduleTasksHotkey()) { - @Override - public void hotkeyPressed() { - // Initialize Pre/Post Schedule Requirements and Tasks if needed - if (config.enablePrePostRequirements()) { - if (getPrePostScheduleTasks() == null) { - log.info("Initializing Pre/Post failed "); - return; - } - // Test only pre-schedule tasks - SchedulableExamplePlugin.this.runPreScheduleTasks(); - - - } else { - log.info("Pre/Post Schedule Requirements are disabled in configuration"); - } - } - }; - - // HotkeyListener for testing post-schedule tasks - private final HotkeyListener testPostScheduleTasksHotkeyListener = new HotkeyListener(() -> config.testPostScheduleTasksHotkey()) { - @Override - public void hotkeyPressed() { - // Initialize Pre/Post Schedule Requirements and Tasks if needed - if (config.enablePrePostRequirements()) { - if (getPrePostScheduleTasks() == null) { - log.info("Initializing Pre/Post failed "); - return; - } - - // Test only post-schedule tasks - runPostScheduleTasks(); - } else { - log.info("Pre/Post Schedule Requirements are disabled in configuration"); - } - } - }; - - // HotkeyListener for cancelling tasks - private final HotkeyListener cancelTasksHotkeyListener = new HotkeyListener(() -> config.cancelTasksHotkey()) { - @Override - public void hotkeyPressed() { - log.info("Cancel tasks hotkey pressed for plugin: {}", getName()); - - if (prePostScheduleTasks != null) { - if (prePostScheduleTasks.isPreScheduleRunning()) { - prePostScheduleTasks.cancelPreScheduleTasks(); - log.info("Cancelled pre-schedule tasks"); - } else if (prePostScheduleTasks.isPostScheduleRunning()) { - prePostScheduleTasks.cancelPostScheduleTasks(); - log.info("Cancelled post-schedule tasks"); - } else { - log.info("No pre/post schedule tasks are currently running"); - } - - // Reset the execution state to allow fresh start - prePostScheduleTasks.reset(); - log.info("Reset pre/post schedule tasks execution state"); - } else { - log.info("No pre/post schedule tasks manager initialized"); - } - } - }; - - @Override - protected void startUp() { - loadLastLocation(); - this.script = new SchedulableExampleScript(); - - - - keyManager.registerKeyListener(this); - - // Register the hotkey listeners - keyManager.registerKeyListener(areaHotkeyListener); - keyManager.registerKeyListener(finishPluginSuccessHotkeyListener); - keyManager.registerKeyListener(finishPluginNotSuccessHotkeyListener); - keyManager.registerKeyListener(lockConditionHotkeyListener); - keyManager.registerKeyListener(testPreScheduleTasksHotkeyListener); - keyManager.registerKeyListener(testPostScheduleTasksHotkeyListener); - keyManager.registerKeyListener(cancelTasksHotkeyListener); - - // Add the overlay - overlayManager.add(overlay); - boolean scheduleMode = Microbot.getConfigManager().getConfiguration( - "SchedulableExample", - "scheduleMode", - Boolean.class - ); - log.info("\n\tSchedulable Example plugin started\n\t -In SchedulerMode:{}\n\t -Press {} to test the PluginScheduleEntryMainTaskFinishedEvent successfully\n\t -Press {} to test the PluginScheduleEntryMainTaskFinishedEvent unsuccessfully\n\t -Use {} to toggle the lock condition (prevents the plugin from being stopped)\n\t -Use {} to test Pre-Schedule Tasks functionality\n\t -Use {} to test Post-Schedule Tasks functionality\n\t -Use {} to cancel running pre/post schedule tasks", - scheduleMode, - config.finishPluginSuccessfulHotkey(), - config.finishPluginNotSuccessfulHotkey(), - config.lockConditionHotkey(), - config.testPreScheduleTasksHotkey(), - config.testPostScheduleTasksHotkey(), - config.cancelTasksHotkey()); - - } - - /** - * Override the default event handler to start the script properly after pre-schedule tasks. - * This follows the same pattern as runPreScheduleTasks() but integrates with the scheduler. - */ - @Subscribe - public void onPluginScheduleEntryPreScheduleTaskEvent(PluginScheduleEntryPreScheduleTaskEvent event) { - - if (event.getPlugin() != this) { - return; // Not for this plugin - } - - log.info("Received PluginScheduleEntryPreScheduleTaskEvent for SchedulableExample plugin"); - - if (prePostScheduleTasks != null && event.isSchedulerControlled() && !prePostScheduleTasks.isPreTaskComplete()) { - // Plugin has pre/post tasks and is under scheduler control - log.info("SchedulableExample starting with pre-schedule tasks from scheduler"); - try { - // Execute pre-schedule tasks with callback to start the script - runPreScheduleTasks(); - } catch (Exception e) { - log.error("Error during Pre-Schedule Tasks for SchedulableExample", e); - } - } - } - - - @Override - protected void shutDown() { - // Clean up PrePostScheduleTasks if initialized - if (prePostScheduleTasks != null) { - try { - prePostScheduleTasks.close(); - log.info("PrePostScheduleTasks cleaned up successfully"); - } catch (Exception e) { - log.error("Error cleaning up PrePostScheduleTasks", e); - } finally { - prePostScheduleTasks = null; - prePostScheduleRequirements = null; - } - } - - if (script != null && script.isRunning()) { - saveCurrentLocation(); - script.shutdown(); - } - unlock((Condition)(stopCondition)); - keyManager.unregisterKeyListener(this); - keyManager.unregisterKeyListener(areaHotkeyListener); - keyManager.unregisterKeyListener(finishPluginSuccessHotkeyListener); - keyManager.unregisterKeyListener(finishPluginNotSuccessHotkeyListener); - keyManager.unregisterKeyListener(lockConditionHotkeyListener); - keyManager.unregisterKeyListener(testPreScheduleTasksHotkeyListener); - keyManager.unregisterKeyListener(testPostScheduleTasksHotkeyListener); - keyManager.unregisterKeyListener(cancelTasksHotkeyListener); - - // Remove the overlay - overlayManager.remove(overlay); - } - - /** - * Toggles the custom area state and updates configuration - */ - private void toggleCustomArea() { - if (!Microbot.isLoggedIn()) { - log.info("Cannot toggle custom area: Not logged in"); - return; - } - - boolean isActive = config.customAreaActive(); - - if (isActive) { - // Clear the custom area - config.setCustomAreaActive(false); - config.setCustomAreaCenter(null); - log.info("Custom area removed"); - } else { - // Create new custom area at current position - WorldPoint currentPos = null; - if (Microbot.isLoggedIn()){ - currentPos = Rs2Player.getWorldLocation(); - } - if (currentPos != null) { - config.setCustomAreaCenter(currentPos); - config.setCustomAreaActive(true); - log.info("Custom area created at: " + currentPos.toString() + " with radius: " + config.customAreaRadius()); - } - } - } - - /** - * Checks if the player is in the custom area - */ - public boolean isPlayerInCustomArea() { - if (!config.customAreaActive() || config.customAreaCenter() == null) { - return false; - } - if (!Microbot.isLoggedIn()) { - return false; - } - WorldPoint currentPos = Rs2Player.getWorldLocation(); - if (currentPos == null) { - return false; - } - - WorldPoint center = config.customAreaCenter(); - int radius = config.customAreaRadius(); - - // Check if player is within radius of the center point and on the same plane - return (currentPos.getPlane() == center.getPlane() && - currentPos.distanceTo(center) <= radius); - } - - private void loadLastLocation() { - WorldPoint savedLocation = config.lastLocation(); - if (savedLocation == null) { - log.warn("No saved location found in config."); - if (Microbot.isLoggedIn()){ - this.lastLocation = Rs2Player.getWorldLocation(); - } - return; - } - this.lastLocation = savedLocation; - } - - private void saveCurrentLocation() { - if (client.getLocalPlayer() != null) { - WorldPoint currentLoc = client.getLocalPlayer().getWorldLocation(); - config.setLastLocation(currentLoc); - } - } - - private LogicalCondition createStopCondition() { - // Create an OR condition - we'll stop when ANY of the enabled conditions are met - OrCondition orCondition = new OrCondition(); - if (this.lockCondition == null) { - this.lockCondition = new LockCondition("Locked because the Plugin "+getName()+" is in a critical operation", false,true); //ensure unlock on shutdown of the plugin ! - } - - // Add enabled conditions based on configuration - if (config.enableTimeCondition()) { - orCondition.addCondition(createTimeCondition()); - } - - if (config.enableLootItemCondition()) { - orCondition.addCondition(createLootItemCondition()); - } - - if (config.enableGatheredResourceCondition()) { - orCondition.addCondition(createGatheredResourceCondition()); - } - - if (config.enableProcessItemCondition()) { - orCondition.addCondition(createProcessItemCondition()); - } - - if (config.enableNpcKillCountCondition()) { - orCondition.addCondition(createNpcKillCountCondition()); - } - - // If no conditions were added, add a fallback time condition - if (orCondition.getConditions().isEmpty()) { - log.warn("No stop conditions were enabled. Adding default time condition of 5 minutes."); - orCondition.addCondition(IntervalCondition.createRandomized(Duration.ofMinutes(5), Duration.ofMinutes(5))); - } - - // Add a lock condition that can be toggled manually - // NOTE: This condition uses AND logic with the other conditions since it's in an AND condition - AndCondition andCondition = new AndCondition(); - //andCondition.addCondition(orCondition); - andCondition.addCondition(lockCondition); - - List all = andCondition.findAllLockConditions(); - log.info("\nCreated stop condition: \n{}"+"\nFound {} lock conditions in stop condition: {}", andCondition.getDescription(), all.size(), all); - - return andCondition; - - } - - - - /** - * Tests only the pre-schedule tasks functionality. - * This method demonstrates how pre-schedule tasks work and logs the results. - */ - private void runPreScheduleTasks() { - if (prePostScheduleTasks != null && !prePostScheduleTasks.isPreTaskRunning() && !prePostScheduleTasks.isPreTaskComplete()) { - executePreScheduleTasks(() -> { - log.info("Pre-Schedule Tasks completed successfully for SchedulableExample"); - // Ensure script is initialized - if (this.script == null) { - this.script = new SchedulableExampleScript(); - } - if (this.script.isRunning()) { - this.script.shutdown(); - } - // Start the actual script after pre-schedule tasks are done - this.script.run(config, lastLocation); - }); - } - - } - private void runPostScheduleTasks( ){ - if (prePostScheduleTasks != null && !prePostScheduleTasks.isPostScheduleRunning() && !prePostScheduleTasks.isPostTaskComplete()) { - executePostScheduleTasks(()->{ - if( this.script != null && this.script.isRunning()) { - this.script.shutdown(); - } - }); - }else { - log.info("Post-Schedule Tasks already completed or running for SchedulableExample"); - } - - - } - - - private LogicalCondition createStartCondition() { - try { - // Default to no start conditions (always allowed to start) - if (!config.enableLocationStartCondition()) { - return null; - } - - // Create a logical condition for start conditions - LogicalCondition startCondition = null; - - // Create location-based condition based on selected type - if (config.locationStartType() == SchedulableExampleConfig.LocationStartType.BANK) { - // Bank-based start condition - BankLocation selectedBank = config.bankStartLocation(); - int distance = config.bankDistance(); - - // Create condition using bank location - startCondition = new OrCondition(); // Use OR to allow multiple possible conditions - Condition bankCondition = LocationCondition.atBank(selectedBank, distance); - ((OrCondition) startCondition).addCondition(bankCondition); - - log.debug("Created bank start condition: " + selectedBank.name() + " within " + distance + " tiles"); - } else if (config.locationStartType() == SchedulableExampleConfig.LocationStartType.CUSTOM_AREA) { - // Custom area start condition - if (config.customAreaActive() && config.customAreaCenter() != null) { - WorldPoint center = config.customAreaCenter(); - int radius = config.customAreaRadius(); - - // Create area condition centered on the saved point - startCondition = new OrCondition(); - AreaCondition areaCondition = LocationCondition.createArea( - "Custom Start Area", - center, - radius * 2, // Width (diameter) - radius * 2 // Height (diameter) - ); - ((OrCondition) startCondition).addCondition(areaCondition); - - log.debug("Created custom area start condition at: " + center + " with radius: " + radius); - } else { - log.warn("Custom area start condition selected but no area is defined"); - // Return null to indicate no start condition (always allowed to start) - return null; - } - } - - return startCondition; - } catch (Exception e) { - log.error("Error creating start condition", e); - e.printStackTrace(); - return new OrCondition(); // Fallback to no conditions - } - } - /** - * Returns a logical condition that determines when the plugin is allowed to start - */ - @Override - public LogicalCondition getStartCondition() { - if (this.startCondition == null) { - this.startCondition = createStartCondition(); - } - return this.startCondition; - - } - @Override - public LogicalCondition getStopCondition() { - // Create a new stop condition - if (this.stopCondition == null) { - this.stopCondition = createStopCondition(); - } - return this.stopCondition; - } - @Override - public AbstractPrePostScheduleTasks getPrePostScheduleTasks() { - SchedulableExampleConfig config = provideConfig(Microbot.getConfigManager()); - if (prePostScheduleRequirements == null || prePostScheduleTasks == null) { - if(Microbot.getClient().getGameState() != GameState.LOGGED_IN) { - log.debug("Schedulable Example - Cannot provide pre/post schedule tasks - not logged in"); - return null; // Return null if not logged in - } - log.info("Initializing Pre/Post Schedule Requirements and Tasks..."); - this.prePostScheduleRequirements = new SchedulableExamplePrePostScheduleRequirements(config); - this.prePostScheduleTasks = new SchedulableExamplePrePostScheduleTasks(this, keyManager,prePostScheduleRequirements); - // Log the requirements status - if (prePostScheduleRequirements.isInitialized()) log.info("\nPrePostScheduleRequirements initialized:\n{}", prePostScheduleRequirements.getDetailedDisplay()); - } - // Return the pre/post schedule tasks instance - return this.prePostScheduleTasks; - } - - /** - * Creates a time-based condition based on config settings - */ - private Condition createTimeCondition() { - // Existing implementation - int minMinutes = config.minRuntime(); - int maxMinutes = config.maxRuntime(); - - return IntervalCondition.createRandomized( - Duration.ofMinutes(minMinutes), - Duration.ofMinutes(maxMinutes) - ); - - } - - /** - * Creates a loot item condition based on config settings - */ - private LogicalCondition createLootItemCondition() { - // Parse the comma-separated list of items - List lootItemsList = parseItemList(config.lootItems()); - if (lootItemsList.isEmpty()) { - log.warn("No valid loot items specified, defaulting to 'Logs'"); - lootItemsList.add("Logs"); - } - - boolean andLogical = config.itemsToLootLogical(); - int minLootItems = config.minItems(); - int maxLootItems = config.maxItems(); - - // Create randomized targets for each item - List minLootItemPerPattern = new ArrayList<>(); - List maxLootItemPerPattern = new ArrayList<>(); - - for (int i = 0; i < lootItemsList.size(); i++) { - int minLoot = Rs2Random.between(minLootItems, maxLootItems); - int maxLoot = Rs2Random.between(minLoot, maxLootItems); - - // Ensure max is not less than min - if (maxLoot < minLoot) { - maxLoot = maxLootItems; - } - - minLootItemPerPattern.add(minLoot); - maxLootItemPerPattern.add(maxLoot); - } - - boolean includeNoted = config.includeNoted(); - boolean allowNoneOwner = config.allowNoneOwner(); - - // Create the appropriate logical condition based on config - if (andLogical) { - return LootItemCondition.createAndCondition( - lootItemsList, - minLootItemPerPattern, - maxLootItemPerPattern, - includeNoted, - allowNoneOwner - ); - } else { - return LootItemCondition.createOrCondition( - lootItemsList, - minLootItemPerPattern, - maxLootItemPerPattern, - includeNoted, - allowNoneOwner - ); - } - } - - /** - * Creates a gathered resource condition based on config settings - */ - private LogicalCondition createGatheredResourceCondition() { - // Parse the comma-separated list of resources - List resourcesList = parseItemList(config.gatheredResources()); - if (resourcesList.isEmpty()) { - log.warn("No valid resources specified, defaulting to 'logs'"); - resourcesList.add("logs"); - } - - boolean andLogical = config.resourcesLogical(); - int minResources = config.minResources(); - int maxResources = config.maxResources(); - boolean includeNoted = config.includeResourceNoted(); - - // Create target lists - List minResourcesPerItem = new ArrayList<>(); - List maxResourcesPerItem = new ArrayList<>(); - - for (int i = 0; i < resourcesList.size(); i++) { - int minCount = Rs2Random.between(minResources, maxResources); - int maxCount = Rs2Random.between(minCount, maxResources); - - // Ensure max is not less than min - if (maxCount < minCount) { - maxCount = maxResources; - } - - minResourcesPerItem.add(minCount); - maxResourcesPerItem.add(maxCount); - } - - // Create the appropriate logical condition - if (andLogical) { - return GatheredResourceCondition.createAndCondition( - resourcesList, - minResourcesPerItem, - maxResourcesPerItem, - includeNoted - ); - } else { - return GatheredResourceCondition.createOrCondition( - resourcesList, - minResourcesPerItem, - maxResourcesPerItem, - includeNoted - ); - } - } - - /** - * Creates a process item condition based on config settings - */ - private Condition createProcessItemCondition() { - ProcessItemCondition.TrackingMode trackingMode; - - // Map config enum to condition enum - switch (config.trackingMode()) { - case SOURCE_CONSUMPTION: - trackingMode = ProcessItemCondition.TrackingMode.SOURCE_CONSUMPTION; - break; - case TARGET_PRODUCTION: - trackingMode = ProcessItemCondition.TrackingMode.TARGET_PRODUCTION; - break; - case EITHER: - trackingMode = ProcessItemCondition.TrackingMode.EITHER; - break; - case BOTH: - trackingMode = ProcessItemCondition.TrackingMode.BOTH; - break; - default: - trackingMode = ProcessItemCondition.TrackingMode.SOURCE_CONSUMPTION; - } - - List sourceItemsList = parseItemList(config.sourceItems()); - List targetItemsList = parseItemList(config.targetItems()); - - int minProcessed = config.minProcessedItems(); - int maxProcessed = config.maxProcessedItems(); - - // Create the appropriate process item condition based on tracking mode - if (trackingMode == ProcessItemCondition.TrackingMode.SOURCE_CONSUMPTION) { - // If tracking source consumption - if (!sourceItemsList.isEmpty()) { - return ProcessItemCondition.forConsumption(sourceItemsList.get(0), - Rs2Random.between(minProcessed, maxProcessed)); - } - } else if (trackingMode == ProcessItemCondition.TrackingMode.TARGET_PRODUCTION) { - // If tracking target production - if (!targetItemsList.isEmpty()) { - return ProcessItemCondition.forProduction(targetItemsList.get(0), - Rs2Random.between(minProcessed, maxProcessed)); - } - } else if (trackingMode == ProcessItemCondition.TrackingMode.BOTH) { - // If tracking both source and target - if (!sourceItemsList.isEmpty() && !targetItemsList.isEmpty()) { - return ProcessItemCondition.forRecipe( - sourceItemsList.get(0), 1, - targetItemsList.get(0), 1, - Rs2Random.between(minProcessed, maxProcessed) - ); - } - } - - // Default fallback - log.warn("Invalid process item configuration, using default"); - return ProcessItemCondition.forConsumption("logs", 10); - } - /** - * Creates an NPC kill count condition based on config settings - */ - private LogicalCondition createNpcKillCountCondition() { - // Parse the comma-separated list of NPC names - List npcNamesList = parseItemList(config.npcNames()); - if (npcNamesList.isEmpty()) { - log.warn("No valid NPC names specified, defaulting to 'goblin'"); - npcNamesList.add("goblin"); - } - - boolean andLogical = config.npcLogical(); - int minKills = config.minKills(); - int maxKills = config.maxKills(); - boolean killsPerType = config.killsPerType(); - - // If we're counting per NPC type, create target lists for each NPC - if (killsPerType) { - List minKillsPerNpc = new ArrayList<>(); - List maxKillsPerNpc = new ArrayList<>(); - - for (int i = 0; i < npcNamesList.size(); i++) { - int minKillCount = Rs2Random.between(minKills, maxKills); - int maxKillCount = Rs2Random.between(minKillCount, maxKills); - - // Ensure max is not less than min - if (maxKillCount < minKillCount) { - maxKillCount = maxKills; - } - - minKillsPerNpc.add(minKillCount); - maxKillsPerNpc.add(maxKillCount); - } - - // Create the appropriate logical condition based on config - if (andLogical) { - return NpcKillCountCondition.createAndCondition( - npcNamesList, - minKillsPerNpc, - maxKillsPerNpc - ); - } else { - return NpcKillCountCondition.createOrCondition( - npcNamesList, - minKillsPerNpc, - maxKillsPerNpc - ); - } - } - // If we're counting total kills across all NPC types - else { - // Generate a single randomized kill count target - int targetMin = minKills; - int targetMax = maxKills; - - // Create multiple individual conditions with same ranges - if (andLogical) { - return NpcKillCountCondition.createAndCondition( - npcNamesList, - targetMin, - targetMax - ); - } else { - return NpcKillCountCondition.createOrCondition( - npcNamesList, - targetMin, - targetMax - ); - } - } - } - - /** - * Helper method to parse a comma-separated list of items - */ - private List parseItemList(String itemsString) { - List itemsList = new ArrayList<>(); - if (itemsString != null && !itemsString.isEmpty()) { - String[] itemsArray = itemsString.split(","); - for (String item : itemsArray) { - String trimmedItem = item.trim(); - try { - // Validate regex pattern - java.util.regex.Pattern.compile(trimmedItem); - itemsList.add(trimmedItem); - log.debug("Valid item pattern found: {}", trimmedItem); - } catch (java.util.regex.PatternSyntaxException e) { - log.warn("Invalid regex pattern: '{}' - {}", trimmedItem, e.getMessage()); - } - } - } - return itemsList; - } - @Override - public ConfigDescriptor getConfigDescriptor() { - if (Microbot.getConfigManager() == null) { - return null; - } - SchedulableExampleConfig conf = Microbot.getConfigManager().getConfig(SchedulableExampleConfig.class); - return Microbot.getConfigManager().getConfigDescriptor(conf); - } - @Override - public void onStopConditionCheck() { - // Update item count when condition is checked - if (script != null) { - itemsCollected = script.getLogsCollected(); - } - } - - // Method for the scheduler to check progress - public int getItemsCollected() { - return itemsCollected; - } - @Subscribe - public void onConfigChanged(ConfigChanged event) - { - final ConfigDescriptor desc = getConfigDescriptor(); - if (desc != null && desc.getGroup() != null && event.getGroup().equals(desc.getGroup().value())) { - - this.startCondition = null; - this.stopCondition = null; - log.info( - "Config change detected for {}: {}={}, config group {}", - getName(), - event.getGroup(), - event.getKey(), - desc.getGroup().value() - ); - if (config.enablePrePostRequirements()) { - if (prePostScheduleTasks != null && !prePostScheduleTasks.isExecuting()) { - if (prePostScheduleRequirements != null) { - prePostScheduleRequirements.setConfig(config); - prePostScheduleRequirements.reset(); - } - // prePostScheduleTasks.reset(); when we allow reexecution of pre/post-schedule tasks on config change - log.info("PrePostScheduleRequirements initialized:\n{}", prePostScheduleRequirements.getDetailedDisplay()); - } - } else { - log.info("Pre/Post Schedule Requirements are disabled in configuration"); - } - } - } - - @Subscribe - public void onGameStateChanged(GameStateChanged event) { - if (event.getGameState() == GameState.LOGGED_IN) { - log.info("GameState changed to LOGGED_IN"); - getPrePostScheduleTasks(); - if( prePostScheduleTasks != null - && prePostScheduleTasks.isScheduleMode() && - !prePostScheduleTasks.isPreTaskComplete() && - !prePostScheduleTasks.isPreScheduleRunning()) { - log.info("Plugin is running in Scheduler Mode - waiting for scheduler to start pre-schedule tasks"); - } else { - log.info("Plugin is running in normal mode"); - } - }else if (event.getGameState() == GameState.LOGIN_SCREEN) { - - } - } - @Override - @Subscribe - public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - // Save location before stopping - if (event.getPlugin() == this) { - WorldPoint currentLocation = null; - if (Microbot.isLoggedIn()) { - currentLocation = Rs2Player.getWorldLocation(); - } - if ( Microbot.getConfigManager() == null) { - log.warn("Cannot save last location - ConfigManager or current location is null"); - return; - } - Microbot.getConfigManager().setConfiguration("SchedulableExample", "lastLocation", currentLocation); - log.info("Scheduling stop for plugin: {}", event.getPlugin().getClass().getSimpleName()); - - runPostScheduleTasks(); - /*try { - Microbot.log("Successfully exited SchedulerExamplePlugin - stopping plugin"); - Microbot.getClientThread().invokeLater(() -> { - Microbot.stopPlugin(this); - return true; - }); - } catch (Exception ex) { - Microbot.log("Error during safe exit: " + ex.getMessage()); - Microbot.getClientThread().invokeLater(() -> { - Microbot.stopPlugin(this); - return true; - }); - }*/ - - - - - // Schedule the stop operation on the client thread - //Microbot.getClientThread().invokeLater(() -> { - // try { - // Microbot.getPluginManager().setPluginEnabled(this, false); - // Microbot.getPluginManager().stopPlugin(this); - // } catch (Exception e) { - // log.error("Error stopping plugin", e); - // } - // }); - } - } - - - - - - - @Override - public void keyTyped(KeyEvent e) { - // Not used - } - - @Override - public void keyPressed(KeyEvent e) { - // Movement handling has been moved to VoxQoL plugin - // This plugin now only handles its core scheduling functionality - } - - - - - - - - - - - - @Override - public void keyReleased(KeyEvent e) { - // Not used but required by the KeyListener interface - } - - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePrePostScheduleRequirements.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePrePostScheduleRequirements.java deleted file mode 100644 index 58183a098a6..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePrePostScheduleRequirements.java +++ /dev/null @@ -1,640 +0,0 @@ - -package net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example; - -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.api.coords.WorldArea; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.gameval.ItemID; -import net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example.enums.UnifiedLocation; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.PrePostScheduleRequirements; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.data.ItemRequirementCollection; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.SpellbookRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.collection.LootRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.ConditionalRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location.LocationRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.OrRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.ShopItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.ShopRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.models.ShopOperation; -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.util.equipment.Rs2Equipment; -import net.runelite.client.plugins.microbot.util.grandexchange.models.TimeSeriesInterval; -import net.runelite.client.plugins.microbot.util.grounditem.models.Rs2SpawnLocation; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.magic.Rs2Spellbook; -import net.runelite.client.plugins.microbot.util.magic.Rs2Staff; -import net.runelite.client.plugins.microbot.util.magic.Runes; -import net.runelite.client.plugins.microbot.util.shop.StoreLocations; -import net.runelite.client.plugins.microbot.util.shop.models.Rs2ShopItem; -import net.runelite.client.plugins.microbot.util.shop.models.Rs2ShopType; - -import java.time.Duration; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.function.BooleanSupplier; - -/** - * Example implementation of PrePostScheduleRequirements for the SchedulableExample plugin. - * This demonstrates configurable requirements that can be enabled/disabled via plugin configuration. - * - * Features demonstrated: - * - Spellbook requirement (optional Lunar spellbook) - * - Location requirements (pre: Varrock West, post: Grand Exchange) - * - Loot requirement (coins near Lumbridge) - * - Equipment requirement (Staff of Air) - * - Inventory requirement (10k coins) - */ -@Slf4j -public class SchedulableExamplePrePostScheduleRequirements extends PrePostScheduleRequirements { - @Setter - private SchedulableExampleConfig config; - - public SchedulableExamplePrePostScheduleRequirements(SchedulableExampleConfig config) { - super("SchedulableExample", "Testing", false); - this.config = config; - } - - /** - * Initialize requirements based on configuration settings. - */ - private boolean initializeConfigurableRequirements() { - if (config == null) { - return false; // Ensure config is initialized before proceeding - } - if (!config.enablePrePostRequirements()) { - return true; // Skip requirements if disabled - } - boolean success = true; - this.getRegistry().clear(); - // Configure spellbook requirements based on dropdown selection - if (!config.preScheduleSpellbook().isNone()) { - SpellbookRequirement preSpellbookRequirement = new SpellbookRequirement( - config.preScheduleSpellbook().getSpellbook(), - TaskContext.PRE_SCHEDULE, - RequirementPriority.MANDATORY, - 7, - "Pre-schedule spellbook: " + config.preScheduleSpellbook().getDisplayName() - ); - this.register(preSpellbookRequirement); - } - - if (!config.postScheduleSpellbook().isNone()) { - SpellbookRequirement postSpellbookRequirement = new SpellbookRequirement( - config.postScheduleSpellbook().getSpellbook(), - TaskContext.POST_SCHEDULE, - RequirementPriority.MANDATORY, - 7, - "Post-schedule spellbook: " + config.postScheduleSpellbook().getDisplayName() - ); - this.register(postSpellbookRequirement); - } - - - - // Configure location requirements based on dropdown selection - if (!config.preScheduleLocation().equals(UnifiedLocation.NONE)) { - LocationRequirement preLocationRequirement; - - // Handle different location types appropriately - switch (config.preScheduleLocation().getType()) { - case BANK: - preLocationRequirement = new LocationRequirement( - (BankLocation) config.preScheduleLocation().getOriginalLocationData(), - true, // use transportation - -1, // no specific world required - TaskContext.PRE_SCHEDULE, - RequirementPriority.MANDATORY - ); - break; - - case DEPOSIT_BOX: - case SLAYER_MASTER: - case FARMING: - case HUNTING: - default: - // For non-bank locations, use WorldPoint - preLocationRequirement = new LocationRequirement( - config.preScheduleLocation().getWorldPoint(), - config.preScheduleLocation().getDisplayName(), - true, // use members - 9, - true, // use transportation - -1, // no specific world required - TaskContext.PRE_SCHEDULE, - RequirementPriority.MANDATORY, - 1, - "Must be at " + config.preScheduleLocation().getDisplayName() + " to begin the schedule" - ); - break; - } - - this.register(preLocationRequirement); - } - - - if (!config.postScheduleLocation().equals(UnifiedLocation.NONE)) { - LocationRequirement postLocationRequirement; - - // Handle different location types appropriately - switch (config.postScheduleLocation().getType()) { - case BANK: - postLocationRequirement = new LocationRequirement( - (BankLocation) config.postScheduleLocation().getOriginalLocationData(), - true, // use transportation - -1, // no specific world required - TaskContext.POST_SCHEDULE, - RequirementPriority.MANDATORY - ); - break; - - case DEPOSIT_BOX: - case SLAYER_MASTER: - case FARMING: - case HUNTING: - default: - // For non-bank locations, use WorldPoint - postLocationRequirement = new LocationRequirement( - config.postScheduleLocation().getWorldPoint(), - config.postScheduleLocation().getDisplayName(), - true, - 10, // acceptable distance - true, // use transportation - -1, // no specific world required - TaskContext.POST_SCHEDULE, - RequirementPriority.MANDATORY - ); - break; - } - - this.register(postLocationRequirement); - } - - - // Loot requirement - Coins near Lumbridge Castle - if (config.enableLootRequirement()) { - List coinSpawns = Arrays.asList( - new WorldPoint(3205, 3229, 0), // Lumbridge Castle ground floor coin spawns - new WorldPoint(3207, 3229, 0), - new WorldPoint(3209, 3229, 0) - ); - - Rs2SpawnLocation coinsSpawnLocation = new Rs2SpawnLocation( - ItemID.COINS, - "Lumbridge Castle", - "Ground Floor - East Wing", - coinSpawns, - false, // Not members only - 0, // Ground floor - Duration.ofSeconds(30) // Respawn time - ); - - LootRequirement coinsLootRequirement = new LootRequirement( - ItemID.COINS, - 5, // Amount to collect - "Test coins collection from Lumbridge Castle spawns", - coinsSpawnLocation - ); - - register(coinsLootRequirement); - } - // Equipment requirement - Staff of Air - if (config.enableEquipmentRequirement()) { - register(new ItemRequirement( - ItemID.STAFF_OF_AIR, - 1, - EquipmentInventorySlot.WEAPON, - -2, // must be equipped (equipment slot enforced) - RequirementPriority.MANDATORY, - 6, - "Staff of Air for basic magic", - TaskContext.PRE_SCHEDULE - )); - ItemRequirementCollection.registerAmuletOfGlory(this, - RequirementPriority.MANDATORY, 4, - TaskContext.PRE_SCHEDULE, - true); - ItemRequirementCollection.registerRingOfDueling(this, - RequirementPriority.MANDATORY, 4, - TaskContext.PRE_SCHEDULE, - true); - ItemRequirementCollection.registerWoodcuttingAxes(this,RequirementPriority.MANDATORY, - TaskContext.PRE_SCHEDULE,-1); // -1 for no inventory slot means the axe can be placed in a any inventory slot, and also be equipped, -2 would mean it can only be equipped - ItemRequirementCollection.registerPickAxes(this, RequirementPriority.MANDATORY, - TaskContext.POST_SCHEDULE); - } - - // Inventory requirement - 10k coins - if (config.enableInventoryRequirement()) { - register(new ItemRequirement( - ItemID.COINS, - 10000, - -1, // inventory slot - RequirementPriority.RECOMMENDED, - 8, - "10,000 coins for general purposes", - TaskContext.PRE_SCHEDULE - )); - - - // Add some basic optional requirements that are always available - register( new ItemRequirement( - ItemID.AIRRUNE, - 50, - null, // no equipment slot - -1, // inventory slot - RequirementPriority.MANDATORY, - 5, - "Basic runes for magic", - TaskContext.PRE_SCHEDULE - )); - register( new ItemRequirement( - ItemID.WATERRUNE, - 50, - null, // no equipment slot - -1, // inventory slot - RequirementPriority.RECOMMENDED, - 5, - "Basic runes for magic", - TaskContext.PRE_SCHEDULE - )); - register( new ItemRequirement( - ItemID.EARTHRUNE, - 50, - null, // no equipment slot - -1, // inventory slot - RequirementPriority.RECOMMENDED, - 5, - "Basic runes for magic", - TaskContext.PRE_SCHEDULE - )); - - // Basic runes for magic - register(new ItemRequirement( - ItemID.LAWRUNE, - 10, - -1, // inventory slot - RequirementPriority.MANDATORY, - 5, - "Law runes for magic", - TaskContext.PRE_SCHEDULE - )); - - // ==================================================================== - // OR REQUIREMENT MODES DEMONSTRATION - // ==================================================================== - // This example demonstrates both OR requirement modes: - // - // 1. ANY_COMBINATION (default): Can fulfill with any combination of food items - // Example: 2 lobsters + 3 swordfish = 5 total food items ✓ - // - // 2. SINGLE_TYPE: Must fulfill with exactly one type of item - // Example: Exactly 5 lobsters OR 5 swordfish OR 5 monkfish ✓ - // But NOT 2 lobsters + 3 swordfish ✗ - // - // You can set the mode using: setOrRequirementMode(OrRequirementMode.SINGLE_TYPE) - // Default mode is ANY_COMBINATION for backward compatibility - // ==================================================================== - - // Basic food for emergencies (demonstrates OR requirement) - register(ItemRequirement.createOrRequirement( - Arrays.asList( - ItemID.LOBSTER, - ItemID.SWORDFISH, - ItemID.MONKFISH, - ItemID.BREAD - ), - 5, - null, // no equipment slot - -1, // inventory slot - RequirementPriority.MANDATORY, - 4, - "Basic food for health restoration (OR requirement - any combination or single type based on mode)", - TaskContext.PRE_SCHEDULE - )); - } - - // Shop requirement - Multi-Item Maple Bow Trading Example - // This demonstrates the unified stock management system with multi-item operations: - // - Single BUY requirement for both maple bow types from Grand Exchange (pre-schedule) - // - Single SELL requirement for both maple bow types to Brian's Archery Supplies (post-schedule) - - // Create shop items for both maple bow types - Rs2ShopItem mapleLongbowGEItem = Rs2ShopItem.createGEItem( - ItemID.MAPLE_LONGBOW, // Maple longbow ID (851) - 0.99, // sell at 0.99% of the GE price fast selling - 1.01 // buy at 101% of the GE price fast buying - ); - - Rs2ShopItem mapleShortbowGEItem = Rs2ShopItem.createGEItem( - ItemID.MAPLE_SHORTBOW, // Maple shortbow ID (853) - 0.99, // sell at 99% of the GE price lasted price - 1.01 // buy at 101% of the GE price lasted price - ); - - // Brian's Archery Supplies shop setup (Rimmington) - WorldPoint brianShopLocation = new WorldPoint(2957, 3204, 0); // Brian's Archery Supplies in Rimmington - WorldArea brianShopArea = new WorldArea(brianShopLocation.getX() - 4, brianShopLocation.getY() - 4, 8, 8, brianShopLocation.getPlane()); - - Rs2ShopItem mapleLongbowShopItem = new Rs2ShopItem( - ItemID.MAPLE_LONGBOW, // Maple longbow ID (851) - "Brian", // Shop NPC name - brianShopArea, // Shop area - Rs2ShopType.ARCHERY_SHOP, - 1.0, // 100% sell rate (from OSRS wiki) - 0.65, // 65% buy rate (from OSRS wiki - Brian buys at 65% value) - 2.0, // Change percent - Map.of(), // No quest requirements - false, // Not members only - "Brian's Archery Supplies in Rimmington", // Notes - Duration.ofMinutes(2), // Restock time: 2 minutes (from OSRS wiki) - 2 // Base stock: 2 maple longbows (from OSRS wiki) - ); - - Rs2ShopItem mapleShortbowShopItem = new Rs2ShopItem( - ItemID.MAPLE_SHORTBOW, // Maple shortbow ID (853) - "Brian", // Shop NPC name - brianShopArea, // Same shop area - Rs2ShopType.ARCHERY_SHOP, - 1.0, // 100% sell rate - 0.65, // 65% buy rate - 2.0, // Change percent - Map.of(), // No quest requirements - false, // Not members only - "Brian's Archery Supplies in Rimmington", // Notes - Duration.ofMinutes(2), // Restock time - 2 // Base stock: 2 maple shortbows - ); - - // Create multi-item shop requirements using the current Map-based system - Map geBuyItems = Map.of( - mapleLongbowGEItem, new ShopItemRequirement(mapleLongbowGEItem, - 2, - 1, - TimeSeriesInterval.FIVE_MINUTES, - true), // 2 longbows, flexible buying - mapleShortbowGEItem, new ShopItemRequirement(mapleShortbowGEItem, - 2, - 1, - TimeSeriesInterval.FIVE_MINUTES, - true) // 2 shortbows, flexible buying - ); - - // Brian's shop: base stock=2, can sell up to 10 per world (max stock=12) - Map brianSellItems = Map.of( - mapleLongbowShopItem, new ShopItemRequirement(mapleLongbowShopItem, 20, 10), // Sell up to 10 longbows per world - mapleShortbowShopItem, new ShopItemRequirement(mapleShortbowShopItem, 20, 10) // Sell up to 10 shortbows per world - ); - - // Single multi-item BUY requirement for Grand Exchange - ShopRequirement buyMapleBowsRequirement = new ShopRequirement( - geBuyItems, - ShopOperation.BUY, - RequirementType.SHOP, - RequirementPriority.MANDATORY, - 8, - "Buy maple bows (longbow x20, shortbow x20) from Grand Exchange (pre-schedule)", - TaskContext.PRE_SCHEDULE - ); - - // Single multi-item SELL requirement for Brian's shop - ShopRequirement sellMapleBowsRequirement = new ShopRequirement( - brianSellItems, - ShopOperation.SELL, - RequirementType.SHOP, - RequirementPriority.MANDATORY, - 8, - "Sell up to 10 maple bows per type to Brian's Archery Supplies (post-schedule)", - TaskContext.POST_SCHEDULE - ); - - if (config.enableShopRequirement()) { - // Register the unified multi-item shop requirements - this.register(buyMapleBowsRequirement); - this.register(sellMapleBowsRequirement); - } - - // Custom Shop Requirement - Hammer and Bucket from nearest General Store - if (config.externalRequirements()) { - // Find the nearest general store that has both hammer and bucket - StoreLocations nearestStore = StoreLocations.getNearestStoreWithAllItems(ItemID.HAMMER, ItemID.BUCKET_EMPTY); - log.info("Nearest general store with hammer and bucket: {}", nearestStore != null ? nearestStore.getName() : "None found"); - if (nearestStore != null) { - // Create shop items for hammer and bucket from the nearest general store - WorldArea storeArea = new WorldArea( - nearestStore.getLocation().getX() - 3, - nearestStore.getLocation().getY() - 3, - 6, 6, - nearestStore.getLocation().getPlane() - ); - - Rs2ShopItem hammerShopItem = new Rs2ShopItem( - ItemID.HAMMER, - nearestStore.getNpcName(), - storeArea, - nearestStore.getShopType(), - nearestStore.getSellRate(), // Standard sell rate for general stores - nearestStore.getBuyRate(), - nearestStore.getChangePercent(), - nearestStore.getQuestRequirements(), - nearestStore.isMembers(), - "Hammer from " + nearestStore.getName(), - Duration.ofMinutes(5), - 5 // Base stock for hammers - ); - - Rs2ShopItem bucketShopItem = new Rs2ShopItem( - ItemID.BUCKET_EMPTY, - nearestStore.getNpcName(), - storeArea, - nearestStore.getShopType(), - nearestStore.getSellRate(), // Standard sell rate for general stores - nearestStore.getBuyRate(), - nearestStore.getChangePercent(), - nearestStore.getQuestRequirements(), - nearestStore.isMembers(), - "Empty bucket from " + nearestStore.getName(), - Duration.ofMinutes(5), - 3 // Base stock for buckets - ); - - // Create shop item requirements - ShopItemRequirement hammerRequirement = new ShopItemRequirement(hammerShopItem, 1, 0); - ShopItemRequirement bucketRequirement = new ShopItemRequirement(bucketShopItem, 1, 0); - - // Create the unified shop requirement for buying both items - Map shopItems = new LinkedHashMap<>(); - shopItems.put(hammerShopItem, hammerRequirement); - shopItems.put(bucketShopItem, bucketRequirement); - - ShopRequirement buyToolsRequirement = new ShopRequirement( - shopItems, - ShopOperation.BUY, - RequirementType.SHOP, - RequirementPriority.MANDATORY, - 7, - "Buy hammer and bucket from nearest general store (" + nearestStore.getName() + ")", - TaskContext.PRE_SCHEDULE - ); - - // Add as custom requirement to test external requirement fulfillment (step 7) - this.addCustomRequirement(buyToolsRequirement, TaskContext.PRE_SCHEDULE); - - log.info("Added custom shop requirement for hammer and bucket from: {}", nearestStore.getName()); - } else { - log.warn("No general store found with both hammer and bucket items"); - success = false; // Mark as failure if no store found - } - } - - // === Alch Conditional Requirement Example === - if (config.enableConditionalItemRequirement()) { - - // Build fire staff requirements (all staves that provide fire runes) - List staffWithFireRunesRequirements = Arrays.stream(Rs2Staff.values()) - .filter(staff -> staff.getRunes().contains(Runes.FIRE) && staff != Rs2Staff.NONE) - .map(staff -> new ItemRequirement( - staff.getItemID(), - 1, - EquipmentInventorySlot.WEAPON, - -2, - RequirementPriority.MANDATORY, - 10, - staff.name() + " equipped", - TaskContext.PRE_SCHEDULE, - null, null, null, null, false)) - .collect(java.util.stream.Collectors.toList()); - // Helper to check if any fire staff is available in inventory or bank - BooleanSupplier hasFireStaffCondition = () -> hasFireStaffAvailable(staffWithFireRunesRequirements); - OrRequirement fireStaffOrRequirement = new OrRequirement( - RequirementPriority.MANDATORY, - "Any fire staff equipped", - TaskContext.PRE_SCHEDULE, - staffWithFireRunesRequirements.toArray(new ItemRequirement[0]) - ); - - ItemRequirement fireRuneRequirement = new ItemRequirement( - ItemID.FIRERUNE, - 5, - -1, - RequirementPriority.MANDATORY, - 10, - "Fire runes in inventory", - TaskContext.PRE_SCHEDULE - ); - - ConditionalRequirement alchConditionalRequirement = new ConditionalRequirement( - RequirementPriority.MANDATORY, - 10, - "Alching: Fire staff or fire runes", - TaskContext.PRE_SCHEDULE, - false - ); - alchConditionalRequirement - .addStep( - () -> { - try { - return !hasFireStaffCondition.getAsBoolean(); - } catch (Throwable t) { - return false; - } - }, - fireRuneRequirement, - "Fire runes in inventory (no fire staff available)" - ) - .addStep( - () -> { - try { - return hasFireStaffCondition.getAsBoolean(); - } catch (Throwable t) { - return false; - } - }, - fireStaffOrRequirement, - "Any fire staff equipped (fire staff available)" - ); - - SpellbookRequirement normalSpellbookRequirement = new SpellbookRequirement( - Rs2Spellbook.MODERN, - TaskContext.PRE_SCHEDULE, - RequirementPriority.MANDATORY, - 10, - "Normal spellbook required for High Alchemy" - ); - - ItemRequirement natureRuneRequirement = new ItemRequirement( - ItemID.NATURERUNE, - 1, - -2, - RequirementPriority.MANDATORY, - 10, - "Nature rune for alching", - TaskContext.PRE_SCHEDULE - ); - - this.register(alchConditionalRequirement); - this.register(normalSpellbookRequirement); - this.register(natureRuneRequirement); - } - - return success; // Return true if all requirements initialized successfully - } - - /** - * Checks if any fire staff from the requirements is available in inventory or bank. - */ - private static boolean hasFireStaffAvailable(List staffReqs) { - int[] staffIds = staffReqs.stream().mapToInt(ItemRequirement::getId).toArray(); - return Rs2Inventory.contains(staffIds) || Rs2Bank.hasItem(staffIds)|| Rs2Equipment.isWearing(staffIds); - } - - - - /** - * Initialize the base item requirements collection. - * This demonstrates basic equipment and inventory requirements. - */ - @Override - protected boolean initializeRequirements() { - if (config == null){ - return false; // Ensure config is initialized before proceeding - } - return initializeConfigurableRequirements(); - } - - /** - * Gets a display string showing which requirements are currently enabled. - * Useful for debugging and logging. - */ - public String getDetailedDisplay() { - StringBuilder sb = new StringBuilder(); - sb.append("SchedulableExample Requirements Status:\n"); - sb.append(" Pre/Post Requirements: ").append(config.enablePrePostRequirements() ? "ENABLED" : "DISABLED").append("\n"); - - if (config.enablePrePostRequirements()) { - // Show new dropdown configurations - sb.append(" - Pre-Schedule Spellbook: ").append(config.preScheduleSpellbook().getDisplayName()).append("\n"); - sb.append(" - Post-Schedule Spellbook: ").append(config.postScheduleSpellbook().getDisplayName()).append("\n"); - sb.append(" - Pre-Schedule Location: ").append(config.preScheduleLocation().getDisplayName()).append("\n"); - sb.append(" - Post-Schedule Location: ").append(config.postScheduleLocation().getDisplayName()).append("\n"); - - // Show legacy configurations - sb.append(" - Loot Requirement: ").append(config.enableLootRequirement() ? "ENABLED (Coins at Lumbridge)" : "DISABLED").append("\n"); - sb.append(" - Equipment Requirement: ").append(config.enableEquipmentRequirement() ? "ENABLED (Staff of Air)" : "DISABLED").append("\n"); - sb.append(" - Inventory Requirement: ").append(config.enableInventoryRequirement() ? "ENABLED (10k Coins)" : "DISABLED").append("\n"); - sb.append(" - Shop Requirement: ").append(config.enableShopRequirement() ? "ENABLED (Hammer & Bucket from nearest general store)" : "DISABLED").append("\n"); - } - sb.append(super.getDetailedDisplay()); - - return sb.toString(); - } - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePrePostScheduleTasks.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePrePostScheduleTasks.java deleted file mode 100644 index 2dfef194ca7..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExamplePrePostScheduleTasks.java +++ /dev/null @@ -1,186 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example; - -import java.util.concurrent.CompletableFuture; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.input.KeyManager; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LockCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.AbstractPrePostScheduleTasks; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.PrePostScheduleRequirements; - -/** - * Implementation of AbstractPrePostScheduleTasks for the SchedulableExample plugin. - * This demonstrates how to implement custom pre and post schedule tasks with requirements. - * - * The class handles: - * - Pre-schedule tasks: Preparation based on configured requirements - * - Post-schedule tasks: Cleanup and resource management - * - Schedule mode detection for proper task execution - * - Requirement fulfillment through the associated PrePostScheduleRequirements - */ -@Slf4j -public class SchedulableExamplePrePostScheduleTasks extends AbstractPrePostScheduleTasks { - - private final SchedulableExamplePlugin examplePlugin; - private final SchedulableExamplePrePostScheduleRequirements requirements; - - /** - * Constructor for SchedulableExamplePrePostScheduleTasks. - * - * @param plugin The SchedulableExamplePlugin instance - * @param requirements The requirements collection for this plugin - */ - public SchedulableExamplePrePostScheduleTasks(SchedulableExamplePlugin plugin, KeyManager keyManager, SchedulableExamplePrePostScheduleRequirements requirements) { - super(plugin,keyManager); - this.examplePlugin = plugin; - this.requirements = requirements; - } - - /** - * Executes custom pre-schedule preparation tasks for the example plugin. - * This method is called AFTER standard requirement fulfillment (equipment, spellbook, location). - * The threading and safety infrastructure is handled by the parent class. - * - * @param lockCondition The lock condition to prevent interruption during critical operations - * @return true if custom preparation was successful, false otherwise - */ - @Override - protected boolean executeCustomPreScheduleTask(CompletableFuture preScheduledFuture, LockCondition lockCondition) { - StringBuilder logBuilder = new StringBuilder(); - logBuilder.append("SchedulableExample: Executing custom pre-schedule tasks...\n"); - - // Check if pre/post requirements are enabled - if (!examplePlugin.getConfig().enablePrePostRequirements()) { - logBuilder.append(" Pre/Post requirements are disabled - skipping custom pre-schedule tasks\n"); - log.info(logBuilder.toString()); - return true; - } - - // Get comprehensive validation summary from RequirementRegistry - logBuilder.append("\n=== PRE-SCHEDULE REQUIREMENTS VALIDATION ===\n"); - String validationSummary = requirements.getRegistry().getValidationSummary(net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext.PRE_SCHEDULE); - logBuilder.append(validationSummary).append("\n"); - - // Get concise status for quick reference - String statusSummary = requirements.getRegistry().getValidationStatusSummary(net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext.PRE_SCHEDULE); - logBuilder.append("Status Summary: ").append(statusSummary).append("\n\n"); - - // Validate critical mandatory requirements - boolean allMandatoryMet = requirements.getRegistry().getAllRequirements().stream() - .filter(req -> req.getPriority() == net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority.MANDATORY) - .filter(req -> req.isPreSchedule()) - .allMatch(net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement::isFulfilled); - - if (!allMandatoryMet) { - logBuilder.append("⚠ïļ WARNING: Some mandatory pre-schedule requirements are not fulfilled\n"); - logBuilder.append(" Continuing execution for testing purposes, but this may affect plugin performance\n"); - } else { - logBuilder.append("✓ All mandatory pre-schedule requirements are properly fulfilled\n"); - } - - // Note about standard requirements handling - logBuilder.append("\n--- Infrastructure Notes ---\n"); - logBuilder.append(" Standard requirements (equipment, spellbook, location) are fulfilled by parent class\n"); - logBuilder.append(" Custom plugin-specific preparation logic can be added here\n"); - logBuilder.append(" Validation summary shows overall requirement status for this context\n"); - - logBuilder.append("\nCustom pre-schedule tasks completed successfully"); - log.info(logBuilder.toString()); - - return true; - } - - /** - * Executes custom post-schedule cleanup tasks for the example plugin. - * This method is called BEFORE standard requirement fulfillment (location, spellbook restoration). - * The threading and safety infrastructure is handled by the parent class. - * - * @param lockCondition The lock condition to prevent interruption during critical operations - * @return true if custom cleanup was successful, false otherwise - */ - @Override - protected boolean executeCustomPostScheduleTask(CompletableFuture postScheduledFuture, LockCondition lockCondition) { - StringBuilder logBuilder = new StringBuilder(); - logBuilder.append("SchedulableExample: Executing custom post-schedule tasks...\n"); - - // Check if pre/post requirements are enabled - if (!examplePlugin.getConfig().enablePrePostRequirements()) { - logBuilder.append(" Pre/Post requirements are disabled - skipping custom post-schedule tasks\n"); - log.info(logBuilder.toString()); - return true; - } - - // Get comprehensive validation summary from RequirementRegistry for post-schedule context - logBuilder.append("\n=== POST-SCHEDULE REQUIREMENTS VALIDATION ===\n"); - String validationSummary = requirements.getRegistry().getValidationSummary(net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext.POST_SCHEDULE); - logBuilder.append(validationSummary).append("\n"); - - // Get concise status for quick reference - String statusSummary = requirements.getRegistry().getValidationStatusSummary(net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext.POST_SCHEDULE); - logBuilder.append("Status Summary: ").append(statusSummary).append("\n\n"); - - // Session completion summary - logBuilder.append("--- Session Completion Summary ---\n"); - - // Overall requirements processed during the session - int totalRequirements = requirements.getRegistry().getAllRequirements().size(); - int externalRequirements = requirements.getRegistry().getExternalRequirements(net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext.BOTH).size(); - - logBuilder.append(" Total requirements processed: ").append(totalRequirements).append("\n"); - if (externalRequirements > 0) { - logBuilder.append(" External requirements: ").append(externalRequirements).append("\n"); - } - - // Validate post-schedule mandatory requirements - boolean allPostMandatoryMet = requirements.getRegistry().getAllRequirements().stream() - .filter(req -> req.getPriority() == net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority.MANDATORY) - .filter(req -> req.isPostSchedule()) - .allMatch(net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement::isFulfilled); - - if (!allPostMandatoryMet) { - logBuilder.append("⚠ïļ WARNING: Some mandatory post-schedule requirements are not fulfilled\n"); - } else if (requirements.getRegistry().getAllRequirements().stream().anyMatch(req -> req.getPriority() == net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority.MANDATORY && req.isPostSchedule())) { - logBuilder.append("✓ All mandatory post-schedule requirements are properly fulfilled\n"); - } - - // Custom cleanup operations for the example plugin - logBuilder.append("\n--- Custom Plugin Cleanup ---\n"); - logBuilder.append(" ✓ Example-specific inventory cleanup completed\n"); - logBuilder.append(" ✓ Example-specific session data saved\n"); - logBuilder.append(" ✓ Plugin state reset to initial configuration\n"); - - // Note about standard requirements handling - logBuilder.append("\n--- Infrastructure Notes ---\n"); - logBuilder.append(" Standard requirements (location, spellbook restoration) will be fulfilled by parent class\n"); - logBuilder.append(" Custom plugin-specific cleanup logic has been executed\n"); - logBuilder.append(" Validation summary shows overall requirement status for post-schedule context\n"); - - logBuilder.append("\nCustom post-schedule tasks completed successfully"); - log.info(logBuilder.toString()); - - return true; - } - - - - /** - * Implementation of the abstract method from AbstractPrePostScheduleTasks. - * Returns the PrePostScheduleRequirements instance for this plugin. - * - * @return The requirements collection - */ - @Override - protected PrePostScheduleRequirements getPrePostScheduleRequirements() { - return requirements; - } - - /** - * Gets a reference to the plugin's configuration for convenience. - * - * @return The SchedulableExampleConfig instance - */ - public SchedulableExampleConfig getConfig() { - return examplePlugin.getConfig(); - } - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleScript.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleScript.java deleted file mode 100644 index d1dbe17cbb3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/SchedulableExampleScript.java +++ /dev/null @@ -1,495 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example; - -import net.runelite.api.Constants; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.gameval.ItemID; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.Script; -import net.runelite.client.plugins.microbot.breakhandler.BreakHandlerScript; -import net.runelite.client.plugins.microbot.util.antiban.Rs2Antiban; -import net.runelite.client.plugins.microbot.util.antiban.Rs2AntibanSettings; -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.equipment.Rs2Equipment; -import net.runelite.client.plugins.microbot.util.events.PluginPauseEvent; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -// import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.regex.Pattern; -import lombok.extern.slf4j.Slf4j; -@Slf4j -public class SchedulableExampleScript extends Script { - private SchedulableExampleConfig config; - private WorldPoint returnPoint; - private int logsCollected = 0; - - // Antiban testing state - private boolean antibanOriginalTakeMicroBreaks = false; - private double antibanOriginalMicroBreakChance = 0.0; - private int antibanOriginalMicroBreakDurationLow = 3; - private int antibanOriginalMicroBreakDurationHigh = 15; - private boolean antibanOriginalActionCooldownActive = false; - private boolean antibanOriginalMoveMouseOffScreen = false; - private long lastStatusReport = 0; - private long lastBreakStatusCheck = 0; - private boolean wasOnBreak = false; - private long breakStartTime = 0; - private long totalBreakTime = 0; - private int microBreakCount = 0; - private boolean antibanInitialized = false; - private int aliveCounter = 0; // Counter to track when to report alive - - enum State { - IDELE, - RESETTING, - BREAK_PAUSED - } - - private State state = State.IDELE; - - - - public boolean main(SchedulableExampleConfig config) { - if (!Microbot.isLoggedIn()) return false; - if (!super.run()) return false; - - // Initialize antiban settings if enabled - if (config.enableAntibanTesting() && !antibanInitialized) { - setupAntibanTesting(); - } - - // Check break status and handle state changes - handleBreakStatusChecks(); - - // Set initial location if none was saved - if (initialPlayerLocation == null) { - initialPlayerLocation = Rs2Player.getWorldLocation(); - } - - if (this.returnPoint == null) { - this.returnPoint = initialPlayerLocation; - } - - // Check if we have an axe - if (!hasAxe()) { - // Microbot.status = "No axe found! Stopping..."; - // return false; - } - - // Handle break pause state - don't do anything while on break - if (state == State.BREAK_PAUSED) { - return true; - } - - // Skip if player is moving or animating, unless resetting - if (state != State.RESETTING && (Rs2Player.isMoving() || Rs2Player.isAnimating())) { - return true; - } - - // Trigger antiban behaviors if enabled - if (config.enableAntibanTesting()) { - handleAntibanBehaviors(); - } - - switch (state) { - case IDELE: - if (Rs2Inventory.isFull()) { - state = State.RESETTING; - return true; - } - break; - - case RESETTING: - resetInventory(); - return true; - - case BREAK_PAUSED: - // Already handled above - return true; - } - - return true; - } - - public boolean run(SchedulableExampleConfig config, WorldPoint savedLocation) { - this.returnPoint = savedLocation; - this.config = config; - this.aliveCounter = 0; - this.mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { - try { - if (!Microbot.isLoggedIn()) return; - if (!super.run()) return; - log.info("aliveCounter: {}", aliveCounter); - // Call the main method with antiban testing - main(config); - // Increment counter and check if we should report alive - aliveCounter++; - // Compute iterations from milliseconds, clamp to at least 1 - final long periodMs = Constants.GAME_TICK_LENGTH * 2L; - final long timeoutMs = Math.max(0L, (long) config.aliveReportTimeout()*1000L); - final int reportThreshold = (int) Math.max(1L, (long) Math.ceil(timeoutMs / (double) periodMs)); - if (aliveCounter >= reportThreshold) { - Rs2ItemModel oneDosePrayerRegeneration= Rs2ItemModel.createFromCache(ItemID._1DOSE1PRAYER_REGENERATION,1,1); - List equipmentActions =oneDosePrayerRegeneration.getEquipmentActions(); - boolean isTradeable = oneDosePrayerRegeneration.isTradeable(); - log.info("{}",oneDosePrayerRegeneration.toString() ); - Rs2ItemModel graceFullHelm= Rs2ItemModel.createFromCache(ItemID.GRACEFUL_HOOD,1,1); - List graceFullHelmActions = graceFullHelm.getEquipmentActions(); - boolean isGraceFullHelmTradeable = graceFullHelm.isTradeable(); - log.info("{}",graceFullHelm.toString() ); - log.info("SchedulableExampleScript is alive! \n- PauseEvent {} (valid),pauseAllScripts: {}, BreakHanlderLook: {}", PluginPauseEvent.isPaused(), Microbot.pauseAllScripts.get(), BreakHandlerScript.lockState.get()); - aliveCounter = 0; // Reset counter - } - - return; //manuel play testing the Scheduler plugin.. doing nothing for now - } catch (Exception ex) { - Microbot.log("SchedulableExampleScript error: " + ex.getMessage()); - } - }, 0, Constants.GAME_TICK_LENGTH*2, TimeUnit.MILLISECONDS); - - return true; - } - - private boolean hasAxe() { - return Rs2Inventory.hasItem("axe") || Rs2Equipment.isWearing("axe"); - } - - private void resetInventory() { - // Update count before moving to next state - updateItemCount(); - state = State.IDELE; - } - - /* - private void bankItems() { - Microbot.status = "Banking "; - - // Find and use nearest bank - if (!Rs2Bank.isOpen()) { - if (!Rs2Bank.useBank()) { - return; - } - } - - // Deposit logs but keep axe - Rs2Bank.depositAllExcept("axe"); - Rs2Bank.closeBank(); - - // Return to woodcutting spot - walkToReturnPoint(); - } - */ - private List getLootItemPatterns(){ - String lootItemsString = config.lootItems(); - List lootItemsList = new ArrayList<>(); - List lootItemsListPattern = new ArrayList<>(); - if (lootItemsString != null && !lootItemsString.isEmpty()) { - String[] lootItemsArray = lootItemsString.split(","); - for (String item : lootItemsArray) { - String trimmedItem = item.trim(); - try { - // Validate regex pattern - lootItemsListPattern.add(java.util.regex.Pattern.compile(trimmedItem)); - lootItemsList.add(trimmedItem); - } catch (java.util.regex.PatternSyntaxException e) { - //log.warn("Invalid regex pattern: '{}' - {}", trimmedItem, e.getMessage()); - } - } - } - return lootItemsListPattern; - } - /* - private void dropItems() { - Microbot.status = "Dropping Items"; - - // Drop all logs - List lootItemsListPattern = getLootItemPatterns(); - //List foundItems = - Rs2Inventory.all().forEach(item -> { - if (lootItemsListPattern.stream().anyMatch(pattern -> pattern.matcher(item.getName()).find())) { - - }else{ - // Drop all logs - Rs2Inventory.dropAll(item.getName()); - } - - }); - // Drop all logs - - } - - - - private void walkToReturnPoint() { - if (Rs2Player.getWorldLocation().distanceTo(returnPoint) > 3) { - Rs2Walker.walkTo(returnPoint); - } - } - */ - - public void updateItemCount() { - List lootItemsListPattern = getLootItemPatterns(); - int currentItems =Rs2Inventory.all().stream().filter(item -> lootItemsListPattern.stream().anyMatch(pattern -> pattern.matcher(item.getName()).find())).mapToInt(Rs2ItemModel::getQuantity).sum(); - - - if (currentItems > 0) { - logsCollected += currentItems; - Microbot.log("Total logs collected: " + logsCollected); - } - } - - public int getLogsCollected() { - return logsCollected; - } - - @Override - public void shutdown() { - // Teardown antiban testing if it was initialized - if (config != null && config.enableAntibanTesting()) { - teardownAntibanTesting(); - } - - Microbot.log("Shutting down SchedulableExampleScript"); - super.shutdown(); - returnPoint = null; - - // Log final antiban stats if testing was enabled - if (config != null && config.enableAntibanTesting()) { - Microbot.log("Final " + getAntibanStats()); - } - } - - /** - * Sets up antiban testing configuration based on plugin config - */ - private void setupAntibanTesting() { - if (antibanInitialized) { - return; - } - - Microbot.log("Setting up antiban testing..."); - - // Store original antiban settings - antibanOriginalTakeMicroBreaks = Rs2AntibanSettings.takeMicroBreaks; - antibanOriginalMicroBreakChance = Rs2AntibanSettings.microBreakChance; - antibanOriginalMicroBreakDurationLow = Rs2AntibanSettings.microBreakDurationLow; - antibanOriginalMicroBreakDurationHigh = Rs2AntibanSettings.microBreakDurationHigh; - antibanOriginalActionCooldownActive = Rs2AntibanSettings.actionCooldownActive; - antibanOriginalMoveMouseOffScreen = Rs2AntibanSettings.moveMouseOffScreen; - - // Apply test configuration - if (config.enableMicroBreaks()) { - Rs2AntibanSettings.takeMicroBreaks = true; - Rs2AntibanSettings.microBreakChance = config.microBreakChancePercent() / 100.0; - Rs2AntibanSettings.microBreakDurationLow = config.microBreakDurationMin(); - Rs2AntibanSettings.microBreakDurationHigh = config.microBreakDurationMax(); - - Microbot.log("Micro breaks enabled - Chance: " + (config.microBreakChancePercent()) + - "%, Duration: " + config.microBreakDurationMin() + "-" + config.microBreakDurationMax() + " minutes"); - } - - if (config.enableActionCooldowns()) { - Rs2AntibanSettings.actionCooldownActive = true; - Microbot.log("Action cooldowns enabled"); - } - - if (config.moveMouseOffScreen()) { - Rs2AntibanSettings.moveMouseOffScreen = true; - Microbot.log("Mouse off-screen movement enabled"); - } - - // Set antiban activity - Rs2Antiban.setActivity(Activity.GENERAL_WOODCUTTING); - Rs2Antiban.setActivityIntensity(ActivityIntensity.MODERATE); - - antibanInitialized = true; - Microbot.log("Antiban testing setup complete"); - } - - /** - * Restores original antiban settings - */ - private void teardownAntibanTesting() { - if (!antibanInitialized) { - return; - } - - Microbot.log("Restoring original antiban settings..."); - - // Restore original settings - Rs2AntibanSettings.takeMicroBreaks = antibanOriginalTakeMicroBreaks; - Rs2AntibanSettings.microBreakChance = antibanOriginalMicroBreakChance; - Rs2AntibanSettings.microBreakDurationLow = antibanOriginalMicroBreakDurationLow; - Rs2AntibanSettings.microBreakDurationHigh = antibanOriginalMicroBreakDurationHigh; - Rs2AntibanSettings.actionCooldownActive = antibanOriginalActionCooldownActive; - Rs2AntibanSettings.moveMouseOffScreen = antibanOriginalMoveMouseOffScreen; - - // Reset antiban activity - Rs2Antiban.resetAntibanSettings(); - - antibanInitialized = false; - Microbot.log("Antiban settings restored"); - } - - /** - * Handles break status monitoring and state transitions - */ - private void handleBreakStatusChecks() { - long currentTime = System.currentTimeMillis(); - - // Check break status every second - if (currentTime - lastBreakStatusCheck < 1000) { - return; - } - lastBreakStatusCheck = currentTime; - - boolean isCurrentlyOnBreak = BreakHandlerScript.isBreakActive() || - Rs2AntibanSettings.microBreakActive || - Rs2AntibanSettings.actionCooldownActive; - - // Detect break start - if (isCurrentlyOnBreak && !wasOnBreak) { - handleBreakStart(); - } - // Detect break end - else if (!isCurrentlyOnBreak && wasOnBreak) { - handleBreakEnd(); - } - - // Report status periodically if on break - if (isCurrentlyOnBreak && config.statusReportInterval() > 0) { - if (currentTime - lastStatusReport >= config.statusReportInterval() * 1000) { - reportBreakStatus(); - lastStatusReport = currentTime; - } - } - - wasOnBreak = isCurrentlyOnBreak; - } - - /** - * Handles the start of a break - */ - private void handleBreakStart() { - breakStartTime = System.currentTimeMillis(); - state = State.BREAK_PAUSED; - - String breakType = getBreakType(); - Microbot.log("Break started - Type: " + breakType + ", Script state: PAUSED"); - - if (Rs2AntibanSettings.microBreakActive) { - microBreakCount++; - } - } - - /** - * Handles the end of a break - */ - private void handleBreakEnd() { - if (breakStartTime > 0) { - long breakDuration = System.currentTimeMillis() - breakStartTime; - totalBreakTime += breakDuration; - - String breakType = getBreakType(); - Microbot.log("Break ended - Type: " + breakType + - ", Duration: " + formatDuration(breakDuration) + - ", Script state: RESUMED"); - - breakStartTime = 0; - } - - // Resume normal operation - if (state == State.BREAK_PAUSED) { - state = State.IDELE; - } - } - - /** - * Reports current break status - */ - private void reportBreakStatus() { - String breakType = getBreakType(); - long currentBreakDuration = breakStartTime > 0 ? - System.currentTimeMillis() - breakStartTime : 0; - - Microbot.log("Break Status - Type: " + breakType + - ", Current Duration: " + formatDuration(currentBreakDuration) + - ", Total Break Time: " + formatDuration(totalBreakTime) + - ", Micro Breaks: " + microBreakCount); - } - - /** - * Determines the current break type - */ - private String getBreakType() { - if (BreakHandlerScript.isBreakActive()) { - return "Regular Break"; - } else if (Rs2AntibanSettings.microBreakActive) { - return "Micro Break"; - } else if (Rs2AntibanSettings.actionCooldownActive) { - return "Action Cooldown"; - } else { - return "Unknown"; - } - } - - /** - * Handles antiban behaviors during normal operation - */ - private void handleAntibanBehaviors() { - // Trigger action cooldown occasionally - if (config.enableActionCooldowns() && Math.random() < 0.01) { // 1% chance per tick - Rs2Antiban.actionCooldown(); - } - - // Take micro breaks by chance - if (config.enableMicroBreaks() && Math.random() < (config.microBreakChancePercent() / 100.0 )) { - Rs2Antiban.takeMicroBreakByChance(); - if (Rs2AntibanSettings.microBreakActive) { - Microbot.log("Taking a new micro break - Count: " + microBreakCount); - } - } - - // Move mouse randomly - if (config.moveMouseOffScreen() && Math.random() < 0.005) { // 0.5% chance per tick - Rs2Antiban.moveMouseRandomly(); - } - } - - /** - * Formats a duration in milliseconds to a readable string - */ - private String formatDuration(long durationMillis) { - long seconds = durationMillis / 1000; - long minutes = seconds / 60; - long hours = minutes / 60; - - if (hours > 0) { - return String.format("%dh %dm %ds", hours, minutes % 60, seconds % 60); - } else if (minutes > 0) { - return String.format("%dm %ds", minutes, seconds % 60); - } else { - return String.format("%ds", seconds); - } - } - - /** - * Gets comprehensive antiban testing statistics - */ - public String getAntibanStats() { - if (!config.enableAntibanTesting()) { - return "Antiban testing disabled"; - } - - return String.format("Antiban Stats - Total Break Time: %s, Micro Breaks: %d, " + - "Current State: %s, Break Active: %s", - formatDuration(totalBreakTime), - microBreakCount, - state.name(), - wasOnBreak ? getBreakType() : "None"); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/enums/SpellbookOption.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/enums/SpellbookOption.java deleted file mode 100644 index c23bd3de976..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/enums/SpellbookOption.java +++ /dev/null @@ -1,49 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example.enums; - -import net.runelite.client.plugins.microbot.util.magic.Rs2Spellbook; -import lombok.Getter; - -/** - * Unified spellbook enum that includes a "NONE" option for configurations - * where no spellbook switching is desired. - */ -@Getter -public enum SpellbookOption { - - NONE("None (No Switching)", null), - MODERN("Standard/Modern Spellbook", Rs2Spellbook.MODERN), - ANCIENT("Ancient Magicks", Rs2Spellbook.ANCIENT), - LUNAR("Lunar Spellbook", Rs2Spellbook.LUNAR), - ARCEUUS("Arceuus Spellbook", Rs2Spellbook.ARCEUUS); - - private final String displayName; - private final Rs2Spellbook spellbook; - - SpellbookOption(String displayName, Rs2Spellbook spellbook) { - this.displayName = displayName; - this.spellbook = spellbook; - } - - /** - * Gets the Rs2Spellbook enum value, or null if this is the NONE option - * - * @return Rs2Spellbook enum value or null - */ - public Rs2Spellbook getSpellbook() { - return spellbook; - } - - /** - * Checks if this option represents no spellbook switching - * - * @return true if this is the NONE option - */ - public boolean isNone() { - return this == NONE; - } - - @Override - public String toString() { - return displayName; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/enums/UnifiedLocation.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/enums/UnifiedLocation.java deleted file mode 100644 index af6bfd6197d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/VoxPlugins/schedulable/example/enums/UnifiedLocation.java +++ /dev/null @@ -1,190 +0,0 @@ -package net.runelite.client.plugins.microbot.VoxPlugins.schedulable.example.enums; - -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; -import net.runelite.client.plugins.microbot.util.depositbox.DepositBoxLocation; -import net.runelite.client.plugins.microbot.util.walker.enums.*; -import lombok.Getter; - -/** - * Unified location enum that encompasses major location types used in the walker system. - * This provides a single interface for selecting locations from various categories - * including banks, deposit boxes, farming locations, slayer masters, and hunting areas. - * - * This is a simplified version containing only the most commonly used locations - * to avoid compatibility issues with varying enum implementations. - */ -@Getter -public enum UnifiedLocation { - - // None option - NONE("None", LocationType.NONE, null), - - // Major Bank Locations - BANK_GRAND_EXCHANGE("Grand Exchange Bank", LocationType.BANK, BankLocation.GRAND_EXCHANGE), - BANK_VARROCK_WEST("Varrock West Bank", LocationType.BANK, BankLocation.VARROCK_WEST), - BANK_VARROCK_EAST("Varrock East Bank", LocationType.BANK, BankLocation.VARROCK_EAST), - BANK_LUMBRIDGE_FRONT("Lumbridge Bank", LocationType.BANK, BankLocation.LUMBRIDGE_FRONT), - BANK_FALADOR_WEST("Falador West Bank", LocationType.BANK, BankLocation.FALADOR_WEST), - BANK_FALADOR_EAST("Falador East Bank", LocationType.BANK, BankLocation.FALADOR_EAST), - BANK_EDGEVILLE("Edgeville Bank", LocationType.BANK, BankLocation.EDGEVILLE), - BANK_DRAYNOR_VILLAGE("Draynor Village Bank", LocationType.BANK, BankLocation.DRAYNOR_VILLAGE), - BANK_AL_KHARID("Al Kharid Bank", LocationType.BANK, BankLocation.AL_KHARID), - BANK_CATHERBY("Catherby Bank", LocationType.BANK, BankLocation.CATHERBY), - BANK_CAMELOT("Camelot Bank", LocationType.BANK, BankLocation.CAMELOT), - BANK_ARDOUGNE_NORTH("Ardougne North Bank", LocationType.BANK, BankLocation.ARDOUGNE_NORTH), - BANK_ARDOUGNE_SOUTH("Ardougne South Bank", LocationType.BANK, BankLocation.ARDOUGNE_SOUTH), - BANK_CANIFIS("Canifis Bank", LocationType.BANK, BankLocation.CANIFIS), - BANK_FISHING_GUILD("Fishing Guild Bank", LocationType.BANK, BankLocation.FISHING_GUILD), - BANK_FOSSIL_ISLAND("Fossil Island Bank", LocationType.BANK, BankLocation.FOSSIL_ISLAND), - BANK_ARCEUUS("Arceuus Bank", LocationType.BANK, BankLocation.ARCEUUS), - BANK_HOSIDIUS("Hosidius Bank", LocationType.BANK, BankLocation.HOSIDIUS), - BANK_LOVAKENGJ("Lovakengj Bank", LocationType.BANK, BankLocation.LOVAKENGJ), - BANK_PISCARILIUS("Piscarilius Bank", LocationType.BANK, BankLocation.PISCARILIUS), - BANK_SHAYZIEN_BANK("Shayzien Bank", LocationType.BANK, BankLocation.SHAYZIEN_BANK), - BANK_FARMING_GUILD("Farming Guild Bank", LocationType.BANK, BankLocation.FARMING_GUILD), - - // Major Deposit Box Locations - DEPOSIT_BOX_GRAND_EXCHANGE("Grand Exchange Deposit Box", LocationType.DEPOSIT_BOX, DepositBoxLocation.GRAND_EXCHANGE), - DEPOSIT_BOX_EDGEVILLE("Edgeville Deposit Box", LocationType.DEPOSIT_BOX, DepositBoxLocation.EDGEVILLE), - DEPOSIT_BOX_BARBARIAN_ASSAULT("Barbarian Assault Deposit Box", LocationType.DEPOSIT_BOX, DepositBoxLocation.BARBARIAN_ASSAULT), - DEPOSIT_BOX_FALADOR("Falador Deposit Box", LocationType.DEPOSIT_BOX, DepositBoxLocation.FALADOR), - DEPOSIT_BOX_VARROCK("Varrock Deposit Box", LocationType.DEPOSIT_BOX, DepositBoxLocation.VARROCK), - DEPOSIT_BOX_LUMBRIDGE("Lumbridge Deposit Box", LocationType.DEPOSIT_BOX, DepositBoxLocation.LUMBRIDGE), - DEPOSIT_BOX_FARMING_GUILD("Farming Guild Deposit Box", LocationType.DEPOSIT_BOX, DepositBoxLocation.FARMING_GUILD), - - // Major Slayer Masters - SLAYER_MASTER_TURAEL("Turael (Burthorpe)", LocationType.SLAYER_MASTER, SlayerMasters.TURAEL), - SLAYER_MASTER_SPRIA("Spria (Draynor Village)", LocationType.SLAYER_MASTER, SlayerMasters.SPRIA), - SLAYER_MASTER_MAZCHNA("Mazchna (Canifis)", LocationType.SLAYER_MASTER, SlayerMasters.MAZCHNA), - SLAYER_MASTER_VANNAKA("Vannaka (Edgeville Dungeon)", LocationType.SLAYER_MASTER, SlayerMasters.VANNAKA), - SLAYER_MASTER_CHAELDAR("Chaeldar (Zanaris)", LocationType.SLAYER_MASTER, SlayerMasters.CHAELDAR), - SLAYER_MASTER_KONAR("Konar quo Maten (Mount Karuulm)", LocationType.SLAYER_MASTER, SlayerMasters.KONAR), - SLAYER_MASTER_NIEVE("Nieve (Gnome Stronghold)", LocationType.SLAYER_MASTER, SlayerMasters.NIEVE), - SLAYER_MASTER_STEVE("Steve (Gnome Stronghold)", LocationType.SLAYER_MASTER, SlayerMasters.STEVE), - SLAYER_MASTER_DURADEL("Duradel (Shilo Village)", LocationType.SLAYER_MASTER, SlayerMasters.DURADEL), - SLAYER_MASTER_KRYSTILIA("Krystilia (Edgeville)", LocationType.SLAYER_MASTER, SlayerMasters.KRYSTILIA), - - // Major Farming Locations - Allotments - FARMING_FALADOR_ALLOTMENT("Falador Allotment", LocationType.FARMING, Allotments.FALADOR), - FARMING_CATHERBY_ALLOTMENT("Catherby Allotment", LocationType.FARMING, Allotments.CATHERBY), - FARMING_ARDOUGNE_ALLOTMENT("Ardougne Allotment", LocationType.FARMING, Allotments.ARDOUGNE), - FARMING_MORYTANIA_ALLOTMENT("Morytania Allotment", LocationType.FARMING, Allotments.MORYTANIA), - FARMING_KOUREND_ALLOTMENT("Kourend Allotment", LocationType.FARMING, Allotments.KOUREND), - FARMING_GUILD_ALLOTMENT("Farming Guild Allotment", LocationType.FARMING, Allotments.FARMING_GUILD), - - // Major Farming Locations - Trees - FARMING_TREE_FALADOR("Falador Tree", LocationType.FARMING, Trees.FALADOR), - FARMING_TREE_FARMING_GUILD("Farming Guild Tree", LocationType.FARMING, Trees.FARMING_GUILD), - FARMING_TREE_GNOME_STRONGHOLD("Gnome Stronghold Tree", LocationType.FARMING, Trees.GNOME_STRONGHOLD), - FARMING_TREE_LUMBRIDGE("Lumbridge Tree", LocationType.FARMING, Trees.LUMBRIDGE), - FARMING_TREE_TAVERLEY("Taverley Tree", LocationType.FARMING, Trees.TAVERLEY), - FARMING_TREE_VARROCK("Varrock Tree", LocationType.FARMING, Trees.VARROCK), - - // Major Farming Locations - Fruit Trees - FARMING_FRUIT_TREE_BRIMHAVEN("Brimhaven Fruit Tree", LocationType.FARMING, FruitTrees.BRIMHAVEN), - FARMING_FRUIT_TREE_CATHERBY("Catherby Fruit Tree", LocationType.FARMING, FruitTrees.CATHERBY), - FARMING_FRUIT_TREE_FARMING_GUILD("Farming Guild Fruit Tree", LocationType.FARMING, FruitTrees.FARMING_GUILD), - FARMING_FRUIT_TREE_GNOME_STRONGHOLD("Gnome Stronghold Fruit Tree", LocationType.FARMING, FruitTrees.GNOME_STRONGHOLD), - FARMING_FRUIT_TREE_TREE_GNOME_VILLAGE("Tree Gnome Village Fruit Tree", LocationType.FARMING, FruitTrees.TREE_GNOME_VILLAGE), - FARMING_FRUIT_TREE_TAI_BWO_WANNAI("Tai Bwo Wannai Fruit Tree", LocationType.FARMING, FruitTrees.TAI_BWO_WANNAI), - FARMING_FRUIT_TREE_PRIFDDINAS("Prifddinas Fruit Tree", LocationType.FARMING, FruitTrees.PRIFDDINAS); - - private final String displayName; - private final LocationType type; - private final Object locationData; - - UnifiedLocation(String displayName, LocationType type, Object locationData) { - this.displayName = displayName; - this.type = type; - this.locationData = locationData; - } - - /** - * Gets the WorldPoint for this location. - * - * @return WorldPoint if available, null otherwise - */ - public WorldPoint getWorldPoint() { - if (locationData == null) { - return null; - } - - switch (type) { - case BANK: - return ((BankLocation) locationData).getWorldPoint(); - - case DEPOSIT_BOX: - return ((DepositBoxLocation) locationData).getWorldPoint(); - - case SLAYER_MASTER: - return ((SlayerMasters) locationData).getWorldPoint(); - - case FARMING: - if (locationData instanceof Allotments) { - return ((Allotments) locationData).getWorldPoint(); - } else if (locationData instanceof Herbs) { - return ((Herbs) locationData).getWorldPoint(); - } else if (locationData instanceof Trees) { - return ((Trees) locationData).getWorldPoint(); - } else if (locationData instanceof FruitTrees) { - return ((FruitTrees) locationData).getWorldPoint(); - } else if (locationData instanceof Bushes) { - return ((Bushes) locationData).getWorldPoint(); - } else if (locationData instanceof Hops) { - return ((Hops) locationData).getWorldPoint(); - } else if (locationData instanceof CompostBins) { - return ((CompostBins) locationData).getWorldPoint(); - } - break; - - case HUNTING: - if (locationData instanceof Birds) { - return ((Birds) locationData).getWorldPoint(); - } else if (locationData instanceof Chinchompas) { - return ((Chinchompas) locationData).getWorldPoint(); - } else if (locationData instanceof Insects) { - return ((Insects) locationData).getWorldPoint(); - } else if (locationData instanceof Kebbits) { - return ((Kebbits) locationData).getWorldPoint(); - } else if (locationData instanceof Salamanders) { - return ((Salamanders) locationData).getWorldPoint(); - } else if (locationData instanceof SpecialHuntingAreas) { - return ((SpecialHuntingAreas) locationData).getWorldPoint(); - } - break; - - case NONE: - default: - return null; - } - - return null; - } - - /** - * Gets the original location data object (BankLocation, DepositBoxLocation, etc.) - * - * @return The original location data object - */ - public Object getOriginalLocationData() { - return locationData; - } - - @Override - public String toString() { - return displayName; - } - - /** - * Enum representing the different types of locations - */ - public enum LocationType { - NONE, - BANK, - DEPOSIT_BOX, - SLAYER_MASTER, - FARMING, - HUNTING - } -} 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/externalplugins/MicrobotPluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java index a3d695cbb74..1ba73828533 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.graph.Graph; import com.google.common.graph.GraphBuilder; @@ -348,7 +349,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()); } @@ -514,9 +519,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(); } @@ -553,9 +572,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"); } 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) { - List allLockCondtions = ((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 getUserBlockingConditions() { - List blockingConditions = new ArrayList<>(); - - if (userLogicalCondition != null && !userLogicalCondition.isSatisfied() && userLogicalCondition instanceof LogicalCondition) { - blockingConditions.addAll(((LogicalCondition) userLogicalCondition).getBlockingConditions()); - } - - return blockingConditions; - } - - /** - * Gets a list of all conditions that are currently blocking the plugin from running. - * This is useful for diagnosing why a plugin is not activating. - * - * @return List of all conditions preventing plugin activation - */ - public List getBlockingConditions() { - List blockingConditions = new ArrayList<>(); - - blockingConditions.addAll(getPluginBlockingConditions()); - blockingConditions.addAll(getUserBlockingConditions()); - - return blockingConditions; - } - - /** - * Gets a list of all plugin-defined "leaf" conditions that are preventing the plugin from running. - * Leaf conditions are the non-logical conditions that represent the actual root causes - * for why the plugin can't run. - * - * @return List of all plugin-defined leaf conditions preventing activation - */ - public List getPluginLeafBlockingConditions() { - List leafBlockingConditions = new ArrayList<>(); - - if (pluginCondition != null && !pluginCondition.isSatisfied()) { - if (pluginCondition instanceof LogicalCondition) { - leafBlockingConditions.addAll(((LogicalCondition) pluginCondition).getLeafBlockingConditions()); - } else { - // If condition is not a LogicalCondition but is not satisfied, add it directly - leafBlockingConditions.add(pluginCondition); - } - } - - return leafBlockingConditions; - } - - /** - * Gets a list of all user-defined "leaf" conditions that are preventing the plugin from running. - * Leaf conditions are the non-logical conditions that represent the actual root causes - * for why the plugin can't run. - * - * @return List of all user-defined leaf conditions preventing activation - */ - public List getUserLeafBlockingConditions() { - List leafBlockingConditions = new ArrayList<>(); - - if (userLogicalCondition != null && !userLogicalCondition.isSatisfied()) { - if (userLogicalCondition instanceof LogicalCondition) { - leafBlockingConditions.addAll(((LogicalCondition) userLogicalCondition).getLeafBlockingConditions()); - } else { - // If condition is not a LogicalCondition but is not satisfied, add it directly - leafBlockingConditions.add(userLogicalCondition); - } - } - - return leafBlockingConditions; - } - - /** - * Gets a list of all "leaf" conditions that are preventing the plugin from running. - * Leaf conditions are the non-logical conditions that represent the actual root causes - * for why the plugin can't run. - * - * @return List of all leaf conditions preventing plugin activation - */ - public List getLeafBlockingConditions() { - List leafBlockingConditions = new ArrayList<>(); - - leafBlockingConditions.addAll(getPluginLeafBlockingConditions()); - leafBlockingConditions.addAll(getUserLeafBlockingConditions()); - - return leafBlockingConditions; - } - - /** - * Gets a human-readable explanation of why the plugin-defined conditions are not satisfied, - * detailing the specific blocking conditions. - * - * @return A string explaining why the plugin-defined conditions are not satisfied - */ - public String getPluginBlockingExplanation() { - if (pluginCondition == null || pluginCondition.isSatisfied()) { - return "Plugin conditions are all satisfied."; - } - - StringBuilder explanation = new StringBuilder(); - explanation.append("Plugin conditions not satisfied because:\n"); - - if (pluginCondition instanceof LogicalCondition) { - explanation.append(((LogicalCondition) pluginCondition).getBlockingExplanation() - .replace("\n", "\n ")).append("\n"); - } else { - explanation.append(" ").append(pluginCondition.getDescription()) - .append(" (").append(pluginCondition.getClass().getSimpleName()).append(")\n"); - } - - // Add root causes summary - explanation.append("\nPlugin Root Causes:\n"); - List leafBlockingConditions = getPluginLeafBlockingConditions(); - for (int i = 0; i < leafBlockingConditions.size(); i++) { - Condition condition = leafBlockingConditions.get(i); - explanation.append(i + 1).append(") ").append(condition.getDescription()) - .append(" (").append(condition.getClass().getSimpleName()).append(")\n"); - } - - return explanation.toString(); - } - - /** - * Gets a human-readable explanation of why the user-defined conditions are not satisfied, - * detailing the specific blocking conditions. - * - * @return A string explaining why the user-defined conditions are not satisfied - */ - public String getUserBlockingExplanation() { - if (userLogicalCondition == null || userLogicalCondition.isSatisfied()) { - return "User conditions are all satisfied."; - } - - StringBuilder explanation = new StringBuilder(); - explanation.append("User conditions not satisfied because:\n"); - - if (userLogicalCondition instanceof LogicalCondition) { - explanation.append(((LogicalCondition) userLogicalCondition).getBlockingExplanation() - .replace("\n", "\n ")).append("\n"); - } else { - explanation.append(" ").append(userLogicalCondition.getDescription()) - .append(" (").append(userLogicalCondition.getClass().getSimpleName()).append(")\n"); - } - - // Add root causes summary - explanation.append("\nUser Root Causes:\n"); - List leafBlockingConditions = getUserLeafBlockingConditions(); - for (int i = 0; i < leafBlockingConditions.size(); i++) { - Condition condition = leafBlockingConditions.get(i); - explanation.append(i + 1).append(") ").append(condition.getDescription()) - .append(" (").append(condition.getClass().getSimpleName()).append(")\n"); - } - - return explanation.toString(); - } - - /** - * Gets a human-readable explanation of why the plugin cannot run, - * detailing the specific blocking conditions from both plugin and user sources. - * - * @return A string explaining why the plugin cannot run - */ - public String getBlockingExplanation() { - if (areAllConditionsMet()) { - return "All conditions are satisfied. The plugin should be running."; - } - - StringBuilder explanation = new StringBuilder(); - explanation.append("Plugin cannot run because:\n\n"); - - // Check plugin conditions - if (pluginCondition != null && !pluginCondition.isSatisfied()) { - explanation.append("Plugin Conditions:\n"); - if (pluginCondition instanceof LogicalCondition) { - explanation.append(((LogicalCondition) pluginCondition).getBlockingExplanation() - .replace("\n", "\n ")).append("\n"); - } else { - explanation.append(" ").append(pluginCondition.getDescription()) - .append(" (").append(pluginCondition.getClass().getSimpleName()).append(")\n"); - } - } - - // Check user conditions - if (userLogicalCondition != null && !userLogicalCondition.isSatisfied()) { - explanation.append("User Conditions:\n"); - if (userLogicalCondition instanceof LogicalCondition) { - explanation.append(((LogicalCondition) userLogicalCondition).getBlockingExplanation() - .replace("\n", "\n ")).append("\n"); - } else { - explanation.append(" ").append(userLogicalCondition.getDescription()) - .append(" (").append(userLogicalCondition.getClass().getSimpleName()).append(")\n"); - } - } - - // Add root causes summary - explanation.append("\nRoot Causes Summary:\n"); - List leafBlockingConditions = getLeafBlockingConditions(); - for (int i = 0; i < leafBlockingConditions.size(); i++) { - Condition condition = leafBlockingConditions.get(i); - explanation.append(i + 1).append(") ").append(condition.getDescription()) - .append(" (").append(condition.getClass().getSimpleName()).append(")\n"); - } - - return explanation.toString(); - } - - /** - * Gets a concise summary of the plugin-defined root causes why the plugin cannot run. - * - * @return A string summarizing the plugin-defined root causes - */ - public String getPluginRootCausesSummary() { - if (pluginCondition == null || pluginCondition.isSatisfied()) { - return "All plugin conditions are satisfied"; - } - - List leafBlockingConditions = getPluginLeafBlockingConditions(); - - if (leafBlockingConditions.isEmpty()) { - return "No specific plugin blocking conditions found"; - } - - StringBuilder summary = new StringBuilder(); - summary.append("Plugin root causes preventing activation (").append(leafBlockingConditions.size()).append("):\n"); - - for (int i = 0; i < leafBlockingConditions.size(); i++) { - Condition condition = leafBlockingConditions.get(i); - summary.append(i + 1).append(") ").append(condition.getDescription()); - - // Add progress information if available - double progress = condition.getProgressPercentage(); - if (progress > 0 && progress < 100) { - summary.append(" - ").append(String.format("%.1f%%", progress)).append(" complete"); - } - - if (i < leafBlockingConditions.size() - 1) { - summary.append("\n"); - } - } - - return summary.toString(); - } - - /** - * Gets a concise summary of the user-defined root causes why the plugin cannot run. - * - * @return A string summarizing the user-defined root causes - */ - public String getUserRootCausesSummary() { - if (userLogicalCondition == null || userLogicalCondition.isSatisfied()) { - return "All user conditions are satisfied"; - } - - List leafBlockingConditions = getUserLeafBlockingConditions(); - - if (leafBlockingConditions.isEmpty()) { - return "No specific user blocking conditions found"; - } - - StringBuilder summary = new StringBuilder(); - summary.append("User root causes preventing activation (").append(leafBlockingConditions.size()).append("):\n"); - - for (int i = 0; i < leafBlockingConditions.size(); i++) { - Condition condition = leafBlockingConditions.get(i); - summary.append(i + 1).append(") ").append(condition.getDescription()); - - // Add progress information if available - double progress = condition.getProgressPercentage(); - if (progress > 0 && progress < 100) { - summary.append(" - ").append(String.format("%.1f%%", progress)).append(" complete"); - } - - if (i < leafBlockingConditions.size() - 1) { - summary.append("\n"); - } - } - - return summary.toString(); - } - - /** - * Gets a concise summary of all root causes why the plugin cannot run, - * combining both plugin-defined and user-defined causes. - * - * @return A string summarizing the root causes preventing plugin activation - */ - public String getRootCausesSummary() { - if (areAllConditionsMet()) { - return "All conditions are satisfied"; - } - - List leafBlockingConditions = getLeafBlockingConditions(); - - if (leafBlockingConditions.isEmpty()) { - return "No specific blocking conditions found"; - } - - StringBuilder summary = new StringBuilder(); - summary.append("Root causes preventing plugin activation (").append(leafBlockingConditions.size()).append("):\n"); - - for (int i = 0; i < leafBlockingConditions.size(); i++) { - Condition condition = leafBlockingConditions.get(i); - summary.append(i + 1).append(") ").append(condition.getDescription()); - - // Add progress information if available - double progress = condition.getProgressPercentage(); - if (progress > 0 && progress < 100) { - summary.append(" - ").append(String.format("%.1f%%", progress)).append(" complete"); - } - - if (i < leafBlockingConditions.size() - 1) { - summary.append("\n"); - } - } - - return summary.toString(); - } - - /** - * Pauses all time-based conditions in both user and plugin logical structures. - * When paused, time conditions cannot be satisfied and their trigger times will be - * shifted by the duration of the pause when resumed. - */ - public void pause(){ - pauseAllConditions(); - // Unregister from events while paused - if (eventsRegistered) { - unregisterEvents(); - } - } - public void pauseUserConditions() { - // Pause all time conditions and unregister events - List timeConditions = getUserConditions(); - for (Condition condition : timeConditions) { - condition.pause(); - } - - } - public void pausePluginConditions() { - // Pause all time conditions and unregister events - List timeConditions = getPluginConditions(); - for (Condition condition : timeConditions) { - condition.pause(); - } - - } - public void pauseAllConditions() { - // Pause all time conditions and unregister events - List timeConditions = getConditions(); - for (Condition condition : timeConditions) { - condition.pause(); - } - - } - - /** - * resumes all time-based conditions in both user and plugin logical structures. - * When resumed, time conditions will resume normal operation with their trigger - * times shifted by the duration of the pause. - */ - public void resume(){ - resumeAllConditions(); - // Re-register for events when resumed - if (!eventsRegistered) { - registerEvents(); - } - - } - - public void resumeAllConditions() { - // resume all time conditions - List timeConditions = getConditions(); - for (Condition condition : timeConditions) { - condition.resume(); - } - - } - public void resumeUserConditions() { - // resume all time conditions - List timeConditions = getUserConditions(); - for (Condition condition : timeConditions) { - condition.resume(); - } - - } - public void resumePluginTimeConditions() { - // resume all time conditions - List timeConditions = getPluginConditions(); - for (Condition condition : timeConditions) { - condition.resume(); - } - - } - - /** - * Checks if any time-based conditions are currently paused. - * - * @return true if at least one time condition is paused, false otherwise - */ - public boolean hasAnyPausedConditions() { - List timeConditions = getAllTimeConditions(); - for (TimeCondition condition : timeConditions) { - if (condition.isPaused()) { - return true; - } - } - return false; - } - /** - * Checks if any time-based conditions are currently paused. - * - * @return true if at least one time condition is paused, false otherwise - */ - public boolean isPaused() { - return hasAnyPausedConditions(); - } - - /** - * Gets the estimated time until the next condition trigger. - * This method uses the new estimation system that provides more accurate - * predictions for when conditions will be satisfied. - * - * @return Optional containing the estimated duration until next trigger, or empty if not determinable - */ - public Optional getEstimatedDurationUntilSatisfied() { - // If conditions are already met, return zero duration - if (areAllConditionsMet()) { - return Optional.of(Duration.ZERO); - } - - return getEstimatedTimeWhenIsSatisfiedForLogical(getFullLogicalCondition()); - } - - /** - * Gets the estimated time until user conditions will be satisfied. - * This method focuses only on user-defined conditions. - * - * @return Optional containing the estimated duration until user conditions are satisfied - */ - public Optional getEstimatedDurationUntilUserConditionsSatisfied() { - if (areUserConditionsMet()) { - return Optional.of(Duration.ZERO); - } - - return getEstimatedTimeWhenIsSatisfiedForLogical(getFullLogicalUserCondition()); - } - - /** - * Gets the estimated time until plugin conditions will be satisfied. - * This method focuses only on plugin-defined conditions. - * - * @return Optional containing the estimated duration until plugin conditions are satisfied - */ - public Optional getEstimatedDurationUntilPluginConditionsSatisfied() { - if (arePluginConditionsMet()) { - return Optional.of(Duration.ZERO); - } - - return getEstimatedTimeWhenIsSatisfiedForLogical(getFullLogicalPluginCondition()); - } - - /** - * Helper method to get estimated satisfaction time for a logical condition. - * This recursively evaluates the condition hierarchy using the new estimation system. - * - * @param logicalCondition The logical condition to evaluate - * @return Optional containing the estimated duration, or empty if not determinable - */ - private Optional getEstimatedTimeWhenIsSatisfiedForLogical(LogicalCondition logicalCondition) { - if (logicalCondition == null) { - return Optional.empty(); - } - - return logicalCondition.getEstimatedTimeWhenIsSatisfied(); - } - - /** - * Formats the estimated trigger time as a human-readable string. - * - * @return A string representing the estimated time until conditions will be satisfied - */ - public String getEstimatedTriggerTimeString() { - Optional estimate = getEstimatedDurationUntilSatisfied(); - if (estimate.isPresent()) { - return formatDurationEstimate(estimate.get()); - } - return "Cannot estimate trigger time"; - } - - /** - * Formats an estimated duration into a human-readable string. - * - * @param duration The duration to format - * @return A formatted string representation of the duration - */ - private String formatDurationEstimate(Duration duration) { - long seconds = duration.getSeconds(); - - if (seconds < 0) { - return "Already satisfied"; - } else if (seconds == 0) { - return "Satisfied now"; - } else if (seconds < 60) { - return String.format("Estimated in ~%d seconds", seconds); - } else if (seconds < 3600) { - return String.format("Estimated in ~%d minutes %d seconds", - seconds / 60, seconds % 60); - } else if (seconds < 86400) { // Less than a day - return String.format("Estimated in ~%d hours %d minutes", - seconds / 3600, (seconds % 3600) / 60); - } else { - // More than a day away - long days = seconds / 86400; - long hours = (seconds % 86400) / 3600; - return String.format("Estimated in ~%d days %d hours", days, hours); - } - } - - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ConditionType.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ConditionType.java deleted file mode 100644 index b41e285792e..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ConditionType.java +++ /dev/null @@ -1,44 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition; - -import net.runelite.api.NPC; - -/** - * Defines the types of conditions available for script execution. - */ -public enum ConditionType { - TIME("TIME"), - SKILL("SKILL"), - RESOURCE("RESOURCE"), - LOCATION("LOCATION"), - LOGICAL("LOGICAL"), - NPC("NPC"), - VARBIT("VARBIT"); - - - private final String identifier; - - ConditionType(String identifier) { - this.identifier = identifier; - } - - public String getIdentifier() { - return identifier; - } - - /** - * Finds a condition type by its identifier string. - */ - public static ConditionType fromIdentifier(String identifier) { - for (ConditionType type : values()) { - if (type.identifier.equals(identifier)) { - return type; - } - } - return null; - } - - @Override - public String toString() { - return identifier; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/AreaCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/AreaCondition.java deleted file mode 100644 index 98ca69fb63c..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/AreaCondition.java +++ /dev/null @@ -1,153 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.location; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.coords.WorldArea; -import net.runelite.api.coords.WorldPoint; - -/** - * Condition that is met when the player is inside a rectangular area - */ -@Slf4j -@EqualsAndHashCode(callSuper = false) - -public class AreaCondition extends LocationCondition { - public static String getVersion() { - return "0.0.1"; - } - @Getter - private final WorldArea area; - - /** - * Create a condition that is met when the player is inside the specified area - * - * @param x1 Southwest corner x - * @param y1 Southwest corner y - * @param x2 Northeast corner x - * @param y2 Northeast corner y - * @param plane The plane the area is on - */ - public AreaCondition(String name, int x1, int y1, int x2, int y2, int plane) { - super(name); - int width = Math.abs(x2 - x1) + 1; - int height = Math.abs(y2 - y1) + 1; - int startX = Math.min(x1, x2); - int startY = Math.min(y1, y2); - this.area = new WorldArea(startX, startY, width, height, plane); - } - - /** - * Create a condition that is met when the player is inside the specified area - * - * @param area The area to check - */ - public AreaCondition(String name, WorldArea area) { - super(name); - if (area == null) { - throw new IllegalArgumentException("Area cannot be null"); - } - this.area = area; - } - - @Override - protected void updateLocationStatus() { - if (!canCheckLocation()) { - return; - } - - try { - WorldPoint location = getCurrentLocation(); - if (location != null) { - satisfied = area.contains(location); - - if (satisfied) { - log.debug("Player entered target area"); - } - } - } catch (Exception e) { - log.error("Error checking if player is in area", e); - } - } - - @Override - public String getDescription() { - WorldPoint location = getCurrentLocation(); - String statusInfo = ""; - - if (location != null) { - boolean inArea = area.contains(location); - statusInfo = String.format(" (currently %s)", inArea ? "inside area" : "outside area"); - } - - return String.format("Player in area: %d,%d to %d,%d (plane %d)%s", - area.getX(), area.getY(), - area.getX() + area.getWidth() - 1, - area.getY() + area.getHeight() - 1, - area.getPlane(), - statusInfo); - } - - @Override - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - // Basic description - sb.append("Area Condition: Player must be within a specific area\n"); - - // Status information - WorldPoint location = getCurrentLocation(); - boolean inArea = location != null && area.contains(location); - sb.append("Status: ").append(inArea ? "Satisfied" : "Not satisfied").append("\n"); - - // Area details - sb.append("Area Coordinates: (").append(area.getX()).append(", ").append(area.getY()).append(") to (") - .append(area.getX() + area.getWidth() - 1).append(", ") - .append(area.getY() + area.getHeight() - 1).append(")\n"); - sb.append("Area Size: ").append(area.getWidth()).append(" x ").append(area.getHeight()).append(" tiles\n"); - sb.append("Plane: ").append(area.getPlane()).append("\n"); - - // Current player position - if (location != null) { - sb.append("Player Position: (").append(location.getX()).append(", ") - .append(location.getY()).append(", ").append(location.getPlane()).append(")\n"); - sb.append("Player In Area: ").append(inArea ? "Yes" : "No"); - } else { - sb.append("Player Position: Unknown"); - } - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - // Basic information - sb.append("AreaCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Type: Area (Player must be within area)\n"); - sb.append(" │ Coordinates: (").append(area.getX()).append(", ").append(area.getY()).append(") to (") - .append(area.getX() + area.getWidth() - 1).append(", ") - .append(area.getY() + area.getHeight() - 1).append(")\n"); - sb.append(" │ Size: ").append(area.getWidth()).append(" x ").append(area.getHeight()).append(" tiles\n"); - sb.append(" │ Plane: ").append(area.getPlane()).append("\n"); - - // Status information - sb.append(" └─ Status ──────────────────────────────────\n"); - WorldPoint location = getCurrentLocation(); - boolean inArea = location != null && area.contains(location); - sb.append(" Satisfied: ").append(inArea).append("\n"); - - // Player location - if (location != null) { - sb.append(" Player Position: (").append(location.getX()).append(", ") - .append(location.getY()).append(", ").append(location.getPlane()).append(")\n"); - sb.append(" In Target Area: ").append(inArea ? "Yes" : "No"); - } else { - sb.append(" Player Position: Unknown"); - } - - return sb.toString(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/LocationCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/LocationCondition.java deleted file mode 100644 index 05c98ba0c7a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/LocationCondition.java +++ /dev/null @@ -1,278 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.location; - -import com.formdev.flatlaf.json.Location; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Client; -import net.runelite.api.GameState; -import net.runelite.api.coords.WorldArea; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.GameTick; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.OrCondition; -import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -/** - * Base class for all location-based conditions. - * Provides common functionality for conditions that depend on player location. - */ -@Slf4j -@EqualsAndHashCode(callSuper = false) -public abstract class LocationCondition implements Condition { - protected transient volatile boolean satisfied = false; - /** - * Indicates whether the condition is satisfied based on the player's location. - */ - @Getter - protected final String name; - public LocationCondition(String name) { - this.name = name; - } - @Override - public boolean isSatisfied() { - return satisfied; //update in the child class, via updateLocationStatus - } - - @Override - public void reset() { - - } - - @Override - public void reset(boolean randomize) { - - } - - @Override - public ConditionType getType() { - return ConditionType.LOCATION; - } - - @Override - public void onGameTick(GameTick event) { - updateLocationStatus(); - } - - /** - * Updates the satisfaction status based on the player's current location. - * Each subclass will implement its own location check logic. - */ - protected abstract void updateLocationStatus(); - - /** - * Gets the player's current location if available. - * - * @return The player's current WorldPoint or null if unavailable - */ - protected WorldPoint getCurrentLocation() { - if (!canCheckLocation()) { - return null; - } - return Rs2Player.getWorldLocation(); - } - - /** - * Checks if the game client is in a state where location checks can be performed. - * - * @return True if location checks can be performed, false otherwise - */ - protected boolean canCheckLocation() { - Client client = Microbot.getClient(); - return client != null && client.getGameState() == GameState.LOGGED_IN; - } - - /** - * Returns a detailed description of the location condition with additional status information. - */ - public abstract String getDetailedDescription(); - - /** - * Creates a condition that is satisfied when the player is at the location for the given bank - * - * @param bank The bank location - * @param distance The maximum distance from the bank point - * @return A condition that is satisfied when the player is within distance of the bank - */ - public static Condition atBank(BankLocation bank, int distance) { - - WorldPoint baPoint = bank.getWorldPoint(); - log .info("Bank location: " + bank.name() + " - " + baPoint); - return new PositionCondition("At " + bank.name(), baPoint, distance); - - - - } - - - // Probelem for now is the enums are protect in worldmap plugin.... - // import net.runelite.client.plugins.worldmap.RareTreeLocation; - // /** - // * Creates a condition that is satisfied when the player is at any of the locations for the given farming patch - // * - // * @param patch The farming patch location - // * @param distance The maximum distance from any patch point - // * @return A condition that is satisfied when the player is at the farming patch - // */ - // public static Condition atFarmingPatch(FarmingPatchLocation patch, int distance) { - // WorldPoint[] locations = patch.getLocations(); - // if (locations.length == 1) { - // return new PositionCondition("At " + patch.name(), locations[0], distance); - // } else { - // OrCondition orCondition = new OrCondition(); - // for (WorldPoint point : locations) { - // orCondition.addCondition( - // new PositionCondition("At " + patch.name() + " location", point, distance) - // ); - // } - // return orCondition; - // } - // } - - // /** - // * Creates a condition that is satisfied when the player is at any of the locations for the given tree type - // * - // * @param treeLocation The rare tree location - // * @param distance The maximum distance from any tree location - // * @return A condition that is satisfied when the player is at the tree - // */ - // public static Condition atRareTree(RareTreeLocation treeLocation, int distance) { - // WorldPoint[] locations = treeLocation.getLocations(); - // if (locations.length == 1) { - // return new PositionCondition("At " + treeLocation.name(), locations[0], distance); - // } else { - // OrCondition orCondition = new OrCondition(); - // for (WorldPoint point : locations) { - // orCondition.addCondition( - // new PositionCondition("At " + treeLocation.name() + " location", point, distance) - // ); - // } - // return orCondition; - // } - // } - /** - * Creates a condition that is satisfied when the player is at any of the given points - * - * @param name Descriptive name for the condition - * @param points Array of points to check - * @param distance The maximum distance from any point - * @return A condition that is satisfied when the player is at any of the points - */ - public static Condition atAnyPoint(String name, WorldPoint[] points, int distance) { - if (points == null || points.length == 0) { - throw new IllegalArgumentException("At least one point must be provided"); - } - if (distance < 0) { - throw new IllegalArgumentException("Distance must be >= 0"); - } - for (int i = 0; i < points.length; i++) { - if (points[i] == null) { - throw new IllegalArgumentException("points[" + i + "] must not be null"); - } - } - if (points.length == 1) { - return new PositionCondition(name, points[0], distance); - } else { - OrCondition orCondition = new OrCondition(); - for (int i = 0; i < points.length; i++) { - orCondition.addCondition(new PositionCondition(name + " (point " + (i + 1) + ")", points[i], distance)); - } - return orCondition; - } - } - - - /** - * Creates a rectangle area condition centered on the given point - * - * @param name Descriptive name for the condition - * @param center The center point of the rectangle - * @param width Width of the area (in tiles) - * @param height Height of the area (in tiles) - * @return A condition that is satisfied when the player is within the area - */ - public static AreaCondition createArea(String name, WorldPoint center, int width, int height) { - int halfWidth = width / 2; - int halfHeight = height / 2; - int x1 = center.getX() - halfWidth; - int y1 = center.getY() - halfHeight; - int x2 = center.getX() + halfWidth; - int y2 = center.getY() + halfHeight; - return new AreaCondition(name, x1, y1, x2, y2, center.getPlane()); - } - - /** - * Creates a condition that is satisfied when the player is within any of the given areas - * - * @param name Descriptive name for the condition - * @param areas Array of WorldAreas to check - * @return A condition that is satisfied when the player is in any of the areas - */ - public static Condition inAnyArea(String name, WorldArea[] areas) { - if (areas.length == 0) { - throw new IllegalArgumentException("At least one area must be provided"); - } - - if (areas.length == 1) { - return new AreaCondition(name, areas[0]); - } else { - OrCondition orCondition = new OrCondition(); - for (int i = 0; i < areas.length; i++) { - orCondition.addCondition( - new AreaCondition(name + " (area " + (i+1) + ")", areas[i]) - ); - } - return orCondition; - } - } - - /** - * Creates a condition that is satisfied when the player is within any of the given rectangular areas - * - * @param name Descriptive name for the condition - * @param areaDefinitions Array of area definitions, each containing [x1, y1, x2, y2, plane] - * @return A condition that is satisfied when the player is in any of the areas - */ - public static Condition inAnyArea(String name, int[][] areaDefinitions) { - if (areaDefinitions.length == 0) { - throw new IllegalArgumentException("At least one area must be provided"); - } - - if (areaDefinitions.length == 1) { - int[] def = areaDefinitions[0]; - if (def.length != 5) { - throw new IllegalArgumentException("Each area definition must contain [x1, y1, x2, y2, plane]"); - } - return new AreaCondition(name, def[0], def[1], def[2], def[3], def[4]); - } else { - OrCondition orCondition = new OrCondition(); - for (int i = 0; i < areaDefinitions.length; i++) { - int[] def = areaDefinitions[i]; - if (def.length != 5) { - throw new IllegalArgumentException("Each area definition must contain [x1, y1, x2, y2, plane]"); - } - orCondition.addCondition( - new AreaCondition(name + " (area " + (i+1) + ")", def[0], def[1], def[2], def[3], def[4]) - ); - } - return orCondition; - } - } - @Override - public void pause() { - - - - } - - @Override - public void resume() { - - } - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/PositionCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/PositionCondition.java deleted file mode 100644 index 3dc99948ab9..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/PositionCondition.java +++ /dev/null @@ -1,248 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.location; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.Microbot; -/** - * Condition that is met when the player is within a certain distance of a specific position - */ -@Slf4j -@EqualsAndHashCode(callSuper = false) -public class PositionCondition extends LocationCondition { - public static String getVersion() { - return "0.0.1"; - } - @Getter - private final WorldPoint targetPosition; - @Getter - private final int maxDistance; - - /** - * Create a condition that is met when the player is within the specified distance of the target position - * - * @param x The target x coordinate - * @param y The target y coordinate - * @param plane The target plane - * @param maxDistance The maximum distance from the target position (in tiles) - */ - public PositionCondition(String name, int x, int y, int plane, int maxDistance) { - super(name); - if (maxDistance < 0) { - throw new IllegalArgumentException("Max distance cannot be negative"); - } - if (x < 0 || y < 0) { - throw new IllegalArgumentException("Coordinates and plane must be non-negative"); - } - if (maxDistance > 0 && maxDistance > 104) { - throw new IllegalArgumentException("Max distance must be within the valid range (0-104)"); - } - this.targetPosition = new WorldPoint(x, y, plane); - this.maxDistance = maxDistance; - } - - /** - * Create a condition that is met when the player is at the exact position - * - * @param x The target x coordinate - * @param y The target y coordinate - * @param plane The target plane - */ - public PositionCondition(String name, int x, int y, int plane) { - this(name, x, y, plane, 0); - } - - /** - * Create a condition that is met when the player is within the specified distance of the target position - * - * @param position The target position - * @param maxDistance The maximum distance from the target position (in tiles) - */ - public PositionCondition(String name, WorldPoint position, int maxDistance) { - super(name); - if (maxDistance < 0) { - throw new IllegalArgumentException("Max distance cannot be negative"); - } - if (position == null) { - throw new IllegalArgumentException("Position cannot be null"); - } - - this.targetPosition = position; - this.maxDistance = maxDistance; - } - - @Override - protected void updateLocationStatus() { - if (Microbot.isDebug()){ - log.info("Checking player position against target position: {}", targetPosition); - } - - if (!canCheckLocation()) { - return; - } - try { - WorldPoint currentPosition = getCurrentLocation(); - if (Microbot.isDebug()){ - log.info("Current position: {}", currentPosition); - log.info("Target position: {}", targetPosition); - log.info("Max distance: {}", maxDistance); - log.info("Current plane: {}", currentPosition != null ? currentPosition.getPlane() : "null"); - } - if (currentPosition != null && currentPosition.getPlane() == targetPosition.getPlane()) { - int distance = currentPosition.distanceTo(targetPosition); - if (Microbot.isDebug()){ - log.info("Distance to target position: {}", distance); - log.info("Max distance: {}", maxDistance); - } - this.satisfied = distance <= maxDistance; - - if (this.satisfied) { - log.debug("Player reached target position, distance: {}", distance); - } - } - } catch (Exception e) { - log.error("Error checking player position", e); - } - } - - @Override - public String getDescription() { - WorldPoint currentLocation = getCurrentLocation(); - String distanceInfo = ""; - String playerPositionInfo = ""; - - if (currentLocation != null) { - int distance = -1; - boolean onSamePlane = currentLocation.getPlane() == targetPosition.getPlane(); - - if (onSamePlane) { - distance = currentLocation.distanceTo(targetPosition); - distanceInfo = String.format(" (current distance: %d tiles)", distance); - } else { - distanceInfo = " (not on same plane)"; - } - - playerPositionInfo = String.format(" | Player at: %d, %d, %d", - currentLocation.getX(), currentLocation.getY(), currentLocation.getPlane()); - } else { - playerPositionInfo = " | Player position unknown"; - } - - if (maxDistance == 0) { - return String.format("Player at position: %d, %d, %d%s%s", - targetPosition.getX(), targetPosition.getY(), targetPosition.getPlane(), distanceInfo, playerPositionInfo); - } else { - return String.format("Player within %d tiles of: %d, %d, %d%s%s", - maxDistance, targetPosition.getX(), targetPosition.getY(), targetPosition.getPlane(), distanceInfo, playerPositionInfo); - } - } - - @Override - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - // Basic description - if (maxDistance == 0) { - sb.append("Position Condition: Player must be at exact position\n"); - } else { - sb.append("Position Condition: Player must be within ").append(maxDistance) - .append(" tiles of target position\n"); - } - - // Status information - WorldPoint currentPosition = getCurrentLocation(); - int distance = -1; - boolean onSamePlane = false; - - if (currentPosition != null) { - onSamePlane = currentPosition.getPlane() == targetPosition.getPlane(); - if (onSamePlane) { - distance = currentPosition.distanceTo(targetPosition); - } - } - - boolean isSatisfied = onSamePlane && distance <= maxDistance && distance != -1; - sb.append("Status: ").append(isSatisfied ? "Satisfied" : "Not satisfied").append("\n"); - - // Target details - sb.append("Target Position: (").append(targetPosition.getX()).append(", ") - .append(targetPosition.getY()).append(", ").append(targetPosition.getPlane()).append(")\n"); - - if (maxDistance > 0) { - sb.append("Max Distance: ").append(maxDistance).append(" tiles\n"); - } - - // Current player position and distance - if (currentPosition != null) { - sb.append("Player Position: (").append(currentPosition.getX()).append(", ") - .append(currentPosition.getY()).append(", ").append(currentPosition.getPlane()).append(")\n"); - - if (onSamePlane) { - sb.append("Current Distance: ").append(distance).append(" tiles"); - if (distance <= maxDistance) { - sb.append(" (within range)"); - } else { - sb.append(" (outside range)"); - } - } else { - sb.append("Current Distance: Not on same plane"); - } - } else { - sb.append("Player Position: Unknown"); - } - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - // Basic information - sb.append("PositionCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - if (maxDistance == 0) { - sb.append(" │ Type: Position (Player must be at exact position)\n"); - } else { - sb.append(" │ Type: Position (Player must be within ").append(maxDistance) - .append(" tiles of target)\n"); - } - sb.append(" │ Target: (").append(targetPosition.getX()).append(", ") - .append(targetPosition.getY()).append(", ").append(targetPosition.getPlane()).append(")\n"); - - if (maxDistance > 0) { - sb.append(" │ Max Distance: ").append(maxDistance).append(" tiles\n"); - } - - // Status information - sb.append(" └─ Status ──────────────────────────────────\n"); - WorldPoint currentPosition = getCurrentLocation(); - int distance = -1; - boolean onSamePlane = false; - - if (currentPosition != null) { - onSamePlane = currentPosition.getPlane() == targetPosition.getPlane(); - if (onSamePlane) { - distance = currentPosition.distanceTo(targetPosition); - } - - sb.append(" Player Position: (").append(currentPosition.getX()).append(", ") - .append(currentPosition.getY()).append(", ").append(currentPosition.getPlane()).append(")\n"); - - if (onSamePlane) { - sb.append(" Current Distance: ").append(distance).append(" tiles\n"); - } else { - sb.append(" Current Distance: Not on same plane\n"); - } - - boolean isSatisfied = onSamePlane && distance <= maxDistance; - sb.append(" Satisfied: ").append(isSatisfied); - } else { - sb.append(" Player Position: Unknown\n"); - sb.append(" Satisfied: false"); - } - - return sb.toString(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/RegionCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/RegionCondition.java deleted file mode 100644 index 22ee8051b8a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/RegionCondition.java +++ /dev/null @@ -1,149 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.location; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.coords.WorldPoint; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -/** - * Condition that is met when the player enters a specific region - */ -@Slf4j -public class RegionCondition extends LocationCondition { - public static String getVersion() { - return "0.0.1"; - } - @Getter - private final Set targetRegions; - - /** - * Create a condition that is met when the player enters any of the specified regions - * - * @param regionIds The region IDs to check for - */ - public RegionCondition(String name,int... regionIds) { - super(name); - if (regionIds == null || regionIds.length == 0) { - throw new IllegalArgumentException("Region IDs cannot be null or empty"); - } - this.targetRegions = new HashSet<>(); - for (int id : regionIds) { - targetRegions.add(id); - } - } - - @Override - protected void updateLocationStatus() { - if (!canCheckLocation()) { - return; - } - - try { - WorldPoint location = getCurrentLocation(); - if (location != null) { - int currentRegion = location.getRegionID(); - satisfied = targetRegions.contains(currentRegion); - - if (satisfied) { - log.debug("Player entered target region: {}", currentRegion); - } - } - } catch (Exception e) { - log.error("Error checking player region", e); - } - } - - @Override - public String getDescription() { - WorldPoint location = getCurrentLocation(); - String currentRegionInfo = ""; - - if (location != null) { - int currentRegion = location.getRegionID(); - boolean inTargetRegion = targetRegions.contains(currentRegion); - currentRegionInfo = String.format(" (current region: %d, %s)", - currentRegion, inTargetRegion ? "matched" : "not matched"); - } - - return "Player in regions: " + Arrays.toString(targetRegions.toArray()) + currentRegionInfo; - } - - @Override - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - // Basic description - int regionCount = targetRegions.size(); - if (regionCount == 1) { - sb.append("Region Condition: Player must be in a specific region\n"); - } else { - sb.append("Region Condition: Player must be in one of ").append(regionCount) - .append(" specified regions\n"); - } - - // Status information - WorldPoint location = getCurrentLocation(); - int currentRegion = -1; - boolean inTargetRegion = false; - - if (location != null) { - currentRegion = location.getRegionID(); - inTargetRegion = targetRegions.contains(currentRegion); - } - - sb.append("Status: ").append(inTargetRegion ? "Satisfied" : "Not satisfied").append("\n"); - - // Target region details - sb.append("Target Regions: ").append(Arrays.toString(targetRegions.toArray())).append("\n"); - - // Current player region - if (location != null) { - sb.append("Current Region: ").append(currentRegion); - sb.append(inTargetRegion ? " (matched)" : " (not matched)"); - } else { - sb.append("Current Region: Unknown"); - } - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - // Basic information - sb.append("RegionCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - - int regionCount = targetRegions.size(); - if (regionCount == 1) { - sb.append(" │ Type: Region (Player must be in specific region)\n"); - } else { - sb.append(" │ Type: Region (Player must be in one of ") - .append(regionCount).append(" regions)\n"); - } - - sb.append(" │ Target Regions: ").append(Arrays.toString(targetRegions.toArray())).append("\n"); - - // Status information - sb.append(" └─ Status ──────────────────────────────────\n"); - WorldPoint location = getCurrentLocation(); - - if (location != null) { - int currentRegion = location.getRegionID(); - boolean inTargetRegion = targetRegions.contains(currentRegion); - - sb.append(" Current Region: ").append(currentRegion).append("\n"); - sb.append(" Matched: ").append(inTargetRegion).append("\n"); - sb.append(" Satisfied: ").append(inTargetRegion); - } else { - sb.append(" Current Region: Unknown\n"); - sb.append(" Satisfied: false"); - } - - return sb.toString(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/readme.md b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/readme.md deleted file mode 100644 index 2e5eb3c59de..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/readme.md +++ /dev/null @@ -1,28 +0,0 @@ - -``` Java -// Check if player is at a bank -Condition atGrandExchange = LocationCondition.atBank(BankLocation.GRAND_EXCHANGE, 5); - -// Check if player is at any farming patch -Condition atAnyAllotmentPatch = LocationCondition.atFarmingPatch(FarmingPatchLocation.ALLOTMENT, 3); - -// Check if player is at a specific yew tree (all yew tree locations) -Condition atYewTree = LocationCondition.atRareTree(RareTreeLocation.YEW, 2); - -// Check if player is in an area around a point -Condition inMiningArea = LocationCondition.createArea("Mining Area", new WorldPoint(3230, 3145, 0), 10, 10); -``` - -``` Java -// Check if player is in any of several areas -WorldArea area1 = new WorldArea(3200, 3200, 10, 10, 0); -WorldArea area2 = new WorldArea(3300, 3300, 5, 5, 0); -Condition inEitherArea = LocationCondition.inAnyArea("Training areas", new WorldArea[]{area1, area2}); - -// Using raw coordinates -int[][] miningAreas = { - {3220, 3145, 3235, 3155, 0}, // Mining area 1 - {3270, 3160, 3278, 3168, 0} // Mining area 2 -}; -Condition inAnyMiningArea = LocationCondition.inAnyArea("Mining spots", miningAreas); -``` \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/AreaConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/AreaConditionAdapter.java deleted file mode 100644 index d12b22637d8..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/AreaConditionAdapter.java +++ /dev/null @@ -1,81 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.location.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.coords.WorldArea; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.AreaCondition; - -import java.lang.reflect.Type; - -/** - * Serializes and deserializes AreaCondition objects - */ -@Slf4j -public class AreaConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(AreaCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add type information - json.addProperty("type", AreaCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Store area information - data.addProperty("name", src.getName()); - WorldArea area = src.getArea(); - data.addProperty("x", area.getX()); - data.addProperty("y", area.getY()); - data.addProperty("width", area.getWidth()); - data.addProperty("height", area.getHeight()); - data.addProperty("plane", area.getPlane()); - data.addProperty("version", AreaCondition.getVersion()); - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public AreaCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(AreaCondition.getVersion())) { - - throw new JsonParseException("Version mismatch in AreaCondition: expected " + - AreaCondition.getVersion() + ", got " + version); - } - } - - // Get area information - String name = dataObj.get("name").getAsString(); - int x = dataObj.get("x").getAsInt(); - int y = dataObj.get("y").getAsInt(); - int width = dataObj.get("width").getAsInt(); - int height = dataObj.get("height").getAsInt(); - int plane = dataObj.get("plane").getAsInt(); - - // Create area and condition - WorldArea area = new WorldArea(x, y, width, height, plane); - return new AreaCondition(name, area); - - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/PositionConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/PositionConditionAdapter.java deleted file mode 100644 index 59087f849f3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/PositionConditionAdapter.java +++ /dev/null @@ -1,76 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.location.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.PositionCondition; - -import java.lang.reflect.Type; - -/** - * Serializes and deserializes PositionCondition objects - */ -@Slf4j -public class PositionConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(PositionCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add type information - json.addProperty("type", PositionCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Store position information - data.addProperty("name", src.getName()); - WorldPoint point = src.getTargetPosition(); - data.addProperty("x", point.getX()); - data.addProperty("y", point.getY()); - data.addProperty("plane", point.getPlane()); - data.addProperty("maxDistance", src.getMaxDistance()); - data.addProperty("version", PositionCondition.getVersion()); - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public PositionCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(PositionCondition.getVersion())) { - throw new JsonParseException("Version mismatch in PositionCondition: expected " + - PositionCondition.getVersion() + ", got " + version); - } - } - - // Get position information - String name = dataObj.get("name").getAsString(); - int x = dataObj.get("x").getAsInt(); - int y = dataObj.get("y").getAsInt(); - int plane = dataObj.get("plane").getAsInt(); - int maxDistance = dataObj.get("maxDistance").getAsInt(); - - // Create condition - return new PositionCondition(name, x, y, plane, maxDistance); - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/RegionConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/RegionConditionAdapter.java deleted file mode 100644 index e46db8c9dde..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/serialization/RegionConditionAdapter.java +++ /dev/null @@ -1,79 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.location.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.RegionCondition; - -import java.lang.reflect.Type; -import java.util.Set; - -/** - * Serializes and deserializes RegionCondition objects - */ -@Slf4j -public class RegionConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(RegionCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add type information - json.addProperty("type", RegionCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Store region information - data.addProperty("name", src.getName()); - JsonArray regionIds = new JsonArray(); - for (Integer regionId : src.getTargetRegions()) { - regionIds.add(regionId); - } - data.add("regionIds", regionIds); - data.addProperty("version", RegionCondition.getVersion()); - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public RegionCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(RegionCondition.getVersion())) { - throw new JsonParseException("Version mismatch in RegionCondition: expected " + - RegionCondition.getVersion() + ", got " + version); - } - } - - // Get region information - String name = dataObj.get("name").getAsString(); - JsonArray regionIdsArray = dataObj.getAsJsonArray("regionIds"); - int[] regionIds = new int[regionIdsArray.size()]; - - for (int i = 0; i < regionIdsArray.size(); i++) { - regionIds[i] = regionIdsArray.get(i).getAsInt(); - } - - // Create condition - return new RegionCondition(name, regionIds); - - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/ui/LocationConditionUtil.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/ui/LocationConditionUtil.java deleted file mode 100644 index 157a65e1cca..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/location/ui/LocationConditionUtil.java +++ /dev/null @@ -1,831 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.location.ui; - -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.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.AreaCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.PositionCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.RegionCondition; -import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import java.awt.*; -import java.util.Set; -import java.util.function.Consumer; - - -/** - * Utility class for creating panels for location-based conditions - */ -public class LocationConditionUtil { - public static final Color BRAND_BLUE = new Color(25, 130, 196); - /** - * Creates a unified location condition panel with tab selection for different location condition types - */ - public static void createLocationConditionPanel(JPanel panel, GridBagConstraints gbc) { - // Main label - JLabel titleLabel = new JLabel("Location Condition:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // Create tabbed pane for different location condition types - gbc.gridy++; - JTabbedPane tabbedPane = new JTabbedPane(); - tabbedPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - tabbedPane.setForeground(Color.WHITE); - - // Create panels for each condition type - JPanel positionPanel = new JPanel(new GridBagLayout()); - positionPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JPanel areaPanel = new JPanel(new GridBagLayout()); - areaPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JPanel regionPanel = new JPanel(new GridBagLayout()); - regionPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Add tabs - tabbedPane.addTab("Position", positionPanel); - tabbedPane.addTab("Area", areaPanel); - tabbedPane.addTab("Region", regionPanel); - - // Create condition panels in each tab - GridBagConstraints tabGbc = new GridBagConstraints(); - tabGbc.gridx = 0; - tabGbc.gridy = 0; - tabGbc.weightx = 1; - tabGbc.fill = GridBagConstraints.HORIZONTAL; - tabGbc.anchor = GridBagConstraints.NORTHWEST; - - createPositionConditionPanel(positionPanel, tabGbc); - createAreaConditionPanel(areaPanel, tabGbc); - createRegionConditionPanel(regionPanel, tabGbc); - - // Add the tabbed pane to the main panel - panel.add(tabbedPane, gbc); - - // Store the tabbed pane for later access - panel.putClientProperty("locationTabbedPane", tabbedPane); - } - - /** - * Sets up panel with values from an existing location condition - * - * @param panel The panel containing the UI components - * @param condition The location condition to read values from - */ - public static void setupLocationCondition(JPanel panel, Condition condition) { - if (condition == null) { - return; - } - - if (condition instanceof PositionCondition) { - setupPositionCondition(panel, (PositionCondition) condition); - } else if (condition instanceof AreaCondition) { - setupAreaCondition(panel, (AreaCondition) condition); - } else if (condition instanceof RegionCondition) { - setupRegionCondition(panel, (RegionCondition) condition); - } - } - - /** - * Sets up a position condition panel with values from an existing condition - */ - private static void setupPositionCondition(JPanel panel, PositionCondition condition) { - JSpinner xSpinner = (JSpinner) panel.getClientProperty("posXSpinner"); - JSpinner ySpinner = (JSpinner) panel.getClientProperty("posYSpinner"); - JSpinner zSpinner = (JSpinner) panel.getClientProperty("posZSpinner"); - JSpinner radiusSpinner = (JSpinner) panel.getClientProperty("posRadiusSpinner"); - - if (xSpinner != null && ySpinner != null && zSpinner != null && radiusSpinner != null) { - WorldPoint position = condition.getTargetPosition(); - if (position != null) { - xSpinner.setValue(position.getX()); - ySpinner.setValue(position.getY()); - zSpinner.setValue(position.getPlane()); - } - radiusSpinner.setValue(condition.getMaxDistance()); - } - } - - /** - * Sets up an area condition panel with values from an existing condition - */ - private static void setupAreaCondition(JPanel panel, AreaCondition condition) { - JSpinner x1Spinner = (JSpinner) panel.getClientProperty("areaX1Spinner"); - JSpinner y1Spinner = (JSpinner) panel.getClientProperty("areaY1Spinner"); - JSpinner z1Spinner = (JSpinner) panel.getClientProperty("areaZ1Spinner"); - JSpinner x2Spinner = (JSpinner) panel.getClientProperty("areaX2Spinner"); - JSpinner y2Spinner = (JSpinner) panel.getClientProperty("areaY2Spinner"); - JSpinner z2Spinner = (JSpinner) panel.getClientProperty("areaZ2Spinner"); - - if (x1Spinner != null && y1Spinner != null && z1Spinner != null && - x2Spinner != null && y2Spinner != null && z2Spinner != null) { - - WorldArea area = condition.getArea(); - if (area != null) { - x1Spinner.setValue(area.getX()); - y1Spinner.setValue(area.getY()); - z1Spinner.setValue(area.getPlane()); - x2Spinner.setValue(area.getX() + area.getWidth() - 1); - y2Spinner.setValue(area.getY() + area.getHeight() - 1); - z2Spinner.setValue(area.getPlane()); - } - } - } - - /** - * Sets up a region condition panel with values from an existing condition - */ - private static void setupRegionCondition(JPanel panel, RegionCondition condition) { - JTextField regionIdsField = (JTextField) panel.getClientProperty("regionIdsField"); - JTextField nameField = (JTextField) panel.getClientProperty("regionNameField"); - - if (regionIdsField != null && condition != null) { - // Format the region IDs as a comma-separated string - Set regionIds = condition.getTargetRegions(); - if (regionIds != null && !regionIds.isEmpty()) { - StringBuilder sb = new StringBuilder(); - for (Integer regionId : regionIds) { - if (sb.length() > 0) { - sb.append(","); - } - sb.append(regionId); - } - regionIdsField.setText(sb.toString()); - } - } - - if (nameField != null) { - nameField.setText(condition.getName()); - } - } - - /** - * Creates a location condition based on the selected tab and configuration - */ - public static Condition createLocationCondition(JPanel configPanel) { - JTabbedPane tabbedPane = (JTabbedPane) configPanel.getClientProperty("locationTabbedPane"); - - if (tabbedPane == null) { - throw new IllegalStateException("Location condition panel not properly configured - locationTabbedPane not found"); - } - - int selectedIndex = tabbedPane.getSelectedIndex(); - - // Get the specific tab panel that contains the components for the selected condition type - JPanel activeTabPanel = (JPanel) tabbedPane.getComponentAt(selectedIndex); - - switch (selectedIndex) { - case 0: // Position - return createPositionCondition(activeTabPanel); - case 1: // Area - return createAreaCondition(activeTabPanel); - case 2: // Region - return createRegionCondition(activeTabPanel); - default: - throw new IllegalStateException("Unknown location condition type"); - } - } - - /** - * Creates a panel for configuring PositionCondition - */ - private static void createPositionConditionPanel(JPanel panel, GridBagConstraints gbc) { - // Section title - JLabel titleLabel = new JLabel("Position Condition (Specific Location):"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // Position coordinates - gbc.gridy++; - JPanel positionPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - positionPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel positionLabel = new JLabel("Target Position:"); - positionLabel.setForeground(Color.WHITE); - positionPanel.add(positionLabel); - - // X input - SpinnerNumberModel xModel = new SpinnerNumberModel(3000, 0, 20000, 1); - JSpinner xSpinner = new JSpinner(xModel); - xSpinner.setPreferredSize(new Dimension(70, xSpinner.getPreferredSize().height)); - positionPanel.add(xSpinner); - - JLabel xLabel = new JLabel("X"); - xLabel.setForeground(Color.WHITE); - positionPanel.add(xLabel); - - // Y input - SpinnerNumberModel yModel = new SpinnerNumberModel(3000, 0, 20000, 1); - JSpinner ySpinner = new JSpinner(yModel); - ySpinner.setPreferredSize(new Dimension(70, ySpinner.getPreferredSize().height)); - positionPanel.add(ySpinner); - - JLabel yLabel = new JLabel("Y"); - yLabel.setForeground(Color.WHITE); - positionPanel.add(yLabel); - - panel.add(positionPanel, gbc); - - // Plane selection - gbc.gridy++; - JPanel planePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - planePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel planeLabel = new JLabel("Plane:"); - planeLabel.setForeground(Color.WHITE); - planePanel.add(planeLabel); - - SpinnerNumberModel planeModel = new SpinnerNumberModel(0, 0, 3, 1); - JSpinner planeSpinner = new JSpinner(planeModel); - planePanel.add(planeSpinner); - - panel.add(planePanel, gbc); - - // Distance range - gbc.gridy++; - JPanel distancePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - distancePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel distanceLabel = new JLabel("Max Distance (tiles):"); - distanceLabel.setForeground(Color.WHITE); - distancePanel.add(distanceLabel); - - SpinnerNumberModel distanceModel = new SpinnerNumberModel(5, 0, 104, 1); - JSpinner distanceSpinner = new JSpinner(distanceModel); - distancePanel.add(distanceSpinner); - - // Add info about distance=0 meaning exact location - JLabel exactLabel = new JLabel("(0 = exact position)"); - exactLabel.setForeground(Color.LIGHT_GRAY); - distancePanel.add(exactLabel); - - panel.add(distancePanel, gbc); - - // Current location getter - gbc.gridy++; - JPanel currentLocPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - currentLocPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JButton useCurrentLocationButton = new JButton("Use Current Location"); - useCurrentLocationButton.setBackground(ColorScheme.BRAND_ORANGE); - useCurrentLocationButton.setForeground(Color.WHITE); - useCurrentLocationButton.setFocusPainted(false); - useCurrentLocationButton.setToolTipText("Use your character's current position"); - useCurrentLocationButton.addActionListener(e -> { - // Get the current player location - if (!Microbot.isLoggedIn() || Microbot.getClient() == null || Microbot.getClient().getLocalPlayer() == null) { - return; - } - WorldPoint currentPoint = Rs2Player.getWorldLocation(); - if (currentPoint != null) { - xSpinner.setValue(currentPoint.getX()); - ySpinner.setValue(currentPoint.getY()); - planeSpinner.setValue(currentPoint.getPlane()); - } - }); - currentLocPanel.add(useCurrentLocationButton); - - JTextField nameField = new JTextField(20); - // Bank location selector - JButton selectBankButton = new JButton("Select Bank Location"); - selectBankButton.setBackground(BRAND_BLUE); // Using consistent color scheme - selectBankButton.setForeground(Color.WHITE); - selectBankButton.setToolTipText("Choose from common bank locations in the game"); - selectBankButton.setFocusPainted(false); // More consistent with other UI elements - selectBankButton.addActionListener(e -> { - // Show bank location selector - showBankLocationSelector(panel, (location) -> { - if (location != null) { - WorldPoint point = location.getWorldPoint(); - xSpinner.setValue(point.getX()); - ySpinner.setValue(point.getY()); - planeSpinner.setValue(point.getPlane()); - - // Update name field to include bank name - nameField.setText("At " + location.name() + " Bank"); - } - }); - }); - currentLocPanel.add(selectBankButton); - - panel.add(currentLocPanel, gbc); - - // Add a helpful description - gbc.gridy++; - JLabel descriptionLabel = new JLabel("Condition is met when player is within specified distance of target"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Name field for the condition - gbc.gridy++; - JPanel namePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - namePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel nameLabel = new JLabel("Condition Name:"); - nameLabel.setForeground(Color.WHITE); - namePanel.add(nameLabel); - - - nameField.setText("Position Condition"); - namePanel.add(nameField); - - panel.add(namePanel, gbc); - - // Store components for later access - panel.putClientProperty("positionXSpinner", xSpinner); - panel.putClientProperty("positionYSpinner", ySpinner); - panel.putClientProperty("positionPlaneSpinner", planeSpinner); - panel.putClientProperty("positionDistanceSpinner", distanceSpinner); - panel.putClientProperty("positionNameField", nameField); - } - /** - * Creates a panel for configuring AreaCondition - */ - private static void createAreaConditionPanel(JPanel panel, GridBagConstraints gbc) { - // Section title - JLabel titleLabel = new JLabel("Area Condition (Rectangular Area):"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // First corner coordinates - gbc.gridy++; - JPanel corner1Panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - corner1Panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel corner1Label = new JLabel("Southwest Corner:"); - corner1Label.setForeground(Color.WHITE); - corner1Panel.add(corner1Label); - - // X1 input - SpinnerNumberModel x1Model = new SpinnerNumberModel(3000, 0, 20000, 1); - JSpinner x1Spinner = new JSpinner(x1Model); - x1Spinner.setPreferredSize(new Dimension(70, x1Spinner.getPreferredSize().height)); - corner1Panel.add(x1Spinner); - - JLabel x1Label = new JLabel("X"); - x1Label.setForeground(Color.WHITE); - corner1Panel.add(x1Label); - - // Y1 input - SpinnerNumberModel y1Model = new SpinnerNumberModel(3000, 0, 20000, 1); - JSpinner y1Spinner = new JSpinner(y1Model); - y1Spinner.setPreferredSize(new Dimension(70, y1Spinner.getPreferredSize().height)); - corner1Panel.add(y1Spinner); - - JLabel y1Label = new JLabel("Y"); - y1Label.setForeground(Color.WHITE); - corner1Panel.add(y1Label); - - panel.add(corner1Panel, gbc); - - // Second corner coordinates - gbc.gridy++; - JPanel corner2Panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - corner2Panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel corner2Label = new JLabel("Northeast Corner:"); - corner2Label.setForeground(Color.WHITE); - corner2Panel.add(corner2Label); - - // X2 input - SpinnerNumberModel x2Model = new SpinnerNumberModel(3010, 0, 20000, 1); - JSpinner x2Spinner = new JSpinner(x2Model); - x2Spinner.setPreferredSize(new Dimension(70, x2Spinner.getPreferredSize().height)); - corner2Panel.add(x2Spinner); - - JLabel x2Label = new JLabel("X"); - x2Label.setForeground(Color.WHITE); - corner2Panel.add(x2Label); - - // Y2 input - SpinnerNumberModel y2Model = new SpinnerNumberModel(3010, 0, 20000, 1); - JSpinner y2Spinner = new JSpinner(y2Model); - y2Spinner.setPreferredSize(new Dimension(70, y2Spinner.getPreferredSize().height)); - corner2Panel.add(y2Spinner); - - JLabel y2Label = new JLabel("Y"); - y2Label.setForeground(Color.WHITE); - corner2Panel.add(y2Label); - - panel.add(corner2Panel, gbc); - - // Plane selection - gbc.gridy++; - JPanel planePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - planePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel planeLabel = new JLabel("Plane:"); - planeLabel.setForeground(Color.WHITE); - planePanel.add(planeLabel); - - SpinnerNumberModel planeModel = new SpinnerNumberModel(0, 0, 3, 1); - JSpinner planeSpinner = new JSpinner(planeModel); - planePanel.add(planeSpinner); - - panel.add(planePanel, gbc); - - // Current location getter - gbc.gridy++; - JPanel currentLocPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - currentLocPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JButton useCurrentLocationButton = new JButton("Create Area Around Current Location"); - useCurrentLocationButton.setBackground(ColorScheme.BRAND_ORANGE); - useCurrentLocationButton.setForeground(Color.WHITE); - useCurrentLocationButton.addActionListener(e -> { - // Get current player location and create area around it - WorldPoint currentPoint = Rs2Player.getWorldLocation(); - if (currentPoint != null) { - // Set corner1 to 5 tiles southwest - x1Spinner.setValue(currentPoint.getX() - 5); - y1Spinner.setValue(currentPoint.getY() - 5); - - // Set corner2 to 5 tiles northeast - x2Spinner.setValue(currentPoint.getX() + 5); - y2Spinner.setValue(currentPoint.getY() + 5); - - // Set plane - planeSpinner.setValue(currentPoint.getPlane()); - } - }); - currentLocPanel.add(useCurrentLocationButton); - - panel.add(currentLocPanel, gbc); - - // Add a helpful description - gbc.gridy++; - JLabel descriptionLabel = new JLabel("Condition is met when player is inside the rectangular area"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Name field for the condition - gbc.gridy++; - JPanel namePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - namePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel nameLabel = new JLabel("Condition Name:"); - nameLabel.setForeground(Color.WHITE); - namePanel.add(nameLabel); - - JTextField nameField = new JTextField(20); - nameField.setText("Area Condition"); - namePanel.add(nameField); - - panel.add(namePanel, gbc); - - // Store components for later access - panel.putClientProperty("areaX1Spinner", x1Spinner); - panel.putClientProperty("areaY1Spinner", y1Spinner); - panel.putClientProperty("areaX2Spinner", x2Spinner); - panel.putClientProperty("areaY2Spinner", y2Spinner); - panel.putClientProperty("areaPlaneSpinner", planeSpinner); - panel.putClientProperty("areaNameField", nameField); - } - /** - * Creates a panel for configuring RegionCondition - */ - private static void createRegionConditionPanel(JPanel panel, GridBagConstraints gbc) { - // Section title - JLabel titleLabel = new JLabel("Region Condition (Game Region):"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // Region IDs input - gbc.gridy++; - JPanel regionPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - regionPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel regionsLabel = new JLabel("Region IDs (comma-separated):"); - regionsLabel.setForeground(Color.WHITE); - regionPanel.add(regionsLabel); - - JTextField regionIdsField = new JTextField(20); - regionIdsField.setToolTipText("Enter region IDs separated by commas (e.g., 12850,12851)"); - regionPanel.add(regionIdsField); - - panel.add(regionPanel, gbc); - - // Current region getter - gbc.gridy++; - JPanel currentRegionPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - currentRegionPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JButton useCurrentRegionButton = new JButton("Use Current Region"); - useCurrentRegionButton.setBackground(ColorScheme.BRAND_ORANGE); - useCurrentRegionButton.setForeground(Color.WHITE); - useCurrentRegionButton.addActionListener(e -> { - // Get the current player region - WorldPoint currentPoint = Rs2Player.getWorldLocation(); - if (currentPoint != null) { - int regionId = currentPoint.getRegionID(); - regionIdsField.setText(String.valueOf(regionId)); - } - }); - currentRegionPanel.add(useCurrentRegionButton); - - panel.add(currentRegionPanel, gbc); - - // Region presets panel - gbc.gridy++; - JPanel presetsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - presetsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel presetsLabel = new JLabel("Common Regions:"); - presetsLabel.setForeground(Color.WHITE); - presetsPanel.add(presetsLabel); - - // Add example regions as presets - String[][] regionPresets = { - {"Lumbridge", "12850"}, - {"Varrock", "12853"}, - {"Grand Exchange", "12598"}, - {"Falador", "12084"}, - {"Edgeville", "12342"} - }; - - JComboBox regionPresetsCombo = new JComboBox<>(); - regionPresetsCombo.addItem("Select a region..."); - for (String[] preset : regionPresets) { - regionPresetsCombo.addItem(preset[0]); - } - - regionPresetsCombo.addActionListener(e -> { - int selectedIndex = regionPresetsCombo.getSelectedIndex(); - if (selectedIndex > 0) { - regionIdsField.setText(regionPresets[selectedIndex - 1][1]); - } - }); - - presetsPanel.add(regionPresetsCombo); - - panel.add(presetsPanel, gbc); - - // Add a helpful description - gbc.gridy++; - JLabel descriptionLabel = new JLabel("Condition is met when player is in any of the specified regions"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Name field for the condition - gbc.gridy++; - JPanel namePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - namePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel nameLabel = new JLabel("Condition Name:"); - nameLabel.setForeground(Color.WHITE); - namePanel.add(nameLabel); - - JTextField nameField = new JTextField(20); - nameField.setText("Region Condition"); - namePanel.add(nameField); - - panel.add(namePanel, gbc); - - // Store components for later access - panel.putClientProperty("regionIdsField", regionIdsField); - panel.putClientProperty("regionNameField", nameField); - } - /** - * Creates a PositionCondition from the panel configuration - */ - public static PositionCondition createPositionCondition(JPanel configPanel) { - JSpinner xSpinner = (JSpinner) configPanel.getClientProperty("positionXSpinner"); - JSpinner ySpinner = (JSpinner) configPanel.getClientProperty("positionYSpinner"); - JSpinner planeSpinner = (JSpinner) configPanel.getClientProperty("positionPlaneSpinner"); - JSpinner distanceSpinner = (JSpinner) configPanel.getClientProperty("positionDistanceSpinner"); - JTextField nameField = (JTextField) configPanel.getClientProperty("positionNameField"); - - if (xSpinner == null || ySpinner == null || planeSpinner == null || distanceSpinner == null) { - throw new IllegalStateException("Position condition panel not properly configured - missing spinner components"); - } - - int x = (Integer) xSpinner.getValue(); - int y = (Integer) ySpinner.getValue(); - int plane = (Integer) planeSpinner.getValue(); - int distance = (Integer) distanceSpinner.getValue(); - - String name = nameField != null ? nameField.getText() : "Position Condition"; - if (name.isEmpty()) { - name = "Position Condition"; - } - - return new PositionCondition(name, x, y, plane, distance); - } - - - /** - * Creates an AreaCondition from the panel configuration - */ - public static AreaCondition createAreaCondition(JPanel configPanel) { - JSpinner x1Spinner = (JSpinner) configPanel.getClientProperty("areaX1Spinner"); - JSpinner y1Spinner = (JSpinner) configPanel.getClientProperty("areaY1Spinner"); - JSpinner x2Spinner = (JSpinner) configPanel.getClientProperty("areaX2Spinner"); - JSpinner y2Spinner = (JSpinner) configPanel.getClientProperty("areaY2Spinner"); - JSpinner planeSpinner = (JSpinner) configPanel.getClientProperty("areaPlaneSpinner"); - JTextField nameField = (JTextField) configPanel.getClientProperty("areaNameField"); - - if (x1Spinner == null || y1Spinner == null || x2Spinner == null || y2Spinner == null || planeSpinner == null) { - throw new IllegalStateException("Area condition panel not properly configured - missing spinner components"); - } - - int x1 = (Integer) x1Spinner.getValue(); - int y1 = (Integer) y1Spinner.getValue(); - int x2 = (Integer) x2Spinner.getValue(); - int y2 = (Integer) y2Spinner.getValue(); - int plane = (Integer) planeSpinner.getValue(); - - String name = nameField != null ? nameField.getText() : "Area Condition"; - if (name.isEmpty()) { - name = "Area Condition"; - } - - return new AreaCondition(name, x1, y1, x2, y2, plane); - } - /** - * Creates a RegionCondition from the panel configuration - */ - public static RegionCondition createRegionCondition(JPanel configPanel) { - JTextField regionIdsField = (JTextField) configPanel.getClientProperty("regionIdsField"); - JTextField nameField = (JTextField) configPanel.getClientProperty("regionNameField"); - - if (regionIdsField == null) { - throw new IllegalStateException("Region condition panel not properly configured - missing regionIdsField"); - } - - String regionIdsText = regionIdsField.getText().trim(); - if (regionIdsText.isEmpty()) { - throw new IllegalArgumentException("Region IDs cannot be empty"); - } - - // Parse comma-separated region IDs - String[] regionIdStrings = regionIdsText.split(","); - int[] regionIds = new int[regionIdStrings.length]; - - try { - for (int i = 0; i < regionIdStrings.length; i++) { - regionIds[i] = Integer.parseInt(regionIdStrings[i].trim()); - } - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid region ID format. Must be comma-separated integers."); - } - - String name = nameField != null ? nameField.getText() : "Region Condition"; - if (name.isEmpty()) { - name = "Region Condition"; - } - - return new RegionCondition(name, regionIds); - } - /** - * Shows a dialog to select a bank location - * - * @param parentComponent The parent component for the dialog - * @param callback Callback function to handle the selected bank location - */ - private static void showBankLocationSelector(Component parentComponent, Consumer callback) { - // Create a dialog for bank location selection - JDialog dialog = new JDialog(SwingUtilities.getWindowAncestor(parentComponent), "Select Bank Location", Dialog.ModalityType.APPLICATION_MODAL); - dialog.setLayout(new BorderLayout()); - dialog.setSize(400, 500); - dialog.setLocationRelativeTo(parentComponent); - - // Create a panel for the dialog content - JPanel contentPanel = new JPanel(new BorderLayout()); - contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - contentPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Search field at the top - JTextField searchField = new JTextField(); - searchField.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - searchField.setForeground(Color.WHITE); - searchField.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - BorderFactory.createEmptyBorder(5, 5, 5, 5))); - - JLabel searchLabel = new JLabel("Search:"); - searchLabel.setForeground(Color.WHITE); - searchLabel.setFont(FontManager.getRunescapeSmallFont()); - - JPanel searchPanel = new JPanel(new BorderLayout()); - searchPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - searchPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0)); - searchPanel.add(searchLabel, BorderLayout.WEST); - searchPanel.add(searchField, BorderLayout.CENTER); - - // List of bank locations - DefaultListModel bankListModel = new DefaultListModel<>(); - for (BankLocation location : BankLocation.values()) { - bankListModel.addElement(location); - } - - JList bankList = new JList<>(bankListModel); - bankList.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - bankList.setForeground(Color.WHITE); - bankList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - bankList.setCellRenderer(new DefaultListCellRenderer() { - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, - boolean isSelected, boolean cellHasFocus) { - super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - if (value instanceof BankLocation) { - setText(((BankLocation) value).name()); - } - return this; - } - }); - - JScrollPane scrollPane = new JScrollPane(bankList); - scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); - scrollPane.getVerticalScrollBar().setUnitIncrement(16); - - // Filter the list when search text changes - searchField.getDocument().addDocumentListener(new DocumentListener() { - @Override - public void insertUpdate(DocumentEvent e) { - filterList(); - } - - @Override - public void removeUpdate(DocumentEvent e) { - filterList(); - } - - @Override - public void changedUpdate(DocumentEvent e) { - filterList(); - } - - private void filterList() { - String searchText = searchField.getText().toLowerCase(); - DefaultListModel filteredModel = new DefaultListModel<>(); - - for (BankLocation location : BankLocation.values()) { - if (location.name().toLowerCase().contains(searchText)) { - filteredModel.addElement(location); - } - } - - bankList.setModel(filteredModel); - } - }); - - // Buttons at the bottom - JButton selectButton = new JButton("Select"); - selectButton.setBackground(ColorScheme.BRAND_ORANGE); - selectButton.setForeground(Color.WHITE); - selectButton.setFocusPainted(false); - selectButton.addActionListener(e -> { - BankLocation selectedLocation = bankList.getSelectedValue(); - callback.accept(selectedLocation); - dialog.dispose(); - }); - - JButton cancelButton = new JButton("Cancel"); - cancelButton.setBackground(ColorScheme.LIGHT_GRAY_COLOR); - cancelButton.setForeground(Color.BLACK); - cancelButton.setFocusPainted(false); - cancelButton.addActionListener(e -> dialog.dispose()); - - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - buttonPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - buttonPanel.add(cancelButton); - buttonPanel.add(selectButton); - - // Add everything to the content panel - contentPanel.add(searchPanel, BorderLayout.NORTH); - contentPanel.add(scrollPane, BorderLayout.CENTER); - contentPanel.add(buttonPanel, BorderLayout.SOUTH); - - // Add content panel to dialog and show it - dialog.add(contentPanel); - dialog.setVisible(true); - } - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/AndCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/AndCondition.java deleted file mode 100644 index 5f1db7465eb..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/AndCondition.java +++ /dev/null @@ -1,245 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.logical; - -import java.time.Duration; -import java.time.ZonedDateTime; -import java.util.Optional; -import java.util.List; -import java.util.ArrayList; -import lombok.EqualsAndHashCode; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; - -/** - * Logical AND combination of conditions - all must be met. - */ -@EqualsAndHashCode(callSuper = true) -public class AndCondition extends LogicalCondition { - @Override - public boolean isSatisfied() { - if (conditions.isEmpty()) return true; - return conditions.stream().allMatch(Condition::isSatisfied); - } - - /** - * Returns a detailed description of the AND condition with additional status information - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - // Basic description - sb.append("AND Logical Condition: All conditions must be satisfied\n"); - - // Status information - boolean satisfied = isSatisfied(); - sb.append("Status: ").append(satisfied ? "Satisfied" : "Not satisfied").append("\n"); - sb.append("Child Conditions: ").append(conditions.size()).append("\n"); - - // Progress information - double progress = getProgressPercentage(); - sb.append(String.format("Overall Progress: %.1f%%\n", progress)); - - // Count satisfied conditions - int satisfiedCount = 0; - for (Condition condition : conditions) { - if (condition.isSatisfied()) { - satisfiedCount++; - } - } - sb.append("Satisfied Conditions: ").append(satisfiedCount).append("/").append(conditions.size()).append("\n\n"); - - // List all child conditions - sb.append("Child Conditions:\n"); - for (int i = 0; i < conditions.size(); i++) { - Condition condition = conditions.get(i); - sb.append(String.format("%d. %s [%s]\n", - i + 1, - condition.getDescription(), - condition.isSatisfied() ? "SATISFIED" : "NOT SATISFIED")); - } - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - // Basic information - sb.append("AndCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Type: AND (All conditions must be satisfied)\n"); - sb.append(" │ Child Conditions: ").append(conditions.size()).append("\n"); - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - boolean allSatisfied = isSatisfied(); - sb.append(" │ Satisfied: ").append(allSatisfied).append("\n"); - - // Count satisfied conditions - int satisfiedCount = 0; - for (Condition condition : conditions) { - if (condition.isSatisfied()) { - satisfiedCount++; - } - } - sb.append(" │ Satisfied Conditions: ").append(satisfiedCount).append("/").append(conditions.size()).append("\n"); - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Child conditions - if (!conditions.isEmpty()) { - sb.append(" ├─ Child Conditions ────────────────────────\n"); - - for (int i = 0; i < conditions.size(); i++) { - Condition condition = conditions.get(i); - String prefix = (i == conditions.size() - 1) ? " └─ " : " ├─ "; - - sb.append(prefix).append(String.format("Condition %d: %s [%s]\n", - i + 1, - condition.getClass().getSimpleName(), - condition.isSatisfied() ? "SATISFIED" : "NOT SATISFIED")); - } - } else { - sb.append(" └─ No Child Conditions ───────────────────────\n"); - } - - return sb.toString(); - } - - /** - * For an AND condition, any unsatisfied child condition blocks the entire AND. - * This method returns all child conditions that are currently not satisfied. - * - * @return List of all unsatisfied child conditions - */ - @Override - public List getBlockingConditions() { - List blockingConditions = new ArrayList<>(); - - // In an AND condition, any unsatisfied condition blocks the entire AND - for (Condition condition : conditions) { - if (!condition.isSatisfied()) { - blockingConditions.add(condition); - } - } - - return blockingConditions; - } - - /** - * Gets the next time this AND condition will be satisfied. - * If all conditions are satisfied, returns the minimum trigger time among all conditions. - * If any condition is not satisfied, returns the maximum trigger time among unsatisfied TimeConditions. - * - * @return Optional containing the next trigger time, or empty if none available - */ - @Override - public Optional getCurrentTriggerTime() { - if (conditions.isEmpty()) { - return Optional.empty(); - } - - boolean allSatisfied = true; - ZonedDateTime minTriggerTime = null; - ZonedDateTime maxUnsatisfiedTimeConditionTriggerTime = null; - - // Check if all conditions are satisfied and track min trigger time - for (Condition condition : conditions) { - // Check if this condition is satisfied - if (!condition.isSatisfied()) { - allSatisfied = false; - - // For unsatisfied TimeConditions, track the maximum trigger time - if (condition instanceof TimeCondition) { - Optional nextTrigger = condition.getCurrentTriggerTime(); - if (nextTrigger.isPresent()) { - ZonedDateTime triggerTime = nextTrigger.get(); - if (maxUnsatisfiedTimeConditionTriggerTime == null || - triggerTime.isAfter(maxUnsatisfiedTimeConditionTriggerTime)) { - maxUnsatisfiedTimeConditionTriggerTime = triggerTime; - } - } - } - } - // If satisfied, track the minimum trigger time - else { - Optional triggerTime = condition.getCurrentTriggerTime(); - if (triggerTime.isPresent()) { - if (minTriggerTime == null || triggerTime.get().isBefore(minTriggerTime)) { - minTriggerTime = triggerTime.get(); - } - } - } - } - - // If all conditions are satisfied, return the minimum trigger time - if (allSatisfied) { - return minTriggerTime != null ? Optional.of(minTriggerTime) : Optional.empty(); - } - - // If at least one condition is not satisfied, return the maximum trigger time - // of unsatisfied TimeConditions - return maxUnsatisfiedTimeConditionTriggerTime != null ? - Optional.of(maxUnsatisfiedTimeConditionTriggerTime) : Optional.empty(); - } - public void pause() { - // Pause all child conditions - for (Condition condition : conditions) { - condition.pause(); - } - - - } - - - public void resume() { - // Resume all child conditions - for (Condition condition : conditions) { - condition.resume(); - } - - } - - /** - * Gets the estimated time until this AND condition will be satisfied. - * For an AND condition, this returns the maximum (latest) estimated time - * among all child conditions, since all conditions must be satisfied - * for the entire AND condition to be satisfied. - * - * @return Optional containing the estimated duration until satisfaction, or empty if not determinable - */ - @Override - public Optional getEstimatedTimeWhenIsSatisfied() { - if (conditions.isEmpty()) { - return Optional.of(Duration.ZERO); - } - - // If all conditions are already satisfied, return zero - if (isSatisfied()) { - return Optional.of(Duration.ZERO); - } - - Duration longestTime = Duration.ZERO; - boolean hasEstimate = false; - boolean allHaveEstimates = true; - - for (Condition condition : conditions) { - Optional estimate = condition.getEstimatedTimeWhenIsSatisfied(); - - if (estimate.isPresent()) { - hasEstimate = true; - Duration currentEstimate = estimate.get(); - - if (currentEstimate.compareTo(longestTime) > 0) { - longestTime = currentEstimate; - } - } else { - // If any condition can't provide an estimate, we can't provide a reliable estimate - // for the entire AND condition - allHaveEstimates = false; - } - } - - // Only return an estimate if all conditions can provide estimates - return (hasEstimate && allHaveEstimates) ? Optional.of(longestTime) : Optional.empty(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/LockCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/LockCondition.java deleted file mode 100644 index e19f76e7ed6..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/LockCondition.java +++ /dev/null @@ -1,167 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.logical; - -import java.time.ZonedDateTime; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.breakhandler.BreakHandlerScript; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; - -/** - * A condition that can be manually locked/unlocked by a plugin. - * When locked, the condition is always unsatisfied regardless of other conditions. - * This can be used to prevent a plugin from being stopped during critical operations. - */ -@Slf4j -@EqualsAndHashCode(callSuper = false) -public class LockCondition implements Condition { - - private final AtomicBoolean locked = new AtomicBoolean(false); - @Getter - private final String reason; - @Getter - private final boolean withBreakHandlerLock; - - @Deprecated - public LockCondition(String reason) { - this(reason, false, true); - } - /** - * Creates a new LockCondition with the specified reason and withBreakHandlerLock flag. - * The lock will be initially unlocked (defaultLock = false). - * - * @param reason The reason or description for this lock condition - * @param withBreakHandlerLock Whether to also lock the BreakHandlerScript when this lock is active - */ - public LockCondition(String reason, boolean withBreakHandlerLock) { - this(reason, false, withBreakHandlerLock); - } - - /** - * Creates a new LockCondition with a default reason and specified initial lock state. - * - * @param defaultLock The initial state of the lock (true for locked, false for unlocked) - * @param withBreakHandlerLock Whether to also lock the BreakHandlerScript when this lock is active - */ - public LockCondition(boolean defaultLock, boolean withBreakHandlerLock) { - this("Plugin is in a critical operation", defaultLock, withBreakHandlerLock); - } - /** - * Creates a new LockCondition with the specified reason and initial lock state. - * - * @param reason The reason or description for this lock condition - * @param defaultLock The initial state of the lock (true for locked, false for unlocked) - */ - public LockCondition(String reason, boolean defaultLock, boolean withBreakHandlerLock) { - this.reason = reason; - this.locked.set(defaultLock); - this.withBreakHandlerLock = withBreakHandlerLock; - } - - /** - * Locks the condition, preventing the plugin from being stopped. - */ - public void lock() { - if (locked == null) { - log.warn("LockCondition is null, cannot lock"); - return; - } - boolean wasLocked = locked.getAndSet(true); - if(withBreakHandlerLock){ - BreakHandlerScript.setLockState(true); - } - if (!wasLocked) { - log.debug("LockCondition locked: {}", reason); - } - } - - /** - * Unlocks the condition, allowing the plugin to be stopped. - */ - public void unlock() { - if (locked == null) { - log.warn("LockCondition is null, cannot unlock"); - return; - } - boolean wasLocked = locked.getAndSet(false); - if (withBreakHandlerLock){ - BreakHandlerScript.setLockState(false); - } - if (wasLocked) { - log.debug("LockCondition unlocked: {}", reason); - } - } - - /** - * Toggles the lock state. - * - * @return The new lock state (true if locked, false if unlocked) - */ - public boolean toggleLock() { - if (locked == null) { - log.warn("LockCondition is null, cannot toggle lock state"); - return false; - } - boolean newState = !locked.get(); - BreakHandlerScript.setLockState(newState); - locked.set(newState); - return newState; - } - - /** - * Checks if the condition is currently locked. - * - * @return true if locked, false otherwise - */ - public boolean isLocked() { - return locked.get(); - } - - @Override - public boolean isSatisfied() { - // If locked, the condition is NOT satisfied, which prevents stopping - return !isLocked(); - } - - @Override - public String getDescription() { - return "Lock Condition: " + (isLocked() ? "\"LOCKED\" - " + reason : "UNLOCKED"); - } - - @Override - public String getDetailedDescription() { - return getDescription(); - } - - @Override - public ConditionType getType() { - return ConditionType.LOGICAL; - } - - @Override - public void reset(boolean randomize) { - // Reset does nothing by default - lock state is controlled manually - } - - @Override - public Optional getCurrentTriggerTime() { - // Lock conditions don't have a specific trigger time - return Optional.empty(); - } - - public void pause() { - - - - } - - - public void resume() { - - } - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/LogicalCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/LogicalCondition.java deleted file mode 100644 index 80be25c99d0..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/LogicalCondition.java +++ /dev/null @@ -1,1504 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.logical; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.events.*; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.enums.UpdateOption; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; - -/** - * Base class for logical combinations of conditions. - * - * IMPORTANT: When adding new event types to the Condition interface, you MUST also: - * 1. Override the event method in this class - * 2. Use the propagateEvent() helper to forward the event to all child conditions - * - * This ensures proper event propagation through the condition hierarchy. - */ -@Slf4j -@EqualsAndHashCode(callSuper = false) -public abstract class LogicalCondition implements Condition { - - public LogicalCondition(Condition... conditions) { - for (Condition condition : conditions) { - addCondition(condition); - } - } - @Getter - protected List conditions = new ArrayList<>(); - - public LogicalCondition addCondition(Condition condition) { - //check if the condition is already in the list, with .equals() - // this prevents duplicates and unnecessary processing, - for (Condition conditionInList : conditions) { - if (conditionInList.equals(condition)) { - return this; - } - } - conditions.add(condition); - return this; - } - - @Override - public ConditionType getType() { - return ConditionType.LOGICAL; - } - /** - * Helper method to propagate any event to all child conditions. - * This centralizes the propagation logic to avoid code duplication. - * - * @param The event type - * @param event The event object to propagate - * @param eventHandler The method reference to the appropriate event handler - */ - protected void propagateEvent(T event, PropagationHandler eventHandler) { - for (Condition condition : conditions) { - try { - eventHandler.handle(condition, event); - } catch (Exception e) { - // Optional: Add logging - log.error("Error propagating event to condition: " + condition.getClass().getSimpleName(), e); - //log stack trace if needed - e.printStackTrace(); - } - } - } - - /** - * Functional interface for event propagation handling - */ - @FunctionalInterface - protected interface PropagationHandler { - void handle(Condition condition, T event); - } - - - @Override - public void onStatChanged(StatChanged event) { - propagateEvent(event, (condition, e) -> condition.onStatChanged(e)); - } - - @Override - public void onItemContainerChanged(ItemContainerChanged event) { - propagateEvent(event, (condition, e) -> condition.onItemContainerChanged(e)); - } - - @Override - public void onGameTick(GameTick event) { - propagateEvent(event, (condition, e) -> condition.onGameTick(e)); - } - - @Override - public void onNpcChanged(NpcChanged event) { - propagateEvent(event, (condition, e) -> condition.onNpcChanged(e)); - } - - @Override - public void onNpcSpawned(NpcSpawned event) { - propagateEvent(event, (condition, e) -> condition.onNpcSpawned(e)); - } - - @Override - public void onNpcDespawned(NpcDespawned event) { - propagateEvent(event, (condition, e) -> condition.onNpcDespawned(e)); - } - - @Override - public void onGroundObjectSpawned(GroundObjectSpawned event) { - propagateEvent(event, (condition, e) -> condition.onGroundObjectSpawned(e)); - } - - @Override - public void onGroundObjectDespawned(GroundObjectDespawned event) { - propagateEvent(event, (condition, e) -> condition.onGroundObjectDespawned(e)); - } - - @Override - public void onItemSpawned(ItemSpawned event) { - propagateEvent(event, (condition, e) -> condition.onItemSpawned(e)); - } - - @Override - public void onItemDespawned(ItemDespawned event) { - propagateEvent(event, (condition, e) -> condition.onItemDespawned(e)); - } - - @Override - public void onMenuOptionClicked(MenuOptionClicked event) { - propagateEvent(event, (condition, e) -> condition.onMenuOptionClicked(e)); - } - - @Override - public void onChatMessage(ChatMessage event) { - propagateEvent(event, (condition, e) -> condition.onChatMessage(e)); - } - - @Override - public void onHitsplatApplied(HitsplatApplied event) { - propagateEvent(event, (condition, e) -> condition.onHitsplatApplied(e)); - } - - @Override - public void onVarbitChanged(VarbitChanged event) { - propagateEvent(event, (condition, e) -> condition.onVarbitChanged(e)); - } - - @Override - public void onInteractingChanged(InteractingChanged event) { - propagateEvent(event, (condition, e) -> condition.onInteractingChanged(e)); - } - - @Override - public void onAnimationChanged(AnimationChanged event) { - propagateEvent(event, (condition, e) -> condition.onAnimationChanged(e)); - } - @Override - public void onGameStateChanged(GameStateChanged gameStateChanged) { - propagateEvent(gameStateChanged, (condition, e) -> condition.onGameStateChanged(e)); - } - - - - /** - * Checks if this logical condition contains the specified condition, - * either directly or within any nested logical conditions. - * - * @param targetCondition The condition to search for - * @return true if the condition exists within this logical structure, false otherwise - */ - public boolean contains(Condition targetCondition) { - // Recursively search in nested logical conditions - for (Condition condition : conditions) { - - // If this is a logical condition, search within it - if (condition instanceof LogicalCondition) { - if (((LogicalCondition) condition).contains(targetCondition)) { - return true; - } - } - // Special case for NotCondition which wraps a single condition - else if (condition instanceof NotCondition) { - if (((NotCondition) condition).getCondition().equals(targetCondition)) { - return true; - } - - // If the wrapped condition is itself a logical condition, search within it - Condition wrappedCondition = ((NotCondition) condition).getCondition(); - if (wrappedCondition instanceof LogicalCondition) { - if (((LogicalCondition) wrappedCondition).contains(targetCondition)) { - return true; - } - } - } - if (condition.equals(targetCondition)) { - //log.info("Found condition: {} equals\nthe condition {}\nin the logical{}",targetCondition.getDescription(), - //condition.getDescription(), this.getDescription()); - return true; - } - } - - // Not found - return false; - } - - @Override - public double getProgressPercentage() { - if (conditions.isEmpty()) { - return isSatisfied() ? 100.0 : 0.0; - } - - // For AND conditions, use the average progress (average over links) - if (this instanceof AndCondition) { - return conditions.stream() - .mapToDouble(Condition::getProgressPercentage) - .average() - .orElse(0.0); - } - // For OR conditions, use the maximum progress (strongest link) - else if (this instanceof OrCondition) { - return conditions.stream() - .mapToDouble(Condition::getProgressPercentage) - .max() - .orElse(0.0); - } - - // Default fallback to average progress - return conditions.stream() - .mapToDouble(Condition::getProgressPercentage) - .average() - .orElse(0.0); - } - - @Override - public int getTotalConditionCount() { - if (conditions.isEmpty()) { - return 0; - } - - // Sum up all nested condition counts - return conditions.stream() - .mapToInt(Condition::getTotalConditionCount) - .sum(); - } - - @Override - public int getMetConditionCount() { - if (conditions.isEmpty()) { - return 0; - } - - // Sum up all nested met condition counts - return conditions.stream() - .mapToInt(Condition::getMetConditionCount) - .sum(); - } - - @Override - public 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(")"); - } - - // Add count of met conditions - int total = getTotalConditionCount(); - int met = getMetConditionCount(); - if (total > 0) { - sb.append(" - ").append(met).append("/").append(total).append(" conditions SATISFIED"); - } - } - - sb.append("\n"); - - // Add nested conditions with additional indent - for (Condition condition : conditions) { - sb.append(condition.getStatusInfo(indent + 2, showProgress)).append("\n"); - } - - return sb.toString(); - } - - /** - * Removes a condition from this logical condition or its nested structure. - * Returns true if the condition was found and removed. - */ - public boolean removeCondition(Condition targetCondition) { - // Direct removal from this logical condition's immediate children - if (conditions.remove(targetCondition)) { - return true; - } - - // Search nested logical conditions - for (int i = 0; i < conditions.size(); i++) { - Condition condition = conditions.get(i); - - // Handle nested logical conditions - if (condition instanceof LogicalCondition) { - LogicalCondition nestedLogical = (LogicalCondition) condition; - if (nestedLogical.removeCondition(targetCondition)) { - // If this left the nested logical empty, remove it too - if (nestedLogical.getConditions().isEmpty()) { - conditions.remove(i); - } - return true; - } - } - // Handle NOT condition as a special case - else if (condition instanceof NotCondition) { - NotCondition notCondition = (NotCondition) condition; - - // If NOT directly wraps our target condition - if (notCondition.getCondition() == targetCondition) { - conditions.remove(i); - return true; - } - - // If NOT wraps a logical condition, try removing from there - if (notCondition.getCondition() instanceof LogicalCondition) { - LogicalCondition nestedLogical = (LogicalCondition) notCondition.getCondition(); - if (nestedLogical.removeCondition(targetCondition)) { - // If this left the nested logical empty, remove the NOT condition too - if (nestedLogical.getConditions().isEmpty()) { - conditions.remove(i); - } - return true; - } - } - } - } - - return false; - } - - /** - * Finds a logical condition that contains the given condition. - * Useful for determining which logical group a condition belongs to. - * - * @param targetCondition The condition to find - * @return The logical condition containing the target, or null if not found - */ - public LogicalCondition findContainingLogical(Condition targetCondition) { - // Check if it's directly in this logical's conditions - if (conditions.contains(targetCondition)) { - return this; - } - - // Search in nested logical conditions - for (Condition condition : conditions) { - if (condition instanceof LogicalCondition) { - LogicalCondition result = ((LogicalCondition) condition).findContainingLogical(targetCondition); - if (result != null) { - return result; - } - } else if (condition instanceof NotCondition) { - NotCondition notCondition = (NotCondition) condition; - - // If NOT directly wraps our target - if (notCondition.getCondition() == targetCondition) { - return this; - } - - // If NOT wraps a logical, search in there - if (notCondition.getCondition() instanceof LogicalCondition) { - LogicalCondition result = - ((LogicalCondition) notCondition.getCondition()).findContainingLogical(targetCondition); - if (result != null) { - return result; - } - } - } - } - - return null; - } - - public void softReset() { - if (isSatisfied()){ - for (Condition condition : conditions) { - condition.reset(); - } - } - } - - public void softReset(boolean randomize) { - if (isSatisfied()){ - for (Condition condition : conditions) { - condition.reset(randomize); - } - } - } - public void reset() { - - for (Condition condition : conditions) { - condition.reset(); - } - - } - - public void reset(boolean randomize) { - - for (Condition condition : conditions) { - condition.reset(randomize); - } - - } - - public void hardReset(){ - for (Condition condition : conditions) { - condition.hardReset(); - } - } - /** - * Adds a condition to a specific position in the condition tree - * Useful for preserving ordering when reconstructing the tree. - */ - public LogicalCondition addConditionAt(int index, Condition condition) { - if (index >= 0 && index <= conditions.size()) { - conditions.add(index, condition); - } else { - conditions.add(condition); - } - return this; - } - - /** - * Gets a human-readable description of this logical condition. - * This provides a default implementation that subclasses can override. - * - * @return A string description of the logical condition - */ - @Override - public String getDescription() { - if (conditions.isEmpty()) { - return "No conditions"; - } - - String conditionType = (this instanceof AndCondition) ? "ALL of" : - ((this instanceof OrCondition) ? "ANY of" : "Logical group of"); - - StringBuilder sb = new StringBuilder(conditionType).append(": ("); - String separator = (this instanceof AndCondition) ? " AND " : - ((this instanceof OrCondition) ? " OR " : ", "); - - for (int i = 0; i < conditions.size(); i++) { - if (i > 0) sb.append(separator); - sb.append(conditions.get(i).getDescription()); - } - sb.append(")"); - return sb.toString(); - } - - /** - * Gets a description formatted for HTML display in UI components. - * This is useful for tooltips and other rich text displays. - * - * @param maxLength Maximum length of descriptions before truncating - * @return HTML formatted description - */ - public String getHtmlDescription(int maxLength) { - if (conditions.isEmpty()) { - return "No conditions"; - } - - StringBuilder sb = new StringBuilder(""); - - // Add operator with appropriate styling - if (this instanceof AndCondition) { - sb.append("ALL of: ("); - } else if (this instanceof OrCondition) { - sb.append("ANY of: ("); - } else { - sb.append("Logical group of: ("); - } - - // Add child conditions with appropriate separators - String separator = (this instanceof AndCondition) ? " AND " : - ((this instanceof OrCondition) ? " OR " : ", "); - - int totalLength = 0; - for (int i = 0; i < conditions.size(); i++) { - if (i > 0) sb.append(separator); - - String description = conditions.get(i).getDescription(); - - // Truncate long descriptions - if (maxLength > 0 && totalLength + description.length() > maxLength) { - description = description.substring(0, Math.max(10, maxLength - totalLength - 3)) + "..."; - } - - // Style based on satisfied state - if (conditions.get(i).isSatisfied()) { - sb.append("").append(description).append(""); - } else { - sb.append("").append(description).append(""); - } - - totalLength += description.length(); - } - - sb.append(")"); - return sb.toString(); - } - - /** - * Gets a simple HTML representation for use in tooltips. - * - * @return HTML formatted description - */ - public String getTooltipHtml() { - return getHtmlDescription(100); - } - /** - * Recursively finds all LockConditions within this LogicalCondition structure. - * This utility method is used by the break handler to detect locked conditions - * that should prevent breaks from occurring. - * - * @return List of all LockConditions found in the structure - */ - public List findAllLockConditions() { - List lockConditions = new ArrayList<>(); - - for (Condition condition : conditions) { - if (condition instanceof LockCondition) { - lockConditions.add((LockCondition) condition); - } else if (condition instanceof LogicalCondition) { - // Recursively search in nested logical conditions - lockConditions.addAll(((LogicalCondition) condition).findAllLockConditions()); - } else if (condition instanceof NotCondition) { - // Check if the wrapped condition is a LockCondition or contains LockConditions - Condition wrappedCondition = ((NotCondition) condition).getCondition(); - if (wrappedCondition instanceof LockCondition) { - lockConditions.add((LockCondition) wrappedCondition); - } else if (wrappedCondition instanceof LogicalCondition) { - lockConditions.addAll(((LogicalCondition) wrappedCondition).findAllLockConditions()); - } - } - } - - return lockConditions; - } - - /** - * Recursively finds all PredicateConditions within this LogicalCondition structure. - * This utility method is used by the break handler to detect predicate conditions - * that may prevent breaks from occurring when their predicate is not satisfied. - * - * @return List of all PredicateConditions found in the structure - */ - public List> findAllPredicateConditions() { - List> predicateConditions = new ArrayList<>(); - - for (Condition condition : conditions) { - if (condition instanceof PredicateCondition) { - predicateConditions.add((PredicateCondition) condition); - } else if (condition instanceof LogicalCondition) { - // Recursively search in nested logical conditions - predicateConditions.addAll(((LogicalCondition) condition).findAllPredicateConditions()); - } else if (condition instanceof NotCondition) { - // Check if the wrapped condition is a PredicateCondition or contains PredicateConditions - Condition wrappedCondition = ((NotCondition) condition).getCondition(); - if (wrappedCondition instanceof PredicateCondition) { - predicateConditions.add((PredicateCondition) wrappedCondition); - } else if (wrappedCondition instanceof LogicalCondition) { - predicateConditions.addAll(((LogicalCondition) wrappedCondition).findAllPredicateConditions()); - } - } - } - - return predicateConditions; - } - /** - * Recursively finds all TimeCondition instances in this logical condition structure. - * This searches through the entire hierarchy including nested logical conditions. - * - * @return A list of all TimeCondition instances found in this structure - */ - public List findTimeConditions() { - List timeConditions = new ArrayList<>(); - - // Recursively search for TimeCondition instances - for (Condition condition : conditions) { - // Check if this condition is a TimeCondition - if (condition.getType() == ConditionType.TIME) { - timeConditions.add(condition); - continue; - } - - // If this is a logical condition, search inside it recursively - if (condition instanceof LogicalCondition) { - timeConditions.addAll(((LogicalCondition) condition).findTimeConditions()); - continue; - } - - // Special case for NotCondition which wraps a single condition - if (condition instanceof NotCondition) { - Condition wrappedCondition = ((NotCondition) condition).getCondition(); - - // Check if the wrapped condition is a TimeCondition - if (wrappedCondition.getType() == ConditionType.TIME) { - timeConditions.add(wrappedCondition); - continue; - } - - // If the wrapped condition is a logical condition, search inside it - if (wrappedCondition instanceof LogicalCondition) { - timeConditions.addAll(((LogicalCondition) wrappedCondition).findTimeConditions()); - } - } - } - - return timeConditions; - } - - /** - * Recursively finds all non-TimeCondition instances in this logical condition structure. - * This searches through the entire hierarchy including nested logical conditions. - * - * @return A list of all non-TimeCondition instances found in this structure - */ - public List findNonTimeConditions() { - List nonTimeConditions = new ArrayList<>(); - - // Recursively search for non-TimeCondition instances - for (Condition condition : conditions) { - // Check if this condition is NOT a TimeCondition - if (condition.getType() != ConditionType.TIME) { - nonTimeConditions.add(condition); - } - - // If this is a logical condition, search inside it recursively - if (condition instanceof LogicalCondition) { - nonTimeConditions.addAll(((LogicalCondition) condition).findNonTimeConditions()); - continue; - } - - // Special case for NotCondition which wraps a single condition - if (condition instanceof NotCondition) { - Condition wrappedCondition = ((NotCondition) condition).getCondition(); - - // Check if the wrapped condition is NOT a TimeCondition - if (wrappedCondition.getType() != ConditionType.TIME) { - nonTimeConditions.add(wrappedCondition); - continue; - } - - // If the wrapped condition is a logical condition, search inside it - if (wrappedCondition instanceof LogicalCondition) { - nonTimeConditions.addAll(((LogicalCondition) wrappedCondition).findNonTimeConditions()); - } - } - } - - return nonTimeConditions; - } - - /** - * Checks if this logical condition structure contains only TimeCondition instances. - * - * @return true if all conditions in this structure are TimeConditions, false otherwise - */ - public boolean hasOnlyTimeConditions() { - return findNonTimeConditions().isEmpty(); - } - - /** - * Creates a new logical condition of the same type (AND/OR) that contains only - * TimeCondition instances from this logical structure. This preserves the nested structure - * of logical conditions rather than flattening. - * - * @return A new logical condition containing only TimeConditions with the same logical structure, - * or null if no time conditions exist in this structure - */ - public LogicalCondition createTimeOnlyLogicalStructure() { - // If there are no conditions at all, return null - if (conditions.isEmpty()) { - return null; - } - - // Create a new logical condition of the same type - LogicalCondition newLogical; - if (this instanceof AndCondition) { - newLogical = new AndCondition(); - } else if (this instanceof OrCondition) { - newLogical = new OrCondition(); - } else { - // For other logical types (like NOT), default to AND logic - newLogical = new AndCondition(); - } - - boolean hasAnyTimeConditions = false; - - // Process each condition, preserving the structure - for (Condition condition : conditions) { - if (condition.getType() == ConditionType.TIME) { - // Directly add time conditions - newLogical.addCondition(condition); - hasAnyTimeConditions = true; - } else if (condition instanceof LogicalCondition) { - // Recursively process nested logical conditions - LogicalCondition nestedTimeOnly = ((LogicalCondition) condition).createTimeOnlyLogicalStructure(); - if (nestedTimeOnly != null && !nestedTimeOnly.getConditions().isEmpty()) { - newLogical.addCondition(nestedTimeOnly); - hasAnyTimeConditions = true; - } - } else if (condition instanceof NotCondition) { - // Special handling for NOT conditions - Condition wrappedCondition = ((NotCondition) condition).getCondition(); - - // If the wrapped condition is a time condition, wrap it in a new NOT - if (wrappedCondition.getType() == ConditionType.TIME) { - newLogical.addCondition(new NotCondition(wrappedCondition)); - hasAnyTimeConditions = true; - } - // If the wrapped condition is a logical condition, process it recursively - else if (wrappedCondition instanceof LogicalCondition) { - LogicalCondition nestedTimeOnly = ((LogicalCondition) wrappedCondition).createTimeOnlyLogicalStructure(); - if (nestedTimeOnly != null && !nestedTimeOnly.getConditions().isEmpty()) { - newLogical.addCondition(new NotCondition(nestedTimeOnly)); - hasAnyTimeConditions = true; - } - } - } - } - - // If no time conditions were found, return null - if (!hasAnyTimeConditions) { - return null; - } - - // Post-processing: if we have a logical with only one nested condition, - // and that nested condition is a logical of the same type, we can flatten it - if (newLogical.getConditions().size() == 1) { - Condition singleCondition = newLogical.getConditions().get(0); - if (singleCondition instanceof LogicalCondition && - ((singleCondition instanceof AndCondition && newLogical instanceof AndCondition) || - (singleCondition instanceof OrCondition && newLogical instanceof OrCondition))) { - return (LogicalCondition) singleCondition; - } - } - - return newLogical; - } - - /** - * Evaluates whether this logical structure would be satisfied based solely on its - * time conditions. This creates a time-only logical structure with the same type (AND/OR) - * and checks if it is satisfied. - * - * @return true if the time-only structure is satisfied, false if not satisfied or no time conditions exist - */ - public boolean isTimeOnlyStructureSatisfied() { - LogicalCondition timeOnlyLogical = createTimeOnlyLogicalStructure(); - if (timeOnlyLogical == null) { - return false; // No time conditions, so can't be satisfied - } - - return timeOnlyLogical.isSatisfied(); - } - - /** - * Evaluates whether this logical structure would be satisfied if all non-time conditions - * were removed and only time conditions were evaluated. This helps determine if a plugin - * schedule would run based solely on its time conditions. - * - * @return true if time conditions alone would satisfy this structure, false otherwise - */ - public boolean wouldBeTimeOnlySatisfied() { - // If there are no time conditions at all, we can't satisfy this structure with time only - List timeConditions = findTimeConditions(); - if (timeConditions.isEmpty()) { - return false; - } - - // For AND logic, if all time conditions are satisfied, the overall structure would - // be satisfied if non-time conditions were not considered - if (this instanceof AndCondition) { - for (Condition condition : timeConditions) { - if (!condition.isSatisfied()) { - return false; - } - } - return true; - } - // For OR logic, if any time condition is satisfied, the overall structure would - // be satisfied if non-time conditions were not considered - else if (this instanceof OrCondition) { - for (Condition condition : timeConditions) { - if (condition.isSatisfied()) { - return true; - } - } - return false; - } - - // For other logic types, create a new structure and evaluate it - return isTimeOnlyStructureSatisfied(); - } - - - /** - * Updates this logical condition structure with new conditions from another logical condition. - * This is a convenience method that uses the default update mode (ADD_ONLY). - * - * @param newLogicalCondition The logical condition containing new conditions to add - * @return true if any changes were made, false if no changes were needed - */ - public boolean updateLogicalStructure(LogicalCondition newLogicalCondition) { - return updateLogicalStructure(newLogicalCondition, UpdateOption.SYNC, true); - } - - /** - * Updates this logical condition structure with new conditions from another logical condition. - * Provides fine-grained control over how conditions are merged. - * - * @param newLogicalCondition The logical condition containing new conditions to add - * @param updateMode Controls how conditions are merged (add only, sync, remove only, replace) - * @param preserveState If true, existing condition state is preserved when possible - * @return true if any changes were made, false if no changes were needed - */ - public boolean updateLogicalStructure(LogicalCondition newLogicalCondition, UpdateOption updateMode, boolean preserveState) { - if (newLogicalCondition == null) { - return false; - } - - if (updateMode == UpdateOption.REPLACE) { - // For REPLACE mode, just copy all conditions from the new structure - boolean anyChanges = false; - this.conditions.clear(); - - for (Condition newCondition : newLogicalCondition.getConditions()) { - this.addCondition(newCondition); - anyChanges = true; - } - - return anyChanges; - } - - boolean anyChanges = false; - - // First, handle removals if needed - if (updateMode == UpdateOption.SYNC || updateMode == UpdateOption.REMOVE_ONLY) { - anyChanges = removeNonMatchingConditions(newLogicalCondition) || anyChanges; - } - - // Then handle additions if needed - if (updateMode == UpdateOption.ADD_ONLY || updateMode == UpdateOption.SYNC) { - anyChanges = addNewConditions(newLogicalCondition, preserveState) || anyChanges; - } - - return anyChanges; - } - - /** - * Removes conditions from this logical structure that don't exist in the new structure. - * This creates a synchronized view between the two condition trees. - * - * @param newLogicalCondition The logical condition to compare against - * @return true if any conditions were removed, false otherwise - */ - private boolean removeNonMatchingConditions(LogicalCondition newLogicalCondition) { - boolean anyRemoved = false; - List toRemove = new ArrayList<>(); - - for (Condition existingCondition : conditions) { - // Check if the existing condition is found in the new structure - boolean foundMatch = false; - - // For non-logical conditions, check direct existence - if (!(existingCondition instanceof LogicalCondition) && !(existingCondition instanceof NotCondition)) { - foundMatch = newLogicalCondition.contains(existingCondition); - } - // For logical conditions, check by type and recursively - else if (existingCondition instanceof LogicalCondition) { - LogicalCondition existingLogical = (LogicalCondition) existingCondition; - - // Try to find a matching logical condition in the new structure - for (Condition newCondition : newLogicalCondition.getConditions()) { - if (newCondition instanceof LogicalCondition && - ((existingLogical instanceof AndCondition && newCondition instanceof AndCondition) || - (existingLogical instanceof OrCondition && newCondition instanceof OrCondition))) { - - // Found a logical condition of the same type, process it recursively - LogicalCondition newLogical = (LogicalCondition) newCondition; - existingLogical.removeNonMatchingConditions(newLogical); - foundMatch = true; - break; - } - } - } - // For not conditions, check if the wrapped condition exists - else if (existingCondition instanceof NotCondition) { - NotCondition existingNot = (NotCondition) existingCondition; - Condition wrappedExistingCondition = existingNot.getCondition(); - - // Check for matching NOT conditions - for (Condition newCondition : newLogicalCondition.getConditions()) { - if (newCondition instanceof NotCondition) { - NotCondition newNot = (NotCondition) newCondition; - if (newNot.getCondition().equals(wrappedExistingCondition)) { - foundMatch = true; - - // If the wrapped conditions are logical, recursively process them - if (wrappedExistingCondition instanceof LogicalCondition && - newNot.getCondition() instanceof LogicalCondition) { - ((LogicalCondition) wrappedExistingCondition).removeNonMatchingConditions( - (LogicalCondition) newNot.getCondition()); - } - break; - } - } - } - } - - // If no match was found, mark for removal - if (!foundMatch) { - toRemove.add(existingCondition); - } - } - - // Remove all conditions that weren't found in the new structure - for (Condition conditionToRemove : toRemove) { - conditions.remove(conditionToRemove); - anyRemoved = true; - log.debug("Removed condition from logical structure: {}", conditionToRemove.getDescription()); - } - - return anyRemoved; - } - - /** - * Identifies and removes conditions that can no longer trigger from a logical condition structure. - * This is useful for cleaning up one-time conditions that have already triggered and cannot trigger again. - * - * @param logicalCondition The logical condition structure to clean up - * @return true if any conditions were removed, false otherwise - */ - public static boolean removeNonTriggerableConditions(LogicalCondition logicalCondition) { - if (logicalCondition == null || logicalCondition.getConditions().isEmpty()) { - return false; - } - - boolean anyRemoved = false; - List conditionsToRemove = new ArrayList<>(); - - // First pass: identify conditions that can no longer trigger - for (Condition condition : logicalCondition.getConditions()) { - // Check direct non-triggerable conditions - if (condition instanceof TimeCondition && !((TimeCondition) condition).canTriggerAgain()) { - log.debug("Found non-triggerable time condition: {}", condition.getDescription()); - conditionsToRemove.add(condition); - continue; - } - - // Handle nested logical conditions - if (condition instanceof LogicalCondition) { - // Recursively clean up nested structure - if (removeNonTriggerableConditions((LogicalCondition) condition)) { - anyRemoved = true; - } - - // If this leaves the nested logical empty, mark it for removal too - if (((LogicalCondition) condition).getConditions().isEmpty()) { - conditionsToRemove.add(condition); - } - } - - // Handle NOT condition as a special case - if (condition instanceof NotCondition) { - NotCondition notCondition = (NotCondition) condition; - Condition wrappedCondition = notCondition.getCondition(); - - // If wrapped condition is a time condition that can't trigger - if (wrappedCondition instanceof TimeCondition && - !((TimeCondition) wrappedCondition).canTriggerAgain()) { - conditionsToRemove.add(condition); - } - // If wrapped condition is a logical, clean it up recursively - else if (wrappedCondition instanceof LogicalCondition) { - if (removeNonTriggerableConditions((LogicalCondition) wrappedCondition)) { - anyRemoved = true; - // If cleaned condition is now empty, mark the NOT for removal - if (((LogicalCondition) wrappedCondition).getConditions().isEmpty()) { - conditionsToRemove.add(condition); - } - } - } - } - } - - // Second pass: remove identified conditions - for (Condition conditionToRemove : conditionsToRemove) { - logicalCondition.removeCondition(conditionToRemove); - log.debug("Removed non-triggerable condition: {}", conditionToRemove.getDescription()); - anyRemoved = true; - } - - return anyRemoved; - } - - /** - * Adds new conditions from the provided structure that don't already exist in this structure. - * This preserves the existing condition state while adding new conditions. - * - * @param newLogicalCondition The logical condition containing new conditions to add - * @param preserveState If true, existing conditions with the same description are kept - * @return true if any conditions were added, false otherwise - */ - private boolean addNewConditions(LogicalCondition newLogicalCondition, boolean preserveState) { - boolean anyChanges = false; - - for (Condition newCondition : newLogicalCondition.getConditions()) { - // For non-logical conditions, check if we need to add them - if (!(newCondition instanceof LogicalCondition) && !(newCondition instanceof NotCondition)) { - if (!this.contains(newCondition)) { - this.addCondition(newCondition); - anyChanges = true; - } - continue; - } - - // Handle NotCondition as a special case - if (newCondition instanceof NotCondition) { - NotCondition newNotCondition = (NotCondition) newCondition; - Condition wrappedNewCondition = newNotCondition.getCondition(); - - // Check if we already have this NOT condition - boolean exists = false; - for (Condition existingCondition : this.conditions) { - if (existingCondition instanceof NotCondition) { - NotCondition existingNotCondition = (NotCondition) existingCondition; - Condition wrappedExistingCondition = existingNotCondition.getCondition(); - - // Check if the NOT conditions are wrapping the same condition - if (wrappedExistingCondition.equals(wrappedNewCondition)) { - exists = true; - break; - } - - // If both wrap logical conditions, we need to update recursively - if (wrappedExistingCondition instanceof LogicalCondition && - wrappedNewCondition instanceof LogicalCondition) { - if (((LogicalCondition) wrappedExistingCondition).updateLogicalStructure( - (LogicalCondition) wrappedNewCondition, - preserveState ? UpdateOption.ADD_ONLY : UpdateOption.SYNC, - preserveState)) { - anyChanges = true; - } - exists = true; - break; - } - } - } - - // If we don't have this NOT condition, add it - if (!exists) { - this.addCondition(newCondition); - anyChanges = true; - } - continue; - } - - // For logical conditions, recursively update if we find a matching logical type - LogicalCondition newLogical = (LogicalCondition) newCondition; - boolean foundMatchingLogical = false; - - for (Condition existingCondition : this.conditions) { - if (existingCondition instanceof LogicalCondition) { - LogicalCondition existingLogical = (LogicalCondition) existingCondition; - - // If they're the same type of logical condition (AND/OR), update recursively - if ((existingLogical instanceof AndCondition && newLogical instanceof AndCondition) || - (existingLogical instanceof OrCondition && newLogical instanceof OrCondition)) { - if (existingLogical.updateLogicalStructure( - newLogical, - preserveState ? UpdateOption.ADD_ONLY : UpdateOption.SYNC, - preserveState)) { - anyChanges = true; - } - foundMatchingLogical = true; - break; - } - } - } - - // If we didn't find a matching logical type, add the entire logical condition - if (!foundMatchingLogical) { - this.addCondition(newLogical); - anyChanges = true; - } - } - - return anyChanges; - } - - /** - * Validates the logical condition structure, checking for common issues - * like empty logical conditions or invalid condition nesting. - * - * @return A list of validation issues, or an empty list if no issues were found - */ - public List validateStructure() { - List issues = new ArrayList<>(); - - // Check for empty logical conditions - if (conditions.isEmpty()) { - issues.add("Empty logical condition: " + getClass().getSimpleName()); - } - - // Check for nested logical conditions of same type that could be flattened - for (Condition condition : conditions) { - if (condition instanceof LogicalCondition) { - LogicalCondition nestedLogical = (LogicalCondition) condition; - - // Recursively validate nested structures - issues.addAll(nestedLogical.validateStructure()); - - // Check for unnecessary nesting (same logical type) - if ((this instanceof AndCondition && nestedLogical instanceof AndCondition) || - (this instanceof OrCondition && nestedLogical instanceof OrCondition)) { - issues.add("Unnecessary nesting of " + getClass().getSimpleName() + - " contains nested " + nestedLogical.getClass().getSimpleName() + - " that could be flattened"); - } - - // Check for empty nested logical conditions - if (nestedLogical.getConditions().isEmpty()) { - issues.add("Empty nested logical condition: " + nestedLogical.getClass().getSimpleName()); - } - } - } - - return issues; - } - - /** - * Optimizes the logical condition structure by flattening unnecessary nesting - * and removing empty logical conditions. - * - * @return true if any optimizations were applied, false otherwise - */ - public boolean optimizeStructure() { - boolean anyChanges = false; - - // Remove empty nested logical conditions - for (int i = conditions.size() - 1; i >= 0; i--) { - Condition condition = conditions.get(i); - if (condition instanceof LogicalCondition) { - LogicalCondition nestedLogical = (LogicalCondition) condition; - - // Recursively optimize nested structure - if (nestedLogical.optimizeStructure()) { - anyChanges = true; - } - - // Remove if empty after optimization - if (nestedLogical.getConditions().isEmpty()) { - conditions.remove(i); - anyChanges = true; - continue; - } - - // Flatten nested logical conditions of same type - if ((this instanceof AndCondition && nestedLogical instanceof AndCondition) || - (this instanceof OrCondition && nestedLogical instanceof OrCondition)) { - - // Get all conditions from nested logical before we remove it - List nestedConditions = new ArrayList<>(nestedLogical.getConditions()); - - // Move all conditions from nested logical to this logical - // Need to iterate through a copy to avoid concurrent modification - for (Condition nestedCondition : nestedConditions) { - // Remove from nested logical first to avoid duplicates when we add to parent - nestedLogical.getConditions().remove(nestedCondition); - - // Add to parent logical if not already present - if (!this.contains(nestedCondition)) { - this.addCondition(nestedCondition); - } - } - - // Remove the now empty nested logical - conditions.remove(i); - anyChanges = true; - } - } - } - - return anyChanges; - } - - /** - * Updates this logical condition structure with new conditions from another logical condition. - * Only adds conditions that don't already exist in the structure. - * - * @param newLogicalCondition The logical condition containing new conditions to add - * @return true if any conditions were added, false if no changes were made - */ - public boolean updateLogicalStructureOld(LogicalCondition newLogicalCondition) { - if (newLogicalCondition == null || newLogicalCondition.getConditions().isEmpty()) { - return false; - } - - boolean anyChanges = false; - - for (Condition newCondition : newLogicalCondition.getConditions()) { - // For non-logical conditions, check if we need to add them - if (!(newCondition instanceof LogicalCondition) && !(newCondition instanceof NotCondition)) { - if (!this.contains(newCondition)) { - this.addCondition(newCondition); - anyChanges = true; - } - continue; - } - - // Handle NotCondition as a special case - if (newCondition instanceof NotCondition) { - NotCondition newNotCondition = (NotCondition) newCondition; - Condition wrappedNewCondition = newNotCondition.getCondition(); - - // Check if we already have this NOT condition - boolean exists = false; - for (Condition existingCondition : this.conditions) { - if (existingCondition instanceof NotCondition) { - NotCondition existingNotCondition = (NotCondition) existingCondition; - Condition wrappedExistingCondition = existingNotCondition.getCondition(); - - // Check if the NOT conditions are wrapping the same condition - if (wrappedExistingCondition.equals(wrappedNewCondition)) { - exists = true; - break; - } - - // If both wrap logical conditions, we need to update recursively - if (wrappedExistingCondition instanceof LogicalCondition && - wrappedNewCondition instanceof LogicalCondition) { - if (((LogicalCondition) wrappedExistingCondition).updateLogicalStructure( - (LogicalCondition) wrappedNewCondition)) { - anyChanges = true; - } - exists = true; - break; - } - } - } - - // If we don't have this NOT condition, add it - if (!exists) { - this.addCondition(newCondition); - anyChanges = true; - } - continue; - } - - // For logical conditions, recursively update if we find a matching logical type - LogicalCondition newLogical = (LogicalCondition) newCondition; - boolean foundMatchingLogical = false; - - for (Condition existingCondition : this.conditions) { - if (existingCondition instanceof LogicalCondition) { - LogicalCondition existingLogical = (LogicalCondition) existingCondition; - - // If they're the same type of logical condition (AND/OR), update recursively - if ((existingLogical instanceof AndCondition && newLogical instanceof AndCondition) || - (existingLogical instanceof OrCondition && newLogical instanceof OrCondition)) { - if (existingLogical.updateLogicalStructure(newLogical)) { - anyChanges = true; - } - foundMatchingLogical = true; - break; - } - } - } - - // If we didn't find a matching logical type, add the entire logical condition - if (!foundMatchingLogical) { - this.addCondition(newLogical); - anyChanges = true; - } - } - - return anyChanges; - } - - /** - * Compares this logical condition structure with another one and returns differences. - * This is useful for debugging and logging what changed during an update. - * - * @param otherLogical The logical condition to compare with - * @return A string describing the differences, or "No differences" if they're the same - */ - public String getStructureDifferences(LogicalCondition otherLogical) { - if (otherLogical == null) { - return "Other logical condition is null"; - } - - StringBuilder differences = new StringBuilder(); - - // Check for differences in logical type - if ((this instanceof AndCondition && !(otherLogical instanceof AndCondition)) || - (this instanceof OrCondition && !(otherLogical instanceof OrCondition))) { - differences.append("Different logical types: ") - .append(this.getClass().getSimpleName()) - .append(" vs ") - .append(otherLogical.getClass().getSimpleName()) - .append("\n"); - } - - // Find conditions in this that aren't in otherLogical - for (Condition thisCondition : this.conditions) { - if (!otherLogical.contains(thisCondition)) { - differences.append("Only in this: \n\t\t").append(thisCondition.getDescription()).append("\n"); - } - } - - // Find conditions in otherLogical that aren't in this - for (Condition otherCondition : otherLogical.conditions) { - if (!this.contains(otherCondition)) { - differences.append("Only in other: \n\t\t").append(otherCondition.getDescription()).append("\n"); - } - } - - // Check for nested differences in logical conditions - for (Condition thisCondition : this.conditions) { - if (thisCondition instanceof LogicalCondition) { - LogicalCondition thisLogical = (LogicalCondition) thisCondition; - - // Find the corresponding logical condition in otherLogical - for (Condition otherCondition : otherLogical.conditions) { - if (otherCondition instanceof LogicalCondition && - ((thisLogical instanceof AndCondition && otherCondition instanceof AndCondition) || - (thisLogical instanceof OrCondition && otherCondition instanceof OrCondition))) { - - LogicalCondition otherLogical2 = (LogicalCondition) otherCondition; - String nestedDifferences = thisLogical.getStructureDifferences(otherLogical2); - - if (!"No differences".equals(nestedDifferences)) { - differences.append("Nested differences in ") - .append(thisLogical.getClass().getSimpleName()) - .append(":\n") - .append(nestedDifferences) - .append("\n"); - } - break; - } - } - } - } - - return differences.length() > 0 ? differences.toString() : "No differences"; - } - - /** - * Gets a list of all conditions that are currently blocking this logical condition - * from being satisfied. This is useful for diagnosing why a complex condition tree - * is not being satisfied. - * is not being satisfied. - * - * The specific behavior depends on the type of logical condition: - * - For AND conditions: Returns all unsatisfied conditions - * - For OR conditions: Returns all conditions only if none are satisfied - * - * @return List of conditions that are preventing satisfaction - */ - public abstract List getBlockingConditions(); - - /** - * Gets a list of all "leaf" conditions that are blocking this logical condition. - * Leaf conditions are the non-logical conditions that represent the actual root causes - * for why the logical structure is not satisfied. - * - * @return List of leaf conditions that are preventing satisfaction - */ - public List getLeafBlockingConditions() { - List blockingLeaves = new ArrayList<>(); - - for (Condition condition : getBlockingConditions()) { - if (condition instanceof LogicalCondition) { - // Recursively get leaf conditions from nested logical conditions - blockingLeaves.addAll(((LogicalCondition) condition).getLeafBlockingConditions()); - } else { - // This is a leaf condition - blockingLeaves.add(condition); - } - } - - return blockingLeaves; - } - - /** - * Gets a human-readable explanation of why this logical condition is not satisfied, - * detailing the specific blocking conditions in the tree structure. - * - * @return A string explaining why the condition is not satisfied - */ - public String getBlockingExplanation() { - if (isSatisfied()) { - return "Condition is already satisfied"; - } - - StringBuilder explanation = new StringBuilder(); - explanation.append(getClass().getSimpleName()).append(" is not satisfied because:\n"); - - // Add explanations for each blocking condition - List blockingConditions = getBlockingConditions(); - for (int i = 0; i < blockingConditions.size(); i++) { - Condition condition = blockingConditions.get(i); - explanation.append(" ").append(i + 1).append(") "); - - if (condition instanceof LogicalCondition) { - // For nested logical conditions, include their blocking explanations - explanation.append(((LogicalCondition) condition).getBlockingExplanation().replace("\n", "\n ")); - } else { - // For leaf conditions, include their descriptions - explanation.append(condition.getDescription()) - .append(" (").append(condition.getClass().getSimpleName()).append(")"); - } - - if (i < blockingConditions.size() - 1) { - explanation.append("\n"); - } - } - - return explanation.toString(); - } - - /** - * Gets a concise summary of the root causes why this logical condition is not satisfied. - * This focuses only on the leaf conditions that are blocking satisfaction. - * - * @return A string summarizing the root causes for non-satisfaction - */ - public String getRootCausesSummary() { - if (isSatisfied()) { - return "Condition is satisfied"; - } - - List leafBlockingConditions = getLeafBlockingConditions(); - - if (leafBlockingConditions.isEmpty()) { - return "No specific blocking conditions found"; - } - - StringBuilder summary = new StringBuilder(); - summary.append("Root causes preventing satisfaction (").append(leafBlockingConditions.size()).append("):\n"); - - for (int i = 0; i < leafBlockingConditions.size(); i++) { - Condition condition = leafBlockingConditions.get(i); - summary.append(" ").append(i + 1).append(") ") - .append(condition.getDescription()) - .append(" (").append(condition.getClass().getSimpleName()).append(")"); - - // Add progress information if available - double progress = condition.getProgressPercentage(); - if (progress > 0 && progress < 100) { - summary.append(" - ").append(String.format("%.1f%%", progress)).append(" complete"); - } - - if (i < leafBlockingConditions.size() - 1) { - summary.append("\n"); - } - } - - return summary.toString(); - } - - /** - * Base implementation for estimated satisfaction time in logical conditions. - * This is overridden by specific logical condition types (And/Or) to provide - * appropriate logic for their semantics. - * - * @return Optional containing the estimated duration until satisfaction, or empty if not determinable - */ - @Override - public Optional getEstimatedTimeWhenIsSatisfied() { - if (conditions.isEmpty()) { - return Optional.of(Duration.ZERO); - } - - // This base implementation should be overridden by concrete classes - // Default behavior: return empty if we can't determine - return Optional.empty(); - } -} - - diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/NotCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/NotCondition.java deleted file mode 100644 index d14e7814a5a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/NotCondition.java +++ /dev/null @@ -1,263 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.logical; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import net.runelite.api.events.ItemContainerChanged; -import net.runelite.api.events.StatChanged; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.DayOfWeekCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition; - -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.Optional; - -/** - * Logical NOT operator - inverts a condition. - */ -@EqualsAndHashCode(callSuper = false) -public class NotCondition implements Condition { - @Getter - private final Condition condition; - - public NotCondition(Condition condition) { - this.condition = condition; - } - - @Override - public boolean isSatisfied() { - if (condition instanceof SingleTriggerTimeCondition) { - if (((SingleTriggerTimeCondition) condition).canTriggerAgain()) { - return !condition.isSatisfied(); - } - // should we only return true if the condition if it can trigger and is not satisfied? as we do it now - return false; - } - return !condition.isSatisfied(); - } - - @Override - public String getDescription() { - return "NOT (" + condition.getDescription() + ")"; - } - - @Override - public ConditionType getType() { - return ConditionType.LOGICAL; - } - - @Override - public void onStatChanged(StatChanged event) { - condition.onStatChanged(event); - } - - @Override - public void onItemContainerChanged(ItemContainerChanged event) { - condition.onItemContainerChanged(event); - } - - @Override - public void reset() { - condition.reset(); - } - @Override - public void reset(boolean randomize) { - condition.reset(randomize); - } - - @Override - public double getProgressPercentage() { - // Invert the progress for NOT conditions - double innerProgress = condition.getProgressPercentage(); - return 100.0 - innerProgress; - } - - @Override - public String getStatusInfo(int indent, boolean showProgress) { - StringBuilder sb = new StringBuilder(); - - // Add the NOT condition info - 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(")"); - } - } - - sb.append("\n"); - - // Add the nested condition with additional indent - sb.append(condition.getStatusInfo(indent + 2, showProgress)); - - return sb.toString(); - } - - /** - * Gets the next time this NOT condition will be satisfied. - * For TimeConditions, this attempts to determine when the inner condition will change state. - * - * @return Optional containing the next trigger time, or empty if none available - */ - @Override - public Optional getCurrentTriggerTime() { - // For NOT condition with a TimeCondition, we need to consider state transitions - if (condition instanceof TimeCondition) { - boolean innerSatisfied = condition.isSatisfied(); - - if (innerSatisfied) { - // Inner condition is satisfied (NOT is not satisfied) - // For DayOfWeekCondition, get the next non-active day - if (condition instanceof DayOfWeekCondition) { - DayOfWeekCondition dayCondition = (DayOfWeekCondition) condition; - return dayCondition.getNextNonActiveDay(); - } - // For TimeWindowCondition specifically, we can try to get its end time - else if (condition instanceof TimeWindowCondition) { - TimeWindowCondition timeWindow = (TimeWindowCondition) condition; - // If we have access to end time, we could return it - if (timeWindow.getCurrentEndDateTime() != null) { - return Optional.of(timeWindow.getCurrentEndDateTime() - .atZone(timeWindow.getZoneId())); - } - } - // For IntervalCondition, estimate when the interval would reset - else if (condition instanceof IntervalCondition) { - IntervalCondition intervalCondition = (IntervalCondition) condition; - // Calculate when the next interval would start after the current one - // This is our best estimate of when the NOT condition would become satisfied again - ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); - return Optional.of(now.plus(intervalCondition.getInterval())); - } - - // For other time conditions or if end time isn't available, - // we can't easily determine when the condition will stop being satisfied - return Optional.empty(); - } else { - // Inner condition is not satisfied (NOT is satisfied) - // The next notable time point is when the inner condition becomes satisfied - // (which would make the NOT condition unsatisfied) - Optional nextInnerTrigger = condition.getCurrentTriggerTime(); - - // If the inner condition has a next trigger time, that's when NOT will become unsatisfied - return nextInnerTrigger; - } - } - - // For non-TimeCondition, use the default behavior - // If the NOT is satisfied, return time in the past - if (isSatisfied()) { - return Optional.of(ZonedDateTime.now(ZoneId.systemDefault()).minusSeconds(1)); - } - - // If the NOT is not satisfied, we can't determine when it will become satisfied - return Optional.empty(); - } - - /** - * Returns a detailed description of the NOT condition with additional status information - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - // Basic description - sb.append("NOT Logical Condition: Inverts the inner condition\n"); - - // Status information - boolean satisfied = isSatisfied(); - sb.append("Status: ").append(satisfied ? "Satisfied" : "Not satisfied").append("\n"); - - // Progress information (inverted) - double progress = getProgressPercentage(); - sb.append(String.format("Inverted Progress: %.1f%%\n", progress)).append("\n"); - - // Inner condition information - sb.append("Inner Condition:\n"); - sb.append(" Type: ").append(condition.getClass().getSimpleName()).append("\n"); - sb.append(" Description: ").append(condition.getDescription()).append("\n"); - sb.append(" Status: ").append(condition.isSatisfied() ? "SATISFIED" : "NOT SATISFIED").append("\n"); - - // If the inner condition has a detailed description and it's not too complex - if (!(condition instanceof LogicalCondition)) { - sb.append("\nInner Condition Details:\n"); - - // Use reflection to safely try to access getDetailedDescription if available - try { - java.lang.reflect.Method detailedDescMethod = - condition.getClass().getMethod("getDetailedDescription"); - if (detailedDescMethod != null) { - String innerDetails = (String) detailedDescMethod.invoke(condition); - // Add indentation to inner details - innerDetails = " " + innerDetails.replace("\n", "\n "); - sb.append(innerDetails); - } - } catch (Exception e) { - // If detailed description isn't available, just use the regular description - sb.append(" ").append(condition.getDescription()); - } - } - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - // Basic information - sb.append("NotCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Type: NOT (Inverts inner condition)\n"); - sb.append(" │ Inner Condition: ").append(condition.getClass().getSimpleName()).append("\n"); - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - boolean satisfied = isSatisfied(); - sb.append(" │ Satisfied: ").append(satisfied).append("\n"); - sb.append(" │ Inner Satisfied: ").append(condition.isSatisfied()).append("\n"); - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Inner condition - sb.append(" └─ Inner Condition ─────────────────────────\n"); - - // Format the inner condition's toString with proper indentation - String innerString = condition.toString(); - String[] lines = innerString.split("\n"); - - // For simple conditions that might not have fancy toString - if (lines.length <= 1) { - sb.append(" ").append(condition.getDescription()); - } else { - // Skip the first line if it's just the class name - for (int i = (lines[0].contains("Condition:") ? 1 : 0); i < lines.length; i++) { - // Indent each line - sb.append(" ").append(lines[i]).append("\n"); - } - } - - return sb.toString(); - } - @Override - public void pause() { - - - - } - - @Override - public void resume() { - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/OrCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/OrCondition.java deleted file mode 100644 index 637ecd5faa5..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/OrCondition.java +++ /dev/null @@ -1,234 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.logical; - -import java.time.Duration; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import lombok.EqualsAndHashCode; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; - -/** - * Logical OR combination of conditions - any can be met. - */ -@EqualsAndHashCode(callSuper = true) -public class OrCondition extends LogicalCondition { - public OrCondition(Condition... conditions) { - super(conditions); - - } - @Override - public boolean isSatisfied() { - if (conditions.isEmpty()) return true; - return conditions.stream().anyMatch(Condition::isSatisfied); - } - - /** - * Gets the estimated time until this OR condition will be satisfied. - * For an OR condition, this returns the minimum (earliest) estimated time - * among all child conditions, since any one of them being satisfied - * will satisfy the entire OR condition. - * - * @return Optional containing the estimated duration until satisfaction, or empty if not determinable - */ - @Override - public Optional getEstimatedTimeWhenIsSatisfied() { - if (conditions.isEmpty()) { - return Optional.of(Duration.ZERO); - } - - // If any condition is already satisfied, return zero - if (isSatisfied()) { - return Optional.of(Duration.ZERO); - } - - Duration shortestTime = null; - boolean hasEstimate = false; - - for (Condition condition : conditions) { - Optional estimate = condition.getEstimatedTimeWhenIsSatisfied(); - if (estimate.isPresent()) { - hasEstimate = true; - Duration currentEstimate = estimate.get(); - - if (shortestTime == null || currentEstimate.compareTo(shortestTime) < 0) { - shortestTime = currentEstimate; - } - - // If any condition has zero duration (satisfied), return immediately - if (currentEstimate.isZero()) { - return Optional.of(Duration.ZERO); - } - } - } - - return hasEstimate ? Optional.of(shortestTime) : Optional.empty(); - } - - /** - * Gets the next time this OR condition will be satisfied. - * If any condition is satisfied, returns the trigger time of the first satisfied condition. - * If no condition is satisfied, returns the earliest next trigger time among TimeConditions. - * - * @return Optional containing the next trigger time, or empty if none available - */ - @Override - public Optional getCurrentTriggerTime() { - if (conditions.isEmpty()) { - return Optional.empty(); - } - - - // If none satisfied, find earliest trigger time among TimeConditions - ZonedDateTime earliestTimeSatisfied = null; - ZonedDateTime earliestTimeUnSatisfied = null; - int satisfiedCount = 0; - for (Condition condition : conditions) { - if (condition instanceof TimeCondition) { - Optional nextTrigger = condition.getCurrentTriggerTime(); - if (condition.isSatisfied()) { - satisfiedCount++; - if (earliestTimeSatisfied == null || nextTrigger.get().isBefore(earliestTimeSatisfied)) { - earliestTimeSatisfied = nextTrigger.get(); - } - }else{ - if (nextTrigger.isPresent()) { - ZonedDateTime triggerTime = nextTrigger.get(); - if (earliestTimeUnSatisfied == null || triggerTime.isBefore(earliestTimeUnSatisfied)) { - earliestTimeUnSatisfied = triggerTime; - } - } - } - } - } - if (satisfiedCount > 0) { - return earliestTimeSatisfied != null ? Optional.of(earliestTimeSatisfied) : Optional.empty(); - }else if (earliestTimeUnSatisfied != null) { - return Optional.of(earliestTimeUnSatisfied); - }else{ - return Optional.empty(); - } - - } - - /** - * For an OR condition, all conditions must be unsatisfied to block the entire OR. - * This method returns all child conditions if none are satisfied, or an empty list - * if at least one is satisfied (meaning the OR condition itself is satisfied). - * - * @return List of all child conditions if none are satisfied, otherwise an empty list - */ - @Override - public List getBlockingConditions() { - // For an OR condition, if any condition is satisfied, nothing is blocking - if (isSatisfied()) { - return new ArrayList<>(); - } - - // If we reach here, none are satisfied, so all conditions are blocking - return new ArrayList<>(conditions); - } - - /** - * Returns a detailed description of the OR condition with additional status information - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - // Basic description - sb.append("OR Logical Condition: Any condition can be satisfied\n"); - - // Status information - boolean satisfied = isSatisfied(); - sb.append("Status: ").append(satisfied ? "Satisfied" : "Not satisfied").append("\n"); - sb.append("Child Conditions: ").append(conditions.size()).append("\n"); - - // Progress information - double progress = getProgressPercentage(); - sb.append(String.format("Overall Progress: %.1f%%\n", progress)); - - // Count satisfied conditions - int satisfiedCount = 0; - for (Condition condition : conditions) { - if (condition.isSatisfied()) { - satisfiedCount++; - } - } - sb.append("Satisfied Conditions: ").append(satisfiedCount).append("/").append(conditions.size()).append("\n\n"); - - // List all child conditions - sb.append("Child Conditions:\n"); - for (int i = 0; i < conditions.size(); i++) { - Condition condition = conditions.get(i); - sb.append(String.format("%d. %s [%s]\n", - i + 1, - condition.getDescription(), - condition.isSatisfied() ? "SATISFIED" : "NOT SATISFIED")); - } - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - // Basic information - sb.append("OrCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Type: OR (Any condition can be satisfied)\n"); - sb.append(" │ Child Conditions: ").append(conditions.size()).append("\n"); - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - boolean anySatisfied = isSatisfied(); - sb.append(" │ Satisfied: ").append(anySatisfied).append("\n"); - - // Count satisfied conditions - int satisfiedCount = 0; - for (Condition condition : conditions) { - if (condition.isSatisfied()) { - satisfiedCount++; - } - } - sb.append(" │ Satisfied Conditions: ").append(satisfiedCount).append("/").append(conditions.size()).append("\n"); - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Child conditions - if (!conditions.isEmpty()) { - sb.append(" ├─ Child Conditions ────────────────────────\n"); - - for (int i = 0; i < conditions.size(); i++) { - Condition condition = conditions.get(i); - String prefix = (i == conditions.size() - 1) ? " └─ " : " ├─ "; - - sb.append(prefix).append(String.format("Condition %d: %s [%s]\n", - i + 1, - condition.getClass().getSimpleName(), - condition.isSatisfied() ? "SATISFIED" : "NOT SATISFIED")); - } - } else { - sb.append(" └─ No Child Conditions ───────────────────────\n"); - } - - return sb.toString(); - } - public void pause() { - // Pause all child conditions - for (Condition condition : conditions) { - condition.pause(); - } - - - } - - - public void resume() { - // Resume all child conditions - for (Condition condition : conditions) { - condition.resume(); - } - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/PredicateCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/PredicateCondition.java deleted file mode 100644 index fe24feef4c5..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/PredicateCondition.java +++ /dev/null @@ -1,196 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.logical; - -import java.time.ZonedDateTime; -import java.util.Optional; -import java.util.function.Predicate; -import java.util.function.Supplier; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; - -/** - * A condition that combines a manual lock with a predicate evaluation. - * The condition is satisfied only when: - * 1. It is not manually locked, AND - * 2. The predicate evaluates to true - * - * This allows plugins to define dynamic conditions that depend on the game state - * while still maintaining the ability to manually lock/unlock the condition. - */ -@Slf4j -@EqualsAndHashCode(callSuper = true) -public class PredicateCondition extends LockCondition { - - @Getter - private final Predicate predicate; - private final Supplier stateSupplier; - private final String predicateDescription; - - /** - * Creates a new predicate condition with a default reason. - * - * @param predicate The predicate to evaluate - * @param stateSupplier A supplier that provides the current state to evaluate against the predicate - * @param predicateDescription A human-readable description of what the predicate checks - * @throws IllegalArgumentException if predicate or stateSupplier is null - */ - public PredicateCondition(boolean withBreakHandlerLock , Predicate predicate, Supplier stateSupplier, String predicateDescription) { - super("Plugin is locked or predicate condition is not met", withBreakHandlerLock); - validateConstructorArguments(predicate, stateSupplier, predicateDescription); - this.predicate = predicate; - this.stateSupplier = stateSupplier; - this.predicateDescription = predicateDescription != null ? predicateDescription : "Unknown predicate"; - } - - /** - * Creates a new predicate condition with a specified reason. - * - * @param reason The reason why the plugin is locked - * @param predicate The predicate to evaluate - * @param stateSupplier A supplier that provides the current state to evaluate against the predicate - * @param predicateDescription A human-readable description of what the predicate checks - * @throws IllegalArgumentException if predicate or stateSupplier is null - */ - public PredicateCondition(String reason, boolean withBreakHandlerLock ,Predicate predicate, Supplier stateSupplier, String predicateDescription) { - super(reason, withBreakHandlerLock); - validateConstructorArguments(predicate, stateSupplier, predicateDescription); - this.predicate = predicate; - this.stateSupplier = stateSupplier; - this.predicateDescription = predicateDescription != null ? predicateDescription : "Unknown predicate"; - } - - /** - * Creates a new predicate condition with a specified reason and initial lock state. - * - * @param reason The reason why the plugin is locked - * @param defaultLocked The initial locked state of the condition. - * @param withBreakHandlerLock Whether this condition participates in BreakHandler coordination (lock hand-off), not an initial lock state. - * @param predicate The predicate to evaluate - * @param stateSupplier A supplier that provides the current state to evaluate against the predicate - * @param predicateDescription A human-readable description of what the predicate checks - * @throws IllegalArgumentException if predicate or stateSupplier is null - */ - public PredicateCondition(String reason, boolean defaultLocked, boolean withBreakHandlerLock, Predicate predicate, Supplier stateSupplier, String predicateDescription) { - super(reason, defaultLocked, withBreakHandlerLock); - validateConstructorArguments(predicate, stateSupplier, predicateDescription); - this.predicate = predicate; - this.stateSupplier = stateSupplier; - this.predicateDescription = predicateDescription != null ? predicateDescription : "Unknown predicate"; - } - - /** - * Validates that required constructor arguments are not null - * - * @param predicate The predicate to evaluate - * @param stateSupplier A supplier that provides the current state - * @param predicateDescription A description of the predicate - * @throws IllegalArgumentException if predicate or stateSupplier is null - */ - private void validateConstructorArguments(Predicate predicate, Supplier stateSupplier, String predicateDescription) { - if (predicate == null) { - log.error("Predicate cannot be null in PredicateCondition constructor"); - throw new IllegalArgumentException("Predicate cannot be null"); - } - if (stateSupplier == null) { - log.error("State supplier cannot be null in PredicateCondition constructor"); - throw new IllegalArgumentException("State supplier cannot be null"); - } - if (predicateDescription == null) { - log.warn("Predicate description is null, using default"); - } - } - - /** - * Evaluates the current state against the predicate. - * This method is thread-safe and handles exceptions safely. - * - * @return True if the predicate is satisfied, false otherwise or if an exception occurs - */ - public synchronized boolean evaluatePredicate() { - try { - if (stateSupplier == null) { - log.warn("State supplier is null in predicateDescription: {}", predicateDescription); - return false; - } - - T currentState = stateSupplier.get(); - - if (predicate == null) { - log.warn("Predicate is null in predicateDescription: {}", predicateDescription); - return false; - } - - return predicate.test(currentState); - } catch (Exception e) { - log.error("Exception in predicateDescription: {} - {}", predicateDescription, e.getMessage(), e); - return false; - } - } - - @Override - public synchronized boolean isSatisfied() { - try { - // The condition is satisfied only if: - // 1. It's not manually locked (from parent class) - // 2. The predicate evaluates to true - return super.isSatisfied() && evaluatePredicate(); - } catch (Exception e) { - log.error("Exception in isSatisfied for predicateDescription: {} - {}", predicateDescription, e.getMessage(), e); - return false; - } - } - - @Override - public synchronized String getDescription() { - try { - boolean predicateSatisfied = evaluatePredicate(); - return "Predicate Condition: " + - (isLocked() ? "\nLOCKED - " + getReason() : "\nUNLOCKED") + - "\nPredicate: " + predicateDescription + - "\nPredicate Satisfied: " + (predicateSatisfied ? "Yes" : "No"); - } catch (Exception e) { - log.error("Exception in getDescription for predicateDescription: {} - {}", predicateDescription, e.getMessage(), e); - return "Predicate Condition: [Error retrieving description]"; - } - } - - @Override - public synchronized String getDetailedDescription() { - return getDescription(); - } - - @Override - public ConditionType getType() { - return ConditionType.LOGICAL; - } - - @Override - public synchronized void reset(boolean randomize) { - try { - // Reset the lock state from parent class - super.reset(randomize); - // No need to reset predicate or supplier - } catch (Exception e) { - log.error("Exception in reset for predicateDescription: {} - {}", predicateDescription, e.getMessage(), e); - } - } - - @Override - public Optional getCurrentTriggerTime() { - // Predicate conditions don't have a specific trigger time - return Optional.empty(); - } - public void pause() { - - - } - - - public void resume() { - - - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/enums/UpdateOption.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/enums/UpdateOption.java deleted file mode 100644 index 4569b050a53..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/enums/UpdateOption.java +++ /dev/null @@ -1,28 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.enums; - -/** - * Update options for condition managers. - * Controls how plugin conditions are merged during updates. - */ -public enum UpdateOption { - /** - * Only add new conditions, preserve existing conditions - */ - ADD_ONLY, - - /** - * Synchronize conditions to match the new structure (add new and remove missing) - */ - SYNC, - - /** - * Only remove conditions that don't exist in the new structure - */ - REMOVE_ONLY, - - /** - * Replace the entire condition structure with the new one - */ - REPLACE -} - \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/serialization/LogicalConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/serialization/LogicalConditionAdapter.java deleted file mode 100644 index 7d0d9337c06..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/serialization/LogicalConditionAdapter.java +++ /dev/null @@ -1,117 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -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.LogicalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.OrCondition; - -import java.lang.reflect.Type; - -/** - * Serializes and deserializes LogicalCondition objects - */ -@Slf4j -public class LogicalConditionAdapter implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(LogicalCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add class info to distinguish different logical conditions - json.addProperty("class", src.getClass().getName()); - - // Serialize the conditions with proper type wrapping - JsonArray conditionsArray = new JsonArray(); - for (Condition condition : src.getConditions()) { - // Create a properly typed wrapper for each condition - JsonObject typedCondition = new JsonObject(); - typedCondition.addProperty("type", condition.getClass().getName()); - - // Serialize the condition data and add it to the wrapper - JsonElement conditionData = context.serialize(condition); - typedCondition.add("data", conditionData); - - conditionsArray.add(typedCondition); - } - json.add("conditions", conditionsArray); - - return json; - } - - @Override - public LogicalCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - try { - JsonObject jsonObject = json.getAsJsonObject(); - LogicalCondition logicalCondition; - - // Determine the concrete class using exact class name matching - if (jsonObject.has("class")) { - String className = jsonObject.get("class").getAsString(); - - // Use exact class name matching - if (className.endsWith(".OrCondition")) { - logicalCondition = new OrCondition(); - } else if (className.endsWith(".AndCondition")) { - logicalCondition = new AndCondition(); - } else { - // Default fallback - log.warn("Unknown logical condition class: {}, defaulting to AndCondition", className); - logicalCondition = new AndCondition(); - } - } else { - // Default if no class info - logicalCondition = new AndCondition(); - } - - // Handle conditions - if (jsonObject.has("conditions")) { - JsonArray conditionsArray = jsonObject.getAsJsonArray("conditions"); - for (JsonElement element : conditionsArray) { - try { - // Check if this is a wrapped condition from ConditionTypeAdapter - if (element.isJsonObject()) { - JsonObject conditionObj = element.getAsJsonObject(); - - // Handle the typed wrapper structure from ConditionTypeAdapter - if (conditionObj.has("type") && conditionObj.has("data")) { - // This is the format from ConditionTypeAdapter - Condition condition = context.deserialize(conditionObj, Condition.class); - if (condition != null) { - logicalCondition.addCondition(condition); - } - } else if (conditionObj.has("data")) { - // Try to get the condition directly from the data field - Condition condition = context.deserialize(conditionObj.get("data"), Condition.class); - if (condition != null) { - logicalCondition.addCondition(condition); - } - } else { - // Try to deserialize directly - Condition condition = context.deserialize(element, Condition.class); - if (condition != null) { - logicalCondition.addCondition(condition); - } - } - } else { - // Try to deserialize directly - Condition condition = context.deserialize(element, Condition.class); - if (condition != null) { - logicalCondition.addCondition(condition); - } - } - } catch (Exception e) { - log.warn("Failed to deserialize a condition in logical condition", e); - } - } - } - - return logicalCondition; - } catch (Exception e) { - log.error("Error deserializing LogicalCondition", e); - // Return empty AndCondition on error - return new AndCondition(); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/serialization/NotConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/serialization/NotConditionAdapter.java deleted file mode 100644 index 3be5d5b7a20..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/logical/serialization/NotConditionAdapter.java +++ /dev/null @@ -1,69 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.NotCondition; - -import java.lang.reflect.Type; - -/** - * Serializes and deserializes NotCondition objects - */ -@Slf4j -public class NotConditionAdapter implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(NotCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add class info - json.addProperty("class", src.getClass().getName()); - - // NotCondition only has one inner condition - Condition innerCondition = src.getCondition(); - - // Create a properly typed wrapper for the inner condition - JsonObject typedCondition = new JsonObject(); - typedCondition.addProperty("type", innerCondition.getClass().getName()); - - // Serialize the condition data and add it to the wrapper - JsonElement conditionData = context.serialize(innerCondition); - typedCondition.add("data", conditionData); - - // Add the inner condition to the object - json.add("condition", typedCondition); - - return json; - } - - @Override - public NotCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - try { - JsonObject jsonObject = json.getAsJsonObject(); - - // Handle inner condition - if (jsonObject.has("condition")) { - log.info("Deserializing NOT condition inner condition: {}", - jsonObject.get("condition").toString()); - - JsonElement element = jsonObject.get("condition"); - Condition innerCondition = context.deserialize(element, Condition.class); - - if (innerCondition != null) { - return new NotCondition(innerCondition); - } else { - log.error("Failed to deserialize inner condition for NotCondition"); - } - } else { - log.error("NotCondition JSON missing 'condition' field"); - } - - // If we reach here, something went wrong - throw new JsonParseException("Invalid NotCondition JSON format"); - } catch (Exception e) { - log.error("Error deserializing NotCondition", e); - throw new JsonParseException("Failed to deserialize NotCondition: " + e.getMessage()); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/NpcCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/NpcCondition.java deleted file mode 100644 index e2fb3d00d55..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/NpcCondition.java +++ /dev/null @@ -1,46 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.npc; - -import java.util.regex.Pattern; - -import lombok.EqualsAndHashCode; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; - -/** - * Abstract base class for all NPC-based conditions. - */ -@EqualsAndHashCode(callSuper = false) -public abstract class NpcCondition implements Condition { - @Override - public ConditionType getType() { - return ConditionType.NPC; - } - - /** - * Creates a pattern for matching NPC names - */ - protected Pattern createNpcNamePattern(String npcName) { - if (npcName == null || npcName.isEmpty()) { - return Pattern.compile(".*"); - } - - // Check if the name is already a regex pattern - if (npcName.startsWith("^") || npcName.endsWith("$") || - npcName.contains(".*") || npcName.contains("[") || - npcName.contains("(")) { - return Pattern.compile(npcName); - } - - // Otherwise, create a contains pattern - return Pattern.compile(".*" + Pattern.quote(npcName) + ".*", Pattern.CASE_INSENSITIVE); - } - - public void pause() { - - } - - - public void resume() { - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/NpcKillCountCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/NpcKillCountCondition.java deleted file mode 100644 index f0210368978..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/NpcKillCountCondition.java +++ /dev/null @@ -1,470 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.npc; - -import lombok.Builder; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import net.runelite.api.Actor; -import net.runelite.api.NPC; -import net.runelite.api.events.InteractingChanged; -import net.runelite.api.events.NpcDespawned; -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.OrCondition; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.regex.Pattern; - -/** - * Condition that tracks the number of NPCs killed by the player. - * Is satisfied when the player has killed a certain number of NPCs. - */ -@Getter -@EqualsAndHashCode(callSuper = true) -public class NpcKillCountCondition extends NpcCondition { - - public static String getVersion() { - return "0.0.1"; - } - private final String npcName; - private final Pattern npcNamePattern; - private final int targetCountMin; - private final int targetCountMax; - private transient volatile int currentTargetCount; - private transient volatile int currentKillCount; - private transient volatile boolean satisfied = false; - private transient boolean registered = false; - - // Set to track NPCs we're currently interacting with - private final Set interactingNpcIndices = new HashSet<>(); - - private long startTimeMillis = System.currentTimeMillis(); - private long lastKillTimeMillis = 0; - - /** - * Creates a condition with a fixed target count - */ - - public NpcKillCountCondition(String npcName, int targetCount) { - this.npcName = npcName; - this.npcNamePattern = createNpcNamePattern(npcName); - this.targetCountMin = targetCount; - this.targetCountMax = targetCount; - this.currentTargetCount = targetCount; - - } - - /** - * Creates a condition with a randomized target count between min and max - */ - @Builder - public NpcKillCountCondition(String npcName, int targetCountMin, int targetCountMax) { - this.npcName = npcName; - this.npcNamePattern = createNpcNamePattern(npcName); - this.targetCountMin = Math.max(0, targetCountMin); - this.targetCountMax = Math.max(this.targetCountMin, targetCountMax); - this.currentTargetCount = Rs2Random.between(this.targetCountMin, this.targetCountMax); - } - - /** - * Creates a condition with randomized target between min and max - */ - public static NpcKillCountCondition createRandomized(String npcName, int minCount, int maxCount) { - return NpcKillCountCondition.builder() - .npcName(npcName) - .targetCountMin(minCount) - .targetCountMax(maxCount) - .build(); - } - - /** - * Creates an AND logical condition requiring kills for multiple NPCs with individual targets - * All conditions must be satisfied (must kill the required number of each NPC) - */ - public static LogicalCondition createAndCondition(List npcNames, List targetCountsMins, List targetCountsMaxs) { - if (npcNames == null || npcNames.isEmpty()) { - throw new IllegalArgumentException("NPC name list cannot be null or empty"); - } - - // Determine the smallest list size for safe iteration - int minSize = Math.min(npcNames.size(), - Math.min(targetCountsMins != null ? targetCountsMins.size() : 0, - targetCountsMaxs != null ? targetCountsMaxs.size() : 0)); - - // If target counts not provided or empty, default to single kill per NPC - if (targetCountsMins == null || targetCountsMins.isEmpty()) { - targetCountsMins = new ArrayList<>(npcNames.size()); - for (int i = 0; i < npcNames.size(); i++) { - targetCountsMins.add(1); - } - } - - if (targetCountsMaxs == null || targetCountsMaxs.isEmpty()) { - targetCountsMaxs = new ArrayList<>(targetCountsMins); - } - - // Create the logical condition - AndCondition andCondition = new AndCondition(); - - // Add a kill count condition for each NPC - for (int i = 0; i < minSize; i++) { - NpcKillCountCondition killCondition = NpcKillCountCondition.builder() - .npcName(npcNames.get(i)) - .targetCountMin(targetCountsMins.get(i)) - .targetCountMax(targetCountsMaxs.get(i)) - .build(); - - andCondition.addCondition(killCondition); - } - - return andCondition; - } - - /** - * Creates an OR logical condition requiring kills for multiple NPCs with individual targets - * Any condition can be satisfied (must kill the required number of any one NPC) - */ - public static LogicalCondition createOrCondition(List npcNames, List targetCountsMins, List targetCountsMaxs) { - if (npcNames == null || npcNames.isEmpty()) { - throw new IllegalArgumentException("NPC name list cannot be null or empty"); - } - - // Determine the smallest list size for safe iteration - int minSize = Math.min(npcNames.size(), - Math.min(targetCountsMins != null ? targetCountsMins.size() : 0, - targetCountsMaxs != null ? targetCountsMaxs.size() : 0)); - - // If target counts not provided or empty, default to single kill per NPC - if (targetCountsMins == null || targetCountsMins.isEmpty()) { - targetCountsMins = new ArrayList<>(npcNames.size()); - for (int i = 0; i < npcNames.size(); i++) { - targetCountsMins.add(1); - } - } - - if (targetCountsMaxs == null || targetCountsMaxs.isEmpty()) { - targetCountsMaxs = new ArrayList<>(targetCountsMins); - } - - // Create the logical condition - OrCondition orCondition = new OrCondition(); - - // Add a kill count condition for each NPC - for (int i = 0; i < minSize; i++) { - NpcKillCountCondition killCondition = NpcKillCountCondition.builder() - .npcName(npcNames.get(i)) - .targetCountMin(targetCountsMins.get(i)) - .targetCountMax(targetCountsMaxs.get(i)) - .build(); - - orCondition.addCondition(killCondition); - } - - return orCondition; - } - - /** - * Creates an AND logical condition requiring kills for multiple NPCs with the same target for all - * All conditions must be satisfied (must kill the required number of each NPC) - */ - public static LogicalCondition createAndCondition(List npcNames, int targetCountMin, int targetCountMax) { - if (npcNames == null || npcNames.isEmpty()) { - throw new IllegalArgumentException("NPC name list cannot be null or empty"); - } - - // Create the logical condition - AndCondition andCondition = new AndCondition(); - - // Add a kill count condition for each NPC with the same targets - for (String npcName : npcNames) { - NpcKillCountCondition killCondition = NpcKillCountCondition.builder() - .npcName(npcName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .build(); - - andCondition.addCondition(killCondition); - } - - return andCondition; - } - - /** - * Creates an OR logical condition requiring kills for multiple NPCs with the same target for all - * Any condition can be satisfied (must kill the required number of any one NPC) - */ - public static LogicalCondition createOrCondition(List npcNames, int targetCountMin, int targetCountMax) { - if (npcNames == null || npcNames.isEmpty()) { - throw new IllegalArgumentException("NPC name list cannot be null or empty"); - } - - // Create the logical condition - OrCondition orCondition = new OrCondition(); - - // Add a kill count condition for each NPC with the same targets - for (String npcName : npcNames) { - NpcKillCountCondition killCondition = NpcKillCountCondition.builder() - .npcName(npcName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .build(); - - orCondition.addCondition(killCondition); - } - - return orCondition; - } - - - @Override - public boolean isSatisfied() { - // Once satisfied, stay satisfied until reset - if (satisfied) { - return true; - } - - // Check if current count meets or exceeds target - if (currentKillCount >= currentTargetCount) { - satisfied = true; - return true; - } - - return false; - } - - @Override - public String getDescription() { - StringBuilder sb = new StringBuilder(); - String npcDisplayName = npcName != null && !npcName.isEmpty() ? npcName : "NPCs"; - - sb.append(String.format("Kill %d %s", currentTargetCount, npcDisplayName)); - - // Add randomization info if applicable - if (targetCountMin != targetCountMax) { - sb.append(String.format(" (randomized from %d-%d)", targetCountMin, targetCountMax)); - } - - // Add progress tracking - sb.append(String.format(" (%d/%d, %.1f%%)", - currentKillCount, - currentTargetCount, - getProgressPercentage())); - - return sb.toString(); - } - - /** - * Returns a detailed description of the kill condition with additional status information - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - String npcDisplayName = npcName != null && !npcName.isEmpty() ? npcName : "NPCs"; - - // Basic description - sb.append(String.format("Kill %d %s", currentTargetCount, npcDisplayName)); - - // Add randomization info if applicable - if (targetCountMin != targetCountMax) { - sb.append(String.format(" (randomized from %d-%d)", targetCountMin, targetCountMax)); - } - - sb.append("\n"); - - // Status information - sb.append("Status: ").append(satisfied ? "Satisfied" : "Not satisfied").append("\n"); - sb.append("Progress: ").append(String.format("%d/%d (%.1f%%)", - currentKillCount, - currentTargetCount, - getProgressPercentage())).append("\n"); - - // NPC information - if (npcName != null && !npcName.isEmpty()) { - sb.append("NPC Name: ").append(npcName).append("\n"); - - if (!npcNamePattern.pattern().equals(".*")) { - sb.append("Pattern: ").append(npcNamePattern.pattern()).append("\n"); - } - } else { - sb.append("NPC: Any\n"); - } - - // Tracking information - sb.append("Currently tracking ").append(interactingNpcIndices.size()).append(" NPCs"); - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - // Basic information - sb.append("NpcKillCountCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ NPC: ").append(npcName != null && !npcName.isEmpty() ? npcName : "Any").append("\n"); - - if (npcNamePattern != null && !npcNamePattern.pattern().equals(".*")) { - sb.append(" │ Pattern: ").append(npcNamePattern.pattern()).append("\n"); - } - - sb.append(" │ Target Count: ").append(currentTargetCount).append("\n"); - - // Randomization - sb.append(" ├─ Randomization ────────────────────────────\n"); - boolean hasRandomization = targetCountMin != targetCountMax; - sb.append(" │ Randomization: ").append(hasRandomization ? "Enabled" : "Disabled").append("\n"); - if (hasRandomization) { - sb.append(" │ Target Range: ").append(targetCountMin).append("-").append(targetCountMax).append("\n"); - } - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - sb.append(" │ Satisfied: ").append(satisfied).append("\n"); - sb.append(" │ Current Kill Count: ").append(currentKillCount).append("\n"); - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Tracking info - sb.append(" └─ Tracking ────────────────────────────────\n"); - sb.append(" Active Interactions: ").append(interactingNpcIndices.size()).append("\n"); - - // List tracked NPCs if there are any - if (!interactingNpcIndices.isEmpty()) { - sb.append(" Tracked NPCs: ").append(interactingNpcIndices.toString()).append("\n"); - } - - return sb.toString(); - } - - @Override - public void reset() { - reset(false); - } - - @Override - public void reset(boolean randomize) { - if (randomize && targetCountMin != targetCountMax) { - currentTargetCount = Rs2Random.between(targetCountMin, targetCountMax); - } - satisfied = false; - currentKillCount = 0; - interactingNpcIndices.clear(); - } - - @Override - public double getProgressPercentage() { - if (satisfied) { - return 100.0; - } - - if (currentTargetCount <= 0) { - return 100.0; - } - - return Math.min(100.0, (currentKillCount * 100.0) / currentTargetCount); - } - - // NOTE: This approach tracks player interactions with NPCs - // It may need adjustment based on how interaction events fire in the game - @Subscribe - public void onInteractingChanged(InteractingChanged event) { - // Only care if the player is doing the interaction - if (event.getSource() != Microbot.getClient().getLocalPlayer()) { - return; - } - - // If the player is now interacting with an NPC, track it - if (event.getTarget() instanceof NPC) { - NPC npc = (NPC) event.getTarget(); - - // Only track NPCs that match our pattern if we have one - if (npcName == null || npcName.isEmpty() || npcNamePattern.matcher(npc.getName()).matches()) { - interactingNpcIndices.add(npc.getIndex()); - } - } - } - - // NOTE: This part checks if an NPC that the player was interacting with died - // It assumes player killed it, which may not always be true in multi-combat areas - @Subscribe - public void onNpcDespawned(NpcDespawned event) { - NPC npc = event.getNpc(); - - // Check if we were tracking this NPC - if (interactingNpcIndices.contains(npc.getIndex())) { - // If the NPC is dead, count it as a kill - if (npc.isDead()) { - // Only count NPCs that match our pattern if we have one - if (npcName == null || npcName.isEmpty() || npcNamePattern.matcher(npc.getName()).matches()) { - currentKillCount++; - lastKillTimeMillis = System.currentTimeMillis(); - } - } - - // Remove the NPC from our tracking regardless - interactingNpcIndices.remove(npc.getIndex()); - } - } - - @Override - public int getTotalConditionCount() { - return 1; - } - - @Override - public int getMetConditionCount() { - return isSatisfied() ? 1 : 0; - } - - /** - * Manually increments the kill counter. - * Useful for testing or when external systems detect kills. - * - * @param count Number of kills to add - */ - public void incrementKillCount(int count) { - currentKillCount += count; - lastKillTimeMillis = System.currentTimeMillis(); - - // Update satisfaction status - if (currentKillCount >= currentTargetCount && !satisfied) { - satisfied = true; - } - } - - /** - * Gets the estimated kills per hour based on current progress. - * - * @return Kills per hour or 0 if not enough data - */ - public double getKillsPerHour() { - long timeElapsedMs = System.currentTimeMillis() - startTimeMillis; - - // Require at least 30 seconds of data and at least one kill - if (timeElapsedMs < 30000 || currentKillCount == 0) { - return 0; - } - - double hoursElapsed = timeElapsedMs / (1000.0 * 60 * 60); - return currentKillCount / hoursElapsed; - } - - /** - * Gets the time since the last kill in milliseconds. - * - * @return Time since last kill in ms, or -1 if no kills yet - */ - public long getTimeSinceLastKill() { - if (lastKillTimeMillis == 0) { - return -1; - } - - return System.currentTimeMillis() - lastKillTimeMillis; - } - - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/ReadMe.md b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/ReadMe.md deleted file mode 100644 index 05bf2207b72..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/ReadMe.md +++ /dev/null @@ -1,14 +0,0 @@ -``` Java -// Track kills for multiple NPCs with different requirements (must kill ALL to satisfy) -List npcNames = Arrays.asList("Goblin", "Cow", "Chicken"); -List minCounts = Arrays.asList(10, 5, 15); -List maxCounts = Arrays.asList(15, 10, 20); -LogicalCondition killAllCondition = NpcKillCountCondition.createAndCondition(npcNames, minCounts, maxCounts); - -// Track kills for multiple NPCs with same requirements (must kill ANY to satisfy) -LogicalCondition killAnyCondition = NpcKillCountCondition.createOrCondition( - Arrays.asList("Dragon", "Demon", "Giant"), 5, 10); - -// Add to condition manager -conditionManager.addCondition(killAllCondition); -``` \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/serialization/NpcKillCountConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/serialization/NpcKillCountConditionAdapter.java deleted file mode 100644 index ff773ddeb96..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/npc/serialization/NpcKillCountConditionAdapter.java +++ /dev/null @@ -1,77 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.npc.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.npc.NpcKillCountCondition; - -import java.lang.reflect.Type; - -/** - * Serializes and deserializes NpcKillCountCondition objects - */ -@Slf4j -public class NpcKillCountConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(NpcKillCountCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add type information - json.addProperty("type", NpcKillCountCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Store NPC information - data.addProperty("npcName", src.getNpcName()); - data.addProperty("targetCountMin", src.getTargetCountMin()); - data.addProperty("targetCountMax", src.getTargetCountMax()); - data.addProperty("version", NpcKillCountCondition.getVersion()); - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public NpcKillCountCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(NpcKillCountCondition.getVersion())) { - throw new JsonParseException("Version mismatch in NpcKillCountCondition: expected " + - NpcKillCountCondition.getVersion() + ", got " + version); - } - } - - // Get NPC information - String npcName = dataObj.get("npcName").getAsString(); - int targetCountMin = dataObj.get("targetCountMin").getAsInt(); - int targetCountMax = dataObj.get("targetCountMax").getAsInt(); - - // Create using builder pattern - NpcKillCountCondition condition = NpcKillCountCondition.builder() - .npcName(npcName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .build(); - - return condition; - - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/BankItemCountCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/BankItemCountCondition.java deleted file mode 100644 index fe725e7ddb3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/BankItemCountCondition.java +++ /dev/null @@ -1,364 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource; - -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; -import lombok.Builder; -import lombok.Getter; -import net.runelite.api.InventoryID; -import net.runelite.api.events.ItemContainerChanged; -import net.runelite.client.eventbus.Subscribe; -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.OrCondition; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; - -/** - * Condition that tracks the number of items in bank. - * Is satisfied when we have a certain number of items in the bank, and stays satisfied until reset. - * TODO make proccesdItemCountCondition -> tracks the number of items processed in the inventory -> for now placeholder - * track first if we "get" it in the inventory, then count down the procces items (not counting item dropped or banked -> track if bank is open or the item was dropp be the player) - */ -@Getter -public class BankItemCountCondition extends ResourceCondition { - - public static String getVersion() { - return "0.0.1"; - } - private final String itemName; - private final int targetCountMin; - private final int targetCountMax; - - private int currentTargetCount; - private int currentItemCount; - private boolean satisfied = false; - - /** - * Creates a condition with fixed target count - */ - public BankItemCountCondition(String itemName, int targetCount) { - super(itemName); - this.itemName = itemName; - this.targetCountMin = targetCount; - this.targetCountMax = targetCount; - this.currentTargetCount = targetCount; - updateCurrentCount(); - } - - /** - * Creates a condition with target count range - */ - @Builder - public BankItemCountCondition(String itemName, int targetCountMin, int targetCountMax) { - super(itemName); - this.itemName = itemName; - this.targetCountMin = Math.max(0, targetCountMin); - this.targetCountMax = Math.max(this.targetCountMin, targetCountMax); - this.currentTargetCount = Rs2Random.between(this.targetCountMin, this.targetCountMax); - updateCurrentCount(); - } - - /** - * Creates a condition with randomized target between min and max - */ - public static BankItemCountCondition createRandomized(String itemName, int minCount, int maxCount) { - return BankItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(minCount) - .targetCountMax(maxCount) - .build(); - } - - @Override - public boolean isSatisfied() { - // A condition cannot be satisfied while paused - if (isPaused) { - return false; - } - - // Once satisfied, stay satisfied until reset - if (satisfied) { - return true; - } - - // Check if current count meets or exceeds target - if (currentItemCount >= currentTargetCount) { - satisfied = true; - return true; - } - - return false; - } - - @Override - public String getDescription() { - return String.format("Have %d %s in bank (%d/%d)", - currentTargetCount, - itemName, - currentItemCount, - currentTargetCount); - } - @Override - public String getDetailedDescription() { - return String.format("BankItemCountCondition: %s\n" + - "Target Count: %d - %d\n" + - "Current Count: %d\n" + - "Satisfied: %s", - itemName, - targetCountMin, - targetCountMax, - currentItemCount, - satisfied ? "YES" : "NO"); - } - - @Override - public void reset() { - reset(false); - } - - @Override - public void reset(boolean randomize) { - if (randomize && targetCountMin != targetCountMax) { - currentTargetCount = Rs2Random.between(targetCountMin, targetCountMax); - } - satisfied = false; - updateCurrentCount(); - } - - @Override - public double getProgressPercentage() { - if (satisfied) { - return 100.0; - } - - if (currentTargetCount <= 0) { - return 100.0; - } - - return Math.min(100.0, (currentItemCount * 100.0) / currentTargetCount); - } - - @Override - @Subscribe - public void onItemContainerChanged(ItemContainerChanged event) { - // Skip processing if paused - if (isPaused) { - return; - } - - // Update count when bank container changes - if (event.getContainerId() == InventoryID.BANK.getId()) { - updateCurrentCount(); - } - } - - private void updateCurrentCount() { - // Check if we're using a specific item name or counting all items - if (itemName == null || itemName.isEmpty()) { - // Count all items in bank - currentItemCount = Rs2Bank.getBankItemCount(); - } else { - // Count specific items by name using pattern matching - if (Rs2Bank.bankItems() != null && !Rs2Bank.bankItems().isEmpty()) { - currentItemCount = Rs2Bank.bankItems().stream() - .filter(item -> { - if (item == null) { - return false; - } - return itemPattern.matcher(item.getName()).matches(); - }) - .mapToInt(Rs2ItemModel::getQuantity) - .sum(); - } else { - // Fallback to direct count if bank items list isn't populated - currentItemCount = Rs2Bank.count(itemName, false); - } - } - } - - @Override - public int getTotalConditionCount() { - return 1; - } - - @Override - public int getMetConditionCount() { - return isSatisfied() ? 1 : 0; - } - - /** - * Creates a pattern for matching item names - */ - protected Pattern createItemPattern(String itemName) { - if (itemName == null || itemName.isEmpty()) { - return Pattern.compile(".*"); - } - - // Check if the name is already a regex pattern - if (itemName.startsWith("^") || itemName.endsWith("$") || - itemName.contains(".*") || itemName.contains("[") || - itemName.contains("(")) { - return Pattern.compile(itemName); - } - - // Otherwise, create a contains pattern - return Pattern.compile(".*" + Pattern.quote(itemName) + ".*", Pattern.CASE_INSENSITIVE); - } - - /** - * Creates an AND logical condition requiring multiple items with individual targets - * All conditions must be satisfied (must have the required number of each item) - */ - public static LogicalCondition createAndCondition(List itemNames, List targetCountsMins, List targetCountsMaxs) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Determine the smallest list size for safe iteration - int minSize = Math.min(itemNames.size(), - Math.min(targetCountsMins != null ? targetCountsMins.size() : 0, - targetCountsMaxs != null ? targetCountsMaxs.size() : 0)); - - // If target counts not provided or empty, default to single item each - if (targetCountsMins == null || targetCountsMins.isEmpty()) { - targetCountsMins = new ArrayList<>(itemNames.size()); - for (int i = 0; i < itemNames.size(); i++) { - targetCountsMins.add(1); - } - } - - if (targetCountsMaxs == null || targetCountsMaxs.isEmpty()) { - targetCountsMaxs = new ArrayList<>(targetCountsMins); - } - - // Create the logical condition - AndCondition andCondition = new AndCondition(); - - // Add a condition for each item - for (int i = 0; i < minSize; i++) { - BankItemCountCondition itemCondition = BankItemCountCondition.builder() - .itemName(itemNames.get(i)) - .targetCountMin(targetCountsMins.get(i)) - .targetCountMax(targetCountsMaxs.get(i)) - .build(); - - andCondition.addCondition(itemCondition); - } - - return andCondition; - } - - /** - * Creates an OR logical condition requiring multiple items with individual targets - * Any condition can be satisfied (must have the required number of any one item) - */ - public static LogicalCondition createOrCondition(List itemNames, List targetCountsMins, List targetCountsMaxs) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Determine the smallest list size for safe iteration - int minSize = Math.min(itemNames.size(), - Math.min(targetCountsMins != null ? targetCountsMins.size() : 0, - targetCountsMaxs != null ? targetCountsMaxs.size() : 0)); - - // If target counts not provided or empty, default to single item each - if (targetCountsMins == null || targetCountsMins.isEmpty()) { - targetCountsMins = new ArrayList<>(itemNames.size()); - for (int i = 0; i < itemNames.size(); i++) { - targetCountsMins.add(1); - } - } - - if (targetCountsMaxs == null || targetCountsMaxs.isEmpty()) { - targetCountsMaxs = new ArrayList<>(targetCountsMins); - } - - // Create the logical condition - OrCondition orCondition = new OrCondition(); - - // Add a condition for each item - for (int i = 0; i < minSize; i++) { - BankItemCountCondition itemCondition = BankItemCountCondition.builder() - .itemName(itemNames.get(i)) - .targetCountMin(targetCountsMins.get(i)) - .targetCountMax(targetCountsMaxs.get(i)) - .build(); - - orCondition.addCondition(itemCondition); - } - - return orCondition; - } - - /** - * Creates an AND logical condition requiring multiple items with the same target for all - * All conditions must be satisfied (must have the required number of each item) - */ - public static LogicalCondition createAndCondition(List itemNames, int targetCountMin, int targetCountMax) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Create the logical condition - AndCondition andCondition = new AndCondition(); - - // Add a condition for each item with the same targets - for (String itemName : itemNames) { - BankItemCountCondition itemCondition = BankItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .build(); - - andCondition.addCondition(itemCondition); - } - - return andCondition; - } - - /** - * Creates an OR logical condition requiring multiple items with the same target for all - * Any condition can be satisfied (must have the required number of any one item) - */ - public static LogicalCondition createOrCondition(List itemNames, int targetCountMin, int targetCountMax) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Create the logical condition - OrCondition orCondition = new OrCondition(); - - // Add a condition for each item with the same targets - for (String itemName : itemNames) { - BankItemCountCondition itemCondition = BankItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .build(); - - orCondition.addCondition(itemCondition); - } - - return orCondition; - } - - @Override - public void pause() { - // Call parent class pause method - super.pause(); - } - - @Override - public void resume() { - if (isPaused) { - // Call parent class resume method - super.resume(); - - // For snapshot-type conditions, refresh current state on resume - updateCurrentCount(); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/GatheredResourceCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/GatheredResourceCondition.java deleted file mode 100644 index 9ad5ede4647..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/GatheredResourceCondition.java +++ /dev/null @@ -1,645 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource; - -import lombok.Builder; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import net.runelite.api.gameval.AnimationID; -import net.runelite.api.InventoryID; -import net.runelite.api.Skill; -import net.runelite.api.events.AnimationChanged; -import net.runelite.api.events.GameTick; -import net.runelite.api.events.InteractingChanged; -import net.runelite.api.events.ItemContainerChanged; -import net.runelite.api.events.StatChanged; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; -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.OrCondition; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** - * Condition that tracks items gathered from resource nodes (mining, fishing, woodcutting, farming, etc.). - * Distinguishes between items gathered from resources versus those obtained from other sources. - */ -@Slf4j -@Getter -public class GatheredResourceCondition extends ResourceCondition { - - public static String getVersion() { - return "0.0.1"; - } - private final boolean includeNoted; - private final String itemName; - private final int targetCountMin; - private final int targetCountMax; - - private final List relevantSkills; - - // Gathering state tracking - private transient boolean isCurrentlyGathering = false; - private transient Instant lastGatheringActivity = Instant.now(); - private transient Map previousItemCounts = new HashMap<>(); - private transient Map gatheredItemCounts = new HashMap<>(); - private transient int currentTargetCount; - private transient int currentGatheredCount; - private transient volatile boolean satisfied = false; - - // Pause-related fields for cumulative tracking - private transient Map pausedItemCounts = new HashMap<>(); - private transient int pausedGatheredCount = 0; - - - // Animation tracking for gathering activities - private static final int[] GATHERING_ANIMATIONS = { - // Woodcutting - AnimationID.HUMAN_WOODCUTTING_BRONZE_AXE, AnimationID.HUMAN_WOODCUTTING_IRON_AXE, AnimationID.HUMAN_WOODCUTTING_STEEL_AXE, - AnimationID.HUMAN_WOODCUTTING_BLACK_AXE, AnimationID.HUMAN_WOODCUTTING_MITHRIL_AXE, AnimationID.HUMAN_WOODCUTTING_ADAMANT_AXE, - AnimationID.HUMAN_WOODCUTTING_RUNE_AXE, AnimationID.HUMAN_WOODCUTTING_DRAGON_AXE, AnimationID.HUMAN_WOODCUTTING_INFERNAL_AXE, - AnimationID.HUMAN_WOODCUTTING_3A_AXE, AnimationID.HUMAN_WOODCUTTING_CRYSTAL_AXE, AnimationID.HUMAN_WOODCUTTING_TRAILBLAZER_AXE_NO_INFERNAL, - - // Fishing - AnimationID.HUMAN_HARPOON, AnimationID.HUMAN_LOBSTER, AnimationID.HUMAN_LARGENET, - AnimationID.HUMAN_SMALLNET, AnimationID.HUMAN_FISHING_CASTING, AnimationID.HUMAN_FISH_ONSPOT_PEARL_OILY, - AnimationID.HUMAN_FISH_ONSPOT_PEARL,AnimationID.HUMAN_FISH_ONSPOT_PEARL_FLY,AnimationID.HUMAN_FISH_ONSPOT_PEARL_BRUT, - AnimationID.HUMAN_FISHING_CASTING_PEARL, - AnimationID.HUMAN_FISHING_CASTING_PEARL,AnimationID.HUMAN_FISHING_CASTING_PEARL_FLY,AnimationID.HUMAN_FISHING_CASTING_PEARL_BRUT, - - AnimationID.HUMAN_HARPOON_BARBED, AnimationID.HUMAN_HARPOON_DRAGON, AnimationID.HUMAN_HARPOON_INFERNAL, - AnimationID.HUMAN_HARPOON_TRAILBLAZER_NO_INFERNAL, AnimationID.HUMAN_HARPOON_CRYSTAL, AnimationID.HUMAN_HARPOON_GAUNTLET_HM, - AnimationID.BRUT_PLAYER_HAND_FISHING_READY, AnimationID.BRUT_PLAYER_HAND_FISHING_START, - AnimationID.BRUT_PLAYER_HAND_FISHING_END_SHARK_1, AnimationID.BRUT_PLAYER_HAND_FISHING_END_SHARK_2, - AnimationID.BRUT_PLAYER_HAND_FISHING_END_SWORDFISH_1, AnimationID.BRUT_PLAYER_HAND_FISHING_END_SWORDFISH_2, - AnimationID.BRUT_PLAYER_HAND_FISHING_END_TUNA_1, AnimationID.BRUT_PLAYER_HAND_FISHING_END_TUNA_2, - AnimationID.HUMAN_FISHING_CASTING_BRUT, AnimationID.HUMAN_FISHING_ONSPOT_BRUT, - AnimationID.SNAKEBOSS_SLICEEEL, //FISHING_CUTTING_SACRED_EELS - AnimationID.INFERNALEEL_BREAK, AnimationID.INFERNALEEL_BREAK_IMCANDO,//FISHING_CRUSHING_INFERNAL_EELS - AnimationID.HUMAN_OCTOPUS_POT,//FISHING_KARAMBWAN - // Mining - AnimationID.HUMAN_MINING_BRONZE_PICKAXE, AnimationID.HUMAN_MINING_IRON_PICKAXE, AnimationID.HUMAN_MINING_STEEL_PICKAXE, - AnimationID.HUMAN_MINING_BLACK_PICKAXE, AnimationID.HUMAN_MINING_MITHRIL_PICKAXE, AnimationID.HUMAN_MINING_ADAMANT_PICKAXE, - AnimationID.HUMAN_MINING_RUNE_PICKAXE, AnimationID.HUMAN_MINING_DRAGON_PICKAXE,AnimationID.HUMAN_MINING_DRAGON_PICKAXE_PRETTY, - AnimationID.HUMAN_MINING_ZALCANO_PICKAXE, AnimationID.HUMAN_MINING_CRYSTAL_PICKAXE,AnimationID.HUMAN_MINING_3A_PICKAXE, - AnimationID.HUMAN_MINING_TRAILBLAZER_PICKAXE_NO_INFERNAL,AnimationID.HUMAN_MINING_INFERNAL_PICKAXE, AnimationID.HUMAN_MINING_LEAGUE_TRAILBLAZER_PICKAXE, - AnimationID.HUMAN_MINING_ZALCANO_LEAGUE_TRAILBLAZER_PICKAXE, - - //CRASHEDSTAR mining - AnimationID.HUMAN_MINING_BRONZE_PICKAXE_NOREACHFORWARD, AnimationID.HUMAN_MINING_IRON_PICKAXE_NOREACHFORWARD, AnimationID.HUMAN_MINING_STEEL_PICKAXE_NOREACHFORWARD, - AnimationID.HUMAN_MINING_BLACK_PICKAXE_NOREACHFORWARD, AnimationID.HUMAN_MINING_MITHRIL_PICKAXE_NOREACHFORWARD, AnimationID.HUMAN_MINING_ADAMANT_PICKAXE_NOREACHFORWARD, - AnimationID.HUMAN_MINING_RUNE_PICKAXE_NOREACHFORWARD, AnimationID.HUMAN_MINING_DRAGON_PICKAXE_NOREACHFORWARD,AnimationID.HUMAN_MINING_DRAGON_PICKAXE_PRETTY_NOREACHFORWARD, - AnimationID.HUMAN_MINING_ZALCANO_PICKAXE_NOREACHFORWARD, AnimationID.HUMAN_MINING_CRYSTAL_PICKAXE_NOREACHFORWARD,AnimationID.HUMAN_MINING_3A_PICKAXE_NOREACHFORWARD, - AnimationID.HUMAN_MINING_TRAILBLAZER_PICKAXE_NO_INFERNAL_NOREACHFORWARD,AnimationID.HUMAN_MINING_INFERNAL_PICKAXE_NOREACHFORWARD, AnimationID.HUMAN_MINING_LEAGUE_TRAILBLAZER_PICKAXE_NOREACHFORWARD, - //Motherload Mine mining - AnimationID.HUMAN_MINING_BRONZE_PICKAXE_WALL, AnimationID.HUMAN_MINING_IRON_PICKAXE_WALL, AnimationID.HUMAN_MINING_STEEL_PICKAXE_WALL, - AnimationID.HUMAN_MINING_BLACK_PICKAXE_WALL, AnimationID.HUMAN_MINING_MITHRIL_PICKAXE_WALL, AnimationID.HUMAN_MINING_ADAMANT_PICKAXE_WALL, - AnimationID.HUMAN_MINING_RUNE_PICKAXE_WALL, AnimationID.HUMAN_MINING_DRAGON_PICKAXE_WALL,AnimationID.HUMAN_MINING_DRAGON_PICKAXE_PRETTY_WALL, - AnimationID.HUMAN_MINING_ZALCANO_PICKAXE_WALL, AnimationID.HUMAN_MINING_CRYSTAL_PICKAXE_WALL,AnimationID.HUMAN_MINING_3A_PICKAXE_WALL, - AnimationID.HUMAN_MINING_TRAILBLAZER_PICKAXE_NO_INFERNAL_WALL,AnimationID.HUMAN_MINING_INFERNAL_PICKAXE_WALL, AnimationID.HUMAN_MINING_LEAGUE_TRAILBLAZER_PICKAXE_WALL, - // Farming - AnimationID.ULTRACOMPOST_MAKE, AnimationID.HUMAN_FARMING, AnimationID.FARMING_RAKING, - AnimationID.FARMING_PICK_MUSHROOM - }; - - /** - * Basic constructor with only item name and target count - */ - public GatheredResourceCondition(String itemName, int targetCount, boolean includeNoted) { - super(itemName); - this.itemName = itemName; - this.targetCountMin = targetCount; - this.targetCountMax = targetCount; - this.currentTargetCount = targetCount; - this.includeNoted = includeNoted; - this.relevantSkills = determineRelevantSkills(itemName); - initializeItemCounts(); - } - - /** - * Full constructor with builder support - */ - @Builder - public GatheredResourceCondition(String itemName, int targetCountMin, int targetCountMax, - boolean includeNoted, List relevantSkills) { - super(itemName); - this.itemName = itemName; - this.targetCountMin = Math.max(0, targetCountMin); - this.targetCountMax = Math.min(Integer.MAX_VALUE, targetCountMax); - this.currentTargetCount = Rs2Random.between(this.targetCountMin, this.targetCountMax); - this.includeNoted = includeNoted; - this.relevantSkills = relevantSkills != null ? relevantSkills : determineRelevantSkills(itemName); - initializeItemCounts(); - } - - /** - * Initialize tracking of inventory item counts - */ - private void initializeItemCounts() { - updatePreviousItemCounts(); - gatheredItemCounts.clear(); - currentGatheredCount = 0; - } - - /** - * Create a map of current inventory item counts for tracking purposes - */ - private void updatePreviousItemCounts() { - previousItemCounts.clear(); - - // Include all inventory items matching our pattern - List items = new ArrayList<>(); - items.addAll(getUnNotedItems()); - if (includeNoted) { - items.addAll(getNotedItems()); - } - - // Count matching items - for (Rs2ItemModel item : items) { - if (item != null && itemPattern.matcher(item.getName()).matches()) { - String name = item.getName(); - previousItemCounts.put(name, previousItemCounts.getOrDefault(name, 0) + 1); - } - } - } - - /** - * Attempts to determine which skills are relevant for the specified item - */ - private List determineRelevantSkills(String itemName) { - List skills = new ArrayList<>(); - - // Pattern matching approach - could be more sophisticated with a proper mapping - String lowerName = itemName.toLowerCase(); - - // Mining related - if (lowerName.contains("ore") || lowerName.contains("rock") || lowerName.contains("coal") || - lowerName.contains("gem") || lowerName.contains("granite") || lowerName.contains("sandstone") || - lowerName.contains("clay")) { - skills.add(Skill.MINING); - } - - // Fishing related - if (lowerName.contains("fish") || lowerName.contains("shrimp") || lowerName.contains("trout") || - lowerName.contains("salmon") || lowerName.contains("lobster") || lowerName.contains("shark") || - lowerName.contains("karambwan") || lowerName.contains("monkfish") || lowerName.contains("anglerfish")) { - skills.add(Skill.FISHING); - } - - // Woodcutting related - if (lowerName.contains("log") || lowerName.contains("root") || lowerName.contains("bark")) { - skills.add(Skill.WOODCUTTING); - } - - // Farming related - if (lowerName.contains("seed") || lowerName.contains("sapling") || lowerName.contains("herb") || - lowerName.contains("leaf") || lowerName.contains("fruit") || lowerName.contains("berry") || - lowerName.contains("vegetable") || lowerName.contains("coconut") || lowerName.contains("banana") || - lowerName.contains("papaya") || lowerName.contains("watermelon") || lowerName.contains("strawberry") || - lowerName.contains("tomato") || lowerName.contains("potato") || lowerName.contains("onion") || - lowerName.contains("cabbage")) { - skills.add(Skill.FARMING); - } - - // Default to all resource-gathering skills if no match - if (skills.isEmpty()) { - skills.add(Skill.MINING); - skills.add(Skill.FISHING); - skills.add(Skill.WOODCUTTING); - skills.add(Skill.FARMING); - skills.add(Skill.HUNTER); - } - - return skills; - } - - /** - * Checks if the player is currently performing a gathering animation - */ - private boolean isCurrentlyGatheringAnimation() { - int currentAnimation = Rs2Player.getAnimation(); - for (int animation : GATHERING_ANIMATIONS) { - if (currentAnimation == animation) { - return true; - } - } - return false; - } - - @Override - public boolean isSatisfied() { - // A condition cannot be satisfied while paused - if (isPaused) { - return false; - } - - // Once satisfied, stay satisfied until reset - if (satisfied) { - return true; - } - - // Check if gathered count meets or exceeds target - if (currentGatheredCount >= currentTargetCount) { - satisfied = true; - return true; - } - - return false; - } - - @Override - public void reset() { - reset(false); - } - - @Override - public void reset(boolean randomize) { - if (randomize && targetCountMin != targetCountMax) { - currentTargetCount = Rs2Random.between(targetCountMin, targetCountMax); - } - satisfied = false; - isCurrentlyGathering = false; - gatheredItemCounts.clear(); - currentGatheredCount = 0; - updatePreviousItemCounts(); - } - - @Override - public ConditionType getType() { - return ConditionType.RESOURCE; - } - - @Override - public String getDescription() { - String itemTypeDesc = includeNoted ? " (including noted)" : ""; - String randomRangeInfo = ""; - - if (targetCountMin != targetCountMax) { - randomRangeInfo = String.format(" (randomized from %d-%d)", targetCountMin, targetCountMax); - } - - return String.format("Gather %d %s%s%s (%d/%d, %.1f%%)", - currentTargetCount, - itemName != null && !itemName.isEmpty() ? itemName : "resources", - itemTypeDesc, - randomRangeInfo, - currentGatheredCount, - currentTargetCount, - getProgressPercentage()); - } - - @Override - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - // Basic description - sb.append("Gathered Resource Condition: ").append(itemName != null && !itemName.isEmpty() ? itemName : "Any resource").append("\n"); - - // Add randomization info if applicable - if (targetCountMin != targetCountMax) { - sb.append("Target Range: ").append(targetCountMin).append("-").append(targetCountMax) - .append(" (current target: ").append(currentTargetCount).append(")\n"); - } else { - sb.append("Target Count: ").append(currentTargetCount).append("\n"); - } - - // Status information - sb.append("Status: ").append(isSatisfied() ? "Satisfied" : "Not satisfied").append("\n"); - sb.append("Progress: ").append(currentGatheredCount).append("/").append(currentTargetCount) - .append(" (").append(String.format("%.1f%%", getProgressPercentage())).append(")\n"); - - // Configuration information - sb.append("Include Noted Items: ").append(includeNoted ? "Yes" : "No").append("\n"); - sb.append("Currently Gathering: ").append(isCurrentlyGathering ? "Yes" : "No").append("\n"); - - // Relevant skills - sb.append("Tracking XP in skills: "); - for (int i = 0; i < relevantSkills.size(); i++) { - sb.append(relevantSkills.get(i).getName()); - if (i < relevantSkills.size() - 1) { - sb.append(", "); - } - } - - return sb.toString(); - } - - @Override - public double getProgressPercentage() { - if (satisfied) { - return 100.0; - } - - if (currentTargetCount <= 0) { - return 100.0; - } - - return Math.min(100.0, (currentGatheredCount * 100.0) / currentTargetCount); - } - - @Override - public void onAnimationChanged(AnimationChanged event) { - // Check if this is our player - if (event.getActor() != Microbot.getClient().getLocalPlayer()) { - return; - } - - // Update gathering state based on animation - if (isCurrentlyGatheringAnimation()) { - isCurrentlyGathering = true; - lastGatheringActivity = Instant.now(); - } - } - - @Override - public void onStatChanged(StatChanged event) { - // Check if XP was gained in a relevant skill - if (relevantSkills.contains(event.getSkill()) && event.getXp() > 0) { - isCurrentlyGathering = true; - lastGatheringActivity = Instant.now(); - } - } - - @Override - public void onInteractingChanged(InteractingChanged event) { - // Check if this is our player - if (event.getSource() != Microbot.getClient().getLocalPlayer()) { - return; - } - - // If player starts interacting with something, consider it gathering - if (event.getTarget() != null) { - isCurrentlyGathering = true; - lastGatheringActivity = Instant.now(); - } - } - - @Override - public void onGameTick(GameTick event) { - // Check if we've timed out on gathering activity - if (isCurrentlyGathering && Instant.now().minusSeconds(5).isAfter(lastGatheringActivity)) { - isCurrentlyGathering = false; - } - } - - @Override - public void onItemContainerChanged(ItemContainerChanged event) { - // Only process inventory changes - if (event.getContainerId() != InventoryID.INVENTORY.getId()) { - return; - } - - // Don't process changes if bank is open (banking items) - if (Rs2Bank.isOpen()) { - updatePreviousItemCounts(); - return; - } - - // Process inventory changes only when actively gathering or within 3 seconds of gathering - if (isCurrentlyGathering || Instant.now().minusSeconds(3).isBefore(lastGatheringActivity)) { - processInventoryChanges(); - } else { - // Just update previous counts if not gathering - updatePreviousItemCounts(); - } - } - - /** - * Process inventory changes to detect newly gathered items - */ - private void processInventoryChanges() { - Map currentCounts = new HashMap<>(); - - // Get current inventory counts - List currentItems = new ArrayList<>(); - currentItems.addAll(getUnNotedItems()); - if (includeNoted) { - currentItems.addAll(getNotedItems()); - } - - // Count matching items - for (Rs2ItemModel item : currentItems) { - if (item != null && itemPattern.matcher(item.getName()).matches()) { - String name = item.getName(); - currentCounts.put(name, currentCounts.getOrDefault(name, 0) + item.getQuantity()); - } - } - - // Calculate differences - for (Map.Entry entry : currentCounts.entrySet()) { - String itemName = entry.getKey(); - int currentCount = entry.getValue(); - int previousCount = previousItemCounts.getOrDefault(itemName, 0); - - // If current count is higher, items were gathered - if (currentCount > previousCount) { - int newItems = currentCount - previousCount; - log.debug("Detected {} newly gathered {}", newItems, itemName); - - // Add to gathered count - gatheredItemCounts.put(itemName, gatheredItemCounts.getOrDefault(itemName, 0) + newItems); - currentGatheredCount += newItems; - } - } - - // Update previous counts for next comparison - previousItemCounts = currentCounts; - } - - /** - * Creates a condition with randomized target between min and max - */ - public static GatheredResourceCondition createRandomized(String itemName, int minCount, int maxCount, - boolean includeNoted) { - return GatheredResourceCondition.builder() - .itemName(itemName) - .targetCountMin(minCount) - .targetCountMax(maxCount) - .includeNoted(includeNoted) - .build(); - } - - /** - * Creates an AND logical condition requiring multiple gathered items with individual targets - */ - public static LogicalCondition createAndCondition(List itemNames, - List targetCountsMins, - List targetCountsMaxs, - boolean includeNoted) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Handle missing target counts - int minSize = Math.min(itemNames.size(), - Math.min(targetCountsMins != null ? targetCountsMins.size() : 0, - targetCountsMaxs != null ? targetCountsMaxs.size() : 0)); - - if (targetCountsMins == null || targetCountsMins.isEmpty()) { - targetCountsMins = new ArrayList<>(itemNames.size()); - for (int i = 0; i < itemNames.size(); i++) { - targetCountsMins.add(1); - } - } - - if (targetCountsMaxs == null || targetCountsMaxs.isEmpty()) { - targetCountsMaxs = new ArrayList<>(targetCountsMins); - } - - // Create AND condition - AndCondition andCondition = new AndCondition(); - - // Add condition for each item - for (int i = 0; i < minSize; i++) { - GatheredResourceCondition itemCondition = GatheredResourceCondition.builder() - .itemName(itemNames.get(i)) - .targetCountMin(targetCountsMins.get(i)) - .targetCountMax(targetCountsMaxs.get(i)) - .includeNoted(includeNoted) - .build(); - - andCondition.addCondition(itemCondition); - } - - return andCondition; - } - - /** - * Creates an OR logical condition requiring any of multiple gathered items with individual targets - */ - public static LogicalCondition createOrCondition(List itemNames, - List targetCountsMins, - List targetCountsMaxs, - boolean includeNoted) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Handle missing target counts - int minSize = Math.min(itemNames.size(), - Math.min(targetCountsMins != null ? targetCountsMins.size() : 0, - targetCountsMaxs != null ? targetCountsMaxs.size() : 0)); - - if (targetCountsMins == null || targetCountsMins.isEmpty()) { - targetCountsMins = new ArrayList<>(itemNames.size()); - for (int i = 0; i < itemNames.size(); i++) { - targetCountsMins.add(1); - } - } - - if (targetCountsMaxs == null || targetCountsMaxs.isEmpty()) { - targetCountsMaxs = new ArrayList<>(targetCountsMins); - } - - // Create OR condition - OrCondition orCondition = new OrCondition(); - - // Add condition for each item - for (int i = 0; i < minSize; i++) { - GatheredResourceCondition itemCondition = GatheredResourceCondition.builder() - .itemName(itemNames.get(i)) - .targetCountMin(targetCountsMins.get(i)) - .targetCountMax(targetCountsMaxs.get(i)) - .includeNoted(includeNoted) - .build(); - - orCondition.addCondition(itemCondition); - } - - return orCondition; - } - - @Override - public void pause() { - if (!isPaused) { - // Call parent class pause method - super.pause(); - - // Capture current inventory state at pause time - pausedItemCounts.clear(); - - // Get current inventory counts for items matching our pattern - List items = new ArrayList<>(); - items.addAll(getUnNotedItems()); - if (includeNoted) { - items.addAll(getNotedItems()); - } - - // Count matching items at pause time - for (Rs2ItemModel item : items) { - if (item != null && itemPattern.matcher(item.getName()).matches()) { - String name = item.getName(); - pausedItemCounts.put(name, pausedItemCounts.getOrDefault(name, 0) + item.getQuantity()); - } - } - - // Save current gathered count - pausedGatheredCount = currentGatheredCount; - - log.debug("GatheredResourceCondition paused. Captured pause state with {} gathered", - pausedGatheredCount); - } - } - - @Override - public void resume() { - if (isPaused) { - // Calculate items gained during pause - Map currentCounts = new HashMap<>(); - - // Get current inventory counts - List currentItems = new ArrayList<>(); - currentItems.addAll(getUnNotedItems()); - if (includeNoted) { - currentItems.addAll(getNotedItems()); - } - - // Count matching items now - for (Rs2ItemModel item : currentItems) { - if (item != null && itemPattern.matcher(item.getName()).matches()) { - String name = item.getName(); - currentCounts.put(name, currentCounts.getOrDefault(name, 0) + item.getQuantity()); - } - } - - // Calculate items gained during pause and adjust gathered counts - int itemsGainedDuringPause = 0; - for (Map.Entry entry : currentCounts.entrySet()) { - String itemName = entry.getKey(); - int currentCount = entry.getValue(); - int pausedCount = pausedItemCounts.getOrDefault(itemName, 0); - - if (currentCount > pausedCount) { - int gainedDuringPause = currentCount - pausedCount; - itemsGainedDuringPause += gainedDuringPause; - - // Remove the pause-period gains from our gathered counts - int existingGathered = gatheredItemCounts.getOrDefault(itemName, 0); - gatheredItemCounts.put(itemName, Math.max(0, existingGathered - gainedDuringPause)); - } - } - - // Adjust total gathered count to exclude pause-period gains - currentGatheredCount = Math.max(0, currentGatheredCount - itemsGainedDuringPause); - - // Call parent class resume method - super.resume(); - - // Update baseline inventory counts for future comparisons - updatePreviousItemCounts(); - - log.debug("GatheredResourceCondition resumed. Adjusted gathered count by {} items gained during pause. " + - "New gathered count: {}", itemsGainedDuringPause, currentGatheredCount); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/InventoryItemCountCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/InventoryItemCountCondition.java deleted file mode 100644 index 42123df2d86..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/InventoryItemCountCondition.java +++ /dev/null @@ -1,383 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource; - -import java.util.ArrayList; -import java.util.List; - - -import lombok.Builder; -import lombok.Getter; -import net.runelite.api.InventoryID; -import net.runelite.api.events.GameTick; -import net.runelite.api.events.ItemContainerChanged; -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.OrCondition; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; - -/** - * Condition that tracks the total number of items in inventory. - * Can be set to track noted items as well. TODO Rename into GatheredItemCountCondition - */ -@Getter -public class InventoryItemCountCondition extends ResourceCondition { - - public static String getVersion() { - return "0.0.1"; - } - private final boolean includeNoted; - private final int targetCountMin; - private final int targetCountMax; - - private transient int currentTargetCount; - private transient int currentItemCount; - private transient volatile boolean satisfied = false; - private transient boolean initialInventoryLoaded = false; - public InventoryItemCountCondition(String itemName, int targetCount, boolean includeNoted) { - super(itemName); - this.includeNoted = includeNoted; - - this.targetCountMin = targetCount; - this.targetCountMax = targetCount; - this.currentTargetCount = targetCount; - updateCurrentCount(); - } - - @Builder - public InventoryItemCountCondition(String itemName,int targetCountMin, int targetCountMax, boolean includeNoted) { - super(itemName); - this.includeNoted = includeNoted; - this.targetCountMin = Math.max(0, targetCountMin); - - // If not tracking noted items, limit max count to inventory size (28) - this.targetCountMax = includeNoted ? - Math.min(Integer.MAX_VALUE, targetCountMax) : - Math.min(28, targetCountMax); - - this.currentTargetCount = Rs2Random.between(this.targetCountMin, this.targetCountMax); - updateCurrentCount(); - } - - /** - * Creates a condition with randomized target between min and max - */ - public static InventoryItemCountCondition createRandomized(int minCount, int maxCount, boolean includeNoted) { - return InventoryItemCountCondition.builder() - .targetCountMin(minCount) - .targetCountMax(maxCount) - .includeNoted(includeNoted) - .build(); - } - - /** - * Creates an AND logical condition requiring multiple items with individual targets - * All conditions must be satisfied (must have the required number of each item) - */ - public static LogicalCondition createAndCondition(List itemNames, - List targetCountsMins, List targetCountsMaxs, boolean includeNoted) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Determine the smallest list size for safe iteration - int minSize = Math.min(itemNames.size(), - Math.min(targetCountsMins != null ? targetCountsMins.size() : 0, - targetCountsMaxs != null ? targetCountsMaxs.size() : 0)); - - // If target counts not provided or empty, default to single item each - if (targetCountsMins == null || targetCountsMins.isEmpty()) { - targetCountsMins = new ArrayList<>(itemNames.size()); - for (int i = 0; i < itemNames.size(); i++) { - targetCountsMins.add(1); - } - } - - if (targetCountsMaxs == null || targetCountsMaxs.isEmpty()) { - targetCountsMaxs = new ArrayList<>(targetCountsMins); - } - - // Create the logical condition - AndCondition andCondition = new AndCondition(); - - // Add a condition for each item - for (int i = 0; i < minSize; i++) { - InventoryItemCountCondition itemCondition = InventoryItemCountCondition.builder() - .itemName(itemNames.get(i)) - .targetCountMin(targetCountsMins.get(i)) - .targetCountMax(targetCountsMaxs.get(i)) - .includeNoted(includeNoted) - .build(); - - andCondition.addCondition(itemCondition); - } - - return andCondition; - } - - /** - * Creates an OR logical condition requiring multiple items with individual targets - * Any condition can be satisfied (must have the required number of any one item) - */ - public static LogicalCondition createOrCondition(List itemNames, List targetCountsMins, List targetCountsMaxs, boolean includeNoted) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Determine the smallest list size for safe iteration - int minSize = Math.min(itemNames.size(), - Math.min(targetCountsMins != null ? targetCountsMins.size() : 0, - targetCountsMaxs != null ? targetCountsMaxs.size() : 0)); - - // If target counts not provided or empty, default to single item each - if (targetCountsMins == null || targetCountsMins.isEmpty()) { - targetCountsMins = new ArrayList<>(itemNames.size()); - for (int i = 0; i < itemNames.size(); i++) { - targetCountsMins.add(1); - } - } - - if (targetCountsMaxs == null || targetCountsMaxs.isEmpty()) { - targetCountsMaxs = new ArrayList<>(targetCountsMins); - } - - // Create the logical condition - OrCondition orCondition = new OrCondition(); - - // Add a condition for each item - for (int i = 0; i < minSize; i++) { - InventoryItemCountCondition itemCondition = InventoryItemCountCondition.builder() - .itemName(itemNames.get(i)) - .targetCountMin(targetCountsMins.get(i)) - .targetCountMax(targetCountsMaxs.get(i)) - .includeNoted(includeNoted) - .build(); - - orCondition.addCondition(itemCondition); - } - - return orCondition; - } - - /** - * Creates an AND logical condition requiring multiple items with the same target for all - * All conditions must be satisfied (must have the required number of each item) - */ - public static LogicalCondition createAndCondition(List itemNames, int targetCountMin, int targetCountMax, boolean includeNoted) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Create the logical condition - AndCondition andCondition = new AndCondition(); - - // Add a condition for each item with the same targets - for (String itemName : itemNames) { - InventoryItemCountCondition itemCondition = InventoryItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .includeNoted(includeNoted) - .build(); - - andCondition.addCondition(itemCondition); - } - - return andCondition; - } - - /** - * Creates an OR logical condition requiring multiple items with the same target for all - * Any condition can be satisfied (must have the required number of any one item) - */ - public static LogicalCondition createOrCondition(List itemNames, int targetCountMin, int targetCountMax, boolean includeNoted) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Create the logical condition - OrCondition orCondition = new OrCondition(); - - // Add a condition for each item with the same targets - for (String itemName : itemNames) { - InventoryItemCountCondition itemCondition = InventoryItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .includeNoted(includeNoted) - .build(); - - orCondition.addCondition(itemCondition); - } - - return orCondition; - } - - @Override - public boolean isSatisfied() { - // A condition cannot be satisfied while paused - if (isPaused) { - return false; - } - - // Once satisfied, stay satisfied until reset - if (satisfied) { - return true; - } - - // Check if current count meets or exceeds target - if (currentItemCount >= currentTargetCount) { - satisfied = true; - return true; - } - - return false; - } - - @Override - public String getDescription() { - String itemTypeDesc = includeNoted ? " (including noted)" : ""; - String randomRangeInfo = ""; - - if (targetCountMin != targetCountMax) { - randomRangeInfo = String.format(" (randomized from %d-%d)", targetCountMin, targetCountMax); - } - - if (getItemName() == null || getItemName().isEmpty()) { - return String.format("Have %d total items in inventory%s%s (%d/%d, %.1f%%)", - currentTargetCount, - itemTypeDesc, - randomRangeInfo, - currentItemCount, - currentTargetCount, - getProgressPercentage()); - } else { - return String.format("Have %d %s in inventory%s%s (%d/%d, %.1f%%)", - currentTargetCount, - getItemName(), - itemTypeDesc, - randomRangeInfo, - currentItemCount, - currentTargetCount, - getProgressPercentage()); - } - } - public String getDetailedDescription() { - return String.format("Inventory Item Count Condition: %s\n" + - "Target Count: %d (current: %d)\n" + - "Include Noted: %s\n" + - "Progress: %.1f%%", - getItemName(), - currentTargetCount, - currentItemCount, - includeNoted ? "Yes" : "No", - getProgressPercentage()); - } - @Override - public void reset() { - reset(false); - } - - @Override - public void reset(boolean randomize) { - if (randomize && targetCountMin != targetCountMax) { - currentTargetCount = Rs2Random.between(targetCountMin, targetCountMax); - } - satisfied = false; - initialInventoryLoaded = false; - updateCurrentCount(); - } - - @Override - public double getProgressPercentage() { - if (satisfied) { - return 100.0; - } - - if (currentTargetCount <= 0) { - return 100.0; - } - - return Math.min(100.0, (currentItemCount * 100.0) / currentTargetCount); - } - - @Override - public void onItemContainerChanged(ItemContainerChanged event) { - // Skip processing if paused - if (isPaused) { - return; - } - - if (event.getContainerId() == InventoryID.INVENTORY.getId()) { - if (Rs2Bank.isOpen()) { - return; - } - updateCurrentCount(); - } - } - - @Override - public void onGameTick(GameTick event) { - // Skip processing if paused - if (isPaused) { - return; - } - - // Load initial inventory if not yet loaded - if (!initialInventoryLoaded) { - updateCurrentCount(); - initialInventoryLoaded = true; - } - } - - private void updateCurrentCount() { - - // Count specific items by name (using existing pattern matching) - - int currentItemCountNoted = getNotedItems().stream().filter(item -> { - if (item == null) { - return false; - } ; - return itemPattern.matcher(item.getName()).matches(); - }).mapToInt(item -> item.getQuantity()).sum(); - int currentItemCountUnNoted = getUnNotedItems().stream().filter(item -> { - if (item == null) { - return false; - } - return itemPattern.matcher(item.getName()).matches(); - }).mapToInt(item -> item.getQuantity()).sum(); - if (includeNoted) { - currentItemCount = currentItemCountNoted + currentItemCountUnNoted; - } else { - currentItemCount = currentItemCountUnNoted; - } - - } - - @Override - public int getTotalConditionCount() { - return 1; - } - - @Override - public int getMetConditionCount() { - return isSatisfied() ? 1 : 0; - } - - @Override - public void pause() { - // Call parent class pause method - super.pause(); - } - - @Override - public void resume() { - if (isPaused) { - // Call parent class resume method - super.resume(); - - // For snapshot-type conditions, refresh current state on resume - updateCurrentCount(); - initialInventoryLoaded = true; // Mark as loaded after resume - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/LootItemCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/LootItemCondition.java deleted file mode 100644 index 156d5968231..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/LootItemCondition.java +++ /dev/null @@ -1,884 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource; - -import lombok.Builder; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Client; -import net.runelite.api.GameState; -import net.runelite.api.InventoryID; -import net.runelite.api.TileItem; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.ItemContainerChanged; -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.ItemDespawned; -import net.runelite.api.events.ItemSpawned; - -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.OrCondition; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItem; -import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; -import net.runelite.client.plugins.microbot.util.models.RS2Item; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -import static net.runelite.api.TileItem.OWNERSHIP_SELF; - -import static net.runelite.api.TileItem.OWNERSHIP_GROUP; -import static net.runelite.api.TileItem.OWNERSHIP_OTHER; -import static net.runelite.api.TileItem.OWNERSHIP_NONE; - -/** - * Condition that tracks a specific looted item quantity. - * Focuses on tracking a single item for clarity and precision. - */ -@Getter -@Slf4j -public class LootItemCondition extends ResourceCondition { - - public static String getVersion() { - return "0.0.1"; - } - private final int targetAmountMin; - private final int targetAmountMax; - private final boolean includeNoted; - private final boolean includeNoneOwner; - private final boolean ignorePlayerDropped; - - private transient int currentTargetAmount; - private transient int currentTrackedCount; - private transient int lastInventoryCount; - - // Pause/resume state for cumulative tracking - private transient int pausedInventoryCount; - private transient int pausedTrackedCount; - - - private final Map trackedItemQuantities = new HashMap<>(); - - // Keep track of recently looted items to avoid double counting - private final Map recentlyLootedItems = new HashMap<>(); - - // Key is a composite key of location and item ID to track multiple items at the same spot - private static class TrackedItem { - public final WorldPoint location; - public final int itemId; - public final int quantity; - public final long timestamp; - public final String itemName; - - public TrackedItem(WorldPoint location, int itemId, int quantity, String itemName) { - this.location = location; - this.itemId = itemId; - this.quantity = quantity; - this.timestamp = System.currentTimeMillis(); - this.itemName = itemName; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TrackedItem that = (TrackedItem) o; - return itemId == that.itemId && - location.equals(that.location); - } - - @Override - public int hashCode() { - return Objects.hash(location, itemId); - } - } - - // Replace the existing tracking collections with new ones - private final Set trackedItems = new HashSet<>(); - private final Map> itemsByLocation = new HashMap<>(); - - public LootItemCondition(String itemName, int targetAmount, boolean includeNoted) { - super(itemName); - - this.targetAmountMin = targetAmount; - this.targetAmountMax = targetAmount; - this.includeNoted = includeNoted; - this.currentTargetAmount = Rs2Random.between(targetAmountMin, targetAmountMax); - this.currentTrackedCount = 0; - this.lastInventoryCount = 0; - this.includeNoneOwner = false; - this.isIncludeNoneOwner(); - this.ignorePlayerDropped = false; - } - - @Builder - public LootItemCondition(String itemName, int targetAmountMin, - int targetAmountMax, - boolean includeNoted, - boolean includeNoneOwner, - boolean ignorePlayerDropped) { - super(itemName); - this.targetAmountMin = targetAmountMin; - this.targetAmountMax = targetAmountMax; - this.currentTargetAmount = Rs2Random.between(targetAmountMin, targetAmountMax); - this.currentTrackedCount = 0; - this.lastInventoryCount = 0; - this.includeNoted = includeNoted; - this.includeNoneOwner = includeNoneOwner; - this.ignorePlayerDropped = ignorePlayerDropped; - // Initialize with existing items on the ground - scanForExistingItems(); - } - - /** - * Creates a condition with randomized target between min and max - */ - public static LootItemCondition createRandomized(String itemName, int minAmount, int maxAmount, boolean includeNoted, boolean includeNoneOwner) { - return LootItemCondition.builder() - .itemName(itemName) - .targetAmountMin(minAmount) - .targetAmountMax(maxAmount) - .includeNoted(includeNoted) - .includeNoneOwner(includeNoneOwner) - .build(); - } - - - /** - * Creates an AND logical condition requiring multiple looted items with individual targets - */ - public static LogicalCondition createAndCondition(List itemNames, List targetAmountsMins, List targetAmountsMaxs,boolean includeNoted, boolean includeNoneOwner) { - // Validate input - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Determine the smallest list size for safe iteration - int minSize = Math.min(itemNames.size(), - Math.min(targetAmountsMins != null ? targetAmountsMins.size() : 0, - targetAmountsMaxs != null ? targetAmountsMaxs.size() : 0)); - - // If target amounts not provided or empty, default to single item each - if (targetAmountsMins == null || targetAmountsMins.isEmpty()) { - targetAmountsMins = new ArrayList<>(itemNames.size()); - for (int i = 0; i < itemNames.size(); i++) { - targetAmountsMins.add(1); - } - } - - if (targetAmountsMaxs == null || targetAmountsMaxs.isEmpty()) { - targetAmountsMaxs = new ArrayList<>(targetAmountsMins); - } - - // Create the logical condition - AndCondition andCondition = new AndCondition(); - - // Add a condition for each item - for (int i = 0; i < minSize; i++) { - LootItemCondition itemCondition = LootItemCondition.builder() - .itemName(itemNames.get(i)) - .targetAmountMin(targetAmountsMins.get(i)) - .targetAmountMax(targetAmountsMaxs.get(i)) - .includeNoted(includeNoted) - .includeNoneOwner(includeNoneOwner) - .build(); - - andCondition.addCondition(itemCondition); - } - - return andCondition; - } - - /** - * Creates an OR logical condition requiring multiple looted items with individual targets - */ - public static LogicalCondition createOrCondition(List itemNames, - List targetAmountsMins, List targetAmountsMaxs, - boolean includeNoted, boolean includeNoneOwner) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Determine the smallest list size for safe iteration - int minSize = Math.min(itemNames.size(), - Math.min(targetAmountsMins != null ? targetAmountsMins.size() : 0, - targetAmountsMaxs != null ? targetAmountsMaxs.size() : 0)); - - // If target amounts not provided or empty, default to single item each - if (targetAmountsMins == null || targetAmountsMins.isEmpty()) { - targetAmountsMins = new ArrayList<>(itemNames.size()); - for (int i = 0; i < itemNames.size(); i++) { - targetAmountsMins.add(1); - } - } - - if (targetAmountsMaxs == null || targetAmountsMaxs.isEmpty()) { - targetAmountsMaxs = new ArrayList<>(targetAmountsMins); - } - - // Create the logical condition - OrCondition orCondition = new OrCondition(); - - // Add a condition for each item - for (int i = 0; i < minSize; i++) { - LootItemCondition itemCondition = LootItemCondition.builder() - .itemName(itemNames.get(i)) - .targetAmountMin(targetAmountsMins.get(i)) - .targetAmountMax(targetAmountsMaxs.get(i)) - .includeNoted(includeNoted) - .includeNoneOwner(includeNoneOwner) - .build(); - - orCondition.addCondition(itemCondition); - } - - return orCondition; - } - - /** - * Creates an AND logical condition requiring multiple looted items with the same target for all - */ - public static LogicalCondition createAndCondition(List itemNames, int targetAmountMin, int targetAmountMax,boolean includeNoted, boolean includeNoneOwner) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Create the logical condition - AndCondition andCondition = new AndCondition(); - - // Add a condition for each item with the same targets - for (String itemName : itemNames) { - LootItemCondition itemCondition = LootItemCondition.builder() - .itemName(itemName) - .targetAmountMin(targetAmountMin) - .targetAmountMax(targetAmountMax) - .includeNoted(includeNoted) - .includeNoneOwner(includeNoneOwner) - .build(); - - andCondition.addCondition(itemCondition); - } - - return andCondition; - } - - /** - * Creates an OR logical condition requiring multiple looted items with the same target for all - */ - public static LogicalCondition createOrCondition(List itemNames, int targetAmountMin, int targetAmountMax,boolean includeNoted, boolean includeNoneOwner) { - if (itemNames == null || itemNames.isEmpty()) { - throw new IllegalArgumentException("Item name list cannot be null or empty"); - } - - // Create the logical condition - OrCondition orCondition = new OrCondition(); - - // Add a condition for each item with the same targets - for (String itemName : itemNames) { - LootItemCondition itemCondition = LootItemCondition.builder() - .itemName(itemName) - .targetAmountMin(targetAmountMin) - .targetAmountMax(targetAmountMax) - .includeNoted(includeNoted) - .includeNoneOwner(includeNoneOwner) - .build(); - - orCondition.addCondition(itemCondition); - } - - return orCondition; - } - - @Override - public boolean isSatisfied() { - // Return false if paused to prevent condition from being satisfied during pause - if (isPaused()) { - return false; - } - return currentTrackedCount >= currentTargetAmount; - } - - @Override - public String getDescription() { - StringBuilder sb = new StringBuilder(); - String noteState = includeNoted ? " (including noted)" : ""; - String ownerState = includeNoneOwner ? " (any owner)" : " (player owned)"; - String randomRangeInfo = ""; - - if (targetAmountMin != targetAmountMax) { - randomRangeInfo = String.format(" (randomized from %d-%d)", targetAmountMin, targetAmountMax); - } - - sb.append(String.format("Loot %d %s%s%s", - currentTargetAmount, - noteState, - ownerState, - randomRangeInfo)); - - // Add progress tracking - sb.append(String.format(" (%d/%d, %.1f%%)", - currentTrackedCount, - currentTargetAmount, - getProgressPercentage())); - - return sb.toString(); - } - - /** - * Returns a detailed description of the loot item condition with additional status information - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - // Basic description - sb.append(String.format("Loot %d %s", currentTargetAmount, getItemPattern().pattern())); - - // Add randomization info if applicable - if (targetAmountMin != targetAmountMax) { - sb.append(String.format(" (randomized from %d-%d)", targetAmountMin, targetAmountMax)); - } - - sb.append("\n"); - - // Status information - sb.append("Status: ").append(isSatisfied() ? "Satisfied" : "Not satisfied").append("\n"); - sb.append("Progress: ").append(String.format("%d/%d (%.1f%%)", - currentTrackedCount, - currentTargetAmount, - getProgressPercentage())).append("\n"); - - // Configuration information - sb.append("Item Pattern: ").append(itemPattern.pattern()).append("\n"); - sb.append("Include Noted Items: ").append(includeNoted ? "Yes" : "No").append("\n"); - sb.append("Include Items from Other Players: ").append(includeNoneOwner ? "Yes" : "No").append("\n"); - - // Tracking information - sb.append("Currently Tracking: ").append(itemsByLocation.size()).append(" ground locations\n"); - sb.append("Current Inventory Count: ").append(lastInventoryCount); - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - String itemName = getItemName(); - // Basic information - sb.append("LootItemCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Item: ").append(itemName != null && !itemName.isEmpty() ? itemName : "Any").append("\n"); - - if (itemPattern != null && !itemPattern.pattern().equals(".*")) { - sb.append(" │ Pattern: ").append(itemPattern.pattern()).append("\n"); - } - - sb.append(" │ Target Amount: ").append(currentTargetAmount).append("\n"); - sb.append(" │ Include Noted: ").append(includeNoted ? "Yes" : "No").append("\n"); - sb.append(" │ Track Non-Owned: ").append(includeNoneOwner ? "Yes" : "No").append("\n"); - - // Randomization - sb.append(" ├─ Randomization ────────────────────────────\n"); - boolean hasRandomization = targetAmountMin != targetAmountMax; - sb.append(" │ Randomization: ").append(hasRandomization ? "Enabled" : "Disabled").append("\n"); - if (hasRandomization) { - sb.append(" │ Target Range: ").append(targetAmountMin).append("-").append(targetAmountMax).append("\n"); - } - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - sb.append(" │ Satisfied: ").append(isSatisfied()).append("\n"); - sb.append(" │ Current Count: ").append(currentTrackedCount).append("\n"); - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Tracking info - sb.append(" └─ Tracking ────────────────────────────────\n"); - sb.append(" Ground Locations: ").append(itemsByLocation.size()).append("\n"); - sb.append(" Inventory Count: ").append(lastInventoryCount).append("\n"); - sb.append(" Recent Loots: ").append(recentlyLootedItems.size()); - - return sb.toString(); - } - - @Override - public void reset() { - reset(false); - } - - @Override - public void reset(boolean randomize) { - if (randomize) { - this.currentTargetAmount = Rs2Random.between(targetAmountMin, targetAmountMax); - } - this.currentTrackedCount = 0; - this.lastInventoryCount = getCurrentInventoryCount(); - this.trackedItems.clear(); - this.itemsByLocation.clear(); - this.recentlyLootedItems.clear(); - // Re-scan for items after reset - scanForExistingItems(); - } - - @Override - public double getProgressPercentage() { - if (currentTargetAmount <= 0) { - return 100.0; - } - - double ratio = (double) currentTrackedCount / currentTargetAmount; - return Math.min(100.0, ratio * 100.0); - } - - /** - * Called when an item spawns on the ground - we check if it matches our target item - */ - @Override - public void onItemSpawned(ItemSpawned event) { - // Skip updates if paused - if (isPaused()) { - return; - } - - TileItem tileItem = event.getItem(); - WorldPoint location = event.getTile().getWorldLocation(); - - Client client = Microbot.getClient(); - if (client == null) { - return; - } - - boolean isPlayerOwned = tileItem.getOwnership() == OWNERSHIP_SELF || - tileItem.getOwnership() == OWNERSHIP_GROUP || - (includeNoneOwner && - (tileItem.getOwnership() == OWNERSHIP_NONE || - tileItem.getOwnership() == OWNERSHIP_OTHER)); - - if (isPlayerOwned) { - // Get the item name - String spawnedItemName = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemComposition(tileItem.getId()).getName() - ).orElse(""); - - // Check if this item was likely dropped by the player - boolean playerDropped = isLikelyPlayerDroppedItem(location, System.currentTimeMillis()); - - // Add to pending events - if (!playerDropped || !ignorePlayerDropped) { - if (itemPattern.matcher(spawnedItemName).matches()) { - pendingEvents.add(new ItemTrackingEvent( - System.currentTimeMillis(), - location, - spawnedItemName, - tileItem.getId(), - tileItem.getQuantity(), - isPlayerOwned, - ItemTrackingEvent.EventType.ITEM_SPAWNED - )); - } - } - } - } - - /** - * Called when an item despawns from the ground - check if it was one of our tracked items - */ - @Override - public void onItemDespawned(ItemDespawned event) { - // Skip updates if paused - if (isPaused()) { - return; - } - - WorldPoint location = event.getTile().getWorldLocation(); - TileItem tileItem = event.getItem(); - - // Get the item name - String despawnedItemName = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemComposition(tileItem.getId()).getName() - ).orElse(""); - boolean isPlayerOwned = tileItem.getOwnership() == OWNERSHIP_SELF || - tileItem.getOwnership() == OWNERSHIP_GROUP || - (includeNoneOwner && - (tileItem.getOwnership() == OWNERSHIP_NONE || - tileItem.getOwnership() == OWNERSHIP_OTHER)); - - // Check if we have any tracked items at this location - if (itemsByLocation.containsKey(location)) { - // Find the specific item that despawned - List itemsAtLocation = itemsByLocation.get(location); - - // Look for a matching item at this location - for (TrackedItem trackedItem : itemsAtLocation) { - if (trackedItem.itemId == tileItem.getId() && - itemPattern.matcher(despawnedItemName).matches()) { - - if(Microbot.isDebug()) { - log.info("Item despawned that we were tracking: {} x{} at {}", - despawnedItemName, trackedItem.quantity, location); - } - - pendingEvents.add(new ItemTrackingEvent( - System.currentTimeMillis(), - location, - despawnedItemName, - tileItem.getId(), - trackedItem.quantity, - isPlayerOwned, // We know it's tracked, so it must be player-owned-> it should be playerowned - ItemTrackingEvent.EventType.ITEM_DESPAWNED - )); - - break; // Found the specific item - } - } - } - } - @Override - public void onGroundObjectSpawned(GroundObjectSpawned event) { - // Not used for this implementation - } - - @Override - public void onGroundObjectDespawned(GroundObjectDespawned event) { - // Not used for this implementation - } - - /** - * Called when item containers change (inventory, bank, etc.) - * We need to check if we gained any of our target items. - */ - @Override - public void onItemContainerChanged(ItemContainerChanged event) { - // Skip updates if paused - if (isPaused()) { - return; - } - - // Only interested in inventory changes - if (event.getContainerId() != InventoryID.INVENTORY.getId()) { - return; - } - log.info("Item container changed: {}", event.getContainerId()); - int currentCount = getCurrentInventoryCount(); - - // Add to pending events if inventory count changed - if (currentCount != lastInventoryCount) { - pendingEvents.add(new ItemTrackingEvent( - System.currentTimeMillis(), - null, // No specific location for inventory changes - getItemName(), - -1, // No specific ID for general inventory changes - Math.abs(currentCount - lastInventoryCount), - true, // Always player owned - ItemTrackingEvent.EventType.INVENTORY_CHANGED - )); - - // Update the last count right away to detect further changes - lastInventoryCount = currentCount; - } - } - - /** - * Process all pending tracking events at the end of a game tick - */ - @Override - protected void processPendingEvents() { - // Sort events by timestamp to process in the correct sequence - pendingEvents.sort(Comparator.comparing(e -> e.timestamp)); - - // Track inventory changes and despawned items in this tick - boolean hadInventoryIncrease = false; - int inventoryGained = 0; - int totalDespawnedQuantity = 0; - List despawnedItems = new ArrayList<>(); - - // First pass - collect information - for (ItemTrackingEvent event : pendingEvents) { - switch (event.eventType) { - case ITEM_SPAWNED: - // Track this new item - if (event.isPlayerOwned && itemPattern.matcher(event.itemName).matches()) { - // Create a new tracked item with unique instance ID (timestamp-based) - TrackedItem newItem = new TrackedItem( - event.location, - event.itemId, - event.quantity, - event.itemName - ); - - // Add to our tracked sets - trackedItems.add(newItem); - - // Add to location mapping - itemsByLocation.computeIfAbsent(event.location, k -> new ArrayList<>()) - .add(newItem); - - if (Microbot.isDebug()) { - log.info("Tracking new item: {} x{} at {} (id: {})", - event.itemName, event.quantity, event.location, newItem.timestamp); - } - } - break; - - case ITEM_DESPAWNED: - // Save despawned events for correlation - despawnedItems.add(event); - - // Find and remove the tracked item - if (itemsByLocation.containsKey(event.location)) { - List itemsAtLocation = itemsByLocation.get(event.location); - - // Find first matching item by ID for removal - ONLY REMOVE ONE INSTANCE - boolean itemRemoved = false; - TrackedItem itemToRemove = null; - - // Find the matching item to remove - for (TrackedItem item : itemsAtLocation) { - if (item.itemId == event.itemId && !itemRemoved) { - itemToRemove = item; - totalDespawnedQuantity += item.quantity; - itemRemoved = true; - break; - } - } - - // Remove the specific item - if (itemToRemove != null) { - itemsAtLocation.remove(itemToRemove); - trackedItems.remove(itemToRemove); - - if (Microbot.isDebug()) { - log.info("Item despawned: {} x{} at {} (id: {})", - event.itemName, itemToRemove.quantity, event.location, itemToRemove.timestamp); - } - } - - // Remove the location entry if no more items there - if (itemsAtLocation.isEmpty()) { - itemsByLocation.remove(event.location); - } - - // Record that we just looted from this location - recentlyLootedItems.put(event.location, System.currentTimeMillis()); - } - break; - - case INVENTORY_CHANGED: - // Only consider increases for looting - if (event.quantity > 0) { - hadInventoryIncrease = true; - inventoryGained += event.quantity; - } - break; - } - } - - // Second pass - correlate despawns with inventory increases - if (hadInventoryIncrease && !despawnedItems.isEmpty()) { - // We had both despawns and inventory increases, likely a loot event - if (Microbot.isDebug()) { - log.info("Correlated loot event: gained {} items after {} despawns totaling {} quantity", - inventoryGained, despawnedItems.size(), totalDespawnedQuantity); - } - - // If we have enough evidence that items were looted - if (totalDespawnedQuantity > 0) { - // If inventory gained matches exactly what was despawned, use that number - // Otherwise, use the despawn quantity as it might be more accurate for stacked items - int countToAdd = (inventoryGained == totalDespawnedQuantity) ? - inventoryGained : totalDespawnedQuantity; - - // Count this as looted items - currentTrackedCount += countToAdd; - - if (Microbot.isDebug()) { - log.info("Added {} to tracking count (now {})", countToAdd, currentTrackedCount); - } - } else { - // Fallback to inventory changes if we can't correlate with despawns - currentTrackedCount += inventoryGained; - } - } - - // Clean up old entries from recently looted map (older than 5 seconds) - long now = System.currentTimeMillis(); - recentlyLootedItems.entrySet().removeIf(entry -> now - entry.getValue() > 5000); - - // Clear processed events - pendingEvents.clear(); - } - - /** - * Check inventory every game tick to catch changes that might not trigger ItemContainerChanged - */ - @Override - public void onGameTick(GameTick gameTick) { - // Skip updates if paused - if (isPaused()) { - return; - } - - // Update player position for dropped item tracking - updatePlayerPosition(); - - // Process any pending events - processPendingEvents(); - - // Also do a final inventory check - int currentCount = getCurrentInventoryCount(); - if (currentCount != lastInventoryCount) { - if (currentCount > lastInventoryCount) { - int gained = currentCount - lastInventoryCount; - if (Microbot.isDebug()) { - log.info("Game tick detected uncaught inventory increase of {} {}", gained, getItemName()); - } - - // Count as looted if we're tracking items on the ground - currentTrackedCount += gained; - } - - lastInventoryCount = currentCount; - } - } - - @Override - public void pause() { - super.pause(); - - // Snapshot current state for adjustment on resume - pausedInventoryCount = getCurrentInventoryCount(); - pausedTrackedCount = currentTrackedCount; - - if (Microbot.isDebug()) { - log.info("LootItemCondition paused: inventory={}, tracked={}", - pausedInventoryCount, pausedTrackedCount); - } - } - - @Override - public void resume() { - super.resume(); - - // Adjust tracked count to exclude items gained during pause - int currentInventoryCount = getCurrentInventoryCount(); - int inventoryGainedDuringPause = Math.max(0, currentInventoryCount - pausedInventoryCount); - - // Subtract the gain from our tracked count to exclude pause progress - currentTrackedCount = Math.max(0, pausedTrackedCount - inventoryGainedDuringPause); - - // Update last inventory count to current state - lastInventoryCount = currentInventoryCount; - - if (Microbot.isDebug()) { - log.info("LootItemCondition resumed: adjusted tracked count from {} to {} (excluded {} items gained during pause)", - pausedTrackedCount, currentTrackedCount, inventoryGainedDuringPause); - } - } - - /** - * Gets the current count of this item in the inventory - */ - private int getCurrentInventoryCount() { - - int currentItemCountNoted = getNotedItems().stream().filter(item -> { - if (item == null) { - return false; - } - - return itemPattern.matcher(item.getName()).matches(); - }).mapToInt(Rs2ItemModel::getQuantity).sum(); - - int currentItemCountUnNoted = getUnNotedItems().stream().filter(item -> { - if (item == null) { - return false; - } - return itemPattern.matcher(item.getName()).matches(); - }).mapToInt(Rs2ItemModel::getQuantity).sum(); - - - if (includeNoted) { - return currentItemCountNoted + currentItemCountUnNoted; - } - return currentItemCountUnNoted; - } - @Override - public void onGameStateChanged(GameStateChanged gameStateChanged) { - // Reset the condition if we log out or change worlds - if (gameStateChanged.getGameState() == GameState.LOGGED_IN ){ - scanForExistingItems(); - } - } - /** - * Scans for existing ground items that match our criteria and adds them to tracking - */ - private void scanForExistingItems() { - // Scan a generous range (maximum view distance) - int scanRange = 32; - if (Microbot.getClient() == null || !Microbot.isLoggedIn()) { - return; - } - // Get all items on the ground - RS2Item[] groundItems = Rs2GroundItem.getAll(scanRange); - - if (Microbot.isDebug()) { - log.info("Scanning for existing ground items - found {} total items", groundItems.length); - } - - // Filter and track matching items - for (RS2Item rs2Item : groundItems) { - if (rs2Item == null) continue; - - // Check if this item matches our criteria - boolean isPlayerOwned = rs2Item.getTileItem().getOwnership() == OWNERSHIP_SELF || - rs2Item.getTileItem().getOwnership() == OWNERSHIP_GROUP || - (includeNoneOwner && - (rs2Item.getTileItem().getOwnership() == OWNERSHIP_NONE || - rs2Item.getTileItem().getOwnership() == OWNERSHIP_OTHER)); - - if (isPlayerOwned && itemPattern.matcher(rs2Item.getItem().getName()).matches()) { - WorldPoint location = rs2Item.getTile().getWorldLocation(); - - // Check if this item was likely dropped by the player - boolean playerDropped = isLikelyPlayerDroppedItem(location, System.currentTimeMillis()); - - // Only track if not player-dropped or we're including player-dropped items - if (!playerDropped || !ignorePlayerDropped) { - // Create a tracked item - TrackedItem newItem = new TrackedItem( - location, - rs2Item.getItem().getId(), - rs2Item.getTileItem().getQuantity(), - rs2Item.getItem().getName() - ); - - // Add to our tracking collections - trackedItems.add(newItem); - - // Add to location mapping - itemsByLocation.computeIfAbsent(location, k -> new ArrayList<>()) - .add(newItem); - - if (Microbot.isDebug()) { - log.info("Found existing item to track: {} x{} at {}", - rs2Item.getItem().getName(), - rs2Item.getTileItem().getQuantity(), - location); - } - } - } - } - - if (Microbot.isDebug()) { - log.info("Now tracking {} items at {} locations after initial scan", - trackedItems.size(), itemsByLocation.size()); - } - } - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ProcessItemCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ProcessItemCondition.java deleted file mode 100644 index 45fdef2c16a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ProcessItemCondition.java +++ /dev/null @@ -1,804 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource; - -import lombok.Builder; -import lombok.Data; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.InventoryID; -import net.runelite.api.events.GameTick; -import net.runelite.api.events.ItemContainerChanged; -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.OrCondition; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - - -/** - * Condition that tracks item processing activities (smithing, herblore, crafting, etc.) - * Can track both source items being consumed and target items being produced - */ -@Slf4j -@Getter -public class ProcessItemCondition extends ResourceCondition { - public static String getVersion() { - return "0.0.1"; - } - // Tracking options - private final List sourceItems; // Items being consumed - private final List targetItems; // Items being produced - private final TrackingMode trackingMode; // How we want to track progress - - // Target count configuration - private final int targetCountMin; - private final int targetCountMax; - private final boolean includeBankForPauseResume; // Whether to include bank items when detecting processes during pause/resume - private transient int currentTargetCount; - - // State tracking - private transient Map previousInventoryCounts = new HashMap<>(); - private transient int processedCount = 0; - private transient volatile boolean satisfied = false; - private transient Instant lastInventoryChange = Instant.now(); - private transient boolean isProcessingActive = false; - private transient boolean initialInventoryLoaded = false; - - // Pause/resume state for cumulative tracking - private transient Map pausedInventoryCounts = new HashMap<>(); - private transient int pausedProcessedCount; - public ListgetInputItemPatterns() { - return sourceItems.stream().map(ItemTracker::getItemPattern).collect(Collectors.toList()); - } - public List getInputItemPatternStrings() { - return sourceItems.stream().map( (trakedItem)-> trakedItem.getItemPattern().toString()).collect(Collectors.toList()); - } - public ListgetOutputItemPatterns() { - return targetItems.stream().map(ItemTracker::getItemPattern).collect(Collectors.toList()); - } - public List getOutputItemPatternStrings() { - return targetItems.stream().map( (trakedItem)-> trakedItem.getItemPattern().toString()).collect(Collectors.toList()); - } - /** - * Tracking mode determines what we're counting - */ - public enum TrackingMode { - SOURCE_CONSUMPTION, // Count when source items are consumed - TARGET_PRODUCTION, // Count when target items are produced - EITHER, // Count either source consumption or target production - BOTH // Require both source consumption and target production - } - - /** - * Inner class to track details about items being processed - */ - @Data - public static class ItemTracker { - private final Pattern itemPattern; - private final int quantityPerProcess; // How many of this item are consumed/produced per process - - public ItemTracker(String itemName, int quantityPerProcess) { - this.itemPattern = createItemPattern(itemName); - this.quantityPerProcess = quantityPerProcess; - } - - private static Pattern createItemPattern(String itemName) { - if (itemName == null || itemName.isEmpty()) { - return Pattern.compile(".*"); - } - - // Check if the name is already a regex pattern - if (itemName.startsWith("^") || itemName.endsWith("$") || - itemName.contains(".*") || itemName.contains("[") || - itemName.contains("(")) { - return Pattern.compile(itemName); - } - - // Otherwise, create a contains pattern - return Pattern.compile(".*" + Pattern.quote(itemName) + ".*", Pattern.CASE_INSENSITIVE); - } - public String getItemName() { - // Extract a clean item name from the pattern for display - String patternStr = itemPattern.pattern(); - // If it's a "contains" pattern (created with .*pattern.*) - if (patternStr.startsWith(".*") && patternStr.endsWith(".*")) { - patternStr = patternStr.substring(2, patternStr.length() - 2); - } - // Handle patterns that were created with Pattern.quote() which escapes special characters - if (patternStr.startsWith("\\Q") && patternStr.endsWith("\\E")) { - patternStr = patternStr.substring(2, patternStr.length() - 2); - } - return patternStr; - } - } - - /** - * Full constructor with Builder support - */ - @Builder - public ProcessItemCondition( - List sourceItems, - List targetItems, - TrackingMode trackingMode, - int targetCountMin, - int targetCountMax, - Boolean includeBankForPauseResume) { - super(); - this.sourceItems = sourceItems != null ? sourceItems : new ArrayList<>(); - this.targetItems = targetItems != null ? targetItems : new ArrayList<>(); - this.trackingMode = trackingMode != null ? trackingMode : TrackingMode.EITHER; - this.targetCountMin = Math.max(0, targetCountMin); - this.targetCountMax = Math.max(this.targetCountMin, targetCountMax); - this.includeBankForPauseResume = includeBankForPauseResume != null ? includeBankForPauseResume : true; // Default to true for better accuracy - this.currentTargetCount = Rs2Random.between(this.targetCountMin, this.targetCountMax); - } - - /** - * Create a condition for tracking production of a specific item - */ - public static ProcessItemCondition forProduction(String targetItemName, int count) { - return forProduction(targetItemName, 1, count); - } - - /** - * Create a condition for tracking production of a specific item with quantity per process - */ - public static ProcessItemCondition forProduction(String targetItemName, int quantityPerProcess, int count) { - List targetItems = new ArrayList<>(); - targetItems.add(new ItemTracker(targetItemName, quantityPerProcess)); - - return ProcessItemCondition.builder() - .targetItems(targetItems) - .trackingMode(TrackingMode.TARGET_PRODUCTION) - .targetCountMin(count) - .targetCountMax(count) - .build(); - } - - /** - * Create a condition for tracking consumption of a specific source item - */ - public static ProcessItemCondition forConsumption(String sourceItemName, int count) { - return forConsumption(sourceItemName, 1, count); - } - - /** - * Create a condition for tracking consumption of a specific source item with quantity per process - */ - public static ProcessItemCondition forConsumption(String sourceItemName, int quantityPerProcess, int count) { - List sourceItems = new ArrayList<>(); - sourceItems.add(new ItemTracker(sourceItemName, quantityPerProcess)); - - return ProcessItemCondition.builder() - .sourceItems(sourceItems) - .trackingMode(TrackingMode.SOURCE_CONSUMPTION) - .targetCountMin(count) - .targetCountMax(count) - .build(); - } - - /** - * Create a condition for tracking a complete recipe (source items and target items) - */ - public static ProcessItemCondition forRecipe(String sourceItemName, int sourceQuantity, - String targetItemName, int targetQuantity, int count) { - return forRecipe(sourceItemName, sourceQuantity, targetItemName, targetQuantity, count, true); - } - - /** - * Create a condition for tracking a complete recipe (source items and target items) - * @param includeBankForPauseResume whether to include bank items when detecting processes during pause/resume - */ - public static ProcessItemCondition forRecipe(String sourceItemName, int sourceQuantity, - String targetItemName, int targetQuantity, int count, boolean includeBankForPauseResume) { - List sourceItems = new ArrayList<>(); - sourceItems.add(new ItemTracker(sourceItemName, sourceQuantity)); - - List targetItems = new ArrayList<>(); - targetItems.add(new ItemTracker(targetItemName, targetQuantity)); - - return ProcessItemCondition.builder() - .sourceItems(sourceItems) - .targetItems(targetItems) - .trackingMode(TrackingMode.BOTH) - .targetCountMin(count) - .targetCountMax(count) - .includeBankForPauseResume(includeBankForPauseResume) - .build(); - } - - /** - * Create a condition for tracking multiple source items being consumed - */ - public static ProcessItemCondition forMultipleConsumption( - List sourceItemNames, - List sourceQuantities, - int count) { - List sourceItems = new ArrayList<>(); - for (int i = 0; i < sourceItemNames.size(); i++) { - int quantity = i < sourceQuantities.size() ? sourceQuantities.get(i) : 1; - sourceItems.add(new ItemTracker(sourceItemNames.get(i), quantity)); - } - - return ProcessItemCondition.builder() - .sourceItems(sourceItems) - .trackingMode(TrackingMode.SOURCE_CONSUMPTION) - .targetCountMin(count) - .targetCountMax(count) - .build(); - } - - /** - * Create a condition for tracking multiple target items being produced - */ - public static ProcessItemCondition forMultipleProduction( - List targetItemNames, - List targetQuantities, - int count) { - List targetItems = new ArrayList<>(); - for (int i = 0; i < targetItemNames.size(); i++) { - int quantity = i < targetQuantities.size() ? targetQuantities.get(i) : 1; - targetItems.add(new ItemTracker(targetItemNames.get(i), quantity)); - } - - return ProcessItemCondition.builder() - .targetItems(targetItems) - .trackingMode(TrackingMode.TARGET_PRODUCTION) - .targetCountMin(count) - .targetCountMax(count) - .build(); - } - - /** - * Create a randomized condition for production - */ - public static ProcessItemCondition createRandomizedProduction(String targetItemName, int minCount, int maxCount) { - List targetItems = new ArrayList<>(); - targetItems.add(new ItemTracker(targetItemName, 1)); - - return ProcessItemCondition.builder() - .targetItems(targetItems) - .trackingMode(TrackingMode.TARGET_PRODUCTION) - .targetCountMin(minCount) - .targetCountMax(maxCount) - .build(); - } - - /** - * Create a randomized condition for consumption - */ - public static ProcessItemCondition createRandomizedConsumption(String sourceItemName, int minCount, int maxCount) { - List sourceItems = new ArrayList<>(); - sourceItems.add(new ItemTracker(sourceItemName, 1)); - - return ProcessItemCondition.builder() - .sourceItems(sourceItems) - .trackingMode(TrackingMode.SOURCE_CONSUMPTION) - .targetCountMin(minCount) - .targetCountMax(maxCount) - .build(); - } - - @Override - public boolean isSatisfied() { - // Return false if paused to prevent condition from being satisfied during pause - if (isPaused()) { - return false; - } - - // Once satisfied, stay satisfied until reset - if (satisfied) { - return true; - } - - // Check if processed count meets or exceeds target - if (processedCount >= currentTargetCount) { - satisfied = true; - return true; - } - - return false; - } - - @Override - public String getDescription() { - StringBuilder sb = new StringBuilder(); - - if (trackingMode == TrackingMode.SOURCE_CONSUMPTION || trackingMode == TrackingMode.BOTH) { - sb.append("Process "); - for (int i = 0; i < sourceItems.size(); i++) { - ItemTracker item = sourceItems.get(i); - sb.append(item.getQuantityPerProcess()).append(" ").append(item.getItemName()); - if (i < sourceItems.size() - 1) { - sb.append(", "); - } - } - } - - if (trackingMode == TrackingMode.TARGET_PRODUCTION || trackingMode == TrackingMode.BOTH) { - if (trackingMode == TrackingMode.BOTH) { - sb.append(" into "); - } else { - sb.append("Create "); - } - - for (int i = 0; i < targetItems.size(); i++) { - ItemTracker item = targetItems.get(i); - sb.append(item.getQuantityPerProcess()).append(" ").append(item.getItemName()); - if (i < targetItems.size() - 1) { - sb.append(", "); - } - } - } - - // Add target info - sb.append(": ").append(processedCount).append("/").append(currentTargetCount); - - if (targetCountMin != targetCountMax) { - sb.append(" (randomized from ").append(targetCountMin).append("-").append(targetCountMax).append(")"); - } - - return sb.toString(); - } - - @Override - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - // Basic description - sb.append("Process Item Condition\n"); - - // Source items - if (!sourceItems.isEmpty()) { - sb.append("Source Items: "); - for (int i = 0; i < sourceItems.size(); i++) { - ItemTracker item = sourceItems.get(i); - sb.append(item.getQuantityPerProcess()).append("x ").append(item.getItemName()); - if (i < sourceItems.size() - 1) { - sb.append(", "); - } - } - sb.append("\n"); - } - - // Target items - if (!targetItems.isEmpty()) { - sb.append("Target Items: "); - for (int i = 0; i < targetItems.size(); i++) { - ItemTracker item = targetItems.get(i); - sb.append(item.getQuantityPerProcess()).append("x ").append(item.getItemName()); - if (i < targetItems.size() - 1) { - sb.append(", "); - } - } - sb.append("\n"); - } - - // Tracking mode - sb.append("Tracking Mode: ").append(trackingMode).append("\n"); - - // Status - sb.append("Status: ").append(isSatisfied() ? "Satisfied" : "Not Satisfied").append("\n"); - sb.append("Progress: ").append(processedCount).append("/").append(currentTargetCount) - .append(" (").append(String.format("%.1f%%", getProgressPercentage())).append(")\n"); - - if (targetCountMin != targetCountMax) { - sb.append("Target Range: ").append(targetCountMin).append("-").append(targetCountMax).append("\n"); - } - - // Current state - sb.append("Processing Active: ").append(isProcessingActive ? "Yes" : "No").append("\n"); - - return sb.toString(); - } - - @Override - public void reset() { - reset(false); - } - - @Override - public void reset(boolean randomize) { - if (randomize && targetCountMin != targetCountMax) { - currentTargetCount = Rs2Random.between(targetCountMin, targetCountMax); - } - satisfied = false; - processedCount = 0; - previousInventoryCounts.clear(); - initialInventoryLoaded = false; - } - - @Override - public double getProgressPercentage() { - if (satisfied) { - return 100.0; - } - - if (currentTargetCount <= 0) { - return 100.0; - } - - return Math.min(100.0, (processedCount * 100.0) / currentTargetCount); - } - @Override - public void onItemContainerChanged(ItemContainerChanged event) { - // Skip updates if paused - if (isPaused()) { - return; - } - - // Only process inventory changes - if (event.getContainerId() != InventoryID.INVENTORY.getId()) { - return; - } - - // Don't process changes if bank is open (banking items) - if (Rs2Bank.isOpen()) { - return; - } - - // Update item tracking - updateItemTracking(); - } - - @Override - public void onGameTick(GameTick event) { - // Skip updates if paused - if (isPaused()) { - return; - } - - // Load initial inventory if not yet loaded - if (!initialInventoryLoaded) { - updateItemTracking(); - initialInventoryLoaded = true; - } - } - - /** - * Update the tracking of items and detect processing - */ - private void updateItemTracking() { - Map currentCounts = getCurrentInventoryCounts(); - - // Skip first inventory load - if (previousInventoryCounts.isEmpty()) { - previousInventoryCounts = currentCounts; - return; - } - - // Detect changes in inventory - boolean sourceConsumed = false; - boolean targetProduced = false; - - // Check source items consumption - if (trackingMode == TrackingMode.SOURCE_CONSUMPTION || - trackingMode == TrackingMode.EITHER || - trackingMode == TrackingMode.BOTH) { - - sourceConsumed = detectSourceConsumption(previousInventoryCounts, currentCounts); - } - - // Check target items production - if (trackingMode == TrackingMode.TARGET_PRODUCTION || - trackingMode == TrackingMode.EITHER || - trackingMode == TrackingMode.BOTH) { - - targetProduced = detectTargetProduction(previousInventoryCounts, currentCounts); - } - - // Update processed count based on tracking mode - boolean processDetected = false; - - switch (trackingMode) { - case SOURCE_CONSUMPTION: - processDetected = sourceConsumed; - break; - case TARGET_PRODUCTION: - processDetected = targetProduced; - break; - case EITHER: - processDetected = sourceConsumed || targetProduced; - break; - case BOTH: - processDetected = sourceConsumed && targetProduced; - break; - } - - if (processDetected) { - processedCount++; - isProcessingActive = true; - lastInventoryChange = Instant.now(); - log.debug("Process detected: {} -> {}/{}", - trackingMode, processedCount, currentTargetCount); - } else { - // If no changes detected for 3 seconds, consider processing inactive - if (isProcessingActive && Instant.now().minusSeconds(3).isAfter(lastInventoryChange)) { - isProcessingActive = false; - } - } - - // Update previous counts for next comparison - previousInventoryCounts = currentCounts; - } - - /** - * Detect if source items were consumed by comparing previous and current inventory - */ - private boolean detectSourceConsumption(Map previous, Map current) { - if (sourceItems.isEmpty()) { - return false; - } - - // Check if all source items were reduced by expected amounts - for (ItemTracker sourceItem : sourceItems) { - int prevCount = previous.getOrDefault(sourceItem.getItemName(), 0); - int currCount = current.getOrDefault(sourceItem.getItemName(), 0); - - // Check if the item was consumed by the expected amount - if (currCount >= prevCount || prevCount - currCount != sourceItem.getQuantityPerProcess()) { - return false; - } - } - - return true; - } - - /** - * Detect if target items were produced by comparing previous and current inventory - */ - private boolean detectTargetProduction(Map previous, Map current) { - if (targetItems.isEmpty()) { - return false; - } - - // Check if all target items were increased by expected amounts - for (ItemTracker targetItem : targetItems) { - int prevCount = previous.getOrDefault(targetItem.getItemName(), 0); - int currCount = current.getOrDefault(targetItem.getItemName(), 0); - - // Check if the item was produced by the expected amount - if (currCount <= prevCount || currCount - prevCount != targetItem.getQuantityPerProcess()) { - return false; - } - } - - return true; - } - - /** - * Get current inventory counts for relevant items - */ - private Map getCurrentInventoryCounts() { - return getCurrentItemCounts(false); - } - - /** - * Get current total item counts (inventory + bank) for relevant items - */ - private Map getCurrentTotalItemCounts() { - return getCurrentItemCounts(true); - } - - /** - * Get current item counts for relevant items - * @param includeBank whether to include banked items in the count - */ - private Map getCurrentItemCounts(boolean includeBank) { - Map counts = new HashMap<>(); - - // Get inventory items - List invItems = Rs2Inventory.all(); - - // Get bank items if requested and bank data is available - List bankItems = new ArrayList<>(); - if (includeBank) { - try { - List bankData = Rs2Bank.bankItems(); - if (bankData != null) { - bankItems.addAll(bankData); - } - } catch (Exception e) { - // Bank might not be accessible, continue with inventory only - if (Microbot.isDebug()) { - log.debug("Could not access bank data: {}", e.getMessage()); - } - } - } - - // Count source items - for (ItemTracker sourceItem : sourceItems) { - int invTotal = countItems(invItems, sourceItem.getItemPattern()); - int bankTotal = includeBank ? countItems(bankItems, sourceItem.getItemPattern()) : 0; - counts.put(sourceItem.getItemName(), invTotal + bankTotal); - } - - // Count target items - for (ItemTracker targetItem : targetItems) { - int invTotal = countItems(invItems, targetItem.getItemPattern()); - int bankTotal = includeBank ? countItems(bankItems, targetItem.getItemPattern()) : 0; - counts.put(targetItem.getItemName(), invTotal + bankTotal); - } - - return counts; - } - - /** - * Count items in inventory that match a pattern - */ - private int countItems(List items, Pattern pattern) { - return items.stream() - .filter(item -> pattern.matcher(item.getName()).matches()) - .mapToInt(Rs2ItemModel::getQuantity) - .sum(); - } - - // Factory methods for logical conditions - - /** - * Creates an AND logical condition requiring multiple processing conditions - */ - public static LogicalCondition createAndCondition(List conditions) { - AndCondition andCondition = new AndCondition(); - for (ProcessItemCondition condition : conditions) { - andCondition.addCondition(condition); - } - return andCondition; - } - - /** - * Creates an OR logical condition requiring any of multiple processing conditions - */ - public static LogicalCondition createOrCondition(List conditions) { - OrCondition orCondition = new OrCondition(); - for (ProcessItemCondition condition : conditions) { - orCondition.addCondition(condition); - } - return orCondition; - } - - @Override - public void pause() { - super.pause(); - - // Snapshot current state for adjustment on resume - // Use total counts (inventory + bank) if configured, otherwise inventory only - if (includeBankForPauseResume) { - pausedInventoryCounts = new HashMap<>(getCurrentTotalItemCounts()); - if (Microbot.isDebug()) { - log.info("ProcessItemCondition paused: processed={}, total item counts (inv+bank) captured", pausedProcessedCount); - } - } else { - pausedInventoryCounts = new HashMap<>(getCurrentInventoryCounts()); - if (Microbot.isDebug()) { - log.info("ProcessItemCondition paused: processed={}, inventory counts captured", pausedProcessedCount); - } - } - pausedProcessedCount = processedCount; - } - - @Override - public void resume() { - // Only proceed if actually paused - if (!isPaused()) { - return; - } - - // Get current item counts for comparison (use same method as pause) - Map currentCounts = includeBankForPauseResume ? - getCurrentTotalItemCounts() : getCurrentInventoryCounts(); - - // Calculate how many processes occurred during pause - int processesDetectedDuringPause = 0; - - // For processing conditions, we need to detect actual processing that occurred - // Use the same counting method (inventory vs total) as used during pause - if (!pausedInventoryCounts.isEmpty()) { - // Check if we can detect processing based on our tracking mode - switch (trackingMode) { - case SOURCE_CONSUMPTION: - processesDetectedDuringPause = detectProcessesDuringPauseByConsumption(pausedInventoryCounts, currentCounts); - break; - case TARGET_PRODUCTION: - processesDetectedDuringPause = detectProcessesDuringPauseByProduction(pausedInventoryCounts, currentCounts); - break; - case EITHER: - // Take the maximum of consumption or production detected - int consumptionProcesses = detectProcessesDuringPauseByConsumption(pausedInventoryCounts, currentCounts); - int productionProcesses = detectProcessesDuringPauseByProduction(pausedInventoryCounts, currentCounts); - processesDetectedDuringPause = Math.max(consumptionProcesses, productionProcesses); - break; - case BOTH: - // For BOTH mode, we need to detect the minimum of both (since both are required) - int consumptionDetected = detectProcessesDuringPauseByConsumption(pausedInventoryCounts, currentCounts); - int productionDetected = detectProcessesDuringPauseByProduction(pausedInventoryCounts, currentCounts); - processesDetectedDuringPause = Math.min(consumptionDetected, productionDetected); - break; - } - } - - // Adjust processed count to exclude progress made during pause - processedCount = Math.max(0, pausedProcessedCount - processesDetectedDuringPause); - - // Call parent class resume method - super.resume(); - - // Update baseline inventory counts for future tracking (inventory only for regular processing) - previousInventoryCounts = getCurrentInventoryCounts(); - - if (Microbot.isDebug()) { - String countingMethod = includeBankForPauseResume ? "total counts (inv+bank)" : "inventory counts"; - log.info("ProcessItemCondition resumed: detected {} processes during pause using {}, " + - "adjusted processed count from {} to {}", - processesDetectedDuringPause, countingMethod, pausedProcessedCount, processedCount); - } - } - - /** - * Detect processes during pause by looking at source item consumption - */ - private int detectProcessesDuringPauseByConsumption(Map pausedCounts, Map currentCounts) { - if (sourceItems.isEmpty()) { - return 0; - } - - int minProcesses = Integer.MAX_VALUE; - boolean anyConsumptionDetected = false; - - // Check each source item to see how much was consumed - for (ItemTracker sourceItem : sourceItems) { - String itemName = sourceItem.getItemName(); - int pausedCount = pausedCounts.getOrDefault(itemName, 0); - int currentCount = currentCounts.getOrDefault(itemName, 0); - - if (pausedCount > currentCount) { - // Items were consumed during pause - int consumed = pausedCount - currentCount; - int processes = consumed / sourceItem.getQuantityPerProcess(); - minProcesses = Math.min(minProcesses, processes); - anyConsumptionDetected = true; - } - } - - return anyConsumptionDetected ? minProcesses : 0; - } - - /** - * Detect processes during pause by looking at target item production - */ - private int detectProcessesDuringPauseByProduction(Map pausedCounts, Map currentCounts) { - if (targetItems.isEmpty()) { - return 0; - } - - int minProcesses = Integer.MAX_VALUE; - boolean anyProductionDetected = false; - - // Check each target item to see how much was produced - for (ItemTracker targetItem : targetItems) { - String itemName = targetItem.getItemName(); - int pausedCount = pausedCounts.getOrDefault(itemName, 0); - int currentCount = currentCounts.getOrDefault(itemName, 0); - - if (currentCount > pausedCount) { - // Items were produced during pause - int produced = currentCount - pausedCount; - int processes = produced / targetItem.getQuantityPerProcess(); - minProcesses = Math.min(minProcesses, processes); - anyProductionDetected = true; - } - } - - return anyProductionDetected ? minProcesses : 0; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/README.md b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/README.md deleted file mode 100644 index 7ff356273ad..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/README.md +++ /dev/null @@ -1,30 +0,0 @@ -``` JAVA -// Track making 50 adamant daggers -ProcessItemCondition makeDaggers = ProcessItemCondition.forProduction("adamant dagger", 50); - -// Track using 100 adamant bars (regardless of what they make) -ProcessItemCondition useAdamantBars = ProcessItemCondition.forConsumption("adamant bar", 100); - -// Track making platebodies specifically (which use 5 bars each) -ProcessItemCondition makePlatebodies = ProcessItemCondition.forRecipe( - "adamant bar", 5, // Source: 5 adamant bars - "adamant platebody", 1, // Target: 1 platebody - 20 // Make 20 platebodies -); - -// Track making any herblore potions with ranarr weed -ProcessItemCondition useRanarrs = ProcessItemCondition.forConsumption("ranarr weed", 50); - -// Track making prayer potions specifically -ProcessItemCondition makePrayerPots = ProcessItemCondition.forMultipleConsumption( - Arrays.asList("ranarr potion (unf)", "snape grass"), - Arrays.asList(1, 1), - 50 // Make 50 prayer potions -); - -// Randomized condition - make between 25-35 necklaces of crafting -ProcessItemCondition makeCraftingNecklaces = ProcessItemCondition.createRandomizedProduction( - "necklace of crafting", 25, 35 -); - -``` \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ResourceCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ResourceCondition.java deleted file mode 100644 index a9e9e8ae30b..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ResourceCondition.java +++ /dev/null @@ -1,397 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.ItemComposition; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.GameTick; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; - -import java.time.Duration; -import java.util.List; -import java.util.Optional; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -/** - * Abstract base class for all resource-based conditions. - * Provides common functionality for tracking items, materials and other resources. - */ -@Slf4j -public abstract class ResourceCondition implements Condition { - @Getter - protected final Pattern itemPattern; - - /** - * Queue of item events waiting to be processed at the end of a game tick. - * Events are accumulated during the tick and processed together for efficiency. - */ - protected final List pendingEvents = new ArrayList<>(); - - /** - * Map tracking recently dropped items by the player, keyed by world location. - * Values are timestamps when the items were dropped, used to identify player actions. - */ - protected final Map playerDroppedItems = new HashMap<>(); - - /** - * The player's last known position in the game world. - * Used for determining if items appearing nearby were likely dropped by the player. - */ - protected WorldPoint lastPlayerPosition = null; - - // Pause-related fields - @Getter - protected transient boolean isPaused = false; - public ResourceCondition() { - this.itemPattern = null; - } - public ResourceCondition(String itemPatternString) { - this.itemPattern = createItemPattern(itemPatternString); - } - public final String getItemPatternString() { - return itemPattern == null ? null : itemPattern.pattern().toString(); - } - /** - * Returns the condition type for resource conditions - */ - @Override - public ConditionType getType() { - return ConditionType.RESOURCE; - } - - /** - * Default implementation for detailed description - subclasses should override - */ - @Override - public String getDetailedDescription() { - return "Resource Condition: " + getDescription(); - } - - /** - * Default implementation for calculating progress percentage - * Subclasses should override for more specific calculations - */ - @Override - public double getProgressPercentage() { - return isSatisfied() ? 100.0 : 0.0; - } - - /** - * Gets the estimated time until this resource condition will be satisfied. - * Resource conditions typically cannot provide reliable time estimates since they - * depend on player actions, game events, or unpredictable external factors. - * - * Subclasses may override this method if they can provide meaningful estimates - * (e.g., based on historical data or known resource generation rates). - * - * @return Optional.empty() for most resource conditions, as time cannot be reliably estimated - */ - @Override - public Optional getEstimatedTimeWhenIsSatisfied() { - // If the condition is already satisfied, return zero duration - if (isSatisfied()) { - return Optional.of(Duration.ZERO); - } - - // Resource conditions generally cannot provide reliable time estimates - // since they depend on unpredictable factors like: - // - Player actions and behavior - // - Random game events - // - External market conditions - // - Item drop rates - // - NPC spawning patterns - - return Optional.empty(); - } - - /** - * Checks if an item is in noted form - * @param itemId The item ID to check - * @return true if the item is noted, false otherwise - */ - public static boolean isNoted(int itemId) { - try { - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - ItemComposition itemComposition = Microbot.getItemManager().getItemComposition(itemId); - - int linkedId = itemComposition.getLinkedNoteId(); - if (linkedId <= 0) { - return false; - } - ItemComposition linkedItemComposition = Microbot.getItemManager().getItemComposition(linkedId); - boolean isNoted = itemComposition.getNote() == 799; - - boolean isNoteable = isNoteable(itemId); - - return isNoted && !isNoteable; - }).orElse(null); - } catch (Exception e) { - log.error("Error checking if item is noted, itemId: {}, error: {}", itemId, e.getMessage()); - return false; - } - } - - /** - * Checks if an item can be noted - * @param itemId The item ID to check - * @return true if the item can be noted, false otherwise - */ - public static boolean isNoteable(int itemId) { - if (itemId < 0) { - return false; - } - try { - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - ItemComposition itemComposition = Microbot.getItemManager().getItemComposition(itemId); - int linkedId = itemComposition.getLinkedNoteId(); - if (linkedId <= 0) { - return false; - } - - ItemComposition linkedItemComposition = Microbot.getItemManager().getItemComposition(linkedId); - - boolean unlinkedIsNoted = itemComposition.getNote() == 799; - return !unlinkedIsNoted; - }).orElse(false); - } catch (Exception e) { - log.error("Error checking if item is noteable, itemId: {}, error: {}", itemId, e.getMessage()); - return false; - } - } - - /** - * Gets a list of noted items from the inventory - * @return List of noted item models - */ - public static List getNotedItems() { - return Rs2Inventory.all().stream() - .filter(item -> item.isNoted() && item.isStackable()) - .collect(Collectors.toList()); - } - - /** - * Gets a list of un-noted items from the inventory that could be noted - * @return List of un-noted item models - */ - public static List getUnNotedItems() { - return Rs2Inventory.all().stream() - .filter(item -> (!item.isNoted()) ) - .collect(Collectors.toList()); - } - - /** - * Checks if an item model represents a noted item - * @param notedItem The item model to check - * @return true if the item is noted, false otherwise - */ - public static boolean isNoted(Rs2ItemModel notedItem) { - return notedItem != null && isNoted(notedItem.getId()); - } - - /** - * Checks if an item model represents an un-noted item - * @param item The item model to check - * @return true if the item is un-noted, false otherwise - */ - public static boolean isUnNoted(Rs2ItemModel item) { - if (item == null) { - return false; - } - return !isNoted(item.getId()); - } - - /** - * Normalizes an item name for comparison (lowercase and trim) - * @param name The item name to normalize - * @return The normalized item name - */ - protected String normalizeItemName(String name) { - if (name == null) return ""; - return name.toLowerCase().trim(); - } - - /** - * Creates a pattern for matching item names - * @param itemName The item name pattern to match - * @return A compiled regex pattern for matching the item name - */ - protected Pattern createItemPattern(String itemName) { - if (itemName == null || itemName.isEmpty()) { - return Pattern.compile(".*", Pattern.CASE_INSENSITIVE); - } - - // Check if the name is already a regex pattern - if (itemName.startsWith("^") || itemName.endsWith("$") || - itemName.contains(".*") || itemName.contains("[") || - itemName.contains("(")) { - return Pattern.compile(itemName,Pattern.CASE_INSENSITIVE); - } - - // Otherwise, create a contains pattern - return Pattern.compile(".*" + Pattern.quote(itemName) + ".*", Pattern.CASE_INSENSITIVE); - } - /** - * Extracts a clean, readable item name from the item's regex pattern. - * - * This method processes the pattern string to remove special regex characters: - * - For "contains" patterns (wrapped in ".*"), the wrapping characters are removed - * - For patterns created with Pattern.quote(), the \Q and \E escape sequences are removed - * - * @return A cleaned string representation of the item name suitable for display - */ - public String getItemName() { - // Extract a clean item name from the pattern for display - String patternStr = itemPattern.pattern(); - // If it's a "contains" pattern (created with .*pattern.*) - if (patternStr.startsWith(".*") && patternStr.endsWith(".*")) { - patternStr = patternStr.substring(2, patternStr.length() - 2); - } - // Handle patterns that were created with Pattern.quote() which escapes special characters - if (patternStr.startsWith("\\Q") && patternStr.endsWith("\\E")) { - patternStr = patternStr.substring(2, patternStr.length() - 2); - } - return patternStr; - } - - - /** - * Class for tracking resource-related events and their metadata. - * Used to record when items are spawned, despawned, or inventory changes occur. - */ - protected static class ItemTrackingEvent { - public final long timestamp; - public final WorldPoint location; - public final String itemName; - public final int itemId; - public final int quantity; - public final boolean isPlayerOwned; - public final EventType eventType; - - /** - * Defines the different types of item events that can be tracked. - */ - public enum EventType { - ITEM_SPAWNED, - ITEM_DESPAWNED, - INVENTORY_CHANGED - } - - /** - * Creates a new item tracking event with the specified parameters. - * - * @param timestamp The time when the event occurred - * @param location The world location where the event occurred - * @param itemName The name of the item involved - * @param itemId The ID of the item involved - * @param quantity The quantity of items involved - * @param isPlayerOwned Whether the item is owned by the player - * @param eventType The type of event (spawn, despawn, inventory change) - */ - public ItemTrackingEvent(long timestamp, WorldPoint location, String itemName, - int itemId, int quantity, boolean isPlayerOwned, EventType eventType) { - this.timestamp = timestamp; - this.location = location; - this.itemName = itemName; - this.itemId = itemId; - this.quantity = quantity; - this.isPlayerOwned = isPlayerOwned; - this.eventType = eventType; - } - } - - - - /** - * Determines if an item spawned at a location was likely dropped by the player. - * This is estimated based on proximity to the player's last known position and - * previously tracked player-dropped items. - * - * @param location The world location where the item appeared - * @param timestamp When the item appeared (millisecond timestamp) - * @return true if the item was likely dropped by the player, false otherwise - */ - protected boolean isLikelyPlayerDroppedItem(WorldPoint location, long timestamp) { - // Check if this is near the player's last position - if (lastPlayerPosition != null) { - int distance = location.distanceTo2D(lastPlayerPosition); - // Items dropped by players typically appear at their location or 1 tile away - if (distance <= 1) { - // Track this as a likely player-dropped item - playerDroppedItems.put(location, timestamp); - return true; - } - } - return playerDroppedItems.containsKey(location); - } - - /** - * Processes all pending item tracking events that accumulated during the game tick. - * This method should be called at the end of each game tick to update resource tracking. - * Base implementation only clears events; subclasses should override with specific logic. - */ - protected void processPendingEvents() { - // Default implementation just clears events - // Subclasses should override with specific implementation - pendingEvents.clear(); - - // Clean up old entries from player dropped items map (older than 10 seconds) - long now = System.currentTimeMillis(); - playerDroppedItems.entrySet().removeIf(entry -> now - entry.getValue() > 10000); - } - - /** - * Updates the player's current position for use in dropped item tracking. - * Called each game tick to maintain accurate position information. - */ - protected void updatePlayerPosition() { - if (Microbot.getClient() != null && Microbot.getClient().getLocalPlayer() != null) { - lastPlayerPosition = Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - } - - /** - * Handles the GameTick event from RuneLite's event system. - * Updates player position and processes any accumulated item events. - * - * @param event The game tick event object - */ - @Override - public void onGameTick(GameTick event) { - // Skip processing if paused - if (isPaused) { - return; - } - - // Update player position - updatePlayerPosition(); - - // Process any pending events - processPendingEvents(); - } - - @Override - public void pause() { - if (!isPaused) { - isPaused = true; - log.debug("Resource condition paused for item pattern: {}", - itemPattern != null ? itemPattern.pattern() : "any"); - } - } - - @Override - public void resume() { - if (isPaused) { - isPaused = false; - log.debug("Resource condition resumed for item pattern: {}", - itemPattern != null ? itemPattern.pattern() : "any"); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/BankItemCountConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/BankItemCountConditionAdapter.java deleted file mode 100644 index c7a7b7a6791..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/BankItemCountConditionAdapter.java +++ /dev/null @@ -1,76 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.BankItemCountCondition; - -import java.lang.reflect.Type; - -/** - * Adapter for handling serialization and deserialization of BankItemCountCondition objects. - */ -@Slf4j -public class BankItemCountConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(BankItemCountCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Add type information - result.addProperty("type", BankItemCountCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Add version information - data.addProperty("version", BankItemCountCondition.getVersion()); - - // Add specific properties for BankItemCountCondition - data.addProperty("itemName", src.getItemName()); - data.addProperty("targetCountMin", src.getTargetCountMin()); - data.addProperty("targetCountMax", src.getTargetCountMax()); - // Add data to wrapper - result.add("data", data); - - return result; - } - - @Override - public BankItemCountCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(BankItemCountCondition.getVersion())) { - - throw new JsonParseException("Version mismatch in BankItemCountCondition: expected " + - BankItemCountCondition.getVersion() + ", got " + version); - } - } - - // Extract basic properties - String itemName = dataObj.has("itemName") ? dataObj.get("itemName").getAsString() : ""; - int targetCountMin = dataObj.has("targetCountMin") ? dataObj.get("targetCountMin").getAsInt() : 1; - int targetCountMax = dataObj.has("targetCountMax") ? dataObj.get("targetCountMax").getAsInt() : targetCountMin; - - // Create the condition - return BankItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .build(); - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/GatheredResourceConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/GatheredResourceConditionAdapter.java deleted file mode 100644 index b9d794c5bd5..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/GatheredResourceConditionAdapter.java +++ /dev/null @@ -1,106 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Skill; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.GatheredResourceCondition; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; - -/** - * Adapter for handling serialization and deserialization of GatheredResourceCondition objects. - */ -@Slf4j -public class GatheredResourceConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(GatheredResourceCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Add type information - result.addProperty("type", GatheredResourceCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Add version information - data.addProperty("version", GatheredResourceCondition.getVersion()); - - // Add specific properties for GatheredResourceCondition - data.addProperty("itemName", src.getItemName()); - data.addProperty("targetCountMin", src.getTargetCountMin()); - data.addProperty("targetCountMax", src.getTargetCountMax()); - data.addProperty("includeNoted", src.isIncludeNoted()); - - // Add relevant skills array - JsonArray skillsArray = new JsonArray(); - for (Skill skill : src.getRelevantSkills()) { - skillsArray.add(skill.name()); - } - data.add("relevantSkills", skillsArray); - - // Add data to wrapper - result.add("data", data); - - return result; - } - - @Override - public GatheredResourceCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(GatheredResourceCondition.getVersion())) { - log.warn("Version mismatch in GatheredResourceCondition: expected {}, got {}", - GatheredResourceCondition.getVersion(), version); - throw new JsonParseException("Version mismatch in GatheredResourceCondition: expected " + - GatheredResourceCondition.getVersion() + ", got " + version); - } - } - - // Extract basic properties - String itemName = dataObj.has("itemName") ? dataObj.get("itemName").getAsString() : ""; - int targetCountMin = dataObj.has("targetCountMin") ? dataObj.get("targetCountMin").getAsInt() : 1; - int targetCountMax = dataObj.has("targetCountMax") ? dataObj.get("targetCountMax").getAsInt() : targetCountMin; - boolean includeNoted = dataObj.has("includeNoted") && dataObj.get("includeNoted").getAsBoolean(); - - // Extract relevant skills if present - List relevantSkills = new ArrayList<>(); - if (dataObj.has("relevantSkills")) { - JsonArray skillsArray = dataObj.getAsJsonArray("relevantSkills"); - for (JsonElement skillElement : skillsArray) { - try { - Skill skill = Skill.valueOf(skillElement.getAsString()); - relevantSkills.add(skill); - } catch (IllegalArgumentException e) { - log.warn("Unknown skill: {}", skillElement.getAsString()); - } - } - } - - // Create the condition - return GatheredResourceCondition.builder() - .itemName(itemName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .includeNoted(includeNoted) - .relevantSkills(relevantSkills.isEmpty() ? null : relevantSkills) - .build(); - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/InventoryItemCountConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/InventoryItemCountConditionAdapter.java deleted file mode 100644 index da6e8abed61..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/InventoryItemCountConditionAdapter.java +++ /dev/null @@ -1,90 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.InventoryItemCountCondition; - -import java.lang.reflect.Type; - -/** - * Adapter for handling serialization and deserialization of InventoryItemCountCondition objects. - */ -@Slf4j -public class InventoryItemCountConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(InventoryItemCountCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Add type information - result.addProperty("type", InventoryItemCountCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Add version information - data.addProperty("version", InventoryItemCountCondition.getVersion()); - - // Add specific properties for InventoryItemCountCondition - data.addProperty("itemName", src.getItemName()); - data.addProperty("targetCountMin", src.getTargetCountMin()); - data.addProperty("targetCountMax", src.getTargetCountMax()); - data.addProperty("includeNoted", src.isIncludeNoted()); - data.addProperty("currentTargetCount", src.getCurrentTargetCount()); - data.addProperty("currentItemCount", src.getCurrentItemCount()); - - // Add data to wrapper - result.add("data", data); - - return result; - } - - @Override - public InventoryItemCountCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - try { - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(InventoryItemCountCondition.getVersion())) { - log.warn("Version mismatch in InventoryItemCountCondition: expected {}, got {}", - InventoryItemCountCondition.getVersion(), version); - } - } - - // Extract basic properties - String itemName = dataObj.has("itemName") ? dataObj.get("itemName").getAsString() : ""; - int targetCountMin = dataObj.has("targetCountMin") ? dataObj.get("targetCountMin").getAsInt() : 1; - int targetCountMax = dataObj.has("targetCountMax") ? dataObj.get("targetCountMax").getAsInt() : targetCountMin; - boolean includeNoted = dataObj.has("includeNoted") && dataObj.get("includeNoted").getAsBoolean(); - - // Create the condition - return InventoryItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .includeNoted(includeNoted) - .build(); - } catch (Exception e) { - log.error("Error deserializing InventoryItemCountCondition", e); - // Return a default condition on error - return InventoryItemCountCondition.builder() - .itemName("Unknown") - .targetCountMin(1) - .targetCountMax(1) - .includeNoted(false) - .build(); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/LootItemConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/LootItemConditionAdapter.java deleted file mode 100644 index bfc30d622a4..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/LootItemConditionAdapter.java +++ /dev/null @@ -1,84 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.LootItemCondition; - -import java.lang.reflect.Type; - -/** - * Adapter for handling serialization and deserialization of LootItemCondition objects. - */ -@Slf4j -public class LootItemConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(LootItemCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Add type information - result.addProperty("type", LootItemCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Add version information - data.addProperty("version", LootItemCondition.getVersion()); - - // Add specific properties for LootItemCondition - data.addProperty("itemName", src.getItemName()); - data.addProperty("targetAmountMin", src.getTargetAmountMin()); - data.addProperty("targetAmountMax", src.getTargetAmountMax()); - data.addProperty("includeNoneOwner", src.isIncludeNoneOwner()); - data.addProperty("includeNoted", src.isIncludeNoted()); - - // Add data to wrapper - result.add("data", data); - - return result; - } - - @Override - public LootItemCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(LootItemCondition.getVersion())) { - log.warn("Version mismatch in LootItemCondition: expected {}, got {}", - LootItemCondition.getVersion(), version); - throw new JsonParseException("Version mismatch in LootItemCondition: expected " + - LootItemCondition.getVersion() + ", got " + version); - } - } - - // Extract basic properties - String itemName = dataObj.has("itemName") ? dataObj.get("itemName").getAsString() : ""; - int targetAmountMin = dataObj.has("targetAmountMin") ? dataObj.get("targetAmountMin").getAsInt() : 1; - int targetAmountMax = dataObj.has("targetAmountMax") ? dataObj.get("targetAmountMax").getAsInt() : targetAmountMin; - boolean includeNoted = dataObj.has("includeNoted") && dataObj.get("includeNoted").getAsBoolean(); - boolean includeNoneOwner = dataObj.has("includeNoneOwner") && dataObj.get("includeNoneOwner").getAsBoolean(); - - // Create the condition - return LootItemCondition.builder() - .itemName(itemName) - .targetAmountMin(targetAmountMin) - .targetAmountMax(targetAmountMax) - .includeNoted(includeNoted) - .includeNoneOwner(includeNoneOwner) - .build(); - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/ProcessItemConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/ProcessItemConditionAdapter.java deleted file mode 100644 index 39ebdacb977..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/ProcessItemConditionAdapter.java +++ /dev/null @@ -1,147 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ProcessItemCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ProcessItemCondition.ItemTracker; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ProcessItemCondition.TrackingMode; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; - -/** - * Adapter for handling serialization and deserialization of ProcessItemCondition objects. - */ -@Slf4j -public class ProcessItemConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ProcessItemCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Add type information - result.addProperty("type", ProcessItemCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Add version information - data.addProperty("version", ProcessItemCondition.getVersion()); - - // Add specific properties for ProcessItemCondition - data.addProperty("targetCountMin", src.getTargetCountMin()); - data.addProperty("targetCountMax", src.getTargetCountMax()); - data.addProperty("trackingMode", src.getTrackingMode().name()); - - // Serialize sourceItems - if (src.getSourceItems() != null && !src.getSourceItems().isEmpty()) { - JsonArray sourceItemsArray = new JsonArray(); - for (ItemTracker item : src.getSourceItems()) { - JsonObject itemObj = new JsonObject(); - itemObj.addProperty("patternString", item.getItemPattern().pattern()); - itemObj.addProperty("quantityPerProcess", item.getQuantityPerProcess()); - sourceItemsArray.add(itemObj); - } - data.add("sourceItems", sourceItemsArray); - } - - // Serialize targetItems - if (src.getTargetItems() != null && !src.getTargetItems().isEmpty()) { - JsonArray targetItemsArray = new JsonArray(); - for (ItemTracker item : src.getTargetItems()) { - JsonObject itemObj = new JsonObject(); - itemObj.addProperty("patternString", item.getItemPattern().pattern()); - itemObj.addProperty("quantityPerProcess", item.getQuantityPerProcess()); - targetItemsArray.add(itemObj); - } - data.add("targetItems", targetItemsArray); - } - - // Add data to wrapper - result.add("data", data); - - return result; - } - - @Override - public ProcessItemCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(ProcessItemCondition.getVersion())) { - throw new JsonParseException("Version mismatch in ProcessItemCondition: expected " + - ProcessItemCondition.getVersion() + ", got " + version); - } - } - - // Extract basic properties - int targetCountMin = dataObj.has("targetCountMin") ? dataObj.get("targetCountMin").getAsInt() : 1; - int targetCountMax = dataObj.has("targetCountMax") ? dataObj.get("targetCountMax").getAsInt() : targetCountMin; - - // Deserialize tracking mode - TrackingMode trackingMode = TrackingMode.EITHER; // Default - if (dataObj.has("trackingMode")) { - try { - trackingMode = TrackingMode.valueOf(dataObj.get("trackingMode").getAsString()); - } catch (IllegalArgumentException e) { - log.warn("Invalid tracking mode: {}", dataObj.get("trackingMode").getAsString()); - } - } - - // Deserialize sourceItems - List sourceItems = new ArrayList<>(); - if (dataObj.has("sourceItems")) { - JsonArray sourceItemsArray = dataObj.getAsJsonArray("sourceItems"); - for (JsonElement element : sourceItemsArray) { - JsonObject itemObj = element.getAsJsonObject(); - String patternString = itemObj.get("patternString").getAsString(); - int quantity = itemObj.get("quantityPerProcess").getAsInt(); - - // Create ItemTracker directly since its constructor needs pattern - ItemTracker tracker = new ItemTracker(patternString, quantity); - sourceItems.add(tracker); - } - } - - // Deserialize targetItems - List targetItems = new ArrayList<>(); - if (dataObj.has("targetItems")) { - JsonArray targetItemsArray = dataObj.getAsJsonArray("targetItems"); - for (JsonElement element : targetItemsArray) { - JsonObject itemObj = element.getAsJsonObject(); - String patternString = itemObj.get("patternString").getAsString(); - int quantity = itemObj.get("quantityPerProcess").getAsInt(); - - // Create ItemTracker directly since its constructor needs pattern - ItemTracker tracker = new ItemTracker(patternString, quantity); - targetItems.add(tracker); - } - } - - // Create the condition - return ProcessItemCondition.builder() - .sourceItems(sourceItems) - .targetItems(targetItems) - .trackingMode(trackingMode) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .build(); - - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/ResourceConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/ResourceConditionAdapter.java deleted file mode 100644 index 0e65cf23d03..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/serialization/ResourceConditionAdapter.java +++ /dev/null @@ -1,66 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ResourceCondition; - -import java.lang.reflect.Type; -import java.util.regex.Pattern; - -/** - * Adapter for handling serialization and deserialization of ResourceCondition objects. - */ -@Slf4j -public class ResourceConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ResourceCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Add type information - result.addProperty("type", src.getClass().getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Add version information - use specific version if available, or default - try { - String version = (String) src.getClass().getMethod("getVersion").invoke(null); - data.addProperty("version", version); - } catch (Exception e) { - data.addProperty("version", "0.0.1"); - log.debug("Could not get version for {}, using default", src.getClass().getName()); - } - - // Add itemName pattern - a common property for all resource conditions - data.addProperty("itemName", src.getItemName()); - - // Add data to wrapper - result.add("data", data); - - return result; - } - - @Override - public ResourceCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - // This base adapter doesn't handle deserialization directly - // It's expected that specific subclass adapters will handle their own types - throw new JsonParseException("Cannot deserialize abstract ResourceCondition directly"); - } - - /** - * Helper method to extract a Pattern object from JSON - */ - protected Pattern deserializePattern(JsonObject jsonObject, String fieldName) { - if (jsonObject.has(fieldName)) { - String patternStr = jsonObject.get(fieldName).getAsString(); - try { - return Pattern.compile(patternStr); - } catch (Exception e) { - log.warn("Failed to parse pattern: {}", patternStr, e); - } - } - return null; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ui/ResourceConditionPanelUtil.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ui/ResourceConditionPanelUtil.java deleted file mode 100644 index 62052d30cfc..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/resource/ui/ResourceConditionPanelUtil.java +++ /dev/null @@ -1,1405 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ui; - -import java.util.ArrayList; -import java.util.List; - -import javax.swing.BorderFactory; -import javax.swing.ButtonGroup; -import javax.swing.DefaultListModel; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JPanel; -import javax.swing.JRadioButton; -import javax.swing.JScrollPane; -import javax.swing.JSpinner; -import javax.swing.JTextField; -import javax.swing.SpinnerNumberModel; -import javax.swing.border.TitledBorder; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Skill; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.BankItemCountCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.GatheredResourceCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.InventoryItemCountCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.LootItemCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ProcessItemCondition; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Dimension; -import java.awt.FlowLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridLayout; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LogicalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.OrCondition; - - - - -@Slf4j -public class ResourceConditionPanelUtil { - /** - * Creates a panel for configuring Inventory Item Count conditions - */ - public static void createInventoryItemCountPanel(JPanel panel, GridBagConstraints gbc) { - // Title - JLabel titleLabel = new JLabel("Inventory Item Count:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // Item name input - gbc.gridy++; - JPanel itemNamePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - itemNamePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel itemNameLabel = new JLabel("Item Name (leave empty for any):"); - itemNameLabel.setForeground(Color.WHITE); - itemNamePanel.add(itemNameLabel); - - JTextField itemNameField = new JTextField(15); - itemNameField.setForeground(Color.WHITE); - itemNameField.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - itemNamePanel.add(itemNameField); - - panel.add(itemNamePanel, gbc); - - // Count range - gbc.gridy++; - JPanel countPanel = new JPanel(new GridLayout(2, 2, 5, 5)); - countPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel minCountLabel = new JLabel("Min Count:"); - minCountLabel.setForeground(Color.WHITE); - countPanel.add(minCountLabel); - - SpinnerNumberModel minCountModel = new SpinnerNumberModel(10, 0, Integer.MAX_VALUE, 1); - JSpinner minCountSpinner = new JSpinner(minCountModel); - countPanel.add(minCountSpinner); - - JLabel maxCountLabel = new JLabel("Max Count:"); - maxCountLabel.setForeground(Color.WHITE); - countPanel.add(maxCountLabel); - - SpinnerNumberModel maxCountModel = new SpinnerNumberModel(10, 0, Integer.MAX_VALUE, 1); - JSpinner maxCountSpinner = new JSpinner(maxCountModel); - countPanel.add(maxCountSpinner); - - // Link the min and max spinners - minCountSpinner.addChangeListener(e -> { - int minValue = (Integer) minCountSpinner.getValue(); - int maxValue = (Integer) maxCountSpinner.getValue(); - if (minValue > maxValue) { - maxCountSpinner.setValue(minValue); - } - }); - - maxCountSpinner.addChangeListener(e -> { - int minValue = (Integer) minCountSpinner.getValue(); - int maxValue = (Integer) maxCountSpinner.getValue(); - if (maxValue < minValue) { - minCountSpinner.setValue(maxValue); - } - }); - - panel.add(countPanel, gbc); - - // Options panel - gbc.gridy++; - JPanel optionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - optionsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JCheckBox includeNotedCheckbox = new JCheckBox("Include noted items"); - includeNotedCheckbox.setForeground(Color.WHITE); - includeNotedCheckbox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - includeNotedCheckbox.setSelected(true); - optionsPanel.add(includeNotedCheckbox); - - - - panel.add(optionsPanel, gbc); - - // Add a helpful description - gbc.gridy++; - JLabel descriptionLabel = new JLabel("Plugin will stop when you have the target number of items"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - gbc.gridy++; - JLabel regexLabel = new JLabel("Item name supports regex patterns (.*bones.*)"); - regexLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - regexLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(regexLabel, gbc); - - // Store the components for later access - panel.putClientProperty("itemNameField", itemNameField); - panel.putClientProperty("minCountSpinner", minCountSpinner); - panel.putClientProperty("maxCountSpinner", maxCountSpinner); - panel.putClientProperty("includeNotedCheckbox", includeNotedCheckbox); - } - public static InventoryItemCountCondition createInventoryItemCountCondition(JPanel configPanel) { - JTextField itemNameField = (JTextField) configPanel.getClientProperty("itemNameField"); - JSpinner minCountSpinner = (JSpinner) configPanel.getClientProperty("minCountSpinner"); - JSpinner maxCountSpinner = (JSpinner) configPanel.getClientProperty("maxCountSpinner"); - JCheckBox includeNotedCheckbox = (JCheckBox) configPanel.getClientProperty("includeNotedCheckbox"); - - - String itemName = itemNameField.getText().trim(); - int minCount = (Integer) minCountSpinner.getValue(); - int maxCount = (Integer) maxCountSpinner.getValue(); - boolean includeNoted = includeNotedCheckbox.isSelected(); - - if (minCount != maxCount) { - return InventoryItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(minCount) - .targetCountMax(maxCount) - .includeNoted(includeNoted) - .build(); - } else { - return InventoryItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(minCount) - .targetCountMax(minCount) - .includeNoted(includeNoted) - .build(); - } - } - /** - * Creates a panel for configuring Bank Item Count conditions - */ - public static void createBankItemCountPanel(JPanel panel, GridBagConstraints gbc, JPanel configPanel) { - // Title - JLabel titleLabel = new JLabel("Bank Item Count:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // Item name input - gbc.gridy++; - JPanel itemNamePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - itemNamePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel itemNameLabel = new JLabel("Item Name (leave empty for total items):"); - itemNameLabel.setForeground(Color.WHITE); - itemNamePanel.add(itemNameLabel); - - JTextField itemNameField = new JTextField(15); - itemNameField.setForeground(Color.WHITE); - itemNameField.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - itemNamePanel.add(itemNameField); - - panel.add(itemNamePanel, gbc); - - // Count range - gbc.gridy++; - JPanel countPanel = new JPanel(new GridLayout(2, 2, 5, 5)); - countPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel minCountLabel = new JLabel("Min Count:"); - minCountLabel.setForeground(Color.WHITE); - countPanel.add(minCountLabel); - - SpinnerNumberModel minCountModel = new SpinnerNumberModel(100, 0, Integer.MAX_VALUE, 10); - JSpinner minCountSpinner = new JSpinner(minCountModel); - countPanel.add(minCountSpinner); - - JLabel maxCountLabel = new JLabel("Max Count:"); - maxCountLabel.setForeground(Color.WHITE); - countPanel.add(maxCountLabel); - - SpinnerNumberModel maxCountModel = new SpinnerNumberModel(100, 0, Integer.MAX_VALUE, 10); - JSpinner maxCountSpinner = new JSpinner(maxCountModel); - countPanel.add(maxCountSpinner); - - // Link the min and max spinners - minCountSpinner.addChangeListener(e -> { - int minValue = (Integer) minCountSpinner.getValue(); - int maxValue = (Integer) maxCountSpinner.getValue(); - if (minValue > maxValue) { - maxCountSpinner.setValue(minValue); - } - }); - - maxCountSpinner.addChangeListener(e -> { - int minValue = (Integer) minCountSpinner.getValue(); - int maxValue = (Integer) maxCountSpinner.getValue(); - if (maxValue < minValue) { - minCountSpinner.setValue(maxValue); - } - }); - - panel.add(countPanel, gbc); - - // Options panel - gbc.gridy++; - JPanel optionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - optionsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - - - panel.add(optionsPanel, gbc); - - // Add a helpful description - gbc.gridy++; - JLabel descriptionLabel = new JLabel("Plugin will stop when you have the target number of items in bank"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - gbc.gridy++; - JLabel regexLabel = new JLabel("Item name supports regex patterns (.*rune.*)"); - regexLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - regexLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(regexLabel, gbc); - - // Store the components for later access - configPanel.putClientProperty("bankItemNameField", itemNameField); - configPanel.putClientProperty("bankMinCountSpinner", minCountSpinner); - configPanel.putClientProperty("bankMaxCountSpinner", maxCountSpinner); - } - - - - public static BankItemCountCondition createBankItemCountCondition(JPanel configPanel) { - JTextField itemNameField = (JTextField) configPanel.getClientProperty("bankItemNameField"); - JSpinner minCountSpinner = (JSpinner) configPanel.getClientProperty("bankMinCountSpinner"); - JSpinner maxCountSpinner = (JSpinner) configPanel.getClientProperty("bankMaxCountSpinner"); - - - String itemName = itemNameField.getText().trim(); - int minCount = (Integer) minCountSpinner.getValue(); - int maxCount = (Integer) maxCountSpinner.getValue(); - - if (minCount != maxCount) { - return BankItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(minCount) - .targetCountMax(maxCount) - .build(); - } else { - return BankItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(minCount) - .targetCountMax(minCount) - .build(); - } - } - /** - * Creates a panel for configuring item collection conditions with enhanced options. - */ - public static void createItemConfigPanel(JPanel panel, GridBagConstraints gbc, JPanel configPanel, boolean stopConditionPanel) { - // Section title - JLabel titleLabel = new JLabel(stopConditionPanel ? "Collect Items to Stop:" : "Required Items to Start:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - panel.add(titleLabel, gbc); - - gbc.gridy++; - - // Item names input - JLabel itemsLabel = new JLabel("Item Names (comma-separated):"); - itemsLabel.setForeground(Color.WHITE); - panel.add(itemsLabel, gbc); - - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - JTextField itemsField = new JTextField(); - itemsField.setToolTipText("Item names are automatically detected as exact matches or regex patterns:
" + - "- Simple names: 'Dragon scimitar', 'Bones', 'Shark' → exact match
" + - "- Pattern names: 'Dragon.*', '^Rune.*sword$', '.*bones.*' → regex match
" + - "- Multiple items: 'Bones, Dragon.*' → mixed exact and regex matching"); - panel.add(itemsField, gbc); - - // Matching mode information - gbc.gridy++; - JPanel matchingPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - matchingPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel matchingInfoLabel = new JLabel("Item name matching: Automatic detection (exact names or regex patterns)"); - matchingInfoLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - matchingInfoLabel.setFont(FontManager.getRunescapeSmallFont()); - matchingInfoLabel.setToolTipText("Item names are automatically detected as either exact matches or regex patterns.
" + - "Simple names like 'Dragon scimitar' are matched exactly.
" + - "Patterns like 'Dragon.*' or '^Rune.*sword$' are treated as regex."); - matchingPanel.add(matchingInfoLabel); - - panel.add(matchingPanel, gbc); - - // Logical operator selection (AND/OR) - only visible with multiple items - gbc.gridy++; - JPanel logicalPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - logicalPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel logicalLabel = new JLabel("Multiple items logic:"); - logicalLabel.setForeground(Color.WHITE); - logicalPanel.add(logicalLabel); - - JRadioButton andRadioButton = new JRadioButton("Need ALL items (AND)"); - andRadioButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - andRadioButton.setForeground(Color.WHITE); - andRadioButton.setSelected(true); - andRadioButton.setToolTipText("All specified items must be collected to satisfy the condition"); - - JRadioButton orRadioButton = new JRadioButton("Need ANY item (OR)"); - orRadioButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - orRadioButton.setForeground(Color.WHITE); - orRadioButton.setToolTipText("Any of the specified items will satisfy the condition"); - - ButtonGroup logicGroup = new ButtonGroup(); - logicGroup.add(andRadioButton); - logicGroup.add(orRadioButton); - - logicalPanel.add(andRadioButton); - logicalPanel.add(orRadioButton); - - // Initially hide the panel - will be shown only when there are commas in the text field - logicalPanel.setVisible(false); - panel.add(logicalPanel, gbc); - - // Listen for changes in the text field to show/hide the logical panel - itemsField.getDocument().addDocumentListener(new DocumentListener() { - private void updateLogicalPanelVisibility() { - String text = itemsField.getText().trim(); - boolean hasMultipleItems = text.contains(","); - logicalPanel.setVisible(hasMultipleItems); - panel.revalidate(); - panel.repaint(); - } - - @Override - public void insertUpdate(DocumentEvent e) { - updateLogicalPanelVisibility(); - } - - @Override - public void removeUpdate(DocumentEvent e) { - updateLogicalPanelVisibility(); - } - - @Override - public void changedUpdate(DocumentEvent e) { - updateLogicalPanelVisibility(); - } - }); - - // Target amount panel - gbc.gridy++; - JPanel amountPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - amountPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel amountLabel = new JLabel("Target Amount:"); - amountLabel.setForeground(Color.WHITE); - amountPanel.add(amountLabel); - - JCheckBox sameAmountCheckBox = new JCheckBox("Same amount for all items"); - sameAmountCheckBox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - sameAmountCheckBox.setForeground(Color.WHITE); - sameAmountCheckBox.setSelected(true); - sameAmountCheckBox.setVisible(false); // Only show with multiple items - sameAmountCheckBox.setToolTipText("Use the same target amount for all items"); - amountPanel.add(sameAmountCheckBox); - - panel.add(amountPanel, gbc); - - // Amount configuration panel (always visible) - gbc.gridy++; - JPanel amountConfigPanel = new JPanel(new GridLayout(1, 4, 5, 0)); - amountConfigPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel minLabel = new JLabel("Min:"); - minLabel.setForeground(Color.WHITE); - amountConfigPanel.add(minLabel); - - SpinnerNumberModel minModel = new SpinnerNumberModel(1, 1, Integer.MAX_VALUE, 1); - JSpinner minSpinner = new JSpinner(minModel); - amountConfigPanel.add(minSpinner); - - JLabel maxLabel = new JLabel("Max:"); - maxLabel.setForeground(Color.WHITE); - amountConfigPanel.add(maxLabel); - - SpinnerNumberModel maxModel = new SpinnerNumberModel(1, 1, Integer.MAX_VALUE, 1); - JSpinner maxSpinner = new JSpinner(maxModel); - amountConfigPanel.add(maxSpinner); - - panel.add(amountConfigPanel, gbc); - - // Additional options panel - gbc.gridy++; - JPanel optionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - optionsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JCheckBox includeNotedCheckBox = new JCheckBox("Include noted items"); - includeNotedCheckBox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - includeNotedCheckBox.setForeground(Color.WHITE); - includeNotedCheckBox.setSelected(true); - includeNotedCheckBox.setToolTipText("If checked, will also count noted versions of the items
" + - "For example, 'Bones' would match both normal and noted bones"); - optionsPanel.add(includeNotedCheckBox); - - JCheckBox includeNoneOwnerCheckBox = new JCheckBox("Include unowned items"); - includeNoneOwnerCheckBox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - includeNoneOwnerCheckBox.setForeground(Color.WHITE); - includeNoneOwnerCheckBox.setToolTipText("If checked, items that don't belong to you will also be tracked
" + - "By default, only items that belong to you are counted"); - optionsPanel.add(includeNoneOwnerCheckBox); - - panel.add(optionsPanel, gbc); - - - - - // Link sameAmountCheckBox visibility to comma presence - itemsField.getDocument().addDocumentListener(new DocumentListener() { - private void updateCheckBoxVisibility() { - String text = itemsField.getText().trim(); - boolean hasMultipleItems = text.contains(","); - sameAmountCheckBox.setVisible(hasMultipleItems); - panel.revalidate(); - panel.repaint(); - } - - @Override - public void insertUpdate(DocumentEvent e) { - updateCheckBoxVisibility(); - } - - @Override - public void removeUpdate(DocumentEvent e) { - updateCheckBoxVisibility(); - } - - @Override - public void changedUpdate(DocumentEvent e) { - updateCheckBoxVisibility(); - } - }); - - // Add value change listeners for min/max validation - minSpinner.addChangeListener(e -> { - int min = (Integer) minSpinner.getValue(); - int max = (Integer) maxSpinner.getValue(); - - if (min > max) { - maxSpinner.setValue(min); - } - }); - - maxSpinner.addChangeListener(e -> { - int min = (Integer) minSpinner.getValue(); - int max = (Integer) maxSpinner.getValue(); - - if (max < min) { - minSpinner.setValue(max); - } - }); - - // Description - gbc.gridy++; - JLabel descriptionLabel; - if (stopConditionPanel) { - descriptionLabel = new JLabel("Plugin will stop when target amount of items is collected"); - } else { - descriptionLabel = new JLabel("Plugin will only start when you have the required items"); - } - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Add help text - gbc.gridy++; - JLabel helpLabel = new JLabel("Tip: Use simple names like 'Dragon scimitar' or regex patterns like 'Dragon.*'"); - helpLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - helpLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(helpLabel, gbc); - - // Store components - configPanel.putClientProperty("itemsField", itemsField); - configPanel.putClientProperty("andRadioButton", andRadioButton); - configPanel.putClientProperty("sameAmountCheckBox", sameAmountCheckBox); - configPanel.putClientProperty("minAmountSpinner", minSpinner); - configPanel.putClientProperty("maxAmountSpinner", maxSpinner); - configPanel.putClientProperty("includeNotedCheckBox", includeNotedCheckBox); - configPanel.putClientProperty("includeNoneOwnerCheckBox", includeNoneOwnerCheckBox); - } - /** - * Creates an appropriate LootItemCondition or logical condition based on user input - */ - public static Condition createItemCondition(JPanel configPanel) { - JTextField itemsField = (JTextField) configPanel.getClientProperty("itemsField"); - JRadioButton andRadioButton = (JRadioButton) configPanel.getClientProperty("andRadioButton"); - JCheckBox sameAmountCheckBox = (JCheckBox) configPanel.getClientProperty("sameAmountCheckBox"); - JSpinner minAmountSpinner = (JSpinner) configPanel.getClientProperty("minAmountSpinner"); - JSpinner maxAmountSpinner = (JSpinner) configPanel.getClientProperty("maxAmountSpinner"); - JCheckBox includeNotedCheckBox = (JCheckBox) configPanel.getClientProperty("includeNotedCheckBox"); - JCheckBox includeNoneOwnerCheckBox = (JCheckBox) configPanel.getClientProperty("includeNoneOwnerCheckBox"); - - // Handle potential component errors - if (itemsField == null) { - log.error("Items field component not found"); - return null; - } - - // Get configuration values - boolean includeNoted = includeNotedCheckBox != null && includeNotedCheckBox.isSelected(); - boolean includeNoneOwner = includeNoneOwnerCheckBox != null && includeNoneOwnerCheckBox.isSelected(); - - int minAmount = minAmountSpinner != null ? (Integer) minAmountSpinner.getValue() : 1; - int maxAmount = maxAmountSpinner != null ? (Integer) maxAmountSpinner.getValue() : minAmount; - - // Ensure max >= min - if (maxAmount < minAmount) { - maxAmount = minAmount; - } - - String itemNamesString = itemsField.getText().trim(); - if (itemNamesString.isEmpty()) { - return null; // Invalid item name - } - - // Split by comma and trim each item name - String[] itemNamesArray = itemNamesString.split(","); - List itemNames = new ArrayList<>(); - - for (String itemName : itemNamesArray) { - itemName = itemName.trim(); - if (!itemName.isEmpty()) { - // Add the item name as-is - the ResourceCondition.createItemPattern() method - // will automatically detect if it's a regex pattern or exact match - itemNames.add(itemName); - } - } - - if (itemNames.isEmpty()) { - return null; - } - - // If only one item, create a simple LootItemCondition - if (itemNames.size() == 1) { - return LootItemCondition.createRandomized(itemNames.get(0), minAmount, maxAmount, includeNoted, includeNoneOwner); - } - - // For multiple items, create a logical condition based on selection - boolean useAndLogic = andRadioButton == null || andRadioButton.isSelected(); - boolean useSameAmount = sameAmountCheckBox != null && sameAmountCheckBox.isSelected(); - - if (useSameAmount) { - if (useAndLogic) { - return LootItemCondition.createAndCondition(itemNames, minAmount, maxAmount, includeNoted, includeNoneOwner); - } else { - return LootItemCondition.createOrCondition(itemNames, minAmount, maxAmount, includeNoted, includeNoneOwner); - } - } else { - // Create lists of min/max amounts for each item (currently using same values for all) - List minAmounts = new ArrayList<>(); - List maxAmounts = new ArrayList<>(); - - for (int i = 0; i < itemNames.size(); i++) { - minAmounts.add(minAmount); - maxAmounts.add(maxAmount); - } - - if (useAndLogic) { - return LootItemCondition.createAndCondition(itemNames, minAmounts, maxAmounts, includeNoted, includeNoneOwner); - } else { - return LootItemCondition.createOrCondition(itemNames, minAmounts, maxAmounts, includeNoted, includeNoneOwner); - } - } - } - - /** - * Creates a panel for configuring GatheredResourceCondition - */ - public static void createGatheredResourcePanel(JPanel panel, GridBagConstraints gbc, JPanel configPanel) { - // Section title - JLabel titleLabel = new JLabel("Gathered Resource Condition:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // Item name input - gbc.gridy++; - JPanel itemNamePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - itemNamePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel itemNameLabel = new JLabel("Resource Name:"); - itemNameLabel.setForeground(Color.WHITE); - itemNamePanel.add(itemNameLabel); - - JTextField itemNameField = new JTextField(15); - itemNameField.setForeground(Color.WHITE); - itemNameField.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - itemNamePanel.add(itemNameField); - - panel.add(itemNamePanel, gbc); - - // Resource type selection (helps with skill detection) - gbc.gridy++; - JPanel resourceTypePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - resourceTypePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel resourceTypeLabel = new JLabel("Resource Type:"); - resourceTypeLabel.setForeground(Color.WHITE); - resourceTypePanel.add(resourceTypeLabel); - - String[] resourceTypes = {"Auto-detect", "Mining", "Fishing", "Woodcutting", "Farming", "Hunter"}; - JComboBox resourceTypeComboBox = new JComboBox<>(resourceTypes); - resourceTypePanel.add(resourceTypeComboBox); - - panel.add(resourceTypePanel, gbc); - - // Count panel - gbc.gridy++; - JPanel countPanel = new JPanel(new GridLayout(2, 2, 5, 5)); - countPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel countLabel = new JLabel("Target Count:"); - countLabel.setForeground(Color.WHITE); - countPanel.add(countLabel); - - - - JPanel minMaxPanel = new JPanel(new GridLayout(1, 4, 5, 0)); - minMaxPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel minLabel = new JLabel("Min:"); - minLabel.setForeground(Color.WHITE); - minMaxPanel.add(minLabel); - - SpinnerNumberModel minModel = new SpinnerNumberModel(50, 1, 10000, 10); - JSpinner minSpinner = new JSpinner(minModel); - minMaxPanel.add(minSpinner); - - JLabel maxLabel = new JLabel("Max:"); - maxLabel.setForeground(Color.WHITE); - minMaxPanel.add(maxLabel); - - SpinnerNumberModel maxModel = new SpinnerNumberModel(150, 1, 10000, 10); - JSpinner maxSpinner = new JSpinner(maxModel); - minMaxPanel.add(maxSpinner); - - minMaxPanel.setVisible(false); - countPanel.add(minMaxPanel); - - - - panel.add(countPanel, gbc); - - // Options panel - gbc.gridy++; - JPanel optionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - optionsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JCheckBox includeNotedCheckbox = new JCheckBox("Include noted items"); - includeNotedCheckbox.setForeground(Color.WHITE); - includeNotedCheckbox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - includeNotedCheckbox.setSelected(true); - includeNotedCheckbox.setToolTipText("Count both noted and unnoted versions of the gathered resource"); - optionsPanel.add(includeNotedCheckbox); - - panel.add(optionsPanel, gbc); - - // Example resources by type - gbc.gridy++; - JPanel examplesPanel = new JPanel(new BorderLayout()); - examplesPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel examplesLabel = new JLabel("Examples by type:"); - examplesLabel.setForeground(Color.WHITE); - examplesPanel.add(examplesLabel, BorderLayout.NORTH); - - JLabel examplesContentLabel = new JLabel("Mining: Coal, Iron ore, Gold ore
" + - "Fishing: Shrimp, Trout, Tuna, Shark
" + - "Woodcutting: Logs, Oak logs, Yew logs
" + - "Farming: Potato, Strawberry, Herbs
" + - "Hunter: Bird meat, Rabbit, Chinchompa"); - examplesContentLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - examplesContentLabel.setFont(FontManager.getRunescapeSmallFont()); - examplesPanel.add(examplesContentLabel, BorderLayout.CENTER); - - panel.add(examplesPanel, gbc); - - // Description - gbc.gridy++; - JLabel descriptionLabel = new JLabel("Tracks resources gathered from skilling activities"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Store components for later access - configPanel.putClientProperty("gatheredItemNameField", itemNameField); - configPanel.putClientProperty("gatheredResourceType", resourceTypeComboBox); - configPanel.putClientProperty("gatheredMinSpinner", minSpinner); - configPanel.putClientProperty("gatheredMaxSpinner", maxSpinner); - configPanel.putClientProperty("gatheredIncludeNotedCheckbox", includeNotedCheckbox); - } - - /** - * Creates a GatheredResourceCondition from the panel configuration - */ - @SuppressWarnings("unchecked") - public static GatheredResourceCondition createGatheredResourceCondition(JPanel configPanel) { - JTextField itemNameField = (JTextField) configPanel.getClientProperty("gatheredItemNameField"); - JComboBox resourceTypeComboBox = (JComboBox) configPanel.getClientProperty("gatheredResourceType"); - JSpinner minSpinner = (JSpinner) configPanel.getClientProperty("gatheredMinSpinner"); - JSpinner maxSpinner = (JSpinner) configPanel.getClientProperty("gatheredMaxSpinner"); - JCheckBox includeNotedCheckbox = (JCheckBox) configPanel.getClientProperty("gatheredIncludeNotedCheckbox"); - - // Get item name - String itemName = itemNameField.getText().trim(); - if (itemName.isEmpty()) { - itemName = "resources"; // Default generic name - } - - // Get relevant skills based on resource type selection - List relevantSkills = new ArrayList<>(); - String selectedResourceType = (String) resourceTypeComboBox.getSelectedItem(); - - if (!"Auto-detect".equals(selectedResourceType)) { - // Add specific skill based on selection - switch (selectedResourceType) { - case "Mining": - relevantSkills.add(Skill.MINING); - break; - case "Fishing": - relevantSkills.add(Skill.FISHING); - break; - case "Woodcutting": - relevantSkills.add(Skill.WOODCUTTING); - break; - case "Farming": - relevantSkills.add(Skill.FARMING); - break; - case "Hunter": - relevantSkills.add(Skill.HUNTER); - break; - } - } - - // Get target count - int minCount, maxCount; - - minCount = (Integer) minSpinner.getValue(); - maxCount = (Integer) maxSpinner.getValue(); - - - - boolean includeNoted = includeNotedCheckbox.isSelected(); - - // Create the condition - return GatheredResourceCondition.builder() - .itemName(itemName) - .targetCountMin(minCount) - .targetCountMax(maxCount) - .includeNoted(includeNoted) - .relevantSkills(relevantSkills.isEmpty() ? null : relevantSkills) - .build(); - } - - /** - * Creates a panel for configuring ProcessItemCondition - */ -public static void createProcessItemPanel(JPanel panel, GridBagConstraints gbc, JPanel configPanel) { - // Section title - JLabel titleLabel = new JLabel("Process Item Condition:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // Tracking mode selection - gbc.gridy++; - JPanel trackingPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - trackingPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel trackingLabel = new JLabel("Tracking Mode:"); - trackingLabel.setForeground(Color.WHITE); - trackingPanel.add(trackingLabel); - - ButtonGroup trackingGroup = new ButtonGroup(); - - JRadioButton sourceButton = new JRadioButton("Source Consumption"); - sourceButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - sourceButton.setForeground(Color.WHITE); - sourceButton.setSelected(true); - trackingGroup.add(sourceButton); - trackingPanel.add(sourceButton); - - JRadioButton targetButton = new JRadioButton("Target Production"); - targetButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - targetButton.setForeground(Color.WHITE); - trackingGroup.add(targetButton); - trackingPanel.add(targetButton); - - JRadioButton eitherButton = new JRadioButton("Either"); - eitherButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - eitherButton.setForeground(Color.WHITE); - trackingGroup.add(eitherButton); - trackingPanel.add(eitherButton); - - JRadioButton bothButton = new JRadioButton("Both"); - bothButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - bothButton.setForeground(Color.WHITE); - trackingGroup.add(bothButton); - trackingPanel.add(bothButton); - - panel.add(trackingPanel, gbc); - - // Source items panel - gbc.gridy++; - JPanel sourcePanel = new JPanel(new BorderLayout()); - sourcePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - sourcePanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Source Items (items consumed)", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeSmallFont(), - Color.WHITE - )); - - JPanel sourceInputPanel = new JPanel(new GridLayout(0, 3, 5, 5)); - sourceInputPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel sourceNameLabel = new JLabel("Item Name"); - sourceNameLabel.setForeground(Color.WHITE); - sourceInputPanel.add(sourceNameLabel); - - JLabel sourceQuantityLabel = new JLabel("Quantity Per Process"); - sourceQuantityLabel.setForeground(Color.WHITE); - sourceInputPanel.add(sourceQuantityLabel); - - // Empty label for alignment - sourceInputPanel.add(new JLabel("")); - - JTextField sourceNameField = new JTextField(); - sourceInputPanel.add(sourceNameField); - - SpinnerNumberModel sourceQuantityModel = new SpinnerNumberModel(1, 1, 100, 1); - JSpinner sourceQuantitySpinner = new JSpinner(sourceQuantityModel); - sourceInputPanel.add(sourceQuantitySpinner); - - JButton addSourceButton = new JButton("+"); - addSourceButton.setBackground(ColorScheme.BRAND_ORANGE); - addSourceButton.setForeground(Color.WHITE); - sourceInputPanel.add(addSourceButton); - - sourcePanel.add(sourceInputPanel, BorderLayout.NORTH); - - // Source items list (will be populated dynamically) - DefaultListModel sourceItemsModel = new DefaultListModel<>(); - JList sourceItemsList = new JList<>(sourceItemsModel); - sourceItemsList.setBackground(ColorScheme.DARKER_GRAY_COLOR); - sourceItemsList.setForeground(Color.WHITE); - JScrollPane sourceScrollPane = new JScrollPane(sourceItemsList); - sourceScrollPane.setPreferredSize(new Dimension(0, 80)); - sourcePanel.add(sourceScrollPane, BorderLayout.CENTER); - - // Add source item button action - addSourceButton.addActionListener(e -> { - String itemName = sourceNameField.getText().trim(); - if (!itemName.isEmpty()) { - int quantity = (Integer) sourceQuantitySpinner.getValue(); - sourceItemsModel.addElement(quantity + "x " + itemName); - sourceNameField.setText(""); - } - }); - - panel.add(sourcePanel, gbc); - - // Target items panel (similar structure to source panel) - gbc.gridy++; - JPanel targetPanel = new JPanel(new BorderLayout()); - targetPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - targetPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Target Items (items produced)", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeSmallFont(), - Color.WHITE - )); - - JPanel targetInputPanel = new JPanel(new GridLayout(0, 3, 5, 5)); - targetInputPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel targetNameLabel = new JLabel("Item Name"); - targetNameLabel.setForeground(Color.WHITE); - targetInputPanel.add(targetNameLabel); - - JLabel targetQuantityLabel = new JLabel("Quantity Per Process"); - targetQuantityLabel.setForeground(Color.WHITE); - targetInputPanel.add(targetQuantityLabel); - - // Empty label for alignment - targetInputPanel.add(new JLabel("")); - - JTextField targetNameField = new JTextField(); - targetInputPanel.add(targetNameField); - - SpinnerNumberModel targetQuantityModel = new SpinnerNumberModel(1, 1, 100, 1); - JSpinner targetQuantitySpinner = new JSpinner(targetQuantityModel); - targetInputPanel.add(targetQuantitySpinner); - - JButton addTargetButton = new JButton("+"); - addTargetButton.setBackground(ColorScheme.BRAND_ORANGE); - addTargetButton.setForeground(Color.WHITE); - targetInputPanel.add(addTargetButton); - - targetPanel.add(targetInputPanel, BorderLayout.NORTH); - - // Target items list - DefaultListModel targetItemsModel = new DefaultListModel<>(); - JList targetItemsList = new JList<>(targetItemsModel); - targetItemsList.setBackground(ColorScheme.DARKER_GRAY_COLOR); - targetItemsList.setForeground(Color.WHITE); - JScrollPane targetScrollPane = new JScrollPane(targetItemsList); - targetScrollPane.setPreferredSize(new Dimension(0, 80)); - targetPanel.add(targetScrollPane, BorderLayout.CENTER); - - // Add target item button action - addTargetButton.addActionListener(e -> { - String itemName = targetNameField.getText().trim(); - if (!itemName.isEmpty()) { - int quantity = (Integer) targetQuantitySpinner.getValue(); - targetItemsModel.addElement(quantity + "x " + itemName); - targetNameField.setText(""); - } - }); - - panel.add(targetPanel, gbc); - - // Count panel - gbc.gridy++; - JPanel countPanel = new JPanel(new GridLayout(2, 3, 5, 5)); - countPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel countLabel = new JLabel("Target Process Count:"); - countLabel.setForeground(Color.WHITE); - countPanel.add(countLabel); - - // Empty space - countPanel.add(new JLabel("")); - - - - JPanel minMaxPanel = new JPanel(new GridLayout(1, 4, 5, 0)); - minMaxPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel minLabel = new JLabel("Min:"); - minLabel.setForeground(Color.WHITE); - minMaxPanel.add(minLabel); - - SpinnerNumberModel minModel = new SpinnerNumberModel(25, 1, 10000, 1); - JSpinner minSpinner = new JSpinner(minModel); - minMaxPanel.add(minSpinner); - - JLabel maxLabel = new JLabel("Max:"); - maxLabel.setForeground(Color.WHITE); - minMaxPanel.add(maxLabel); - - SpinnerNumberModel maxModel = new SpinnerNumberModel(75, 1, 10000, 1); - JSpinner maxSpinner = new JSpinner(maxModel); - minMaxPanel.add(maxSpinner); - - minMaxPanel.setVisible(false); - - - - countPanel.add(minMaxPanel); - - panel.add(countPanel, gbc); - - // Description - gbc.gridy++; - JLabel descriptionLabel = new JLabel("Tracks items being processed (crafting, cooking, etc.)"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Store components for later access - configPanel.putClientProperty("procSourceRadio", sourceButton); - configPanel.putClientProperty("procTargetRadio", targetButton); - configPanel.putClientProperty("procEitherRadio", eitherButton); - configPanel.putClientProperty("procBothRadio", bothButton); - configPanel.putClientProperty("procSourceItemsModel", sourceItemsModel); - configPanel.putClientProperty("procTargetItemsModel", targetItemsModel); - configPanel.putClientProperty("procMinSpinner", minSpinner); - configPanel.putClientProperty("procMaxSpinner", maxSpinner); -} - -/** - * Creates a ProcessItemCondition from the panel configuration - */ -@SuppressWarnings("unchecked") -public static ProcessItemCondition createProcessItemCondition(JPanel configPanel) { - JRadioButton sourceButton = (JRadioButton) configPanel.getClientProperty("procSourceRadio"); - JRadioButton targetButton = (JRadioButton) configPanel.getClientProperty("procTargetRadio"); - JRadioButton eitherButton = (JRadioButton) configPanel.getClientProperty("procEitherRadio"); - - DefaultListModel sourceItemsModel = (DefaultListModel) configPanel.getClientProperty("procSourceItemsModel"); - DefaultListModel targetItemsModel = (DefaultListModel) configPanel.getClientProperty("procTargetItemsModel"); - - JSpinner minSpinner = (JSpinner) configPanel.getClientProperty("procMinSpinner"); - JSpinner maxSpinner = (JSpinner) configPanel.getClientProperty("procMaxSpinner"); - - // Determine tracking mode - ProcessItemCondition.TrackingMode trackingMode; - if (sourceButton.isSelected()) { - trackingMode = ProcessItemCondition.TrackingMode.SOURCE_CONSUMPTION; - } else if (targetButton.isSelected()) { - trackingMode = ProcessItemCondition.TrackingMode.TARGET_PRODUCTION; - } else if (eitherButton.isSelected()) { - trackingMode = ProcessItemCondition.TrackingMode.EITHER; - } else { - trackingMode = ProcessItemCondition.TrackingMode.BOTH; - } - - // Parse source items - List sourceItems = new ArrayList<>(); - for (int i = 0; i < sourceItemsModel.getSize(); i++) { - String entry = sourceItemsModel.getElementAt(i); - // Parse "Nx ItemName" format - int xIndex = entry.indexOf('x'); - if (xIndex > 0) { - try { - int quantity = Integer.parseInt(entry.substring(0, xIndex).trim()); - String itemName = entry.substring(xIndex + 1).trim(); - sourceItems.add(new ProcessItemCondition.ItemTracker(itemName, quantity)); - } catch (NumberFormatException e) { - // If parsing fails, default to quantity 1 - sourceItems.add(new ProcessItemCondition.ItemTracker(entry, 1)); - } - } else { - sourceItems.add(new ProcessItemCondition.ItemTracker(entry, 1)); - } - } - - // Parse target items - List targetItems = new ArrayList<>(); - for (int i = 0; i < targetItemsModel.getSize(); i++) { - String entry = targetItemsModel.getElementAt(i); - // Parse "Nx ItemName" format - int xIndex = entry.indexOf('x'); - if (xIndex > 0) { - try { - int quantity = Integer.parseInt(entry.substring(0, xIndex).trim()); - String itemName = entry.substring(xIndex + 1).trim(); - targetItems.add(new ProcessItemCondition.ItemTracker(itemName, quantity)); - } catch (NumberFormatException e) { - // If parsing fails, default to quantity 1 - targetItems.add(new ProcessItemCondition.ItemTracker(entry, 1)); - } - } else { - targetItems.add(new ProcessItemCondition.ItemTracker(entry, 1)); - } - } - - // Get target count - int minCount, maxCount; - - minCount = (Integer) minSpinner.getValue(); - maxCount = (Integer) maxSpinner.getValue(); - - - - - // Build the condition - return ProcessItemCondition.builder() - .sourceItems(sourceItems) - .targetItems(targetItems) - .trackingMode(trackingMode) - .targetCountMin(minCount) - .targetCountMax(maxCount) - .build(); -} - -/** - * Sets up panel with values from an existing condition - * - * @param panel The panel containing the UI components - * @param condition The resource condition to read values from - */ -public static void setupResourceCondition(JPanel panel, Condition condition) { - if (condition == null) { - return; - } - - if (condition instanceof InventoryItemCountCondition) { - setupInventoryItemCountCondition(panel, (InventoryItemCountCondition) condition); - } else if (condition instanceof BankItemCountCondition) { - setupBankItemCountCondition(panel, (BankItemCountCondition) condition); - } else if (condition instanceof LootItemCondition || condition instanceof LogicalCondition) { - setupLootItemCondition(panel, condition); - } else if (condition instanceof ProcessItemCondition) { - setupProcessItemCondition(panel, (ProcessItemCondition) condition); - } else if (condition instanceof GatheredResourceCondition) { - setupGatheredResourceCondition(panel, (GatheredResourceCondition) condition); - } -} - -/** - * Sets up inventory item count condition panel - */ -private static void setupInventoryItemCountCondition(JPanel panel, InventoryItemCountCondition condition) { - JTextField itemNameField = (JTextField) panel.getClientProperty("itemNameField"); - JSpinner minCountSpinner = (JSpinner) panel.getClientProperty("minCountSpinner"); - JSpinner maxCountSpinner = (JSpinner) panel.getClientProperty("maxCountSpinner"); - JCheckBox includeNotedCheckbox = (JCheckBox) panel.getClientProperty("includeNotedCheckbox"); - - if (itemNameField != null) { - itemNameField.setText(condition.getItemPattern().toString()); - } - - if (minCountSpinner != null && maxCountSpinner != null) { - - minCountSpinner.setValue(condition.getTargetCountMin()); - maxCountSpinner.setValue(condition.getTargetCountMax()); - - - } - if (includeNotedCheckbox != null) { - includeNotedCheckbox.setSelected(condition.isIncludeNoted()); - } -} - -/** - * Sets up bank item count condition panel - */ -private static void setupBankItemCountCondition(JPanel panel, BankItemCountCondition condition) { - JTextField itemNameField = (JTextField) panel.getClientProperty("bankItemNameField"); - JSpinner minCountSpinner = (JSpinner) panel.getClientProperty("bankMinCountSpinner"); - JSpinner maxCountSpinner = (JSpinner) panel.getClientProperty("bankMaxCountSpinner"); - - - if (itemNameField != null) { - itemNameField.setText(condition.getItemPattern().toString()); - } - - if (minCountSpinner != null && maxCountSpinner != null) { - - minCountSpinner.setValue(condition.getTargetCountMin()); - maxCountSpinner.setValue(condition.getTargetCountMax()); - - } -} - -/** - * Sets up item collection condition panel - */ -private static void setupLootItemCondition(JPanel panel, Condition condition) { - // Retrieve the UI components - JTextField itemsField = (JTextField) panel.getClientProperty("itemsField"); - JRadioButton andRadioButton = (JRadioButton) panel.getClientProperty("andRadioButton"); - JCheckBox sameAmountCheckBox = (JCheckBox) panel.getClientProperty("sameAmountCheckBox"); - JSpinner minAmountSpinner = (JSpinner) panel.getClientProperty("minAmountSpinner"); - JSpinner maxAmountSpinner = (JSpinner) panel.getClientProperty("maxAmountSpinner"); - JCheckBox includeNotedCheckBox = (JCheckBox) panel.getClientProperty("includeNotedCheckBox"); - JCheckBox includeNoneOwnerCheckBox = (JCheckBox) panel.getClientProperty("includeNoneOwnerCheckBox"); - - if (condition instanceof LootItemCondition || condition instanceof LogicalCondition) { - Condition conditionBaseCondition = condition; - boolean isAndLogic = true; - if (!(condition instanceof LootItemCondition)) { - conditionBaseCondition = ((LogicalCondition) condition).getConditions().get(0); - if (condition instanceof OrCondition) { - isAndLogic = false; - } - } - LootItemCondition itemCondition = (LootItemCondition) conditionBaseCondition; - - // Set item names - if (itemsField != null) { - // For single condition, just use the pattern - String itemPatternString = itemCondition.getItemPatternString(); - if (itemPatternString != null) { - // Clean up the pattern for display - String displayName = itemPatternString; - if (displayName.startsWith(".*") && displayName.endsWith(".*") && !displayName.contains("|")) { - // Remove surrounding .* for cleaner display - displayName = displayName.substring(2, displayName.length() - 2); - } - - // Handle patterns that were created with Pattern.quote() which escapes special characters - if (displayName.startsWith("\\Q") && displayName.endsWith("\\E")) { - displayName = displayName.substring(2, displayName.length() - 2); - } - - itemsField.setText(displayName); - } - } - - // Set logical operator - if (andRadioButton != null) { - andRadioButton.setSelected(isAndLogic); - } - - // Set min/max amounts - if (minAmountSpinner != null && maxAmountSpinner != null) { - minAmountSpinner.setValue(itemCondition.getTargetAmountMin()); - maxAmountSpinner.setValue(itemCondition.getTargetAmountMax()); - } - - // Set same amount for all - if (sameAmountCheckBox != null) { - sameAmountCheckBox.setSelected(true); // Default to same amount - } - - // Set options - if (includeNotedCheckBox != null) { - includeNotedCheckBox.setSelected(itemCondition.isIncludeNoted()); - } - if (includeNoneOwnerCheckBox != null) { - includeNoneOwnerCheckBox.setSelected(itemCondition.isIncludeNoneOwner()); - } - } -} - -/** - * Sets up process item condition panel - */ -@SuppressWarnings("unchecked") -private static void setupProcessItemCondition(JPanel panel, ProcessItemCondition condition) { - JRadioButton sourceButton = (JRadioButton) panel.getClientProperty("procSourceRadio"); - JRadioButton targetButton = (JRadioButton) panel.getClientProperty("procTargetRadio"); - JRadioButton eitherButton = (JRadioButton) panel.getClientProperty("procEitherRadio"); - JRadioButton procBothRadio = (JRadioButton) panel.getClientProperty("procBothRadio"); - - DefaultListModel sourceItemsModel = (DefaultListModel) panel.getClientProperty("procSourceItemsModel"); - DefaultListModel targetItemsModel = (DefaultListModel) panel.getClientProperty("procTargetItemsModel"); - JSpinner minCountSpinner = (JSpinner) panel.getClientProperty("procMinSpinner"); - JSpinner maxCountSpinner = (JSpinner) panel.getClientProperty("procMaxSpinner"); - - // Set tracking mode - ProcessItemCondition.TrackingMode trackingMode = condition.getTrackingMode(); - switch (trackingMode) { - case SOURCE_CONSUMPTION: - if (sourceButton != null) sourceButton.setSelected(true); - break; - case TARGET_PRODUCTION: - if (targetButton != null) targetButton.setSelected(true); - break; - case EITHER: - if (eitherButton != null) eitherButton.setSelected(true); - break; - case BOTH: - if (procBothRadio != null) procBothRadio.setSelected(true); - break; - } - - // Populate source items list - if (sourceItemsModel != null) { - sourceItemsModel.clear(); - for (ProcessItemCondition.ItemTracker tracker : condition.getSourceItems()) { - sourceItemsModel.addElement(tracker.getQuantityPerProcess() + "x " + tracker.getItemName()); - } - } - - // Populate target items list - if (targetItemsModel != null) { - targetItemsModel.clear(); - for (ProcessItemCondition.ItemTracker tracker : condition.getTargetItems()) { - targetItemsModel.addElement(tracker.getQuantityPerProcess() + "x " + tracker.getItemName()); - } - } - - // Set count values - if (minCountSpinner != null && maxCountSpinner != null) { - minCountSpinner.setValue(condition.getTargetCountMin()); - maxCountSpinner.setValue(condition.getTargetCountMax()); - } -} - -/** - * Sets up gathered resource condition panel - */ -@SuppressWarnings("unchecked") -private static void setupGatheredResourceCondition(JPanel panel, GatheredResourceCondition condition) { - JComboBox resourceTypeComboBox = (JComboBox) panel.getClientProperty("gatheredResourceType"); - JTextField resourceNameField = (JTextField) panel.getClientProperty("gatheredItemNameField"); - JSpinner minCountSpinner = (JSpinner) panel.getClientProperty("gatheredMinSpinner"); - JSpinner maxCountSpinner = (JSpinner) panel.getClientProperty("gatheredMaxSpinner"); - JCheckBox includeNotedCheckbox = (JCheckBox) panel.getClientProperty("gatheredIncludeNotedCheckbox"); - - if (resourceTypeComboBox != null) { - resourceTypeComboBox.setSelectedItem("Auto-detect"); - } - - if (resourceNameField != null) { - resourceNameField.setText(condition.getItemName()); - } - - if (includeNotedCheckbox != null) { - includeNotedCheckbox.setSelected(condition.isIncludeNoted()); - } - - if (minCountSpinner != null && maxCountSpinner != null) { - minCountSpinner.setValue(condition.getTargetCountMin()); - maxCountSpinner.setValue(condition.getTargetCountMax()); - } -} - -/** - * Checks if an item name string appears to be a regex pattern - * This method uses the same logic as ResourceCondition.createItemPattern() - * @param itemName The item name to check - * @return true if the item name appears to be a regex pattern, false otherwise - */ - public static boolean isRegexPattern(String itemName) { - if (itemName == null || itemName.isEmpty()) { - return false; - } - - // Check for regex special characters that indicate a pattern - return itemName.startsWith("^") || itemName.endsWith("$") || - itemName.contains(".*") || itemName.contains("[") || - itemName.contains("(") || itemName.contains("|"); - } - - /** - * Formats a list of item names for display, showing whether they are exact matches or regex patterns - * @param itemNames The list of item names - * @return A formatted string showing the item names and their matching mode - */ - public static String formatItemNamesForDisplay(List itemNames) { - if (itemNames == null || itemNames.isEmpty()) { - return ""; - } - - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < itemNames.size(); i++) { - String itemName = itemNames.get(i); - if (i > 0) { - sb.append(", "); - } - sb.append(itemName); - if (isRegexPattern(itemName)) { - sb.append(" (regex)"); - } - } - return sb.toString(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillCondition.java deleted file mode 100644 index 8d0c483c918..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillCondition.java +++ /dev/null @@ -1,422 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.skill; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.GameState; -import net.runelite.api.Skill; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.StatChanged; -import net.runelite.client.game.SkillIconManager; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.util.ImageUtil; - -import javax.swing.*; - -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.awt.image.BufferedImage; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Abstract base class for skill-based conditions. - */ -@Getter -@EqualsAndHashCode(callSuper = false) -@Slf4j -public abstract class SkillCondition implements Condition { - // Static icon cache to prevent repeated loading of the same icons - private static final ConcurrentHashMap ICON_CACHE = new ConcurrentHashMap<>(); - private static Icon OVERALL_ICON = null; - - // Static skill data caching for performance improvements - private static final ConcurrentHashMap SKILL_LEVELS = new ConcurrentHashMap<>(); - private static final ConcurrentHashMap SKILL_XP = new ConcurrentHashMap<>(); - private static transient int TOTAL_LEVEL = 0; - private static transient long TOTAL_XP = 0; - private static transient boolean SKILL_DATA_INITIALIZED = false; - private static transient long LAST_UPDATE_TIME = 0; - private static final long UPDATE_THROTTLE_MS = 600; // Update at most once every 600ms - - private static final int ICON_SIZE = 24; // Standard size for all skill icons - protected final Skill skill; - - // Pause-related fields - @Getter - protected transient boolean isPaused = false; - protected transient Map pausedSkillLevels = new HashMap<>(); - protected transient Map pausedSkillXp = new HashMap<>(); - protected transient int pausedTotalLevel = 0; - protected transient long pausedTotalXp = 0; - - /** - * Constructor requiring a skill to be set - */ - protected SkillCondition(Skill skill) { - this.skill = skill; - - // Initialize skill data if needed - if (!SKILL_DATA_INITIALIZED) { - initializeSkillData(); - } - } - - /** - * Gets the skill associated with this condition - */ - public Skill getSkill() { - return skill; - } - - /** - * Checks if this condition is for the total of all skills - */ - public boolean isTotal() { - return skill == null || skill == Skill.OVERALL; - } - - /** - * Gets all skills to be considered for total calculations - * Excludes TOTAL itself and other non-tracked skills - */ - protected Skill[] getAllTrackableSkills() { - return Skill.values(); - } - - /** - * Gets a properly scaled icon for the skill (24x24 pixels) - * Uses a cache to avoid repeatedly loading the same icons - */ - public Icon getSkillIcon() { - try { - // First check if we have a cached icon - if (isTotal()) { - if (OVERALL_ICON != null) { - return OVERALL_ICON; - } - } else if (skill != null && ICON_CACHE.containsKey(skill)) { - return ICON_CACHE.get(skill); - } - - // If not in cache, create the icon and cache it - Icon icon = createSkillIcon(); - - // Store in the appropriate cache - if (isTotal()) { - OVERALL_ICON = icon; - } else if (skill != null) { - ICON_CACHE.put(skill, icon); - } - - return icon; - } catch (Exception e) { - // Fall back to generic skill icon - return null; - } - } - - /** - * Creates a skill icon - now separated from getSkillIcon to support caching - */ - private Icon createSkillIcon() { - try { - // This only needs to be done once per skill, not on every UI render - SkillIconManager iconManager = Microbot.getClientThread().runOnClientThreadOptional( - () -> Microbot.getInjector().getInstance(SkillIconManager.class)).orElse(null); - - if (iconManager != null) { - // Get the skill image (small=true for smaller version) - BufferedImage skillImage; - String skillName = isTotal() ? "overall" : skill.getName().toLowerCase(); - if (isTotal()) { - String skillIconPath = "/skill_icons/" + skillName + ".png"; - skillImage = ImageUtil.loadImageResource(getClass(), skillIconPath); - } else { - skillImage = iconManager.getSkillImage(skill, true); - } - - // Scale the image if needed - if (skillImage.getWidth() != ICON_SIZE || skillImage.getHeight() != ICON_SIZE) { - skillImage = ImageUtil.resizeImage(skillImage, ICON_SIZE, ICON_SIZE); - } - - return new ImageIcon(skillImage); - } - } catch (Exception e) { - // Silently fail and return null - } - return null; - } - - /** - * Reset condition - must be implemented by subclasses - */ - @Override - public void reset() { - reset(false); - } - - /** - * Reset condition with option to randomize targets - */ - public abstract void reset(boolean randomize); - - /** - * Initializes skill data tracking for better performance - */ - private static void initializeSkillData() { - if (!Microbot.isLoggedIn()){ - SKILL_DATA_INITIALIZED = false; - return; - } - if (SKILL_DATA_INITIALIZED) { - return; - } - Microbot.getClientThread().invoke(() -> { - try { - // Initialize skill level and XP caches - for (Skill skill : Skill.values()) { - SKILL_LEVELS.put(skill, Microbot.getClient().getRealSkillLevel(skill)); - SKILL_XP.put(skill, (long) Microbot.getClient().getSkillExperience(skill)); - } - TOTAL_LEVEL = Microbot.getClient().getTotalLevel(); - TOTAL_XP = Microbot.getClient().getOverallExperience(); - SKILL_DATA_INITIALIZED = true; - LAST_UPDATE_TIME = System.currentTimeMillis(); - } catch (Exception e) { - // Ignore errors during initialization - } - }); - - } - - /** - * Gets the current level for a skill from the cache - */ - public static int getSkillLevel(Skill skill) { - if (!SKILL_DATA_INITIALIZED) { - initializeSkillData(); - } - - // If the skill is null or OVERALL, return the total level - if (skill == null || skill == Skill.OVERALL) { - return TOTAL_LEVEL; - } - - return SKILL_LEVELS.getOrDefault(skill, 0); - } - - /** - * Gets the current XP for a skill from the cache - */ - public static long getSkillXp(Skill skill) { - if (!SKILL_DATA_INITIALIZED) { - initializeSkillData(); - } - - // If the skill is null or OVERALL, return the total XP - if (skill == null || skill == Skill.OVERALL) { - return TOTAL_XP; - } - - return SKILL_XP.getOrDefault(skill, 0L); - } - - /** - * Gets the current total level from the cache - */ - public static int getTotalLevel() { - if (!SKILL_DATA_INITIALIZED) { - initializeSkillData(); - } - return TOTAL_LEVEL; - } - - /** - * Gets the current total XP from the cache - */ - public static long getTotalXp() { - if (!SKILL_DATA_INITIALIZED) { - initializeSkillData(); - } - return TOTAL_XP; - } - - /** - * Forces an update of all skill data (throttled to prevent performance issues) - */ - public static void forceUpdate() { - // Only update once every UPDATE_THROTTLE_MS milliseconds - long currentTime = System.currentTimeMillis(); - if (currentTime - LAST_UPDATE_TIME < UPDATE_THROTTLE_MS) { - return; - } - SKILL_DATA_INITIALIZED = false; - initializeSkillData(); - sleepUntil(()-> SKILL_DATA_INITIALIZED); - LAST_UPDATE_TIME = currentTime; - } - - /** - * Updates skill data when stats change - */ - @Override - public void onStatChanged(StatChanged event) { - - if (!SKILL_DATA_INITIALIZED) { - initializeSkillData(); - return; - } - - Skill updatedSkill = event.getSkill(); - - // Update throttling - only update once every UPDATE_THROTTLE_MS milliseconds - long currentTime = System.currentTimeMillis(); - if (currentTime - LAST_UPDATE_TIME < UPDATE_THROTTLE_MS) { - return; - } - - // Update cached values - Microbot.getClientThread().invokeLater(() -> { - try { - // Update the specific skill - int newLevel = Microbot.getClient().getRealSkillLevel(updatedSkill); - long newXp = Microbot.getClient().getSkillExperience(updatedSkill); - - SKILL_LEVELS.put(updatedSkill, newLevel); - SKILL_XP.put(updatedSkill, newXp); - - // Update total level and XP - TOTAL_LEVEL = Microbot.getClient().getTotalLevel(); - TOTAL_XP = Microbot.getClient().getOverallExperience(); - LAST_UPDATE_TIME = currentTime; - } catch (Exception e) { - // Ignore errors during update - } - - }); - } - @Override - public void onGameStateChanged(GameStateChanged gameStateChanged) { - if (gameStateChanged.getGameState() == GameState.LOGGED_IN) { - SKILL_DATA_INITIALIZED = false; - initializeSkillData(); - }else{ - SKILL_DATA_INITIALIZED = false; - } - } - - @Override - public void pause() { - if (!isPaused) { - isPaused = true; - - // Capture current skill values at pause time - this.pausedSkillLevels.clear(); - this.pausedSkillXp.clear(); - - // Force update skill data before capturing pause state - forceUpdate(); - - StringBuilder pauseStateLog = new StringBuilder("Captured pause state for skills:\n"); - - // Capture all individual skill levels and XP - for (Skill skill : Skill.values()) { - pausedSkillLevels.put(skill, SKILL_LEVELS.getOrDefault(skill, 0)); - pausedSkillXp.put(skill, SKILL_XP.getOrDefault(skill, 0L)); - pauseStateLog.append("\t") - .append(skill.getName()) - .append("\tLevel: ") - .append(pausedSkillLevels.get(skill)) - .append("\tXP: ") - .append(pausedSkillXp.get(skill)) - .append("\n"); - } - - - - // Capture total values - pausedTotalLevel = TOTAL_LEVEL; - pausedTotalXp = TOTAL_XP; - pauseStateLog.append("\nSkill condition paused. Captured pause state for skill tracking."); - log.debug(pauseStateLog.toString()); - } - } - - @Override - public void resume() { - if (isPaused) { - isPaused = false; - - // Force update skill data to get current values after resume - forceUpdate(); - // Log current skill values after resume using StringBuilder - StringBuilder resumeStateLog = new StringBuilder("Skill condition resumed. Current skill values:\n"); - for (Skill skill : Skill.values()) { - resumeStateLog.append("\t") - .append(skill.getName()) - .append("\tLevel: ") - .append(SKILL_LEVELS.get(skill)) - .append("\tXP: ") - .append(SKILL_XP.get(skill)) - .append("\n"); - } - resumeStateLog.append("\nSkill condition resumed. Pause state cleared for skill tracking."); - log.debug(resumeStateLog.toString()); - } - } - - /** - * Gets the amount of XP or levels gained while paused for a specific skill. - * This is used to adjust baseline values during resume. - * - * @param skill The skill to check - * @return XP gained during pause, or 0 if not paused or skill not tracked - */ - protected long getXpGainedDuringPause(Skill skill) { - long currentXp = SKILL_XP.getOrDefault(skill, 0L); - long pausedXp = this.pausedSkillXp.getOrDefault(skill, 0L); - return Math.max(0, currentXp - pausedXp); - } - - /** - * Gets the number of levels gained while paused for a specific skill. - * - * @param skill The skill to check - * @return Levels gained during pause, or 0 if not paused or skill not tracked - */ - protected int getLevelsGainedDuringPause(Skill skill) { - int currentLevel = SKILL_LEVELS.getOrDefault(skill, 0); - int pausedLevel = this.pausedSkillLevels.getOrDefault(skill, 0); - return Math.max(0, currentLevel - pausedLevel); - } - - /** - * Gets the total XP gained during pause across all skills. - * - * @return Total XP gained during pause, or 0 if not paused - */ - protected long getTotalXpGainedDuringPause() { - if (!isPaused) { - return 0; - } - - return Math.max(0, TOTAL_XP - pausedTotalXp); - } - - /** - * Gets the total levels gained during pause across all skills. - * - * @return Total levels gained during pause, or 0 if not paused - */ - protected int getTotalLevelsGainedDuringPause() { - if (!isPaused) { - return 0; - } - - return Math.max(0, TOTAL_LEVEL - pausedTotalLevel); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillLevelCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillLevelCondition.java deleted file mode 100755 index c5acf993864..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillLevelCondition.java +++ /dev/null @@ -1,450 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.skill; - - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Skill; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; - -/** - * Skill level-based condition for script execution. - */ -@Getter -@EqualsAndHashCode(callSuper = true) -@Slf4j -public class SkillLevelCondition extends SkillCondition { - /** - * Version of the condition class. - * Used for serialization and deserialization. - */ - public static String getVersion() { - return "0.0.1"; - } - private transient int currentTargetLevel; - @Getter - private final int targetLevelMin; - @Getter - private final int targetLevelMax; - private transient int startLevel; - private transient int[] startLevelsBySkill; // Used for total level tracking - private transient boolean SKILL_LEVEL_INITIALIZED = false; - @Getter - private final boolean randomized; - @Getter - private final boolean relative; // Whether this is a relative or absolute level target - - /** - * Creates an absolute level condition (must reach a specific level) - */ - public SkillLevelCondition(Skill skill, int targetLevel) { - super(skill); // Call parent constructor with skill - this.currentTargetLevel = targetLevel; - this.targetLevelMin = targetLevel; - this.targetLevelMax = targetLevel; - this.randomized = false; - this.relative = false; // Absolute level target - initializeLevelTracking(); - } - - /** - * Creates a randomized absolute level condition (must reach a level within a range) - */ - public SkillLevelCondition(Skill skill, int targetMinLevel, int targetMaxLevel) { - super(skill); // Call parent constructor with skill - targetMinLevel = Math.max(1, targetMinLevel); - targetMaxLevel = Math.min(99, targetMaxLevel); - this.currentTargetLevel = Rs2Random.between(targetMinLevel, targetMaxLevel); - this.targetLevelMin = targetMinLevel; - this.targetLevelMax = targetMaxLevel; - this.randomized = true; - this.relative = false; // Absolute level target - initializeLevelTracking(); - } - - /** - * Creates a relative level condition (must gain specific levels from current) - */ - public SkillLevelCondition(Skill skill, int targetLevel, boolean relative) { - super(skill); // Call parent constructor with skill - this.currentTargetLevel = targetLevel; - this.targetLevelMin = targetLevel; - this.targetLevelMax = targetLevel; - this.randomized = false; - this.relative = relative; - initializeLevelTracking(); - } - - /** - * Creates a randomized relative level condition (must gain a random number of levels from current) - */ - public SkillLevelCondition(Skill skill, int targetMinLevel, int targetMaxLevel, boolean relative) { - super(skill); // Call parent constructor with skill - targetMinLevel = Math.max(1, targetMinLevel); - targetMaxLevel = Math.min(99, targetMaxLevel); - this.currentTargetLevel = Rs2Random.between(targetMinLevel, targetMaxLevel); - this.targetLevelMin = targetMinLevel; - this.targetLevelMax = targetMaxLevel; - this.randomized = true; - this.relative = relative; - initializeLevelTracking(); - } - - /** - * Initialize level tracking for individual skill or all skills if total - */ - private void initializeLevelTracking() { - - if (!Microbot.isLoggedIn()){ - this.SKILL_LEVEL_INITIALIZED = false; - return; // Don't initialize if not logged in - } - if( SKILL_LEVEL_INITIALIZED) { - return; // Already initialized, no need to re-initialize - } - log.info("\n\t--Initializing level tracking for skill: \"{}\"", skill); - super.forceUpdate(); - if (isTotal()) { - Skill[] skills = getAllTrackableSkills(); - startLevelsBySkill = new int[skills.length]; - startLevel = getTotalLevel(); - } else { - startLevel = getCurrentLevel(); - } - } - - @Override - public void reset(boolean randomize) { - if (randomize) { - currentTargetLevel = Rs2Random.between(targetLevelMin, targetLevelMax); - } - SKILL_LEVEL_INITIALIZED = false; // Reset skill data initialization flag - initializeLevelTracking(); - } - - /** - * Create an absolute skill level condition with random target between min and max - */ - public static SkillLevelCondition createRandomized(Skill skill, int minLevel, int maxLevel) { - if (minLevel == maxLevel) { - return new SkillLevelCondition(skill, minLevel); - } - - return new SkillLevelCondition(skill, minLevel, maxLevel); - } - - /** - * Create a relative skill level condition (gain levels from current) - */ - public static SkillLevelCondition createRelative(Skill skill, int targetLevel) { - return new SkillLevelCondition(skill, targetLevel, true); - } - - /** - * Create a relative skill level condition with random target between min and max - */ - public static SkillLevelCondition createRelativeRandomized(Skill skill, int minLevel, int maxLevel) { - if (minLevel == maxLevel) { - return new SkillLevelCondition(skill, minLevel, true); - } - - return new SkillLevelCondition(skill, minLevel, maxLevel, true); - } - - @Override - public boolean isSatisfied() { - // A condition cannot be satisfied while paused - if (isPaused) { - return false; - } - - if (relative) { - // For relative mode, check if we've gained the target number of levels - return getLevelsGained() >= currentTargetLevel; - } else { - // For absolute mode, check if our current level is at or above the target - return getCurrentLevel() >= currentTargetLevel; - } - } - - /** - * Gets the number of levels gained since condition was created - */ - public int getLevelsGained() { - return getCurrentLevel() - startLevel; - } - - /** - * Gets the number of levels remaining to reach target - */ - public int getLevelsRemaining() { - if (relative) { - return Math.max(0, currentTargetLevel - getLevelsGained()); - } else { - return Math.max(0, currentTargetLevel - getCurrentLevel()); - } - } - - /** - * Gets the current skill level or total level if this is a total skill condition - * Uses the SkillCondition's cached data to avoid client thread calls - */ - public int getCurrentLevel() { - // Use static cached data from SkillCondition class - if (isTotal()) { - return SkillCondition.getTotalLevel(); - } - return SkillCondition.getSkillLevel(skill); - } - - /** - * Gets the starting skill level - */ - public int getStartingLevel() { - return startLevel; - } - - /** - * Gets the target skill level to reach (for absolute mode), - * or the target level gain (for relative mode) - */ - public int getCurrentTargetLevel() { - return currentTargetLevel; - } - - @Override - public String getDescription() { - String skillName = isTotal() ? "Total" : skill.getName(); - String randomRangeInfo = ""; - - if (targetLevelMin != targetLevelMax) { - randomRangeInfo = String.format(" (randomized from %d-%d)", targetLevelMin, targetLevelMax); - } - - if (relative) { - int levelsGained = getLevelsGained(); - - return String.format("Gain %d %s levels%s (gained: %d - %.1f%%)", - currentTargetLevel, - skillName, - randomRangeInfo, - levelsGained, - getProgressPercentage()); - } else { - int currentLevel = getCurrentLevel(); - int levelsNeeded = Math.max(0, currentTargetLevel - currentLevel); - - if (levelsNeeded <= 0) { - return String.format("%s level %d or higher%s (currently %d, goal reached)", - skillName, currentTargetLevel, randomRangeInfo, currentLevel); - } else { - return String.format("%s level %d or higher%s (currently %d, need %d more)", - skillName, currentTargetLevel, randomRangeInfo, currentLevel, levelsNeeded); - } - } - } - - /** - * Returns a detailed description of the level condition with additional status information - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - String skillName = isTotal() ? "Total" : skill.getName(); - - // Basic description - if (relative) { - sb.append("Skill Level Condition: Gain ").append(currentTargetLevel) - .append(" ").append(skillName).append(" levels from starting level\n"); - } else { - sb.append("Skill Level Condition: Reach ").append(currentTargetLevel) - .append(" ").append(skillName).append(" level\n"); - } - - // Randomization info if applicable - if (targetLevelMin != targetLevelMax) { - sb.append("Target Range: ").append(targetLevelMin) - .append("-").append(targetLevelMax).append(" (randomized)\n"); - } - - // Status information - int currentLevel = getCurrentLevel(); - boolean satisfied = isSatisfied(); - sb.append("Status: ").append(satisfied ? "Satisfied" : "Not satisfied").append("\n"); - - // Progress information - int levelsGained = getLevelsGained(); - sb.append("Starting Level: ").append(startLevel).append("\n"); - sb.append("Current Level: ").append(currentLevel).append("\n"); - sb.append("Levels Gained: ").append(levelsGained).append("\n"); - - if (!satisfied) { - sb.append("Levels Remaining: ").append(getLevelsRemaining()).append("\n"); - } - - sb.append("Progress: ").append(String.format("%.1f%%", getProgressPercentage())); - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - String skillName = isTotal() ? "Total" : skill.getName(); - - // Basic information - sb.append("SkillLevelCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Skill: ").append(skillName).append("\n"); - - if (relative) { - sb.append(" │ Mode: Relative (gain from current)\n"); - sb.append(" │ Target Level gain: ").append(currentTargetLevel).append("\n"); - } else { - sb.append(" │ Mode: Absolute (reach total)\n"); - sb.append(" │ Target Level: ").append(currentTargetLevel).append("\n"); - } - - // Randomization - boolean hasRandomization = targetLevelMin != targetLevelMax; - if (hasRandomization) { - sb.append(" │ Randomization: Enabled\n"); - sb.append(" │ Target Range: ").append(targetLevelMin).append("-").append(targetLevelMax).append("\n"); - } - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - int currentLevel = getCurrentLevel(); - boolean satisfied = isSatisfied(); - sb.append(" │ Satisfied: ").append(satisfied).append("\n"); - - if (relative) { - sb.append(" │ Levels Gained: ").append(getLevelsGained()).append("\n"); - - if (!satisfied) { - sb.append(" │ Levels Remaining: ").append(getLevelsRemaining()).append("\n"); - } - } else { - if (currentLevel >= currentTargetLevel) { - sb.append(" │ Current Level: ").append(currentLevel).append(" (goal reached)\n"); - } else { - sb.append(" │ Current Level: ").append(currentLevel).append("\n"); - sb.append(" │ Levels Remaining: ").append(getLevelsRemaining()).append("\n"); - } - } - - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Current state - sb.append(" └─ Current State ──────────────────────────\n"); - sb.append(" Starting Level: ").append(startLevel).append("\n"); - sb.append(" Current Level: ").append(currentLevel); - - return sb.toString(); - } - - @Override - public ConditionType getType() { - return ConditionType.SKILL; - } - - @Override - public double getProgressPercentage() { - if (relative) { - int levelsGained = getLevelsGained(); - - if (levelsGained >= currentTargetLevel) { - return 100.0; - } - - if (currentTargetLevel <= 0) { - return 100.0; - } - - return (100.0 * levelsGained) / currentTargetLevel; - } else { - int currentLevel = getCurrentLevel(); - int startingLevel = getStartingLevel(); - int targetLevel = getCurrentTargetLevel(); - if (currentLevel==0 || startingLevel == 0 || targetLevel ==0){ - return 0.0; - } - if (currentLevel >= targetLevel) { - return 100.0; - } - - if (currentLevel == startingLevel) { - // If we haven't gained any levels yet, estimate progress using XP - // This provides better feedback for the user - Skill skill = getSkill(); - if (skill != null) { - // Use cached XP data - long currentXp = SkillCondition.getSkillXp(skill); - - long levelStartXp = net.runelite.api.Experience.getXpForLevel(currentLevel); - long nextLevelXp = net.runelite.api.Experience.getXpForLevel(currentLevel + 1); - long xpInLevel = currentXp - levelStartXp; - long xpNeededForNextLevel = nextLevelXp - levelStartXp; - - // Progress within the current level (0-100%) - double levelProgress = (100.0 * xpInLevel) / xpNeededForNextLevel; - - // Total levels needed and percentage of one level - int levelsNeeded = targetLevel - currentLevel; - double oneLevel = 100.0 / levelsNeeded; - - // Return progress for partially completed level - return (levelProgress * oneLevel) / 100.0; - } - } - - int levelsGained = currentLevel - startingLevel; - int levelsNeeded = targetLevel - startingLevel; - - if (levelsNeeded <= 0) { - return 100.0; - } - - return (100.0 * levelsGained) / levelsNeeded; - } - } - - @Override - public void pause() { - // Call parent class pause method to capture pause state - super.pause(); - } - - @Override - public void resume() { - if (isPaused) { - // Call parent class resume method to clear pause state FIRST - super.resume(); - // Calculate level gains during pause BEFORE calling super.resume() which clears pause state - int levelsGainedDuringPause; - if (isTotal()) { - levelsGainedDuringPause = getTotalLevelsGainedDuringPause(); - } else { - levelsGainedDuringPause = getLevelsGainedDuringPause(skill); - } - // Now adjust baselines to exclude levels gained during pause - if (levelsGainedDuringPause > 0) { - if (relative) { - // For relative mode, adjust the starting baseline - startLevel += levelsGainedDuringPause; - log.debug("Adjusted {} level baseline by {} levels gained during pause for relative mode. New startLevel: {}", - isTotal() ? "Total" : skill.getName(), levelsGainedDuringPause, startLevel); - } else { - // For absolute mode, increase target to exclude paused gains - currentTargetLevel += levelsGainedDuringPause; - log.debug("Adjusted {} level target by {} levels gained during pause for absolute mode. New target: {}", - isTotal() ? "Total" : skill.getName(), levelsGainedDuringPause, currentTargetLevel); - } - } else { - log.debug("No level adjustment needed for {} - no gains during pause", - isTotal() ? "Total" : skill.getName()); - } - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillXpCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillXpCondition.java deleted file mode 100644 index 7b1e1950f9a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/SkillXpCondition.java +++ /dev/null @@ -1,455 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.skill; - - - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.GameState; -import net.runelite.api.Skill; -import net.runelite.api.events.GameStateChanged; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; - -/** - * Skill XP-based condition for script execution. - */ -@Getter -@EqualsAndHashCode(callSuper = true) -@Slf4j -public class SkillXpCondition extends SkillCondition { - - - public static String getVersion() { - return "0.0.1"; - } - private transient long currentTargetXp;// relative and absolute mode difference - private final long targetXpMin; - private final long targetXpMax; - private transient long startXp = -1; - private transient long[] startXpBySkill; // Used for total XP tracking - private transient boolean SKILL_DATA_INITIALIZED = false; - private final boolean randomized; - @Getter - private final boolean relative; // Whether this is a relative or absolute XP target - - /** - * Creates an absolute XP condition (must reach specific XP amount) - */ - public SkillXpCondition(Skill skill, long targetXp) { - super(skill); - this.currentTargetXp = targetXp; - this.targetXpMin = targetXp; - this.targetXpMax = targetXp; - this.randomized = false; - this.relative = false; // Absolute XP target - initializeXpTracking(); - } - - /** - * Creates a randomized absolute XP condition (must reach specific XP amount within a range) - */ - public SkillXpCondition(Skill skill, long targetXpMin, long targetXpMax) { - super(skill); - targetXpMin = Math.max(0, targetXpMin); - targetXpMax = Math.min(Long.MAX_VALUE, targetXpMax); - - this.currentTargetXp = Rs2Random.between((int)targetXpMin, (int)targetXpMax); - this.targetXpMin = targetXpMin; - this.targetXpMax = targetXpMax; - this.randomized = true; - this.relative = false; // Absolute XP target - initializeXpTracking(); - } - - /** - * Creates a relative XP condition (must gain specific amount of XP from current) - */ - public SkillXpCondition(Skill skill, long targetXp, boolean relative) { - super(skill); - this.currentTargetXp = targetXp; - this.targetXpMin = targetXp; - this.targetXpMax = targetXp; - this.randomized = false; - this.relative = relative; - initializeXpTracking(); - } - - /** - * Creates a randomized relative XP condition (must gain a random amount of XP from current) - */ - public SkillXpCondition(Skill skill, long targetXpMin, long targetXpMax, boolean relative) { - super(skill); - targetXpMin = Math.max(0, targetXpMin); - targetXpMax = Math.min(Long.MAX_VALUE, targetXpMax); - - this.currentTargetXp = Rs2Random.between((int)targetXpMin, (int)targetXpMax); - this.targetXpMin = targetXpMin; - this.targetXpMax = targetXpMax; - this.randomized = true; - this.relative = relative; - initializeXpTracking(); - } - - /** - * Initialize XP tracking for individual skill or all skills if total - */ - private void initializeXpTracking() { - - if (!Microbot.isLoggedIn()){ - this.SKILL_DATA_INITIALIZED = false; - return; // Don't initialize if not logged in - } - if( SKILL_DATA_INITIALIZED) { - - return; // Already initialized, no need to re-initialize - } - log.info("\n\t--Initializing XP tracking for skill: \"{}\"", skill); - super.forceUpdate(); - if (isTotal()) { - Skill[] skills = getAllTrackableSkills(); - this.startXpBySkill = new long[skills.length]; - long totalXp = getTotalXp(); - this.startXp = totalXp; - } else { - this.startXp = getCurrentXp(); - } - SKILL_DATA_INITIALIZED = true; - } - - @Override - public void reset(boolean randomize) { - if (randomize) { - currentTargetXp = (long)Rs2Random.between((int)targetXpMin, (int)targetXpMax); - } - SKILL_DATA_INITIALIZED = false; // Reset initialization state - initializeXpTracking(); - } - - /** - * Create an absolute skill XP condition with random target between min and max - */ - public static SkillXpCondition createRandomized(Skill skill, long minXp, long maxXp) { - if (minXp == maxXp) { - return new SkillXpCondition(skill, minXp); - } - - return new SkillXpCondition(skill, minXp, maxXp); - } - - /** - * Create a relative skill XP condition (gain XP from current) - */ - public static SkillXpCondition createRelative(Skill skill, long targetXp) { - return new SkillXpCondition(skill, targetXp, true); - } - - /** - * Create a relative skill XP condition with random target between min and max - */ - public static SkillXpCondition createRelativeRandomized(Skill skill, long minXp, long maxXp) { - if (minXp == maxXp) { - return new SkillXpCondition(skill, minXp, true); - } - - return new SkillXpCondition(skill, minXp, maxXp, true); - } - - @Override - public boolean isSatisfied() { - // A condition cannot be satisfied while paused - if (isPaused) { - return false; - } - - if (relative) { - // For relative mode, we need to check if we've gained the target amount of XP - return getXpGained() >= currentTargetXp; - } else { - // For absolute mode, we need to check if our current XP is at or above the target - return getCurrentXp() >= currentTargetXp; - } - } - - /** - * Gets the amount of XP gained since condition was created - */ - public long getXpGained() { - if (startXp != -1){ - if (isTotal()) { - return getTotalXp() - startXp; - } else { - return getCurrentXp() - startXp; - } - }else{ - return 0; - } - } - - - - /** - * Gets the amount of XP remaining to reach target - */ - public long getXpRemaining() { - if (relative) { - return Math.max(0, currentTargetXp - getXpGained()); - } else { - return Math.max(0, currentTargetXp - getCurrentXp()); - } - } - - /** - * Gets the current XP - * Uses static cached data from SkillCondition - */ - public long getCurrentXp() { - if (isTotal()) { - return getTotalXp(); - } - - // Use static cached data from SkillCondition class - return SkillCondition.getSkillXp(skill); - } - - /** - * Gets the starting XP - */ - public long getStartingXp() { - return startXp; - } - - /** - * Gets progress percentage towards target - */ - @Override - public double getProgressPercentage() { - if (relative) { - long xpGained = getXpGained(); - if (xpGained >= currentTargetXp) { - return 100.0; - } - - if (currentTargetXp <= 0) { - return 0; - } - - return (100.0 * xpGained) / currentTargetXp; - } else { - // For absolute targets, we need to calculate progress from 0 to target - long currentXp = getCurrentXp(); - - if (currentXp >= currentTargetXp) { - return 100.0; - } - - if (currentTargetXp <= 0) { - return 100.0; - } - - return (100.0 * currentXp) / currentTargetXp; - } - } - - @Override - public String getDescription() { - String skillName = isTotal() ? "Total" : skill.getName(); - - if (relative) { - long xpGained = getXpGained(); - long currentXp = getCurrentXp(); - long startXp = getStartingXp(); - String randomRangeInfo = ""; - - if (targetXpMin != targetXpMax) { - randomRangeInfo = String.format(" (randomized from %d-%d)", targetXpMin, targetXpMax); - } - - return String.format("Gain Relative %d %s XP%s (gained: %d - %.1f%%, current total: %d starting: %d)", - currentTargetXp, - skillName, - randomRangeInfo, - xpGained, - getProgressPercentage(),currentXp, startXp); - } else { - long currentXp = getCurrentXp(); - String randomRangeInfo = ""; - - if (targetXpMin != targetXpMax) { - randomRangeInfo = String.format(" (randomized from %d-%d)", targetXpMin, targetXpMax); - } - - if (currentXp >= currentTargetXp) { - return String.format("Reach Total %d %s XP%s (currently: %d, goal reached)", - currentTargetXp, - skillName, - randomRangeInfo, - currentXp); - } else { - return String.format("Reach Total %d %s XP%s (currently: %d, need %d more ( %.1f%%))", - currentTargetXp, - skillName, - randomRangeInfo, - currentXp, - getXpRemaining(), - getProgressPercentage() - ); - } - } - } - - @Override - public ConditionType getType() { - return ConditionType.SKILL; - } - - /** - * Returns a detailed description of the XP condition with additional status information - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - String skillName = isTotal() ? "Total" : skill.getName(); - - // Basic description - if (relative) { - sb.append("Skill XP Condition: Gain ").append(currentTargetXp) - .append(" ").append(skillName).append(" XP from starting XP\n"); - } else { - sb.append("Skill XP Condition: Reach ").append(currentTargetXp) - .append(" ").append(skillName).append(" XP total\n"); - } - - // Randomization info if applicable - if (targetXpMin != targetXpMax) { - sb.append("Target Range: ").append(targetXpMin) - .append("-").append(targetXpMax).append(" XP (randomized)\n"); - } - - // Status information - boolean satisfied = isSatisfied(); - sb.append("Status: ").append(satisfied ? "Satisfied" : "Not satisfied").append("\n"); - - // Progress information - long xpGained = getXpGained(); - sb.append("XP Gained: ").append(xpGained).append("\n"); - sb.append("Starting XP: ").append(startXp).append("\n"); - sb.append("Current XP: ").append(getCurrentXp()).append("\n"); - - if (!satisfied) { - sb.append("XP Remaining: ").append(getXpRemaining()).append("\n"); - } - - sb.append("Progress: ").append(String.format("%.1f%%", getProgressPercentage())); - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - String skillName = isTotal() ? "Total" : skill.getName(); - - // Basic information - sb.append("\nSkillXpCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Skill: ").append(skillName).append("\n"); - - if (relative) { - sb.append(" │ Mode: Relative (gain from current)\n"); - sb.append(" │ Target XP gain: ").append(currentTargetXp).append("\n"); - } else { - sb.append(" │ Mode: Absolute (reach total)\n"); - sb.append(" │ Target XP total: ").append(currentTargetXp).append("\n"); - } - - // Randomization - boolean hasRandomization = targetXpMin != targetXpMax; - if (hasRandomization) { - sb.append(" │ Randomization: Enabled\n"); - sb.append(" │ Target Range: ").append(targetXpMin).append("-").append(targetXpMax).append(" XP\n"); - } - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - boolean satisfied = isSatisfied(); - sb.append(" │ Satisfied: ").append(satisfied).append("\n"); - - if (relative) { - sb.append(" │ XP Gained: ").append(getXpGained()).append("\n"); - - if (!satisfied) { - sb.append(" │ XP Remaining: ").append(getXpRemaining()).append("\n"); - } - } else { - long currentXp = getCurrentXp(); - if (currentXp >= currentTargetXp) { - sb.append(" │ Current XP: ").append(currentXp).append(" (goal reached)\n"); - } else { - sb.append(" │ Current XP: ").append(currentXp).append("\n"); - sb.append(" │ XP Remaining: ").append(getXpRemaining()).append("\n"); - } - } - - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Current state - sb.append(" └─ Current State ──────────────────────────\n"); - sb.append(" Starting XP: ").append(startXp).append("\n"); - sb.append(" Current XP: ").append(getCurrentXp()); - - return sb.toString(); - } - @Override - public void onGameStateChanged(GameStateChanged gameStateChanged) { - if (gameStateChanged.getGameState() == GameState.LOGGED_IN) { - super.onGameStateChanged(gameStateChanged); - log.debug("Game state changed to LOGGED_IN, re-initializing XP tracking for skill: {}", skill); - initializeXpTracking(); - }else{ - - } - } - @Override - public void pause() { - // Call parent class pause method to capture pause state - super.pause(); - } - - @Override - public void resume() { - if (isPaused) { - // Call parent class resume method to clear pause state - super.resume(); - // For both relative and absolute mode, we need to adjust baselines to exclude XP gained during pause - if (isTotal()) { //tracking total XP - // Adjust total XP baseline - long xpGainedDuringPause = getTotalXpGainedDuringPause(); - if (relative) { - startXp += xpGainedDuringPause; - log.debug("Adjusted total XP baseline by {} XP gained during pause for relative mode", xpGainedDuringPause); - } else { - // For absolute mode, increase target to exclude paused gains - currentTargetXp += xpGainedDuringPause; - log.debug("Adjusted total XP target by {} XP gained during pause for absolute mode. New target: {}", - xpGainedDuringPause, currentTargetXp); - } - } else { - // Adjust individual skill XP baseline - long xpGainedDuringPause = getXpGainedDuringPause(skill); - if (relative) { - startXp += xpGainedDuringPause; - log.info("Adjusted {} XP baseline by {} XP gained during pause for relative mode", - skill.getName(), xpGainedDuringPause); - } else { - // For absolute mode, increase target to exclude paused gains - currentTargetXp += xpGainedDuringPause; - log.info("Adjusted {} XP target by {} XP gained during pause for absolute mode. New target: {}", - skill.getName(), xpGainedDuringPause, currentTargetXp); - } - } - - - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/serialization/SkillLevelConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/serialization/SkillLevelConditionAdapter.java deleted file mode 100644 index 6ce80f5a685..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/serialization/SkillLevelConditionAdapter.java +++ /dev/null @@ -1,85 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Skill; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.SkillLevelCondition; - -import java.lang.reflect.Type; - -/** - * Serializes and deserializes SkillLevelCondition objects - */ -@Slf4j -public class SkillLevelConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(SkillLevelCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add type information - json.addProperty("type", SkillLevelCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Store skill information - Skill skill = src.getSkill(); - data.addProperty("skill", skill != null ? skill.name() : "OVERALL"); - - // Store level information - data.addProperty("targetLevelMin", src.getTargetLevelMin()); - data.addProperty("targetLevelMax", src.getTargetLevelMax()); - data.addProperty("relative", src.isRelative()); - data.addProperty("version", SkillLevelCondition.getVersion()); - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public SkillLevelCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(SkillLevelCondition.getVersion())) { - log.warn("Version mismatch in SkillLevelCondition: expected {}, got {}", - SkillLevelCondition.getVersion(), version); - throw new JsonParseException("Version mismatch in SkillLevelCondition: expected " + - SkillLevelCondition.getVersion() + ", got " + version); - } - } - - // Get skill - Skill skill; - if (dataObj.get("skill").getAsString().equals("OVERALL")) { - skill = Skill.OVERALL; - } else { - skill = Skill.valueOf(dataObj.get("skill").getAsString()); - } - - // Get level information - int targetLevelMin = dataObj.get("targetLevelMin").getAsInt(); - int targetLevelMax = dataObj.get("targetLevelMax").getAsInt(); - boolean relative = dataObj.get("relative").getAsBoolean(); - - // Create condition - return new SkillLevelCondition(skill, targetLevelMin, targetLevelMax, relative); - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/serialization/SkillXpConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/serialization/SkillXpConditionAdapter.java deleted file mode 100644 index ee0090d4f2d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/serialization/SkillXpConditionAdapter.java +++ /dev/null @@ -1,84 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Skill; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.SkillXpCondition; - -import java.lang.reflect.Type; - -/** - * Serializes and deserializes SkillXpCondition objects - */ -@Slf4j -public class SkillXpConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(SkillXpCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add type information - json.addProperty("type", SkillXpCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Store skill information - Skill skill = src.getSkill(); - data.addProperty("skill", skill != null ? skill.name() : "OVERALL"); - - // Store XP information - data.addProperty("targetXpMin", src.getTargetXpMin()); - data.addProperty("targetXpMax", src.getTargetXpMax()); - data.addProperty("relative", src.isRelative()); - data.addProperty("version", src.getVersion()); - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public SkillXpCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Version check - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(SkillXpCondition.getVersion())) { - throw new JsonParseException("Version mismatch in SkillXpCondition: expected " + - SkillXpCondition.getVersion() + ", got " + version); - } - } - - // Get skill - Skill skill; - if (dataObj.get("skill").getAsString().equals("OVERALL")) { - skill = Skill.OVERALL; - } else { - skill = Skill.valueOf(dataObj.get("skill").getAsString()); - } - - // Get XP information - long targetXpMin = dataObj.get("targetXpMin").getAsLong(); - long targetXpMax = dataObj.get("targetXpMax").getAsLong(); - boolean relative = dataObj.get("relative").getAsBoolean(); - - // Create condition - return new SkillXpCondition(skill, targetXpMin, targetXpMax, relative); - - - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/ui/SkillConditionPanelUtil.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/ui/SkillConditionPanelUtil.java deleted file mode 100644 index 8e655b5356e..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/skill/ui/SkillConditionPanelUtil.java +++ /dev/null @@ -1,662 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.ui; - -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSpinner; -import javax.swing.SpinnerNumberModel; -import javax.swing.JToggleButton; -import javax.swing.ButtonGroup; -import javax.swing.JRadioButton; -import javax.swing.Box; - -import java.awt.Color; -import java.awt.Dimension; -import java.awt.FlowLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridLayout; -import java.awt.BorderLayout; - -import net.runelite.api.Skill; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.SkillLevelCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.SkillXpCondition; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -public class SkillConditionPanelUtil { - /** - * Creates the skill level condition configuration panel - */ - public static void createSkillLevelConfigPanel(JPanel panel, GridBagConstraints gbc, boolean stopConditionPanel) { - // --- Add skill selection section --- - JComboBox skillComboBox = createSkillSelector(); - addSkillSelectionSection(panel, gbc, skillComboBox); - - // --- Add mode selection (relative vs absolute) --- - JRadioButton relativeButton = new JRadioButton(); - JRadioButton absoluteButton = new JRadioButton(); - JPanel modePanel = createModeSelectionPanel(relativeButton, absoluteButton); - gbc.gridy++; - panel.add(modePanel, gbc); - - // --- Target level section --- - JLabel targetLevelLabel = new JLabel("Levels to gain:"); - targetLevelLabel.setForeground(Color.WHITE); - targetLevelLabel.setFont(FontManager.getRunescapeSmallFont()); - - SpinnerNumberModel levelModel = new SpinnerNumberModel(1, 1, 99, 1); - JSpinner levelSpinner = new JSpinner(levelModel); - levelSpinner.setPreferredSize(new Dimension(70, levelSpinner.getPreferredSize().height)); - - JCheckBox randomizeCheckBox = new JCheckBox("Randomize"); - randomizeCheckBox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - randomizeCheckBox.setForeground(Color.WHITE); - - JPanel levelPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - levelPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - levelPanel.add(targetLevelLabel); - levelPanel.add(levelSpinner); - levelPanel.add(randomizeCheckBox); - - gbc.gridy++; - panel.add(levelPanel, gbc); - - // --- Min/Max level panel --- - JPanel minMaxPanel = createMinMaxPanel("Min Level:", "Max Level:", 1, 99, 1); - minMaxPanel.setVisible(false); - gbc.gridy++; - panel.add(minMaxPanel, gbc); - - // Save references to the spinners - JSpinner minSpinner = (JSpinner) minMaxPanel.getClientProperty("minSpinner"); - JSpinner maxSpinner = (JSpinner) minMaxPanel.getClientProperty("maxSpinner"); - - // --- Mode selection listener --- - setupModeListeners(relativeButton, absoluteButton, targetLevelLabel, "Levels to gain:", "Target level:"); - - // --- Randomize checkbox behavior --- - setupRandomizeListener(randomizeCheckBox, levelSpinner, minSpinner, maxSpinner, - relativeButton, minMaxPanel, panel); - - // --- Min/Max validation --- - setupMinMaxValidation(minSpinner, maxSpinner); - - // --- Description --- - addDescriptionLabel(panel, gbc, stopConditionPanel); - - // Store components in configPanel client properties for later access - storeConfigComponents(panel, skillComboBox, levelSpinner, minSpinner, maxSpinner, - randomizeCheckBox, relativeButton, absoluteButton, targetLevelLabel); - } - - /** - * Creates the skill XP condition configuration panel - */ - public static void createSkillXpConfigPanel(JPanel panel, GridBagConstraints gbc, JPanel configPanel) { - // --- Add skill selection section --- - JComboBox skillComboBox = createSkillSelector(); - addSkillSelectionSection(panel, gbc, skillComboBox); - - // --- Add mode selection (relative vs absolute) --- - JRadioButton relativeButton = new JRadioButton(); - JRadioButton absoluteButton = new JRadioButton(); - JPanel modePanel = createModeSelectionPanel(relativeButton, absoluteButton); - gbc.gridy++; - panel.add(modePanel, gbc); - - // --- Target XP section --- - JLabel targetXpLabel = new JLabel("XP to gain:"); - targetXpLabel.setForeground(Color.WHITE); - targetXpLabel.setFont(FontManager.getRunescapeSmallFont()); - - SpinnerNumberModel xpModel = new SpinnerNumberModel(10000, 1, 200000000, 1000); - JSpinner xpSpinner = new JSpinner(xpModel); - xpSpinner.setPreferredSize(new Dimension(100, xpSpinner.getPreferredSize().height)); - - JCheckBox randomizeCheckBox = new JCheckBox("Randomize"); - randomizeCheckBox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - randomizeCheckBox.setForeground(Color.WHITE); - - JPanel xpPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - xpPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - xpPanel.add(targetXpLabel); - xpPanel.add(xpSpinner); - xpPanel.add(randomizeCheckBox); - - gbc.gridy++; - panel.add(xpPanel, gbc); - - // --- Min/Max XP panel --- - JPanel minMaxPanel = createMinMaxPanel("Min XP:", "Max XP:", 1, 200000000, 1000); - minMaxPanel.setVisible(false); - gbc.gridy++; - panel.add(minMaxPanel, gbc); - - // Save references to the spinners - JSpinner minSpinner = (JSpinner) minMaxPanel.getClientProperty("minSpinner"); - JSpinner maxSpinner = (JSpinner) minMaxPanel.getClientProperty("maxSpinner"); - - // --- Mode selection listener --- - setupModeListeners(relativeButton, absoluteButton, targetXpLabel, "XP to gain:", "Target XP:"); - - // --- Randomize checkbox behavior --- - setupXpRandomizeListener(randomizeCheckBox, xpSpinner, minSpinner, maxSpinner, - minMaxPanel, panel); - - // --- Min/Max validation --- - setupMinMaxValidation(minSpinner, maxSpinner); - - // --- Description --- - gbc.gridy++; - JLabel descriptionLabel = new JLabel(); - descriptionLabel.setText("XP is tracked from the time condition is created. Relative mode tracks gains from that point."); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Store components in configPanel client properties for later access - configPanel.putClientProperty("xpSkillComboBox", skillComboBox); - configPanel.putClientProperty("xpSpinner", xpSpinner); - configPanel.putClientProperty("minXpSpinner", minSpinner); - configPanel.putClientProperty("maxXpSpinner", maxSpinner); - configPanel.putClientProperty("randomizeSkillXp", randomizeCheckBox); - configPanel.putClientProperty("xpRelativeMode", relativeButton); - configPanel.putClientProperty("xpAbsoluteMode", absoluteButton); - configPanel.putClientProperty("xpTargetLabel", targetXpLabel); - } - - /** - * Creates a combo box with all skills and "Total" option - */ - private static JComboBox createSkillSelector() { - JComboBox skillComboBox = new JComboBox<>(); - for (Skill skill : Skill.values()) { - skillComboBox.addItem(skill.getName()); - } - skillComboBox.addItem("Total"); - skillComboBox.setPreferredSize(new Dimension(150, skillComboBox.getPreferredSize().height)); - return skillComboBox; - } - - /** - * Adds the skill selection section to the panel - */ - private static void addSkillSelectionSection(JPanel panel, GridBagConstraints gbc, JComboBox skillComboBox) { - JPanel skillSelectionPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - skillSelectionPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel skillLabel = new JLabel("Skill:"); - skillLabel.setForeground(Color.WHITE); - skillLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - skillSelectionPanel.add(skillLabel); - skillSelectionPanel.add(skillComboBox); - - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(skillSelectionPanel, gbc); - } - - /** - * Creates the mode selection panel (relative vs absolute) - */ - private static JPanel createModeSelectionPanel(JRadioButton relativeButton, JRadioButton absoluteButton) { - JPanel modePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - modePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel modeLabel = new JLabel("Mode:"); - modeLabel.setForeground(Color.WHITE); - modeLabel.setFont(FontManager.getRunescapeSmallFont()); - modePanel.add(modeLabel); - - relativeButton.setText("Relative (gain from current)"); - relativeButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - relativeButton.setForeground(Color.WHITE); - relativeButton.setSelected(true); // Default to relative mode - relativeButton.setToolTipText("Track gains from the current value when the condition starts"); - - absoluteButton.setText("Absolute (reach specific)"); - absoluteButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - absoluteButton.setForeground(Color.WHITE); - absoluteButton.setToolTipText("Track progress toward a specific target value"); - - ButtonGroup modeGroup = new ButtonGroup(); - modeGroup.add(relativeButton); - modeGroup.add(absoluteButton); - - modePanel.add(relativeButton); - modePanel.add(absoluteButton); - return modePanel; - } - - /** - * Creates a min/max panel for randomization - */ - private static JPanel createMinMaxPanel(String minLabel, String maxLabel, int minValue, int maxValue, int step) { - JPanel minMaxPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - minMaxPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel minLabelComponent = new JLabel(minLabel); - minLabelComponent.setForeground(Color.WHITE); - minLabelComponent.setFont(FontManager.getRunescapeSmallFont()); - - SpinnerNumberModel minModel = new SpinnerNumberModel(minValue, minValue, maxValue, step); - JSpinner minSpinner = new JSpinner(minModel); - minSpinner.setPreferredSize(new Dimension(100, minSpinner.getPreferredSize().height)); - - JLabel maxLabelComponent = new JLabel(maxLabel); - maxLabelComponent.setForeground(Color.WHITE); - maxLabelComponent.setFont(FontManager.getRunescapeSmallFont()); - - SpinnerNumberModel maxModel = new SpinnerNumberModel( - Math.min(maxValue, minValue + (step * 10)), - minValue, - maxValue, - step - ); - JSpinner maxSpinner = new JSpinner(maxModel); - maxSpinner.setPreferredSize(new Dimension(100, maxSpinner.getPreferredSize().height)); - - minMaxPanel.add(minLabelComponent); - minMaxPanel.add(minSpinner); - minMaxPanel.add(maxLabelComponent); - minMaxPanel.add(maxSpinner); - - // Store references for later access - minMaxPanel.putClientProperty("minSpinner", minSpinner); - minMaxPanel.putClientProperty("maxSpinner", maxSpinner); - - return minMaxPanel; - } - - /** - * Sets up listeners for the mode buttons - */ - private static void setupModeListeners(JRadioButton relativeButton, JRadioButton absoluteButton, - JLabel targetLabel, String relativeText, String absoluteText) { - relativeButton.addActionListener(e -> { - targetLabel.setText(relativeText); - }); - - absoluteButton.addActionListener(e -> { - targetLabel.setText(absoluteText); - }); - } - - /** - * Sets up the randomize checkbox behavior for level conditions - */ - private static void setupRandomizeListener(JCheckBox randomizeCheckBox, JSpinner mainSpinner, - JSpinner minSpinner, JSpinner maxSpinner, - JRadioButton relativeButton, JPanel minMaxPanel, - JPanel parentPanel) { - randomizeCheckBox.addChangeListener(e -> { - minMaxPanel.setVisible(randomizeCheckBox.isSelected()); - mainSpinner.setEnabled(!randomizeCheckBox.isSelected()); - - // If enabling randomize, set min/max from current value - if (randomizeCheckBox.isSelected()) { - int value = (Integer) mainSpinner.getValue(); - if (relativeButton.isSelected()) { - // For relative mode, set reasonable min/max values - minSpinner.setValue(Math.max(1, value - 1)); - maxSpinner.setValue(Math.min(99, value + 1)); - } else { - // For absolute mode, set wider range - minSpinner.setValue(Math.max(1, value - 5)); - maxSpinner.setValue(Math.min(99, value + 5)); - } - } - - parentPanel.revalidate(); - parentPanel.repaint(); - }); - } - - /** - * Sets up the randomize checkbox behavior for XP conditions - */ - private static void setupXpRandomizeListener(JCheckBox randomizeCheckBox, JSpinner xpSpinner, - JSpinner minSpinner, JSpinner maxSpinner, - JPanel minMaxPanel, JPanel parentPanel) { - randomizeCheckBox.addChangeListener(e -> { - minMaxPanel.setVisible(randomizeCheckBox.isSelected()); - xpSpinner.setEnabled(!randomizeCheckBox.isSelected()); - - // If enabling randomize, set min/max from current XP - if (randomizeCheckBox.isSelected()) { - int xp = (Integer) xpSpinner.getValue(); - // Set min/max values based on percentage of target XP - int variation = Math.max(1000, xp / 5); // 20% variation or at least 1000 XP - minSpinner.setValue(Math.max(1, xp - variation)); - maxSpinner.setValue(Math.min(200000000, xp + variation)); - } - - parentPanel.revalidate(); - parentPanel.repaint(); - }); - } - - /** - * Sets up validation for min/max spinners to ensure min <= max - */ - private static void setupMinMaxValidation(JSpinner minSpinner, JSpinner maxSpinner) { - // Ensure min doesn't exceed max - minSpinner.addChangeListener(e -> { - int min = (Integer) minSpinner.getValue(); - int max = (Integer) maxSpinner.getValue(); - - if (min > max) { - maxSpinner.setValue(min); - } - }); - - // Ensure max doesn't go below min - maxSpinner.addChangeListener(e -> { - int min = (Integer) minSpinner.getValue(); - int max = (Integer) maxSpinner.getValue(); - - if (max < min) { - minSpinner.setValue(max); - } - }); - } - - /** - * Adds a description label to the panel - */ - private static void addDescriptionLabel(JPanel panel, GridBagConstraints gbc, boolean stopConditionPanel) { - gbc.gridy++; - JLabel descriptionLabel; - if (stopConditionPanel) { - descriptionLabel = new JLabel("Plugin will stop when skill reaches target level"); - } else { - descriptionLabel = new JLabel("Plugin will only start when skill is at or above target level"); - } - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - } - - /** - * Stores components in the configPanel for later access - */ - private static void storeConfigComponents(JPanel configPanel, JComboBox skillComboBox, - JSpinner levelSpinner, JSpinner minSpinner, JSpinner maxSpinner, - JCheckBox randomizeCheckBox, JRadioButton relativeButton, - JRadioButton absoluteButton, JLabel targetLabel) { - configPanel.putClientProperty("skillComboBox", skillComboBox); - configPanel.putClientProperty("levelSpinner", levelSpinner); - configPanel.putClientProperty("minLevelSpinner", minSpinner); - configPanel.putClientProperty("maxLevelSpinner", maxSpinner); - configPanel.putClientProperty("randomizeSkillLevel", randomizeCheckBox); - configPanel.putClientProperty("relativeMode", relativeButton); - configPanel.putClientProperty("absoluteMode", absoluteButton); - configPanel.putClientProperty("skillLevelLabel", targetLabel); - } - - /** - * Update min/max spinner ranges based on mode - */ - private static void updateMinMaxRanges(JSpinner levelSpinner, JSpinner minSpinner, JSpinner maxSpinner) { - int currentValue = (Integer) levelSpinner.getValue(); - - // Ensure all spinners use consistent min/max values - SpinnerNumberModel levelModel = (SpinnerNumberModel) levelSpinner.getModel(); - SpinnerNumberModel minModel = (SpinnerNumberModel) minSpinner.getModel(); - SpinnerNumberModel maxModel = (SpinnerNumberModel) maxSpinner.getModel(); - - levelModel.setMinimum(1); - levelModel.setMaximum(99); - minModel.setMinimum(1); - minModel.setMaximum(99); - maxModel.setMinimum(1); - maxModel.setMaximum(99); - - // Update range based on current value - minSpinner.setValue(Math.max(1, Math.min(currentValue - 1, 98))); - maxSpinner.setValue(Math.min(99, Math.max(currentValue + 1, 2))); - } - - /** - * Creates a skill level condition based on UI components - */ - public static SkillLevelCondition createSkillLevelCondition(JPanel configPanel) { - JComboBox skillComboBox = (JComboBox) configPanel.getClientProperty("skillComboBox"); - if (skillComboBox == null) { - throw new IllegalStateException("Skill combo box not found. Please check the panel configuration."); - } - - JCheckBox randomizeCheckBox = (JCheckBox) configPanel.getClientProperty("randomizeSkillLevel"); - if (randomizeCheckBox == null) { - throw new IllegalStateException("Randomize checkbox not found. Please check the panel configuration."); - } - - JRadioButton relativeButton = (JRadioButton) configPanel.getClientProperty("relativeMode"); - if (relativeButton == null) { - throw new IllegalStateException("Mode buttons not found. Please check the panel configuration."); - } - - boolean isRelative = relativeButton.isSelected(); - - String skillName = (String) skillComboBox.getSelectedItem(); - if (skillName == null) { - // Provide a default if somehow no skill is selected - skillName = Skill.ATTACK.getName(); - } - - Skill skill = null; - if (skillName.equals("Total")) { - skill = null; - } else { - skill = Skill.valueOf(skillName.toUpperCase()); - } - - if (randomizeCheckBox.isSelected()) { - JSpinner minLevelSpinner = (JSpinner) configPanel.getClientProperty("minLevelSpinner"); - JSpinner maxLevelSpinner = (JSpinner) configPanel.getClientProperty("maxLevelSpinner"); - - if (minLevelSpinner == null || maxLevelSpinner == null) { - throw new IllegalStateException("Min/max level spinners not found. Please check the panel configuration."); - } - - int minLevel = (Integer) minLevelSpinner.getValue(); - int maxLevel = (Integer) maxLevelSpinner.getValue(); - - if (isRelative) { - return SkillLevelCondition.createRelativeRandomized(skill, minLevel, maxLevel); - } else { - return SkillLevelCondition.createRandomized(skill, minLevel, maxLevel); - } - } else { - JSpinner levelSpinner = (JSpinner) configPanel.getClientProperty("levelSpinner"); - if (levelSpinner == null) { - throw new IllegalStateException("Level spinner not found. Please check the panel configuration."); - } - - int level = (Integer) levelSpinner.getValue(); - - if (isRelative) { - return SkillLevelCondition.createRelative(skill, level); - } else { - return new SkillLevelCondition(skill, level); - } - } - } - - /** - * Creates a skill XP condition based on UI components - */ - public static SkillXpCondition createSkillXpCondition(JPanel configPanel) { - JComboBox skillComboBox = (JComboBox) configPanel.getClientProperty("xpSkillComboBox"); - if (skillComboBox == null) { - throw new IllegalStateException("XP skill combo box not found. Please check the panel configuration."); - } - - JCheckBox randomizeCheckBox = (JCheckBox) configPanel.getClientProperty("randomizeSkillXp"); - if (randomizeCheckBox == null) { - throw new IllegalStateException("Randomize XP checkbox not found. Please check the panel configuration."); - } - - JRadioButton relativeButton = (JRadioButton) configPanel.getClientProperty("xpRelativeMode"); - if (relativeButton == null) { - throw new IllegalStateException("XP mode buttons not found. Please check the panel configuration."); - } - - boolean isRelative = relativeButton.isSelected(); - - String skillName = (String) skillComboBox.getSelectedItem(); - if (skillName == null) { - // Provide a default if somehow no skill is selected - skillName = Skill.ATTACK.getName(); - } - - Skill skill = null; - if (skillName.equals("Total")) { - skill = null; - } else { - skill = Skill.valueOf(skillName.toUpperCase()); - } - - if (randomizeCheckBox.isSelected()) { - JSpinner minXpSpinner = (JSpinner) configPanel.getClientProperty("minXpSpinner"); - JSpinner maxXpSpinner = (JSpinner) configPanel.getClientProperty("maxXpSpinner"); - - if (minXpSpinner == null || maxXpSpinner == null) { - throw new IllegalStateException("Min/max XP spinners not found. Please check the panel configuration."); - } - - int minXp = (Integer) minXpSpinner.getValue(); - int maxXp = (Integer) maxXpSpinner.getValue(); - - if (isRelative) { - return SkillXpCondition.createRelativeRandomized(skill, minXp, maxXp); - } else { - return SkillXpCondition.createRandomized(skill, minXp, maxXp); - } - } else { - JSpinner xpSpinner = (JSpinner) configPanel.getClientProperty("xpSpinner"); - if (xpSpinner == null) { - throw new IllegalStateException("XP spinner not found. Please check the panel configuration."); - } - - int xp = (Integer) xpSpinner.getValue(); - - if (isRelative) { - return SkillXpCondition.createRelative(skill, xp); - } else { - return new SkillXpCondition(skill, xp); - } - } - } - - /** - * Sets up UI components based on an existing condition - */ - public static void setupSkillCondition(JPanel panel, Condition condition) { - if (condition == null) { - return; - } - - if (condition instanceof SkillLevelCondition) { - setupSkillLevelCondition(panel, (SkillLevelCondition) condition); - } else if (condition instanceof SkillXpCondition) { - setupSkillXpCondition(panel, (SkillXpCondition) condition); - } - } - - /** - * Sets up UI for an existing skill level condition - */ - private static void setupSkillLevelCondition(JPanel panel, SkillLevelCondition condition) { - JComboBox skillComboBox = (JComboBox) panel.getClientProperty("skillComboBox"); - JSpinner levelSpinner = (JSpinner) panel.getClientProperty("levelSpinner"); - JSpinner minLevelSpinner = (JSpinner) panel.getClientProperty("minLevelSpinner"); - JSpinner maxLevelSpinner = (JSpinner) panel.getClientProperty("maxLevelSpinner"); - JCheckBox randomizeCheckBox = (JCheckBox) panel.getClientProperty("randomizeSkillLevel"); - JRadioButton relativeButton = (JRadioButton) panel.getClientProperty("relativeMode"); - JRadioButton absoluteButton = (JRadioButton) panel.getClientProperty("absoluteMode"); - JLabel levelLabel = (JLabel) panel.getClientProperty("skillLevelLabel"); - - if (skillComboBox != null) { - Skill skill = condition.getSkill(); - String skillName = skill == null ? "Total" : skill.getName(); - skillComboBox.setSelectedItem(skillName); - } - - // Set mode - if (relativeButton != null && absoluteButton != null) { - boolean isRelative = condition.isRelative(); - relativeButton.setSelected(isRelative); - absoluteButton.setSelected(!isRelative); - - // Update label text based on mode - if (levelLabel != null) { - levelLabel.setText(isRelative ? "Levels to gain:" : "Target level:"); - } - } - - if (randomizeCheckBox != null) { - boolean isRandomized = condition.isRandomized(); - randomizeCheckBox.setSelected(isRandomized); - - if (isRandomized) { - if (minLevelSpinner != null && maxLevelSpinner != null) { - minLevelSpinner.setValue(condition.getTargetLevelMin()); - maxLevelSpinner.setValue(condition.getTargetLevelMax()); - } - } else if (levelSpinner != null) { - levelSpinner.setValue(condition.getCurrentTargetLevel()); - } - } - } - - /** - * Sets up UI for an existing skill XP condition - */ - private static void setupSkillXpCondition(JPanel panel, SkillXpCondition condition) { - JComboBox skillComboBox = (JComboBox) panel.getClientProperty("xpSkillComboBox"); - JSpinner xpSpinner = (JSpinner) panel.getClientProperty("xpSpinner"); - JSpinner minXpSpinner = (JSpinner) panel.getClientProperty("minXpSpinner"); - JSpinner maxXpSpinner = (JSpinner) panel.getClientProperty("maxXpSpinner"); - JCheckBox randomizeCheckBox = (JCheckBox) panel.getClientProperty("randomizeSkillXp"); - JRadioButton relativeButton = (JRadioButton) panel.getClientProperty("xpRelativeMode"); - JRadioButton absoluteButton = (JRadioButton) panel.getClientProperty("xpAbsoluteMode"); - JLabel xpLabel = (JLabel) panel.getClientProperty("xpTargetLabel"); - - if (skillComboBox != null) { - Skill skill = condition.getSkill(); - String skillName = skill == null ? "Total" : skill.getName(); - skillComboBox.setSelectedItem(skillName); - } - - // Set mode - if (relativeButton != null && absoluteButton != null) { - boolean isRelative = condition.isRelative(); - relativeButton.setSelected(isRelative); - absoluteButton.setSelected(!isRelative); - - // Update label text based on mode - if (xpLabel != null) { - xpLabel.setText(isRelative ? "XP to gain:" : "Target XP:"); - } - } - - if (randomizeCheckBox != null) { - boolean isRandomized = condition.isRandomized(); - randomizeCheckBox.setSelected(isRandomized); - - if (isRandomized) { - if (minXpSpinner != null && maxXpSpinner != null) { - minXpSpinner.setValue((int)condition.getTargetXpMin()); - maxXpSpinner.setValue((int)condition.getTargetXpMax()); - } - } else if (xpSpinner != null) { - xpSpinner.setValue((int)condition.getCurrentTargetXp()); - } - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/DayOfWeekCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/DayOfWeekCondition.java deleted file mode 100644 index 84b6f12e1ae..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/DayOfWeekCondition.java +++ /dev/null @@ -1,1057 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.events.GameTick; -import net.runelite.client.eventbus.Subscribe; -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.OrCondition; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; - -import java.time.DayOfWeek; -import java.time.Duration; -import java.time.LocalDate; - -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.temporal.ChronoUnit; - -import java.time.temporal.WeekFields; -import java.util.*; -import java.util.stream.Collectors; - -/** - * Condition that is met on specific days of the week. - * This allows scheduling tasks to run only on certain days. - */ -@Getter -@Slf4j -@EqualsAndHashCode(callSuper = true) -public class DayOfWeekCondition extends TimeCondition { - - public static String getVersion() { - return "0.0.2"; - } - private final Set activeDays; - private final long maxRepeatsPerDay; - private final long maxRepeatsPerWeek; - - @Getter - @Setter - private transient Map dailyResetCounts = new HashMap<>(); - @Getter - @Setter - private transient Map weeklyResetCounts = new HashMap<>(); - - private final AndCondition combinedCondition; - public void setIntervalCondition(IntervalCondition intervalCondition) { - combinedCondition.addCondition(intervalCondition); - } - public Optional getIntervalCondition() { - assert(combinedCondition.getConditions().size() >= 1 && combinedCondition.getConditions().size() <= 2); - for (Condition condition : combinedCondition.getConditions()) { - if (condition instanceof IntervalCondition) { - return Optional.of((IntervalCondition) condition); - } - } - return Optional.empty(); - } - public boolean hasIntervalCondition() { - return getIntervalCondition().isPresent(); - } - /** - * Creates a day of week condition for the specified days. - * This condition will be satisfied when the current day of the week matches any day in the provided set. - * - * @param maximumNumberOfRepeats The maximum number of times this condition can trigger (0 for unlimited) - * @param activeDays The set of days on which this condition should be active - */ - public DayOfWeekCondition(long maximumNumberOfRepeats, Set activeDays) { - this(maximumNumberOfRepeats, 0, 0, activeDays); - } - - /** - * Creates a day of week condition for the specified days with limits on per-day usage. - * This condition will be satisfied when the current day of the week matches any day in - * the provided set and hasn't exceeded the daily repeat limit. - * - * @param maximumNumberOfRepeats The maximum number of times this condition can trigger overall (0 for unlimited) - * @param maxRepeatsPerDay The maximum number of times this condition can trigger per day (0 for unlimited) - * @param activeDays The set of days on which this condition should be active - */ - public DayOfWeekCondition(long maximumNumberOfRepeats, long maxRepeatsPerDay, Set activeDays) { - this(maximumNumberOfRepeats, maxRepeatsPerDay, 0, activeDays); - } - - /** - * Creates a day of week condition for the specified days with limits on per-day and per-week usage. - * - * @param maximumNumberOfRepeats The maximum number of times this condition can trigger overall (0 for unlimited) - * @param maxRepeatsPerDay The maximum number of times this condition can trigger per day (0 for unlimited) - * @param maxRepeatsPerWeek The maximum number of times this condition can trigger per week (0 for unlimited) - * @param activeDays The set of days on which this condition should be active - */ - public DayOfWeekCondition(long maximumNumberOfRepeats, long maxRepeatsPerDay, long maxRepeatsPerWeek, Set activeDays) { - super(maximumNumberOfRepeats); - this.activeDays = EnumSet.copyOf(activeDays); - this.maxRepeatsPerDay = maxRepeatsPerDay; - this.maxRepeatsPerWeek = maxRepeatsPerWeek; - this.dailyResetCounts = new HashMap<>(); - this.weeklyResetCounts = new HashMap<>(); - updateNextTriggerDay(getNow()); - this.combinedCondition = new AndCondition(); - this.combinedCondition.addCondition(this); - } - - /** - * Creates a day of week condition for the specified days. - * This condition will be satisfied when the current day of the week matches any of the provided days. - * - * @param maximumNumberOfRepeats The maximum number of times this condition can trigger (0 for unlimited) - * @param days The array of days on which this condition should be active - */ - public DayOfWeekCondition(long maximumNumberOfRepeats, DayOfWeek... days) { - this(maximumNumberOfRepeats, 0, 0, days); - } - - /** - * Creates a day of week condition for the specified days with limits on per-day usage. - * - * @param maximumNumberOfRepeats The maximum number of times this condition can trigger overall (0 for unlimited) - * @param maxRepeatsPerDay The maximum number of times this condition can trigger per day (0 for unlimited) - * @param days The array of days on which this condition should be active - */ - public DayOfWeekCondition(long maximumNumberOfRepeats, long maxRepeatsPerDay, DayOfWeek... days) { - this(maximumNumberOfRepeats, maxRepeatsPerDay, 0, days); - } - - /** - * Creates a day of week condition for the specified days with limits on per-day and per-week usage. - * - * @param maximumNumberOfRepeats The maximum number of times this condition can trigger overall (0 for unlimited) - * @param maxRepeatsPerDay The maximum number of times this condition can trigger per day (0 for unlimited) - * @param maxRepeatsPerWeek The maximum number of times this condition can trigger per week (0 for unlimited) - * @param days The array of days on which this condition should be active - */ - public DayOfWeekCondition(long maximumNumberOfRepeats, long maxRepeatsPerDay, long maxRepeatsPerWeek, DayOfWeek... days) { - super(maximumNumberOfRepeats); - this.activeDays = EnumSet.noneOf(DayOfWeek.class); - this.activeDays.addAll(Arrays.asList(days)); - this.maxRepeatsPerDay = maxRepeatsPerDay; - this.maxRepeatsPerWeek = maxRepeatsPerWeek; - this.dailyResetCounts = new HashMap<>(); - this.weeklyResetCounts = new HashMap<>(); - updateNextTriggerDay(getNow()); - this.combinedCondition = new AndCondition(); - this.combinedCondition.addCondition(this); - } - - /** - * Creates a condition for weekdays (Monday through Friday) - */ - public static DayOfWeekCondition weekdays() { - return new DayOfWeekCondition(0, - DayOfWeek.MONDAY, - DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, - DayOfWeek.FRIDAY); - } - - /** - * Creates a condition for weekends (Saturday and Sunday) - */ - public static DayOfWeekCondition weekends() { - return new DayOfWeekCondition(0, - DayOfWeek.SATURDAY, - DayOfWeek.SUNDAY); - } - - /** - * Creates a condition for weekdays with specified daily limits - */ - public static DayOfWeekCondition weekdaysWithDailyLimit(long maxRepeatsPerDay) { - return new DayOfWeekCondition(0, maxRepeatsPerDay, - DayOfWeek.MONDAY, - DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, - DayOfWeek.FRIDAY); - } - - /** - * Creates a condition for weekends with specified daily limits - */ - public static DayOfWeekCondition weekendsWithDailyLimit(long maxRepeatsPerDay) { - return new DayOfWeekCondition(0, maxRepeatsPerDay, - DayOfWeek.SATURDAY, - DayOfWeek.SUNDAY); - } - - /** - * Creates a condition for weekdays with specified weekly limit - */ - public static DayOfWeekCondition weekdaysWithWeeklyLimit(long maxRepeatsPerWeek) { - return new DayOfWeekCondition(0, 0, maxRepeatsPerWeek, - DayOfWeek.MONDAY, - DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, - DayOfWeek.FRIDAY); - } - - /** - * Creates a condition for weekends with specified weekly limit - */ - public static DayOfWeekCondition weekendsWithWeeklyLimit(long maxRepeatsPerWeek) { - return new DayOfWeekCondition(0, 0, maxRepeatsPerWeek, - DayOfWeek.SATURDAY, - DayOfWeek.SUNDAY); - } - - /** - * Creates a condition for all days with specified daily and weekly limits - */ - public static DayOfWeekCondition allDaysWithLimits(long maxRepeatsPerDay, long maxRepeatsPerWeek) { - return new DayOfWeekCondition(0, maxRepeatsPerDay, maxRepeatsPerWeek, - DayOfWeek.MONDAY, - DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, - DayOfWeek.FRIDAY, - DayOfWeek.SATURDAY, - DayOfWeek.SUNDAY); - } - - /** - * Creates a day of week condition from a ZonedDateTime, using its day of week. - * - * @param dateTime The date/time to extract day of week from - * @return A condition for that specific day of the week - */ - public static DayOfWeekCondition fromZonedDateTime(ZonedDateTime dateTime) { - DayOfWeek day = dateTime.getDayOfWeek(); - return new DayOfWeekCondition(0, day); - } - - /** - * Creates a randomized DayOfWeekCondition with a random number of consecutive days. - * - * @param minDays Minimum number of consecutive days - * @param maxDays Maximum number of consecutive days - * @return A condition with randomly selected consecutive days - */ - public static DayOfWeekCondition createRandomized(int minDays, int maxDays) { - int numDays = Rs2Random.between(minDays, maxDays); - numDays = Math.min(numDays, 7); // Cap at 7 days - - // Randomly pick a starting day - int startDayIndex = Rs2Random.between(0, 6); - Set selectedDays = EnumSet.noneOf(DayOfWeek.class); - - for (int i = 0; i < numDays; i++) { - int dayIndex = (startDayIndex + i) % 7; - selectedDays.add(DayOfWeek.of(dayIndex == 0 ? 7 : dayIndex)); // DayOfWeek is 1-based - } - - return new DayOfWeekCondition(0, selectedDays); - } - - /** - * {@inheritDoc} - * Determines if the condition is currently satisfied based on whether the current - * day of the week is in the set of active days and the condition can still trigger. - * Also checks if the daily and weekly reset counts haven't exceeded the maximum allowed. - * - * @return true if today is one of the active days and neither the overall, daily, nor weekly - * limits have been exceeded, false otherwise - */ - @Override - public boolean isSatisfied() { - return isSatisfiedAt(getNextTriggerTimeWithPause().orElse(getNow())); - } - @Override - public boolean isSatisfiedAt(ZonedDateTime triggerAt) { - if (isPaused()) { - return false; - } - // First make sure this condition can trigger again - if (!canTriggerAgain()) { - return false; - } - - // Check if today is an active day - ZonedDateTime now = getNow(); - DayOfWeek today = now.getDayOfWeek(); - if (!activeDays.contains(today)) { - return false; - } - - // Check daily reset count hasn't been exceeded - LocalDate todayDate = now.toLocalDate(); - int todayCount = dailyResetCounts.getOrDefault(todayDate, 0); - if (maxRepeatsPerDay > 0 && todayCount >= maxRepeatsPerDay) { - return false; - } - - // Check weekly reset count hasn't been exceeded - if (maxRepeatsPerWeek > 0) { - int currentWeek = getCurrentWeekNumber(now); - int weekCount = weeklyResetCounts.getOrDefault(currentWeek, 0); - if (weekCount >= maxRepeatsPerWeek) { - return false; - } - } - - // If we have an interval condition, it must also be satisfied - IntervalCondition intervalCondition = getIntervalCondition().orElse(null); - if (intervalCondition != null && !intervalCondition.isSatisfied()) { - return false; - } - - return true; - } - - /** - * {@inheritDoc} - * Provides a user-friendly description of this condition, showing which days are active. - * Special cases are handled for "Every day", "Weekdays", and "Weekends". - * - * @return A human-readable string describing the active days for this condition - */ - @Override - public String getDescription() { - if (activeDays.isEmpty()) { - return "No active days"; - } - - String daysDescription; - if (activeDays.size() == 7) { - daysDescription = "Every day"; - } else if (activeDays.size() == 5 && - activeDays.contains(DayOfWeek.MONDAY) && - activeDays.contains(DayOfWeek.TUESDAY) && - activeDays.contains(DayOfWeek.WEDNESDAY) && - activeDays.contains(DayOfWeek.THURSDAY) && - activeDays.contains(DayOfWeek.FRIDAY)) { - daysDescription = "Weekdays"; - } else if (activeDays.size() == 2 && - activeDays.contains(DayOfWeek.SATURDAY) && - activeDays.contains(DayOfWeek.SUNDAY)) { - daysDescription = "Weekends"; - } else { - daysDescription = "On " + activeDays.stream() - .map(day -> day.toString().charAt(0) + day.toString().substring(1).toLowerCase()) - .collect(Collectors.joining(", ")); - } - - StringBuilder sb = new StringBuilder(daysDescription); - - if (maxRepeatsPerDay > 0) { - sb.append(" (max ").append(maxRepeatsPerDay).append(" per day)"); - } - - if (maxRepeatsPerWeek > 0) { - sb.append(" (max ").append(maxRepeatsPerWeek).append(" per week)"); - } - - return sb.toString(); - } - - /** - * Provides a detailed description of the condition, including the current day, - * whether today is active, the next upcoming active day, progress percentage, - * and basic condition information. - * - * @return A detailed human-readable description with status information - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - sb.append(getDescription()); - - ZonedDateTime now = getNow(); - DayOfWeek today = now.getDayOfWeek(); - LocalDate todayDate = now.toLocalDate(); - int currentWeek = getCurrentWeekNumber(now); - - sb.append("\nToday is ").append(today.toString().charAt(0) + today.toString().substring(1).toLowerCase()); - sb.append(" (").append(activeDays.contains(today) ? "active" : "inactive").append(")"); - - // Daily and weekly usage - if (activeDays.contains(today)) { - int todayUsage = dailyResetCounts.getOrDefault(todayDate, 0); - sb.append("\nToday's usage: ").append(todayUsage); - if (maxRepeatsPerDay > 0) { - sb.append("/").append(maxRepeatsPerDay); - } - - int weekUsage = weeklyResetCounts.getOrDefault(currentWeek, 0); - sb.append("\nThis week's usage: ").append(weekUsage); - if (maxRepeatsPerWeek > 0) { - sb.append("/").append(maxRepeatsPerWeek); - } - } - - // Show next trigger day if today is not active or limits are reached - boolean dailyLimitReached = maxRepeatsPerDay > 0 && - dailyResetCounts.getOrDefault(todayDate, 0) >= maxRepeatsPerDay; - boolean weeklyLimitReached = maxRepeatsPerWeek > 0 && - weeklyResetCounts.getOrDefault(currentWeek, 0) >= maxRepeatsPerWeek; - - if (!activeDays.contains(today) || dailyLimitReached || weeklyLimitReached) { - if (getNextTriggerTimeWithPause().orElse(null) != null) { - DayOfWeek nextDay = getNextTriggerTimeWithPause().get().getDayOfWeek(); - long daysUntil = ChronoUnit.DAYS.between(now.toLocalDate(), getNextTriggerTimeWithPause().get().toLocalDate()); - - sb.append("\nNext active day: ") - .append(nextDay.toString().charAt(0) + nextDay.toString().substring(1).toLowerCase()) - .append(" (in ").append(daysUntil).append(daysUntil == 1 ? " day)" : " days)"); - - if (weeklyLimitReached) { - // Calculate days until next week starts - LocalDate today_date = now.toLocalDate(); - LocalDate startOfNextWeek = today_date.plusDays(8 - today.getValue()); // Monday is 1, Sunday is 7 - long daysUntilNextWeek = ChronoUnit.DAYS.between(today_date, startOfNextWeek); - - sb.append("\nWeekly limit reached. New week starts in ").append(daysUntilNextWeek) - .append(daysUntilNextWeek == 1 ? " day" : " days"); - } - } - } - - // Include interval condition details if present - getIntervalCondition().ifPresent(intervalCondition -> { - sb.append("\n\nInterval condition: ").append(intervalCondition.getDescription()); - sb.append("\nInterval satisfied: ").append(intervalCondition.isSatisfied()); - - // Add next trigger time if available - intervalCondition.getCurrentTriggerTime().ifPresent(triggerTime -> { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - sb.append("\nNext interval trigger: ").append(triggerTime.format(formatter)); - - if (now.isBefore(triggerTime)) { - Duration timeUntil = Duration.between(now, triggerTime); - long seconds = timeUntil.getSeconds(); - sb.append(" (in ").append(String.format("%02d:%02d:%02d", - seconds / 3600, (seconds % 3600) / 60, seconds % 60)).append(")"); - } else { - sb.append(" (ready now)"); - } - }); - }); - - sb.append("\nProgress: ").append(String.format("%.1f%%", getProgressPercentage())); - sb.append("\n").append(super.getDescription()); - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - - // Basic information - sb.append("DayOfWeekCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Active Days: ").append(getDescription()).append("\n"); - if (maxRepeatsPerDay > 0) { - sb.append(" │ Max Repeats Per Day: ").append(maxRepeatsPerDay).append("\n"); - } - if (maxRepeatsPerWeek > 0) { - sb.append(" │ Max Repeats Per Week: ").append(maxRepeatsPerWeek).append("\n"); - } - -// Add interval condition information if present - getIntervalCondition().ifPresent(intervalCondition -> { - sb.append(" │ Interval: ").append(intervalCondition.toString()).append("\n"); - }); - // Staus information - sb.append(" ├─ Status ──────────────────────────────────\n"); - sb.append(" │ Satisfied: ").append(isSatisfied()).append("\n"); - sb.append(" │ Paused: ").append(isPaused()).append("\n"); - - ZonedDateTime now = getNow(); - DayOfWeek today = now.getDayOfWeek(); - LocalDate todayDate = now.toLocalDate(); - int currentWeek = getCurrentWeekNumber(now); - - sb.append(" │ Current Day: ").append(today.toString().charAt(0) + today.toString().substring(1).toLowerCase()) - .append(" (").append(activeDays.contains(today) ? "active" : "inactive").append(")\n"); - - if (activeDays.contains(today)) { - int todayUsage = dailyResetCounts.getOrDefault(todayDate, 0); - sb.append(" │ Today's Usage: ").append(todayUsage); - if (maxRepeatsPerDay > 0) { - sb.append("/").append(maxRepeatsPerDay); - } - sb.append("\n"); - - int weekUsage = weeklyResetCounts.getOrDefault(currentWeek, 0); - sb.append(" │ This Week's Usage: ").append(weekUsage); - if (maxRepeatsPerWeek > 0) { - sb.append("/").append(maxRepeatsPerWeek); - } - sb.append("\n"); - } - - // If not active today or limits reached, show next active day - boolean dailyLimitReached = maxRepeatsPerDay > 0 && - dailyResetCounts.getOrDefault(todayDate, 0) >= maxRepeatsPerDay; - boolean weeklyLimitReached = maxRepeatsPerWeek > 0 && - weeklyResetCounts.getOrDefault(currentWeek, 0) >= maxRepeatsPerWeek; - - if ((!activeDays.contains(today) || dailyLimitReached || weeklyLimitReached) - && getNextTriggerTimeWithPause().get() != null) { - - DayOfWeek nextDay = getNextTriggerTimeWithPause().get().getDayOfWeek(); - long daysUntil = ChronoUnit.DAYS.between(now.toLocalDate(), getNextTriggerTimeWithPause().get().toLocalDate()); - - sb.append(" │ Next Active Day: ") - .append(nextDay.toString().charAt(0) + nextDay.toString().substring(1).toLowerCase()) - .append(" (").append(getNextTriggerTimeWithPause().get().toLocalDate()) - .append(", in ").append(daysUntil).append(daysUntil == 1 ? " day)\n" : " days)\n"); - - if (weeklyLimitReached) { - LocalDate today_date = now.toLocalDate(); - LocalDate startOfNextWeek = today_date.plusDays(8 - today.getValue()); - long daysUntilNextWeek = ChronoUnit.DAYS.between(today_date, startOfNextWeek); - - sb.append(" │ Weekly Limit Reached: New week starts in ") - .append(daysUntilNextWeek).append(daysUntilNextWeek == 1 ? " day\n" : " days\n"); - } - } - - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Tracking info - sb.append(" └─ Tracking ────────────────────────────────\n"); - sb.append(" Reset Count: ").append(currentValidResetCount); - if (this.getMaximumNumberOfRepeats() > 0) { - sb.append("/").append(getMaximumNumberOfRepeats()); - } else { - sb.append(" (unlimited)"); - } - sb.append("\n"); - if (lastValidResetTime != null) { - sb.append(" Last Reset: ").append(lastValidResetTime.format(dateTimeFormatter)).append("\n"); - } - sb.append(" Can Trigger Again: ").append(canTriggerAgain()).append("\n"); - - // If paused, show pause information - if (isPaused()) { - Duration currentPauseDuration = Duration.between(pauseStartTime, getNow()); - sb.append(" Current Pause Duration: ").append(formatDuration(currentPauseDuration)).append("\n"); - } - if (totalPauseDuration.getSeconds() > 0) { - sb.append(" Total Pause Duration: ").append(formatDuration(totalPauseDuration)).append("\n"); - } - - return sb.toString(); - } - - /** - * Updates the tracking of which days have been used and how many times - * Also tracks weekly usage - */ - private void updateDailyResetCount() { - ZonedDateTime now = getNow(); - LocalDate today = now.toLocalDate(); - int currentWeek = getCurrentWeekNumber(now); - - // Increment today's reset count - dailyResetCounts.put(today, dailyResetCounts.getOrDefault(today, 0) + 1); - - // Increment this week's reset count - weeklyResetCounts.put(currentWeek, weeklyResetCounts.getOrDefault(currentWeek, 0) + 1); - - // Clean up old entries (optional, to prevent map growth) - Set oldDates = new HashSet<>(); - for (LocalDate date : dailyResetCounts.keySet()) { - if (ChronoUnit.DAYS.between(date, today) > 14) { // Keep only last two weeks - oldDates.add(date); - } - } - for (LocalDate oldDate : oldDates) { - dailyResetCounts.remove(oldDate); - } - - // Clean up old week entries - Set oldWeeks = new HashSet<>(); - for (Integer week : weeklyResetCounts.keySet()) { - if (week < currentWeek - 2) { // Keep only last few weeks - oldWeeks.add(week); - } - } - for (Integer oldWeek : oldWeeks) { - weeklyResetCounts.remove(oldWeek); - } - } - - /** - * Gets the current ISO week number in the year - */ - private int getCurrentWeekNumber(ZonedDateTime dateTime) { - WeekFields weekFields = WeekFields.of(Locale.getDefault()); - return dateTime.get(weekFields.weekOfWeekBasedYear()); - } - - /** - * Updates the next trigger day based on current day and daily/weekly limits - */ - private void updateNextTriggerDay(ZonedDateTime nextPossibleDateTime) { - - DayOfWeek possibleDay = nextPossibleDateTime.getDayOfWeek(); - LocalDate possibleDate = nextPossibleDateTime.toLocalDate(); - int currentWeek = getCurrentWeekNumber(nextPossibleDateTime); - - // If today is active and hasn't reached limits, it's the trigger day - if (activeDays.contains(possibleDay) && - (maxRepeatsPerDay <= 0 || dailyResetCounts.getOrDefault(possibleDate, 0) < maxRepeatsPerDay) && - (maxRepeatsPerWeek <= 0 || weeklyResetCounts.getOrDefault(currentWeek, 0) < maxRepeatsPerWeek)) { - super.setNextTriggerTime(nextPossibleDateTime.minusSeconds(1)); - - return; - } - - // Check if we've hit the weekly limit but not the daily limit - boolean weeklyLimitReached = (maxRepeatsPerWeek > 0 && - weeklyResetCounts.getOrDefault(currentWeek, 0) >= maxRepeatsPerWeek); - - // If weekly limit reached, find first active day in next week - if (weeklyLimitReached) { - // Start from next Monday (first day of next week) - LocalDate mondayNextWeek = possibleDate.plusDays(8 - possibleDay.getValue()); - ZonedDateTime nextWeekStart = nextPossibleDateTime.with(mondayNextWeek.atStartOfDay()); - - // Find first active day in next week - for (int daysToAdd = 0; daysToAdd < 7; daysToAdd++) { - ZonedDateTime checkDay = nextWeekStart.plusDays(daysToAdd); - if (activeDays.contains(checkDay.getDayOfWeek())) { - super.setNextTriggerTime(checkDay); - return; - } - } - } - - // Otherwise, find the next active day in current week that hasn't reached daily limit - int daysToAdd = 1; - while (daysToAdd <= 7) { - ZonedDateTime checkDay = nextPossibleDateTime.plusDays(daysToAdd); - DayOfWeek checkDayOfWeek = checkDay.getDayOfWeek(); - LocalDate checkDate = checkDay.toLocalDate(); - int checkWeek = getCurrentWeekNumber(checkDay); - - // Skip to next week if current week has reached limit - if (weeklyLimitReached && checkWeek == currentWeek) { - // Skip to Monday of next week - int daysToMonday = 8 - possibleDay.getValue(); - daysToAdd = daysToMonday; - continue; - } - - if (activeDays.contains(checkDayOfWeek) && - (maxRepeatsPerDay <= 0 || dailyResetCounts.getOrDefault(checkDate, 0) < maxRepeatsPerDay) && - (maxRepeatsPerWeek <= 0 || weeklyResetCounts.getOrDefault(checkWeek, 0) < maxRepeatsPerWeek)) { - // Found the next active day that hasn't reached limits - super.setNextTriggerTime( nextPossibleDateTime.plusDays(daysToAdd).truncatedTo(ChronoUnit.DAYS)); - return; - } - - daysToAdd++; - } - - // If no active days found within next 7 days, set to null - super.setNextTriggerTime( null); - - } - - /** - * {@inheritDoc} - * Resets the condition state and increments the reset counter. - * For day of week conditions, this tracks when the condition was last reset, - * updates daily and weekly usage counters, and recalculates the next trigger day. - * - * @param randomize Whether to randomize aspects of the condition (not used for this condition type) - */ - @Override - public void reset(boolean randomize) { - updateValidReset(); - getIntervalCondition() - .ifPresent(IntervalCondition::reset); - updateDailyResetCount(); - updateNextTriggerDay(getNow()); - } - @Override - public void hardReset() { - super.hardReset(); - dailyResetCounts.clear(); - weeklyResetCounts.clear(); - resume(); - updateNextTriggerDay(getNow()); - } - - /** - * {@inheritDoc} - * Calculates a percentage indicating progress toward the next active day. - * If today is an active day and hasn't reached limits, returns 100%. - * Otherwise, returns a percentage based on how close we are to the next active day. - * - * @return A percentage from 0-100 indicating progress toward the next active day - */ - @Override - public double getProgressPercentage() { - if (isSatisfied()) { - return 100.0; - } - - // If no active days, return 0 - if (activeDays.isEmpty()) { - return 0.0; - } - - ZonedDateTime now = getNow(); - DayOfWeek today = now.getDayOfWeek(); - LocalDate todayDate = now.toLocalDate(); - int currentWeek = getCurrentWeekNumber(now); - - // If today is an active day but reached daily limit - if (activeDays.contains(today) && maxRepeatsPerDay > 0) { - int todayCount = dailyResetCounts.getOrDefault(todayDate, 0); - - if (todayCount >= maxRepeatsPerDay) { - // Calculate hours until midnight as progress toward next day - long hoursUntilMidnight = 24 - now.getHour(); - return 100.0 * ((24.0 - hoursUntilMidnight) / 24.0); - } - } - - // If weekly limit reached - if (maxRepeatsPerWeek > 0) { - int weekCount = weeklyResetCounts.getOrDefault(currentWeek, 0); - - if (weekCount >= maxRepeatsPerWeek) { - // Calculate days until next week as progress - int daysUntilNextWeek = 8 - today.getValue(); // Days until next Monday - return 100.0 * ((7.0 - daysUntilNextWeek) / 7.0); - } - } - - // Calculate days until the next active day - if (getNextTriggerTimeWithPause().orElse(null) == null) { - updateNextTriggerDay(getNow()); - } - - if (getNextTriggerTimeWithPause().orElse(null) != null) { - long daysUntil = ChronoUnit.DAYS.between(now.toLocalDate(), getNextTriggerTimeWithPause().get().toLocalDate()); - if (daysUntil == 0) { - return 100.0; // Same day - } else if (daysUntil >= 7) { - return 0.0; // A week or more away - } else { - return 100.0 * (1.0 - (daysUntil / 7.0)); - } - } - - return 0.0; - } - - /** - * {@inheritDoc} - * Calculates the next time this condition will be satisfied. - * This accounts for daily and weekly limits and finding the next valid active day. - * - * @return An Optional containing the time when this condition will next be satisfied, - * or empty if the condition cannot trigger again or has no active days - */ - @Override - public Optional getCurrentTriggerTime() { - if (!canTriggerAgain()) { - return Optional.empty(); - } - - ZonedDateTime now = getNow(); - DayOfWeek today = now.getDayOfWeek(); - LocalDate todayDate = now.toLocalDate(); - int currentWeek = getCurrentWeekNumber(now); - - // If today is active and hasn't reached daily or weekly limits - if (activeDays.contains(today) && - (maxRepeatsPerDay <= 0 || dailyResetCounts.getOrDefault(todayDate, 0) < maxRepeatsPerDay) && - (maxRepeatsPerWeek <= 0 || weeklyResetCounts.getOrDefault(currentWeek, 0) < maxRepeatsPerWeek)) { - IntervalCondition intervalCondition = getIntervalCondition().orElse(null); - if (intervalCondition != null) { - return Optional.of(intervalCondition.getCurrentTriggerTime().orElse(null)); - } - return Optional.of(now.minusSeconds(1)); - - } - - // If no active days defined, return empty - if (activeDays.isEmpty()) { - return Optional.empty(); - } - - // If nextTriggerDay isn't calculated yet or needs update - if (getNextTriggerTimeWithPause().orElse(null) == null) { - updateNextTriggerDay(getNow()); - } - - return getNextTriggerTimeWithPause(); - } - - /** - * Returns the count of resets that have occurred on the given date. - * This can be used to track usage across days. - * - * @param date The date to check - * @return The number of resets that occurred on that date - */ - public int getResetCountForDate(LocalDate date) { - return dailyResetCounts.getOrDefault(date, 0); - } - - /** - * Returns the count of resets that have occurred in the given week. - * This can be used to track usage across weeks. - * - * @param weekNumber The ISO week number to check - * @return The number of resets that occurred in that week - */ - public int getResetCountForWeek(int weekNumber) { - return weeklyResetCounts.getOrDefault(weekNumber, 0); - } - - /** - * Returns the count of resets that have occurred in the current week. - * - * @return The number of resets that occurred in the current week - */ - public int getCurrentWeekResetCount() { - ZonedDateTime now = getNow(); - int currentWeek = getCurrentWeekNumber(now); - return weeklyResetCounts.getOrDefault(currentWeek, 0); - } - - /** - * Checks if this DayOfWeekCondition has reached its daily limit for the current day. - * - * @return true if the daily limit has been reached, false otherwise (including if there's no limit) - */ - public boolean isDailyLimitReached() { - if (maxRepeatsPerDay <= 0) { - return false; // No daily limit - } - - ZonedDateTime now = getNow(); - LocalDate today = now.toLocalDate(); - int todayCount = dailyResetCounts.getOrDefault(today, 0); - - return todayCount >= maxRepeatsPerDay; - } - - /** - * Checks if this DayOfWeekCondition has reached its weekly limit for the current week. - * - * @return true if the weekly limit has been reached, false otherwise (including if there's no limit) - */ - public boolean isWeeklyLimitReached() { - if (maxRepeatsPerWeek <= 0) { - return false; // No weekly limit - } - - ZonedDateTime now = getNow(); - int currentWeek = getCurrentWeekNumber(now); - int weekCount = weeklyResetCounts.getOrDefault(currentWeek, 0); - - return weekCount >= maxRepeatsPerWeek; - } - - /** - * Finds the next day that is NOT an active day. - * This can be useful for determining when the condition will stop being satisfied. - * - * @return An Optional containing the start time of the next non-active day, - * or empty if all days are active - */ - public Optional getNextNonActiveDay() { - ZonedDateTime now = getNow(); - DayOfWeek today = now.getDayOfWeek(); - - // If today is already non-active, return today - if (!activeDays.contains(today)) { - return Optional.of(now.truncatedTo(ChronoUnit.DAYS)); - } - - // Otherwise, find the next non-active day - int daysToAdd = 1; - while (daysToAdd <= 7) { - DayOfWeek nextDay = today.plus(daysToAdd); - if (!activeDays.contains(nextDay)) { - // Found the next non-active day - ZonedDateTime nextNonActiveDay = now.plusDays(daysToAdd) - .truncatedTo(ChronoUnit.DAYS); // Start of the day - return Optional.of(nextNonActiveDay); - } - daysToAdd++; - } - - // If all days are active, return empty - return Optional.empty(); - } - - /** - * Gets the date when the next week starts (Monday) - * - * @return The date of next Monday - */ - public LocalDate getNextWeekStartDate() { - ZonedDateTime now = getNow(); - DayOfWeek today = now.getDayOfWeek(); - return now.toLocalDate().plusDays(8 - today.getValue()); // Monday is 1, Sunday is 7 - } - - /** - * Handles game tick events from the RuneLite event bus. - * This method exists primarily to ensure the condition stays registered with the event bus. - * - * @param tick The game tick event - */ - @Subscribe - public void onGameTick(GameTick tick) { - // Just used to ensure we stay registered with the event bus - } - - /** - * Creates a combined condition that requires both specific days of the week and a time interval. - * This factory method makes it easy to create a schedule that runs for specific durations on certain days. - * - * @param dayCondition The day of week condition that specifies on which days the condition is active - * @param intervalDuration The duration for the interval condition (how long to run) - * @return An AndCondition that combines both conditions - */ - public static DayOfWeekCondition withInterval(DayOfWeekCondition dayCondition, Duration intervalDuration) { - dayCondition.setIntervalCondition( new IntervalCondition(intervalDuration)); - return dayCondition; - } - - /** - * Creates a combined condition that requires both specific days of the week and a randomized time interval. - * - * @param dayCondition The day of week condition that specifies on which days the condition is active - * @param minDuration The minimum duration for the interval - * @param maxDuration The maximum duration for the interval - * @return An AndCondition that combines both conditions - */ - public static DayOfWeekCondition withRandomizedInterval(DayOfWeekCondition dayCondition, - Duration minDuration, - Duration maxDuration) { - dayCondition.setIntervalCondition (IntervalCondition.createRandomized(minDuration, maxDuration)); - return dayCondition; - } - - /** - * Factory method to create a condition that runs only on weekdays with a specified interval. - * - * @param intervalHours The number of hours to run for - * @return A combined condition that runs for the specified duration on weekdays - */ - public static DayOfWeekCondition weekdaysWithHourLimit(int intervalHours) { - return withInterval(weekdays(), Duration.ofHours(intervalHours)); - } - - /** - * Factory method to create a condition that runs only on weekends with a specified interval. - * - * @param intervalHours The number of hours to run for - * @return A combined condition that runs for the specified duration on weekends - */ - public static DayOfWeekCondition weekendsWithHourLimit(int intervalHours) { - return withInterval(weekends(), Duration.ofHours(intervalHours)); - } - - /** - * Factory method to create a condition for specific days with a limit on triggers per day - * and a time limit per session. - * - * @param days The days on which the condition should be active - * @param maxRepeatsPerDay Maximum number of times the condition can trigger per day - * @param sessionDuration How long each session should last - * @return A combined condition with both day and interval constraints - */ - public static DayOfWeekCondition createDailySchedule(Set days, long maxRepeatsPerDay, Duration sessionDuration) { - DayOfWeekCondition dayCondition = new DayOfWeekCondition(0, maxRepeatsPerDay, days); - return withInterval(dayCondition, sessionDuration); - } - - /** - * Factory method to create a condition for specific days with a limit on triggers per day - * and a randomized time limit per session. - * - * @param days The days on which the condition should be active - * @param maxRepeatsPerDay Maximum number of times the condition can trigger per day - * @param minSessionDuration Minimum session duration - * @param maxSessionDuration Maximum session duration - * @return A combined condition with both day and randomized interval constraints - */ - public static DayOfWeekCondition createRandomizedDailySchedule(Set days, - long maxRepeatsPerDay, - Duration minSessionDuration, - Duration maxSessionDuration) { - DayOfWeekCondition dayCondition = new DayOfWeekCondition(0, maxRepeatsPerDay, days); - return withRandomizedInterval(dayCondition, minSessionDuration, maxSessionDuration); - } - - /** - * Factory method to create a humanized play schedule that mimics natural human gaming patterns. - * Creates a schedule that: - * - Plays more on weekends (2 sessions of 1-3 hours each) - * - Plays less on weekdays (1 session of 30min-1.5hrs) - * - * @return A natural-seeming play schedule - */ - public static OrCondition createHumanizedPlaySchedule() { - // Create weekday condition: 1 session per day, 30-90 minutes each - DayOfWeekCondition weekdayCondition = new DayOfWeekCondition(0, 1, EnumSet.of( - DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, DayOfWeek.FRIDAY)); - Condition weekdaySchedule = withRandomizedInterval( - weekdayCondition, - Duration.ofMinutes(30), - Duration.ofMinutes(90)); - - // Create weekend condition: 2 sessions per day, 1-3 hours each - DayOfWeekCondition weekendCondition = new DayOfWeekCondition(0, 2, EnumSet.of( - DayOfWeek.SATURDAY, DayOfWeek.SUNDAY)); - Condition weekendSchedule = withRandomizedInterval( - weekendCondition, - Duration.ofHours(1), - Duration.ofHours(3)); - - // Use OrCondition to combine them - OrCondition combinedSchedule = - new OrCondition(); - combinedSchedule.addCondition(weekdaySchedule); - combinedSchedule.addCondition(weekendSchedule); - - return combinedSchedule; - } - - @Override - protected void onResume(Duration pauseDuration) { - if (isPaused) { - return; - } - ZonedDateTime nextTriggerTimeWithPauseDuration = getNextTriggerTimeWithPause().orElse(null); - if (nextTriggerTimeWithPauseDuration != null) { - // Shift the next trigger time by the pause duration - // getNextTriggerTimeWithPause() provide old next trigger time -> we are resumed.. - nextTriggerTimeWithPauseDuration = nextTriggerTimeWithPauseDuration.plus(pauseDuration); - updateNextTriggerDay(nextTriggerTimeWithPauseDuration); - log.info("DayOfWeekCondition resumed, next trigger time shifted to: {}", getNextTriggerTimeWithPause().get()); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/IntervalCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/IntervalCondition.java deleted file mode 100644 index 5d16378fe14..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/IntervalCondition.java +++ /dev/null @@ -1,781 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.questhelper.requirements.zone.Zone; - -import java.time.Duration; -import java.time.LocalDateTime; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Optional; -import java.util.concurrent.ThreadLocalRandom; - - - -/** - * Condition that is met at regular intervals. - * Can be used for periodic tasks or for creating natural breaks. - */ - -@Slf4j -@EqualsAndHashCode(callSuper = false , exclude = {"nextTriggerTime"}) -public class IntervalCondition extends TimeCondition { - /** - * Version of the IntervalCondition class - */ - public static String getVersion() { - return "0.0.4"; - } - - /** - * The base/average interval between triggers, primarily used for display purposes - */ - @Getter - private final Duration interval; - - /** - * The next time this condition should trigger - */ - //@Getter - //@Setter - //private transient ZonedDateTime nextTriggerTime; - - /** - * The variation factor (0.0-1.0) representing how much intervals can vary from the mean - * For example, 0.2 means intervals can vary by ±20% from the mean value - */ - @Getter - private final double randomFactor; - - /** - * The minimum possible interval duration when randomization is enabled - */ - @Getter - private final Duration minInterval; - - /** - * The maximum possible interval duration when randomization is enabled - */ - @Getter - private final Duration maxInterval; - - /** - * Whether this interval uses randomization (true) or fixed intervals (false) - */ - @Getter - private final boolean randomize; - - /** - * Optional condition for initial delay before first trigger - * When present, this condition must be satisfied before the interval triggers can begin - */ - @Getter - private final SingleTriggerTimeCondition initialDelayCondition; - /** - * Creates an interval condition that triggers at regular intervals - * - * @param interval The time interval between triggers - */ - public IntervalCondition(Duration interval) { - this(interval, false, 0.0, 0); - } - - /** - * Creates an interval condition with optional randomization - * - * @param interval The base time interval - * @param randomize Whether to randomize intervals - * @param variationFactor Variation factor (0-1.0) - how much to vary the interval by percentage - * @param maximumNumberOfRepeats Maximum number of times this condition can trigger - */ - public IntervalCondition(Duration interval, boolean randomize, double variationFactor, long maximumNumberOfRepeats) { - this(interval, randomize, variationFactor, maximumNumberOfRepeats, null); - } - - /** - * Creates an interval condition with optional randomization and initial delay - * - * @param interval The base time interval - * @param randomize Whether to randomize intervals - * @param variationFactor Variation factor (0-1.0) - how much to vary the interval by percentage - * @param maximumNumberOfRepeats Maximum number of times this condition can trigger - * @param initialDelaySeconds Initial delay in seconds before first trigger - */ - public IntervalCondition(Duration interval, boolean randomize, double variationFactor, long maximumNumberOfRepeats, Long initialDelaySeconds) { - super(maximumNumberOfRepeats); - this.interval = interval; - this.randomFactor = Math.max(0, Math.min(1.0, variationFactor)); - - // Set min/max intervals based on variation factor - if (randomize && variationFactor > 0) { - long baseMillis = interval.toMillis(); - long variation = (long) (baseMillis * variationFactor); - this.minInterval = Duration.ofMillis(Math.max(0, baseMillis - variation)); - this.maxInterval = Duration.ofMillis(baseMillis + variation); - this.randomize = true; - } else { - this.minInterval = interval; - this.maxInterval = interval; - this.randomize = false; - } - - // Initialize initial delay if specified - if (initialDelaySeconds != null && initialDelaySeconds > 0) { - this.initialDelayCondition = SingleTriggerTimeCondition.afterDelay(initialDelaySeconds); - } else { - this.initialDelayCondition = null; - } - - setNextTriggerTime (calculateNextTriggerTime()); - } - - /** - * Private constructor with explicit min/max interval values - * - * @param interval The base/average time interval for display purposes - * @param minInterval Minimum possible interval duration - * @param maxInterval Maximum possible interval duration - * @param randomize Whether to randomize intervals - * @param variationFactor Variation factor (0-1.0) representing how much variation is allowed - * @param maximumNumberOfRepeats Maximum number of times this condition can trigger - */ - private IntervalCondition(Duration interval, Duration minInterval, Duration maxInterval, - boolean randomize, double variationFactor, long maximumNumberOfRepeats) { - this(interval, minInterval, maxInterval, randomize, variationFactor, maximumNumberOfRepeats, 0L); - } - - /** - * Private constructor with explicit min/max interval values and initial delay - * - * @param interval The base/average time interval for display purposes - * @param minInterval Minimum possible interval duration - * @param maxInterval Maximum possible interval duration - * @param randomize Whether to randomize intervals - * @param variationFactor Variation factor (0-1.0) representing how much variation is allowed - * @param maximumNumberOfRepeats Maximum number of times this condition can trigger - * @param initialDelaySeconds Initial delay in seconds before first trigger - */ - public IntervalCondition(Duration interval, Duration minInterval, Duration maxInterval, - boolean randomize, double variationFactor, long maximumNumberOfRepeats, - Long initialDelaySeconds) { - super(maximumNumberOfRepeats); - this.interval = interval; - this.randomFactor = Math.max(0, Math.min(1.0, variationFactor)); - this.minInterval = minInterval; - this.maxInterval = maxInterval; - // We consider it randomized if min and max are different - this.randomize = !minInterval.equals(maxInterval); - - // Initialize initial delay if specified - if (initialDelaySeconds != null && initialDelaySeconds > 0) { - this.initialDelayCondition = SingleTriggerTimeCondition.afterDelay(initialDelaySeconds); - } else { - this.initialDelayCondition = null; - } - - setNextTriggerTime(calculateNextTriggerTime()); - } - - /** - * Private constructor with explicit min/max interval values and initial delay - * - * @param interval The base/average time interval for display purposes - * @param minInterval Minimum possible interval duration - * @param maxInterval Maximum possible interval duration - * @param randomize Whether to randomize intervals - * @param variationFactor Variation factor (0-1.0) representing how much variation is allowed - * @param maximumNumberOfRepeats Maximum number of times this condition can trigger - * @param initialDelaySeconds Initial delay in seconds before first trigger - */ - public IntervalCondition(Duration interval, Duration minInterval, Duration maxInterval, - boolean randomize, double variationFactor, long maximumNumberOfRepeats, - SingleTriggerTimeCondition initialDelayCondition) { - super(maximumNumberOfRepeats); - this.interval = interval; - this.randomFactor = Math.max(0, Math.min(1.0, variationFactor)); - this.minInterval = minInterval; - this.maxInterval = maxInterval; - // We consider it randomized if min and max are different - this.randomize = !minInterval.equals(maxInterval); - - // Initialize initial delay if specified - if (initialDelayCondition != null) { - this.initialDelayCondition = initialDelayCondition.copy(); - } else { - this.initialDelayCondition = null; - } - - setNextTriggerTime(calculateNextTriggerTime()); - } - - /** - * Creates an interval condition with minutes - */ - public static IntervalCondition everyMinutes(int minutes) { - return new IntervalCondition(Duration.ofMinutes(minutes)); - } - - /** - * Creates an interval condition with hours - */ - public static IntervalCondition everyHours(int hours) { - return new IntervalCondition(Duration.ofHours(hours)); - } - - /** - * Creates an interval condition with randomized timing using a base time and variation factor - * - * @param baseMinutes The average interval duration in minutes - * @param variationFactor How much the interval can vary (0-1.0, e.g., 0.2 means ±20%) - * @param maximumNumberOfRepeats Maximum number of times this condition can trigger - * @return A randomized interval condition - */ - public static IntervalCondition randomizedMinutesWithVariation(int baseMinutes, double variationFactor, long maximumNumberOfRepeats) { - return new IntervalCondition(Duration.ofMinutes(baseMinutes), true, variationFactor, maximumNumberOfRepeats); - } - - /** - * Creates an interval condition that triggers at intervals between the provided min and max durations. - * - * @param minDuration Minimum interval duration - * @param maxDuration Maximum interval duration - * @return A randomized interval condition - */ - public static IntervalCondition createRandomized(Duration minDuration, Duration maxDuration) { - // Validate inputs - if (minDuration.compareTo(maxDuration) > 0) { - throw new IllegalArgumentException("Minimum duration must be less than or equal to maximum duration"); - } - - // Create an average interval for display purposes - long minMillis = minDuration.toMillis(); - long maxMillis = maxDuration.toMillis(); - Duration avgInterval = Duration.ofMillis((minMillis + maxMillis) / 2); - - // Calculate a randomization factor - represents how much the intervals can vary - // from the mean value (as a percentage of the mean) - double variationFactor = 0.0; - if (minMillis < maxMillis) { - // Calculate as a percentage of the average - long halfRange = (maxMillis - minMillis) / 2; - variationFactor = halfRange / (double) avgInterval.toMillis(); - } - - log.debug("createRandomized: min={}, max={}, avg={}, variationFactor={}", - minDuration, maxDuration, avgInterval, variationFactor); - - return new IntervalCondition(avgInterval, minDuration, maxDuration, true, variationFactor, 0); - } - - /** - * Creates an interval condition with randomized timing using seconds range - * - * @param minSeconds Minimum interval in seconds - * @param maxSeconds Maximum interval in seconds - * @return A randomized interval condition - */ - public static IntervalCondition randomizedSeconds(int minSeconds, int maxSeconds) { - return createRandomized(Duration.ofSeconds(minSeconds), Duration.ofSeconds(maxSeconds)); - } - - /** - * Creates an interval condition with randomized timing using minutes range - * - * @param minMinutes Minimum interval in minutes - * @param maxMinutes Maximum interval in minutes - * @return A randomized interval condition - */ - public static IntervalCondition randomizedMinutes(int minMinutes, int maxMinutes) { - return createRandomized(Duration.ofMinutes(minMinutes), Duration.ofMinutes(maxMinutes)); - } - - /** - * Creates an interval condition with randomized timing using hours range - * - * @param minHours Minimum interval in hours - * @param maxHours Maximum interval in hours - * @return A randomized interval condition - */ - public static IntervalCondition randomizedHours(int minHours, int maxHours) { - return createRandomized(Duration.ofHours(minHours), Duration.ofHours(maxHours)); - } - - /** - * Creates an interval condition with minutes and an initial delay - * - * @param minutes The interval in minutes - * @param initialDelaySeconds The initial delay in seconds before first trigger - */ - public static IntervalCondition everyMinutesWithDelay(int minutes, Long initialDelaySeconds) { - return new IntervalCondition(Duration.ofMinutes(minutes), false, 0.0, 0, initialDelaySeconds); - } - - /** - * Creates an interval condition with hours and an initial delay - * - * @param hours The interval in hours - * @param initialDelaySeconds The initial delay in seconds before first trigger - */ - public static IntervalCondition everyHoursWithDelay(int hours, Long initialDelaySeconds) { - return new IntervalCondition(Duration.ofHours(hours), false, 0.0, 0, initialDelaySeconds); - } - - /** - * Creates an interval condition with randomized timing using a base time and variation factor, - * plus an initial delay before the first trigger - * - * @param baseMinutes The average interval duration in minutes - * @param variationFactor How much the interval can vary (0-1.0, e.g., 0.2 means ±20%) - * @param maximumNumberOfRepeats Maximum number of times this condition can trigger - * @param initialDelaySeconds The initial delay in seconds before first trigger - * @return A randomized interval condition with initial delay - */ - public static IntervalCondition randomizedMinutesWithVariationAndDelay( - int baseMinutes, double variationFactor, long maximumNumberOfRepeats, Long initialDelaySeconds) { - return new IntervalCondition(Duration.ofMinutes(baseMinutes), - true, variationFactor, maximumNumberOfRepeats, initialDelaySeconds); - } - - /** - * Creates an interval condition with randomized timing using seconds range and an initial delay - * - * @param minSeconds Minimum interval in seconds - * @param maxSeconds Maximum interval in seconds - * @param initialDelaySeconds The initial delay in seconds before first trigger - * @return A randomized interval condition with initial delay - */ - public static IntervalCondition randomizedSecondsWithDelay(int minSeconds, int maxSeconds, Long initialDelaySeconds) { - IntervalCondition condition = createRandomized(Duration.ofSeconds(minSeconds), Duration.ofSeconds(maxSeconds)); - return new IntervalCondition( - condition.interval, - condition.minInterval, - condition.maxInterval, - condition.randomize, - condition.randomFactor, - 0, - initialDelaySeconds); - } - - /** - * Creates an interval condition with randomized timing using minutes range and an initial delay - * - * @param minMinutes Minimum interval in minutes - * @param maxMinutes Maximum interval in minutes - * @param initialDelaySeconds The initial delay in seconds before first trigger - * @return A randomized interval condition with initial delay - */ - public static IntervalCondition randomizedMinutesWithDelay(int minMinutes, int maxMinutes, Long initialDelaySeconds) { - IntervalCondition condition = createRandomized(Duration.ofMinutes(minMinutes), Duration.ofMinutes(maxMinutes)); - return new IntervalCondition( - condition.interval, - condition.minInterval, - condition.maxInterval, - condition.randomize, - condition.randomFactor, - 0, - initialDelaySeconds); - } - - /** - * Creates an interval condition with randomized timing using hours range and an initial delay - * - * @param minHours Minimum interval in hours - * @param maxHours Maximum interval in hours - * @param initialDelaySeconds The initial delay in seconds before first trigger - * @return A randomized interval condition with initial delay - */ - public static IntervalCondition randomizedHoursWithDelay(int minHours, int maxHours, Long initialDelaySeconds) { - IntervalCondition condition = createRandomized(Duration.ofHours(minHours), Duration.ofHours(maxHours)); - return new IntervalCondition( - condition.interval, - condition.minInterval, - condition.maxInterval, - condition.randomize, - condition.randomFactor, - 0, - initialDelaySeconds); - } - - @Override - public boolean isSatisfied() { - return isSatisfiedAt(getNextTriggerTimeWithPause().orElse(getNow())); - } - @Override - public boolean isSatisfiedAt(ZonedDateTime triggerTime) { - if (triggerTime == null) { - return false; - } - if(!canTriggerAgain()) { - return false; - } - - // Check if condition is paused (handled by superclass, but adding for clarity) - if (isPaused) { - return false; - } - - // Check initial delay condition first (if exists) - if (initialDelayCondition != null && !initialDelayCondition.isSatisfiedAt(initialDelayCondition.getNextTriggerTimeWithPause().orElse(getNow()))) { - return false; // Initial delay hasn't been met yet - } - - ZonedDateTime now = getNow(); - if (now.isAfter(triggerTime) || now.isEqual(triggerTime)) { - return true; - } - return false; - } - @Override - public String getDescription() { - ZonedDateTime now = getNow(); - String timeLeft = ""; - String initialDelayInfo = ""; - String pauseInfo = isPaused ? " (PAUSED)" : ""; - - // Check initial delay status - if (initialDelayCondition != null && !initialDelayCondition.isSatisfied()) { - Duration initialDelayRemaining = Duration.between(now, initialDelayCondition.getNextTriggerTimeWithPause().orElse(now)); - long seconds = initialDelayRemaining.getSeconds(); - if (seconds > 0) { - initialDelayInfo = String.format(" (initial delay: %02d:%02d:%02d)", - seconds / 3600, (seconds % 3600) / 60, seconds % 60); - } - } - - if (getNextTriggerTimeWithPause().orElse(null) != null && (initialDelayCondition == null || initialDelayCondition.isSatisfied())) { - if (now.isAfter(getNextTriggerTimeWithPause().orElse(getNow()))) { - timeLeft = " (ready now)"; - } else { - Duration remaining = Duration.between(now, getNextTriggerTimeWithPause().orElse(getNow())); - long seconds = remaining.getSeconds(); - timeLeft = String.format(" (next in %02d:%02d:%02d)", - seconds / 3600, (seconds % 3600) / 60, seconds % 60); - } - } - - // The condition was randomized if min and max intervals are different - if (randomize) { - // Show as a range when we have min and max - return String.format("Every %s-%s%s%s%s", - formatDuration(minInterval), - formatDuration(maxInterval), - timeLeft, - initialDelayInfo, - pauseInfo); - } else { - // Fixed interval - return String.format("Every %s%s%s%s", formatDuration(interval), timeLeft, initialDelayInfo, pauseInfo); - } - } - - /** - * Returns a detailed description of the interval condition with additional status information - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - sb.append(getDescription()).append("\n"); - - ZonedDateTime now = getNow(); - if (getNextTriggerTimeWithPause().orElse(null) != null) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - sb.append("Next trigger at: ").append(getNextTriggerTimeWithPause().orElse(getNow()).format(formatter)).append("\n"); - - if (now.isAfter(getNextTriggerTimeWithPause().orElse(getNow()))) { - sb.append("Status: Ready to trigger\n"); - } else { - Duration remaining = Duration.between(now, getNextTriggerTimeWithPause().orElse(getNow())); - long seconds = remaining.getSeconds(); - sb.append("Time remaining: ") - .append(String.format("%02d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, seconds % 60)) - .append("\n"); - } - } - - sb.append("Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - if (randomize) { - sb.append("Randomization: Enabled (±").append(String.format("%.0f", randomFactor * 100)).append("%)\n"); - } - - // Add lastValidResetTime information - if (lastValidResetTime != null && currentValidResetCount > 0) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - sb.append("Last reset: ").append(lastValidResetTime.format(formatter)).append("\n"); - - // Calculate time since the last reset - Duration sinceLastReset = Duration.between(lastValidResetTime, LocalDateTime.now()); - long seconds = sinceLastReset.getSeconds(); - sb.append("Time since last reset: ") - .append(String.format("%02d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, seconds % 60)) - .append("\n"); - } - - sb.append(super.getDescription()); - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - - // Basic information - sb.append("IntervalCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Interval: ").append(formatDuration(interval)).append("\n"); - - // Initial Delay information - if (initialDelayCondition != null) { - sb.append(" │ Initial Delay: "); - ZonedDateTime now = getNow(); - if (initialDelayCondition.isSatisfied()) { - sb.append("Completed\n"); - } else { - Duration initialDelayRemaining = Duration.between(now, initialDelayCondition.getNextTriggerTimeWithPause().orElse(now)); - long seconds = initialDelayRemaining.getSeconds(); - if (seconds > 0) { - sb.append(String.format("%02d:%02d:%02d remaining\n", - seconds / 3600, (seconds % 3600) / 60, seconds % 60)); - } else { - sb.append("Ready\n"); - } - } - } - - // Randomization - sb.append(" ├─ Randomization ────────────────────────────\n"); - if (randomize) { - sb.append(" │ Min Interval: ").append(formatDuration(minInterval)).append("\n"); - sb.append(" │ Max Interval: ").append(formatDuration(maxInterval)).append("\n"); - sb.append(" │ Randomization: Enabled\n"); - sb.append(" │ Random Factor: ±").append(String.format("%.0f%%", randomFactor * 100)).append("\n"); - } else { - sb.append(" │ Randomization: Disabled\n"); - } - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - sb.append(" │ Satisfied: ").append(isSatisfied()).append("\n"); - if (isPaused) { - sb.append(" │ Status: PAUSED\n"); - } - - ZonedDateTime now = getNow(); - if (getNextTriggerTimeWithPause().orElse(null) != null) { - sb.append(" │ Next Trigger: ").append(getNextTriggerTimeWithPause().orElse(getNow()).format(dateTimeFormatter)).append("\n"); - - if (!now.isAfter(getNextTriggerTimeWithPause().orElse(getNow()))) { - Duration remaining = Duration.between(now, getNextTriggerTimeWithPause().orElse(getNow())); - long seconds = remaining.getSeconds(); - sb.append(" │ Time Remaining: ") - .append(String.format("%02d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, seconds % 60)) - .append("\n"); - } else { - sb.append(" │ Ready to trigger\n"); - } - } - - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Tracking info - sb.append(" └─ Tracking ────────────────────────────────\n"); - sb.append(" Reset Count: ").append(currentValidResetCount); - if (this.getMaximumNumberOfRepeats() > 0) { - sb.append("/").append(getMaximumNumberOfRepeats()); - } else { - sb.append(" (unlimited)"); - } - sb.append("\n"); - if (lastValidResetTime != null) { - sb.append(" Last Reset: ").append(lastValidResetTime.format(dateTimeFormatter)).append("\n"); - - // Add time since last reset - Duration sinceLastReset = Duration.between(lastValidResetTime, LocalDateTime.now()); - long seconds = sinceLastReset.getSeconds(); - sb.append(" Time Since Reset: ") - .append(String.format("%02d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, seconds % 60)) - .append("\n"); - } - sb.append(" Can Trigger Again: ").append(canTriggerAgain()).append("\n"); - - return sb.toString(); - } - - @Override - public void reset(boolean randomize) { - updateValidReset(); - setNextTriggerTime(calculateNextTriggerTime()); - this.lastValidResetTime = LocalDateTime.now(); - // Reset initial delay condition if it exists - if (initialDelayCondition != null) { - initialDelayCondition.reset(false); - } - - log.debug("IntervalCondition reset, next trigger at: {}", getNextTriggerTimeWithPause().orElse(null)); - } - @Override - public void hardReset() { - // Reset the condition state - this.currentValidResetCount = 0; - this.lastValidResetTime = LocalDateTime.now(); - - // Reset initial delay condition if it exists - if (initialDelayCondition != null) { - initialDelayCondition.hardReset(); - } - setNextTriggerTime(calculateNextTriggerTime()); - } - - @Override - public double getProgressPercentage() { - - - ZonedDateTime now = getNow(); - if (getNextTriggerTimeWithPause().orElse(null) == null) { - return 0.0; - } - ZonedDateTime nextTriggerTime = getNextTriggerTimeWithPause().orElse(null); - if (now.isAfter(nextTriggerTime)) { - return 100.0; - } - - // Calculate how much time has passed since the last trigger - Duration timeUntilNextTrigger = Duration.between(now, nextTriggerTime); - - // If this is the first trigger (no lastValidResetTime), we need to use the interval - // from initialization to calculate progress - Duration lastInterval; - if (lastValidResetTime == null) { - // Use the average interval for randomized conditions - if (randomize) { - lastInterval = interval; // Average interval - } else { - lastInterval = interval; // Fixed interval - } - } else { - // If we've had a previous trigger, calculate from that time to the next trigger - Duration actualInterval = Duration.between(lastValidResetTime.atZone(getNow().getZone()), nextTriggerTime); - lastInterval = actualInterval; - if(isPaused) { - lastInterval = lastInterval.plus(getCurrentPauseDuration()); - } - } - - - // Calculate ratio of elapsed time - long remainingMillis = timeUntilNextTrigger.toMillis(); - long totalMillis = interval.toMillis(); - - double elapsedRatio = 1.0 - (remainingMillis / (double) totalMillis); - return Math.max(0, Math.min(100, elapsedRatio * 100)); - } - @Override - public Optional getCurrentTriggerTime() { - // If paused or can't trigger again, don't provide a trigger time - if ( getNextTriggerTimeWithPause().orElse(null) == null || !canTriggerAgain()) { - return Optional.empty(); // No trigger time during pause or if already triggered too often - } - - ZonedDateTime now = getNow(); - ZonedDateTime nextTriggerTime = getNextTriggerTimeWithPause().orElse(null); - - if (initialDelayCondition != null && !initialDelayCondition.isSatisfied()) { - return initialDelayCondition.getCurrentTriggerTime(); // Return the initial delay condition's trigger time - } - // If already satisfied (past the trigger time) - if (now.isAfter(nextTriggerTime)) { - return Optional.of(nextTriggerTime); // Return the passed time until reset - } - - // Otherwise return the scheduled next trigger time - return Optional.of(nextTriggerTime); - } - - /** - * Calculates the next trigger time based on the current configuration. - * - * @return The next time this condition should trigger - */ - private ZonedDateTime calculateNextTriggerTime() { - ZonedDateTime now = getNow(); - - // Skip the future interval calculation during initial creation or if can't trigger again - boolean skipFutureInterval = !canTriggerAgain() || this.currentValidResetCount == 0; - Duration nextInterval; - - // Generate a randomized interval if randomization is enabled - if (randomize) { - // Generate a random value between min and max interval - long minMillis = minInterval.toMillis(); - long maxMillis = maxInterval.toMillis(); - long randomMillis = ThreadLocalRandom.current().nextLong(minMillis, maxMillis + 1); - nextInterval = Duration.ofMillis(randomMillis); - - log.debug("Randomized interval: {}ms (between {}ms and {}ms)", - randomMillis, minMillis, maxMillis); - } - // Use fixed interval otherwise - else { - nextInterval = interval; - } - - // For initial creation or when max triggers reached, trigger immediately - if (skipFutureInterval) { - return now; - } - // Otherwise, schedule the next trigger based on the calculated interval - else { - return now.plus(nextInterval); - } - } - - @Override - protected String formatDuration(Duration duration) { - long seconds = duration.getSeconds(); - if (seconds < 60) { - return seconds + "s"; - } else if (seconds < 3600) { - return String.format("%dm %ds", seconds / 60, seconds % 60); - } else { - return String.format("%dh %dm", seconds / 3600, (seconds % 3600) / 60); - } - } - - /** - * Handles what happens when this condition is resumed. - * Shifts the next trigger time by the pause duration to maintain the same - * relative timing after a pause. - * - * @param pauseDuration The duration for which this condition was paused - */ - @Override - protected void onResume(Duration pauseDuration) { - if (isPaused()) { - return; - } - // If there's an initial delay condition, let it handle its own resume - if (initialDelayCondition != null) { - // Only shift if the initial delay hasn't been satisfied yet - if (initialDelayCondition instanceof TimeCondition) { - initialDelayCondition.onResume(pauseDuration); - // If initial delay already implements pause/resume, it will be handled by TimeCondition - } - } - // getNextTriggerTimeWithPause() provide old next trigger time -> we are resumed.. - ZonedDateTime nextTriggerTimeWithPauseDuration = getNextTriggerTimeWithPause().orElse(null); - if (nextTriggerTimeWithPauseDuration != null) { - nextTriggerTimeWithPauseDuration = nextTriggerTimeWithPauseDuration.plus(pauseDuration); - // Shift the next trigger time by the pause duration - setNextTriggerTime(nextTriggerTimeWithPauseDuration); - - if (lastValidResetTime != null) { - lastValidResetTime = lastValidResetTime.plus(pauseDuration); - } - log.debug("IntervalCondition resumed, next trigger time shifted to: {}", getNextTriggerTimeWithPause().get()); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/SingleTriggerTimeCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/SingleTriggerTimeCondition.java deleted file mode 100644 index 51741b84c1a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/SingleTriggerTimeCondition.java +++ /dev/null @@ -1,281 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.events.GameTick; -import net.runelite.client.eventbus.Subscribe; - -import java.time.Duration; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Optional; - -/** - * A time condition that triggers exactly once when the target time is reached. - * After triggering, this condition remains in a "triggered" state until explicitly reset. - * Perfect for one-time scheduled events or deadlines. - */ -@Slf4j -@EqualsAndHashCode(callSuper = false, exclude = {}) -public class SingleTriggerTimeCondition extends TimeCondition { - @Getter - private Duration definedDelay; - @Getter - private long maximumNumberOfRepeats = 1; - public static String getVersion() { - return "0.0.1"; - } - - - private static final DateTimeFormatter FORMATTER = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - public SingleTriggerTimeCondition copy(boolean reset){ - SingleTriggerTimeCondition copy = new SingleTriggerTimeCondition(getNextTriggerTimeWithPause().orElse(getNow()), this.definedDelay, this.maximumNumberOfRepeats); - if (reset) { - copy.hardReset(); - } - return copy; - } - public SingleTriggerTimeCondition copy(){ - SingleTriggerTimeCondition copy = new SingleTriggerTimeCondition(getNextTriggerTimeWithPause().orElse(getNow()), this.definedDelay, this.maximumNumberOfRepeats); - - return copy; - } - /** - * Creates a condition that triggers once at the specified time - * - * @param targetTime The time at which this condition should trigger - */ - public SingleTriggerTimeCondition(ZonedDateTime targetTime, Duration definedDelay, - long maximumNumberOfRepeats) { - super(maximumNumberOfRepeats); // Only allow one trigger - setNextTriggerTime(targetTime); - this.definedDelay = definedDelay; - } - - /** - * Creates a condition that triggers once after the specified delay - * - * @param delaySeconds Number of seconds in the future to trigger - * @return A new SingleTriggerTimeCondition - */ - public static SingleTriggerTimeCondition afterDelay(long delaySeconds) { - ZonedDateTime triggerTime = ZonedDateTime.now(ZoneId.systemDefault()) - .plusSeconds(delaySeconds); - return new SingleTriggerTimeCondition(triggerTime ,Duration.ofSeconds(delaySeconds), 1); - } - - - @Override - public boolean isSatisfied() { - return isSatisfiedAt(getNextTriggerTimeWithPause().orElse(getNow())); - } - @Override - public boolean isSatisfiedAt(ZonedDateTime triggerTime) { - if (isPaused()) { - return false; // Don't trigger if paused - } - // If already triggered, return true - if (hasTriggered()) { - if (!canTriggerAgain()) { - return true; // Only return true once after triggering - } - return false; // Return false after reset and we have triggered before - } - - // Check if current time has passed the target time - ZonedDateTime now = getNow(); - if (now.isAfter(triggerTime) || now.isEqual(triggerTime)) { - log.debug("SingleTriggerTimeCondition triggered at: {}", now.format(FORMATTER)); - return true; - } - - return false; - } - - - @Override - public String getDescription() { - String triggerStatus = hasTriggered() ? "triggered" : "not yet triggered"; - String baseDescription = super.getDescription(); - return String.format("One-time trigger at %s (%s)\n%s", - getNextTriggerTimeWithPause().orElse(getNow()).format(FORMATTER), triggerStatus, baseDescription); - } - - /** - * Returns a detailed description of the single trigger condition with additional status information - */ - @Override - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - ZonedDateTime now = getNow(); - String triggerStatus = hasTriggered() ? "triggered" : "not yet triggered"; - String pauseStatus = isPaused() ? " (PAUSED)" : ""; - - sb.append("One-time trigger at ").append(getNextTriggerTimeWithPause().orElse(getNow()).format(FORMATTER)) - .append(" (").append(triggerStatus).append(")").append(pauseStatus).append("\n"); - - if (!hasTriggered() && !isPaused()) { - if (now.isAfter(getNextTriggerTimeWithPause().orElse(getNow()))) { - sb.append("Ready to trigger now\n"); - } else { - Duration timeUntilTrigger = Duration.between(now, getNextTriggerTimeWithPause().orElse(getNow())); - long seconds = timeUntilTrigger.getSeconds(); - sb.append("Time until trigger: ") - .append(String.format("%02d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, seconds % 60)) - .append("\n"); - } - } else if (isPaused()) { - sb.append("Trigger time is paused and will be adjusted when resumed\n"); - Duration currentPauseDuration = Duration.between(pauseStartTime, now); - sb.append("Current pause duration: ").append(formatDuration(currentPauseDuration)).append("\n"); - } - - sb.append("Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - sb.append(super.getDescription()); - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - - // Basic information - sb.append("SingleTriggerTimeCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Target Time: ").append(getNextTriggerTimeWithPause().orElse(getNow()).format(dateTimeFormatter)).append("\n"); - sb.append(" │ Time Zone: ").append(getNextTriggerTimeWithPause().orElse(getNow()).getZone().getId()).append("\n"); - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - sb.append(" │ Satisfied: ").append(isSatisfied()).append("\n"); - sb.append(" │ Triggered: ").append(hasTriggered()).append("\n"); - sb.append(" │ Paused: ").append(isPaused()).append("\n"); - - ZonedDateTime now = getNow(); - // Only show trigger time info if not paused - if (!hasTriggered() && !isPaused()) { - if (now.isAfter(getNextTriggerTimeWithPause().orElse(getNow()))) { - sb.append(" │ Ready to trigger now\n"); - } else { - Duration timeUntilTrigger = Duration.between(now, getNextTriggerTimeWithPause().orElse(getNow())); - long seconds = timeUntilTrigger.getSeconds(); - sb.append(" │ Time Until Trigger: ") - .append(String.format("%02d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, seconds % 60)) - .append("\n"); - } - } else if (isPaused()) { - sb.append(" │ Trigger time paused and will be adjusted when resumed\n"); - } - - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Tracking info - sb.append(" └─ Tracking ────────────────────────────────\n"); - sb.append(" Reset Count: ").append(currentValidResetCount); - if (this.getMaximumNumberOfRepeats() > 0) { - sb.append("/").append(getMaximumNumberOfRepeats()); - } else { - sb.append(" (unlimited)"); - } - sb.append("\n"); - if (lastValidResetTime != null) { - sb.append(" Last Reset: ").append(lastValidResetTime.format(dateTimeFormatter)).append("\n"); - } - sb.append(" Can Trigger Again: ").append(canTriggerAgain()).append("\n"); - - // If paused, show pause duration - if (isPaused()) { - Duration currentPauseDuration = Duration.between(pauseStartTime, getNow()); - sb.append(" Current Pause Duration: ").append(formatDuration(currentPauseDuration)).append("\n"); - } - if (totalPauseDuration.getSeconds() > 0) { - sb.append(" Total Pause Duration: ").append(formatDuration(totalPauseDuration)).append("\n"); - } - - return sb.toString(); - } - - @Override - public void reset(boolean randomize) { - if (!isSatisfied()) { - return; - } - currentValidResetCount++; - lastValidResetTime = LocalDateTime.now(); - log.debug("SingleTriggerTimeCondition reset, will trigger again at: {}", - getNextTriggerTimeWithPause().orElse(getNow()).format(FORMATTER)); - } - @Override - public void hardReset() { - // Reset the condition state - this.currentValidResetCount = 0; - this.lastValidResetTime = LocalDateTime.now(); - setNextTriggerTime(ZonedDateTime.now(ZoneId.systemDefault()) - .plusSeconds(definedDelay.getSeconds())); - } - - - @Override - public double getProgressPercentage() { - - ZonedDateTime now = getNow(); - if (now.isAfter(getNextTriggerTimeWithPause().orElse(getNow()))) { - return 100.0; - } - - // Calculate time progress as percentage - long totalSeconds = java.time.Duration.between( - ZonedDateTime.now().withSecond(0).withNano(0), getNextTriggerTimeWithPause().orElse(getNow())).getSeconds(); - long secondsRemaining = java.time.Duration.between(now, getNextTriggerTimeWithPause().orElse(getNow())).getSeconds(); - - if (totalSeconds <= 0) { - return 0.0; - } - - double progress = 100.0 * (1.0 - (secondsRemaining / (double) totalSeconds)); - return Math.min(99.9, Math.max(0.0, progress)); // Cap between 0-99.9% - } - - - - @Subscribe - public void onGameTick(GameTick event) { - // Used to stay registered with the event bus - } - - @Override - public Optional getCurrentTriggerTime() { - // If already triggered and reset occurred, no future trigger - if (hasTriggered() && canTriggerAgain()) { - return Optional.empty(); - } - - // If already triggered but not reset, return the target time (in the past) - if (hasTriggered()) { - return Optional.of(getNextTriggerTimeWithPause().orElse(getNow())); - } - - // Not triggered yet, return future target time - return Optional.of(getNextTriggerTimeWithPause().orElse(getNow())); - } - @Override - protected void onResume(Duration pauseDuration) { - if (isPaused()) { - return; - } - // getNextTriggerTimeWithPause() provide old next trigger time -> we are resumed.. - ZonedDateTime nextTriggerTimeWithPauseDuration = getNextTriggerTimeWithPause().orElse(null); - if (nextTriggerTimeWithPauseDuration != null) { - nextTriggerTimeWithPauseDuration = nextTriggerTimeWithPauseDuration.plus(pauseDuration); - // Shift the next trigger time by the pause duration - setNextTriggerTime(nextTriggerTimeWithPauseDuration); - log.info("SingleTriggerTimeCondition resumed, next trigger time shifted to: {}", nextTriggerTimeWithPauseDuration); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/TimeCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/TimeCondition.java deleted file mode 100644 index cd31e3a13cd..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/TimeCondition.java +++ /dev/null @@ -1,487 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.events.GameTick; - -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; - -import java.time.Duration; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Optional; - -/** - * Abstract base class for all time-based conditions. - * Provides common functionality for time calculations and event handling. - */ -@Slf4j -@EqualsAndHashCode(callSuper = false) -public abstract class TimeCondition implements Condition { - @Getter - private final long maximumNumberOfRepeats; - @Getter - @Setter - protected transient long currentValidResetCount = 0; - // Last reset timestamp tracking - protected transient LocalDateTime lastValidResetTime; - protected static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss"); - - // Pause-related fields - @Getter - protected transient boolean isPaused = false; - protected transient ZonedDateTime pauseStartTime; - @Getter - protected transient Duration totalPauseDuration = Duration.ZERO; - - @Setter - private transient ZonedDateTime nextTriggerTime; - - /** - * Calculates the current pause duration without unpausing the condition. - * Provides high-resolution timing for pause duration with nanosecond precision. - * - * @return The current pause duration, or Duration.ZERO if not paused - */ - protected Duration getCurrentPauseDuration() { - if (!isPaused || pauseStartTime == null) { - return Duration.ZERO; - } - // Calculate with nanosecond precision for higher resolution - return Duration.between(pauseStartTime, getNow()); - } - - /** - * Gets the effective "now" time, adjusted for any active pause. - * This method is used to provide a consistent time reference point that - * doesn't advance during pauses. - * - * @return The current time if not paused, or the pause start time if paused - */ - protected ZonedDateTime getEffectiveNow() { - return isPaused ? pauseStartTime : getNow(); - } - - /** - * This method returns the next trigger time, adjusted for any pauses. - * If the condition is paused, the next trigger time is shifted by the duration of the pause. - * This allows the condition to account for time spent in a paused state when calculating the next trigger. - * - * Uses high-resolution pause duration tracking for more accurate calculations. - * - * @return Optional containing the adjusted next trigger time, or empty if no trigger is set - */ - public Optional getNextTriggerTimeWithPause() { - if (nextTriggerTime == null) { - return Optional.empty(); - } - - // If paused, adjust the trigger time by the current pause duration - if (isPaused) { - Duration currentPauseDuration = getCurrentPauseDuration(); - return Optional.of(nextTriggerTime.plus(currentPauseDuration)); - } - - return Optional.of(nextTriggerTime); - } - public TimeCondition() { - // Default constructor - this(0); - } - /** - * Constructor for TimeCondition with a specified repeat count - * - * @param maximumNumberOfRepeats Maximum number of times this condition can repeat, zero or negative means infinite repeats - */ - public TimeCondition(final long maximumNumberOfRepeats) { - this.maximumNumberOfRepeats = maximumNumberOfRepeats; - lastValidResetTime = LocalDateTime.now(); - } - /** - * Gets the current date and time in the system default time zone with maximum precision. - * Uses the most precise clock available in the system for consistent timing. - * - * @return The current ZonedDateTime with nanosecond precision - */ - protected ZonedDateTime getNow() { - return ZonedDateTime.now(ZoneId.systemDefault()); - } - - @Override - public ConditionType getType() { - return ConditionType.TIME; - } - - /** - * Pauses this time condition, preventing it from being satisfied until resumed. - * When paused, the condition's trigger time will be shifted by the pause duration. - * Uses high-precision time tracking to ensure accurate pause duration calculation. - */ - public void pause() { - if (!isPaused) { - isPaused = true; - pauseStartTime = getNow(); - log.debug("Time condition paused at: {}", pauseStartTime); - } - } - - /** - * resumes this time condition, allowing it to be satisfied again. - * The trigger time will be shifted by the duration of the pause with high precision. - * Uses nanosecond-level precision for duration calculations. - */ - public void resume() { - if (isPaused) { - ZonedDateTime now = getNow(); - Duration pauseDuration = Duration.between(pauseStartTime, now); - totalPauseDuration = totalPauseDuration.plus(pauseDuration); - isPaused = false; - - // Keep track of pause end time before nulling pauseStartTime - ZonedDateTime pauseEndTime = now; - pauseStartTime = null; - - // Call the subclass implementation to handle specific adjustments - onResume(pauseDuration); - - log.debug("Time condition resumed at: {}, pause duration: {}, total pause duration: {}", - pauseEndTime, formatDuration(pauseDuration), formatDuration(totalPauseDuration)); - } - } - - /** - * Called when the condition is resumed. - * Subclasses should implement this method to adjust their trigger times. - * - * @param pauseDuration The duration of the most recent pause - */ - protected abstract void onResume(Duration pauseDuration); - - /** - * Formats a duration into a human-readable string with appropriate precision. - * Shows milliseconds for durations less than 1 second for higher precision. - * - * @param duration The duration to format - * @return A human-readable string representation of the duration - */ - protected String formatDuration(Duration duration) { - long seconds = duration.getSeconds(); - int nanos = duration.getNano(); - - // For very short durations, show milliseconds - if (seconds == 0 && nanos > 0) { - return String.format("%dms", nanos / 1_000_000); - } else if (seconds < 60) { - // For durations under a minute, show seconds with decimal precision if needed - if (nanos > 0) { - return String.format("%.2fs", seconds + (nanos / 1_000_000_000.0)); - } - return seconds + "s"; - } else if (seconds < 3600) { - return String.format("%dm %ds", seconds / 60, seconds % 60); - } else { - return String.format("%dh %dm", seconds / 3600, (seconds % 3600) / 60); - } - } - - @Override - public String getDescription() { - boolean canTrigger = canTriggerAgain(); - String triggerStatus = canTrigger ? "Can trigger" : "Cannot trigger"; - String pauseStatus = isPaused ? " (PAUSED)" : ""; - String triggerCount = "Trigger Count: " + (maximumNumberOfRepeats > 0 ? - " (" + currentValidResetCount + "/" + maximumNumberOfRepeats + ")" : - String.valueOf(currentValidResetCount)); - - String lastReset = lastValidResetTime != null ? - "Last reset: " + lastValidResetTime.format(TIME_FORMATTER) : ""; - - // Enhanced pause information - StringBuilder pauseInfo = new StringBuilder(); - if (isPaused) { - Duration currentPauseDuration = getCurrentPauseDuration(); - pauseInfo.append("Current pause: ").append(formatDuration(currentPauseDuration)).append("\n"); - } - - if (totalPauseDuration.toMillis() > 0) { - pauseInfo.append("Total pause duration: ").append(formatDuration(totalPauseDuration)); - } - - return triggerStatus + pauseStatus + "\n" + triggerCount + "\n" + lastReset + - (pauseInfo.length() > 0 ? "\n" + pauseInfo.toString() : ""); - } - - - /** - * Default GameTick handler that subclasses can override - */ - public void onGameTick(GameTick gameTick) { - // Default implementation does nothing - } - - @Override - public void reset() { - this.reset(true); - } - - /** - * Resets the condition with optional randomization. - * Clears pause state and updates trigger times based on the randomize parameter. - * - * @param randomize Whether to apply randomization during reset - */ - @Override - public void reset(boolean randomize) { - // If paused, resume first - if (isPaused) { - resume(); - } - - // Reset total pause duration - totalPauseDuration = Duration.ZERO; - - // Subclasses should override this to implement specific reset behavior - } - - @Override - public void hardReset() { - // Reset the condition state completely - this.currentValidResetCount = 0; - this.lastValidResetTime = LocalDateTime.now(); - this.totalPauseDuration = Duration.ZERO; - - // Ensure not paused - if (isPaused) { - isPaused = false; - pauseStartTime = null; - } - - // Call normal reset with randomization - reset(true); - } - - void updateValidReset() { - if (isSatisfied()) { - this.currentValidResetCount++; - this.lastValidResetTime = LocalDateTime.now(); - } - - } - - /** - * Gets the next time this time condition will be satisfied, accounting for pauses. - * When paused, the trigger time is still calculated but effectively frozen until resumed. - * - * @return Optional containing the next trigger time, or empty if not applicable - */ - @Override - public Optional getCurrentTriggerTime() { - // If can't trigger again, don't provide a trigger time - if (!canTriggerAgain()) { - return Optional.empty(); - } - - // Calculate next trigger time (subclasses should override this) - // Note: We don't return empty for paused conditions anymore, instead - // we use getEffectiveNow() in calculations to freeze progress during pause - if (isSatisfied()) { - return Optional.of(getEffectiveNow()); - } - - // Default to using the next trigger time with pause handling - return getNextTriggerTimeWithPause(); - } - - /** - * Gets the duration until the next trigger time, accounting for pauses. - * When paused, the duration is calculated from the pause start time, effectively - * freezing the countdown until the condition is resumed. - * - * @return Optional containing the duration until next trigger, or empty if not applicable - */ - public Optional getDurationUntilNextTrigger() { - if(!canTriggerAgain()) { - return Optional.empty(); // No duration if already triggered too often - } - - Optional nextTrigger = getCurrentTriggerTime(); - if (nextTrigger.isPresent()) { - // Use effective now for consistent pause behavior - ZonedDateTime now = getEffectiveNow(); - 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)); - } - else { - // If trigger time is in the past, return zero duration - return Optional.of(Duration.ZERO); - } - } - return Optional.empty(); - } - - /** - * Calculates progress percentage toward next trigger. - * If the condition is paused, the progress remains frozen at its current value - * rather than resetting to zero. This provides a more accurate representation - * of the condition's state during pauses. - * - * @return Progress percentage (0-100) toward next trigger time - */ - @Override - public double getProgressPercentage() { - if (!canTriggerAgain()) { - return 0.0; // No progress if already triggered too often - } - - // If already satisfied, return 100% - if (isSatisfied()) { - return 100.0; - } - - // Calculate progress based on time until next trigger - Optional nextTrigger = getCurrentTriggerTime(); - if (nextTrigger.isPresent()) { - // When paused, use the pause start time as the reference point - // to keep progress frozen rather than resetting to 0% - ZonedDateTime now = getEffectiveNow(); - ZonedDateTime triggerTime = nextTrigger.get(); - - // If trigger is in the past, it's either 0% or 100% - if (!triggerTime.isAfter(now)) { - return isSatisfied() ? 100.0 : 0.0; - } - - // Calculate progress based on reference point and trigger time - return calculateProgressTowardTrigger(now, triggerTime); - } - - // Default behavior - return 0.0; - } - - /** - * Calculates progress percentage toward a specific trigger time. - * Base implementation for subclasses to override with specific calculations. - * This method should account for pause states by using the effective now time. - * - * When the progress calculation is requested during a paused state, - * the same progress value should be maintained rather than advancing. - * - * @param now Current time (or effective current time during pause) - * @param triggerTime Target trigger time - * @return Progress percentage (0-100) - */ - protected double calculateProgressTowardTrigger(ZonedDateTime now, ZonedDateTime triggerTime) { - // Default implementation uses a simple linear progress calculation - // Subclasses should override this to provide more specific implementations - if (now.isAfter(triggerTime)) { - return 100.0; - } - - try { - // Calculate total duration from start to trigger - Duration totalDuration = Duration.between(getNextTriggerTimeWithPause().orElse(now), triggerTime); - // Calculate elapsed duration - Duration elapsedDuration = Duration.between(now, triggerTime); - - if (totalDuration.isZero()) { - return 100.0; // Avoid division by zero - } - - // Calculate progress percentage - double progress = 100.0 * (1.0 - (elapsedDuration.toMillis() / (double) totalDuration.toMillis())); - // Ensure progress stays within 0-100 range - return Math.min(100.0, Math.max(0.0, progress)); - } catch (Exception e) { - // If any calculation errors occur, return 0% - return 0.0; - } - } - - /** - * Check if this condition uses randomization - * @return true if randomization is enabled, false otherwise - */ - public boolean isUseRandomization() { - return false; // Default implementation, subclasses should override if needed - } - /** - * Checks if this condition can trigger again (hasn't triggered yet) - * - * @return true if the condition hasn't triggered yet - */ - public boolean canTriggerAgain(){ - if (maximumNumberOfRepeats <= 0){ - return true; - } - if (currentValidResetCount < maximumNumberOfRepeats) { - return true; - } - return false; - } - abstract public boolean isSatisfiedAt(ZonedDateTime time); - - /** - * Checks if this condition has already triggered - * - * @return true if the condition has triggered at least once - */ - public boolean hasTriggered() { - return currentValidResetCount > 0; - } - - @Override - public boolean isSatisfied() { - // A condition cannot be satisfied while paused - if (isPaused) { - return false; - } - - // Default implementation defers to subclasses using isSatisfiedAt - return isSatisfiedAt(getNow()); - } - - /** - * Gets the estimated time until this time condition will be satisfied. - * This implementation leverages getCurrentTriggerTime() to provide accurate estimates - * for time-based conditions, taking into account pause adjustments. - * - * @return Optional containing the estimated duration until satisfaction, or empty if not determinable - */ - @Override - public Optional getEstimatedTimeWhenIsSatisfied() { - // If the condition is already satisfied, return zero duration - if (isSatisfied()) { - return Optional.of(Duration.ZERO); - } - - // Get the next trigger time, accounting for pauses - Optional triggerTime = getNextTriggerTimeWithPause(); - if (!triggerTime.isPresent()) { - // Try the regular getCurrentTriggerTime as fallback - triggerTime = getCurrentTriggerTime(); - } - - if (triggerTime.isPresent()) { - ZonedDateTime now = getEffectiveNow(); - 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); - } - - // If we can't determine the trigger time, return empty - return Optional.empty(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/TimeWindowCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/TimeWindowCondition.java deleted file mode 100644 index b169c04d770..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/TimeWindowCondition.java +++ /dev/null @@ -1,1292 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.enums.RepeatCycle; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.ui.TimeConditionPanelUtil; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; - -import java.time.Duration; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.temporal.ChronoUnit; -import java.util.Optional; - -import java.util.logging.Level; - -@Data -@EqualsAndHashCode(callSuper = false, exclude = { }) -@Slf4j -public class TimeWindowCondition extends TimeCondition { - - // Constants for unlimited date ranges - public static final LocalDate UNLIMITED_START_DATE = LocalDate.of(1900, 1, 1); - public static final LocalDate UNLIMITED_END_DATE = LocalDate.of(2100, 12, 31); - - public static String getVersion() { - return "0.0.3"; - } - // Time window bounds (daily start/end times) - private final LocalTime startTime; - private final LocalTime endTime; - - // Date range for validity period - private final LocalDate startDate; - private final LocalDate endDate; - - // Repeat cycle configuration - private final RepeatCycle repeatCycle; - private final int repeatIntervalUnit; //based on the repeat cycle, defines the interval unit (e.g., days, weeks, etc.) - - - // Next window tracking (for non-daily cycles) - //@Getter - //@Setter - - @Getter - @Setter - private transient LocalDateTime currentEndDateTime; - - // Randomization - private boolean useRandomization = false; - private int randomizerValue = 0; // Randomization value, depends on the repeat cycle - // randomizerValueUnit is now automatically determined based on repeatCycle - no longer stored as field - // Cached timezone for computation - not serialized - private transient ZoneId zoneId; - - - - @Getter - @Setter - private transient int transientNumberOfResetsWithinDailyInterval = 0; // Number of resets since last calculation - - /** - * Checks if the start date represents an unlimited (no restriction) start date - * @return true if start date is unlimited - */ - public boolean isUnlimitedStartDate() { - return startDate != null && startDate.equals(UNLIMITED_START_DATE); - } - - /** - * Checks if the end date represents an unlimited (no restriction) end date - * @return true if end date is unlimited - */ - public boolean isUnlimitedEndDate() { - return endDate != null && endDate.equals(UNLIMITED_END_DATE); - } - - /** - * Checks if this condition has unlimited date range (both start and end are unlimited) - * @return true if both dates are unlimited - */ - public boolean hasUnlimitedDateRange() { - return isUnlimitedStartDate() && isUnlimitedEndDate(); - } - - - /** - * Creates a time window condition with just daily start and end times. - * Uses unlimited date range (no start/end date restrictions), daily repeat cycle, and unlimited repeats. - * - * @param startTime The daily start time of the window - * @param endTime The daily end time of the window - */ - public TimeWindowCondition(LocalTime startTime, LocalTime endTime) { - this( - startTime, - endTime, - UNLIMITED_START_DATE, - UNLIMITED_END_DATE, - RepeatCycle.DAYS, - 1, - 0// 0 means infinity - ); - } - - /** - * Creates a time window condition with all parameters specified. - * This is the full constructor that allows complete configuration of the time window. - * - * @param startTime The daily start time of the window - * @param endTime The daily end time of the window - * @param startDate The earliest date the window can be active - * @param endDate The latest date the window can be active - * @param repeatCycle The cycle type for window repetition (DAYS, WEEKS, etc.) - * @param repeatIntervalUnit The interval between repetitions (e.g., 2 for every 2 days) - * @param maximumNumberOfRepeats Maximum number of times this condition can trigger (0 for unlimited) - */ - public TimeWindowCondition( - LocalTime startTime, - LocalTime endTime, - LocalDate startDate, - LocalDate endDate, - RepeatCycle repeatCycle, - int repeatIntervalUnit, - long maximumNumberOfRepeats - ) { - super(maximumNumberOfRepeats); - - this.startTime = startTime; - this.endTime = endTime; - this.startDate = startDate; - this.endDate = endDate; - this.repeatCycle = repeatCycle; - this.repeatIntervalUnit = Math.max(1, repeatIntervalUnit); // Ensure positive interval - this.zoneId = ZoneId.systemDefault(); // Initialize with system default - this.lastValidResetTime = LocalDateTime.now(); - transientNumberOfResetsWithinDailyInterval = 0; - this.randomizerValue = 0; - this.useRandomization = false; - - // Initialize next window times based on repeat cycle - calculateNextWindow(getNow().toLocalDateTime()); - } - /** - * Factory method to create a simple daily time window that repeats every day. - * Creates a window that starts and ends at the specified times each day, - * with unlimited date range (no start/end date restrictions). - * - * @param startTime The daily start time of the window - * @param endTime The daily end time of the window - * @return A configured TimeWindowCondition for daily repetition - */ - public static TimeWindowCondition createDaily(LocalTime startTime, LocalTime endTime) { - return new TimeWindowCondition( - startTime, - endTime, - UNLIMITED_START_DATE, - UNLIMITED_END_DATE, - RepeatCycle.DAYS, - 1, - 0// 0 means infinity - ); - - } - - /** - * {@inheritDoc} - * Returns the type of this condition, which is TIME. - * - * @return The condition type ConditionType.TIME - */ - @Override - public ConditionType getType() { - return ConditionType.TIME; - } - /** - * Gets the timezone used for time calculations in this condition. - * - * @return The ZoneId representing the timezone - */ - public ZoneId getZoneId() { - return zoneId; - } - - /** - * Sets the timezone to use for time calculations in this condition. - * Changes to the timezone will affect when the time window activates. - * - * @param zoneId The timezone to use for calculations - */ - public void setZoneId(ZoneId zoneId) { - this.zoneId = zoneId; - } - - /** - * Calculate the next window start and end times based on current time and reset settings - */ - private void calculateNextWindow(LocalDateTime lastValidTime) { - ZonedDateTime now = ZonedDateTime.now(getZoneId()); - LocalDateTime nowLocal = now.toLocalDateTime(); - - - LocalDateTime referenceTime = lastValidTime != null ? lastValidTime : nowLocal; - - //LocalDateTime todayStartDateTime = LocalDateTime.of(nowLocal.toLocalDate(), startTime); - //LocalDateTime todayEndDateTime = LocalDateTime.of(nowLocal.toLocalDate(), endTime); - if(Microbot.isDebug() ) log.info("Calculating next window - current: \n" + this,Level.INFO); - - switch (repeatCycle) { - case ONE_TIME: - calculateOneTimeWindow(referenceTime); - break; - case DAYS: - calculateCycleWindow(referenceTime); - - break; - case WEEKS: - calculateCycleWindow(referenceTime); - - break; - case MINUTES: - calculateCycleWindow(referenceTime); - - break; - case HOURS: - calculateCycleWindow(referenceTime); - - break; - - default: - log.warn("Unsupported repeat cycle: {}", repeatCycle); - break; - } - - if(Microbot.isDebug() ) - { - log.info(this.getDetailedDescription()); - log.info("After calculate new cycle window : \n" + this); - } - // Apply randomization if enabled - - // Only check end date bounds if not unlimited - if (!isUnlimitedEndDate()) { - LocalDateTime lastEnd = LocalDateTime.of(endDate, endTime); - if (getNextTriggerTimeWithPause().orElse(null) != null) { - LocalDateTime nextTrigger = getNextTriggerTimeWithPause().get().toLocalDateTime(); - if (nextTrigger.isAfter(lastEnd)){ - setNextTriggerTime(null); - this.currentEndDateTime = null; - } - }else{ - this.currentEndDateTime = null; - } - } - if(Microbot.isDebug() ) log.info("Calculating done - new time window: \n" + this,Level.INFO); - } - - /** - * Calculates window for ONE_TIME repeat cycle - */ - private void calculateOneTimeWindow(LocalDateTime referenceTime) { - ZonedDateTime now = ZonedDateTime.now(getZoneId()); - LocalDateTime nowLocal = now.toLocalDateTime(); - LocalDateTime todayStartDateTime = LocalDateTime.of(nowLocal.toLocalDate(), startTime); - LocalDateTime todayEndDateTime = LocalDateTime.of(nowLocal.toLocalDate(), endTime); - if (transientNumberOfResetsWithinDailyInterval == 0 ){ - if (todayEndDateTime.isBefore(todayStartDateTime)) { - todayEndDateTime = todayEndDateTime.plusDays(1); - } - if (referenceTime.isAfter(todayStartDateTime) && referenceTime.isBefore(todayEndDateTime)) { - setNextTriggerTime(todayStartDateTime.atZone(getZoneId())); - this.currentEndDateTime = todayEndDateTime; - } else { - setNextTriggerTime(todayEndDateTime.plusDays(1).atZone(getZoneId())); - this.currentEndDateTime = todayEndDateTime.plusDays(1); - - } - }else{ - // If the reset time is after the end of the window, set to null - if (lastValidResetTime.isAfter(currentEndDateTime)) { - setNextTriggerTime(null); - this.currentEndDateTime = null; - } else { - // wait until we are outside current vaild window - } - } - } - - - - /** - * Calculates window for sub-day repeat cycles (MINUTES, HOURS) - */ - private void calculateCycleWindow(LocalDateTime referenceTime) { - ZonedDateTime now = ZonedDateTime.now(getZoneId()); - // First, determine the bounds of today's overall window - LocalDate today = now.toLocalDate(); - LocalDateTime currentDayWindowStart = LocalDateTime.of(today, startTime); - LocalDateTime currentDayWindowEnd = LocalDateTime.of(today, endTime); - - // Handle cross-midnight windows - if (currentDayWindowEnd.isBefore(currentDayWindowStart)) { - currentDayWindowEnd = currentDayWindowEnd.plusDays(1); - } - - - - LocalDateTime nextTriggerTime = calculateNextStartWindow(referenceTime); - setNextTriggerTime(nextTriggerTime.atZone(getZoneId())); - if (Microbot.isDebug()) log.info("calculation of new cycle window after calculation of next start window:\n {}", this); - - - // If next interval starts after the outer window end, it's not valid today - LocalDate startday = nextTriggerTime.toLocalDate(); - this.currentEndDateTime = LocalDateTime.of(startday, endTime); - if (currentEndDateTime.isBefore(nextTriggerTime)) { - this.currentEndDateTime = calculateNextTime( this.currentEndDateTime); - LocalDateTime endDateTimeNextDay = LocalDateTime.of(startday.plusDays(1), endTime); - if (this.currentEndDateTime.isBefore(nextTriggerTime) || endDateTimeNextDay.isBefore(this.currentEndDateTime)) { - throw new IllegalStateException("Invalid end time calculation: " + this.currentEndDateTime); - } - } - - } - - private LocalDateTime calculateNextTime( LocalDateTime referenceTime) { - LocalDateTime nextStartTime; - - if (repeatIntervalUnit == 0) { - return referenceTime; - } - - // First calculate the base next time without randomization - switch (repeatCycle) { - case ONE_TIME: - nextStartTime = referenceTime; - break; - case MINUTES: - nextStartTime = referenceTime.plusMinutes(repeatIntervalUnit); - break; - case HOURS: - nextStartTime = referenceTime.plusHours(repeatIntervalUnit); - break; - case DAYS: - nextStartTime = referenceTime.plusDays(repeatIntervalUnit); - break; - case WEEKS: - nextStartTime = referenceTime.plusWeeks(repeatIntervalUnit); - break; - default: - log.warn("Unsupported repeat cycle: {}", repeatCycle); - nextStartTime = referenceTime; - break; - } - log.info("Base next start time calculated: \n\t{}\n\t{}", nextStartTime); - - // Apply user-configured randomization if enabled - if (useRandomization && randomizerValue > 0) { - // Calculate maximum allowed randomization based on interval and cycle - int maxAllowedRandomization = calculateMaxAllowedRandomization(); - - // Cap the randomizer value to the maximum allowed - int cappedRandomizerValue = Math.min(randomizerValue, maxAllowedRandomization); - - // Generate random offset between -cappedRandomizerValue and +cappedRandomizerValue - int randomOffset = Rs2Random.between(-cappedRandomizerValue, cappedRandomizerValue); - - // Store the base time before applying randomization for logging - LocalDateTime baseTime = nextStartTime; - - // Automatically determine the appropriate randomization unit based on repeat cycle - RepeatCycle randomUnit = getAutomaticRandomizerValueUnit(); - switch (randomUnit) { - case SECONDS: - nextStartTime = nextStartTime.plusSeconds(randomOffset); - break; - case MINUTES: - nextStartTime = nextStartTime.plusMinutes(randomOffset); - break; - case HOURS: - nextStartTime = nextStartTime.plusHours(randomOffset); - break; - default: - // Default to minutes if unsupported unit - nextStartTime = nextStartTime.plusMinutes(randomOffset); - break; - } - - log.info("Applied randomization: {} {} offset to next trigger time. Base: {}, Final: {} (capped from {} to {})", - randomOffset, randomUnit, baseTime, nextStartTime, - randomizerValue, cappedRandomizerValue); - } - log.info("Next start time after randomization: {}", nextStartTime); - return nextStartTime; - } - - /** - * Calculates the maximum allowed randomization value based on the repeat cycle and interval. - * This ensures randomization stays within meaningful bounds relative to the interval. - * - * @return Maximum allowed randomization value in the automatic randomization unit - */ - private int calculateMaxAllowedRandomization() { - // Convert interval to the same unit as randomization for comparison - RepeatCycle randomUnit = getAutomaticRandomizerValueUnit(); - return TimeConditionPanelUtil.calculateMaxAllowedRandomization(getRepeatCycle(), getRepeatIntervalUnit()); - // Calculate total interval in the randomization unit - } - - /** - * Converts an interval value from one unit to another for comparison purposes. - * - * @param value The interval value to convert - * @param fromUnit The original unit - * @param toUnit The target unit - * @return The converted value - */ - public static long convertToRandomizationUnit(int value, RepeatCycle fromUnit, RepeatCycle toUnit) { - // Convert to seconds first, then to target unit - long totalSeconds; - switch (fromUnit) { - case MINUTES: - totalSeconds = value * 60L; - break; - case HOURS: - totalSeconds = value * 3600L; - break; - case DAYS: - totalSeconds = value * 86400L; - break; - case WEEKS: - totalSeconds = value * 604800L; - break; - default: - totalSeconds = value; - break; - } - - // Convert from seconds to target unit - switch (toUnit) { - case SECONDS: - return totalSeconds; - case MINUTES: - return totalSeconds / 60L; - case HOURS: - return totalSeconds / 3600L; - default: - return totalSeconds / 60L; // Default to minutes - } - } - /** - * Helper method to calculate interval from a reference point - */ - private LocalDateTime calculateNextStartWindow( LocalDateTime referenceTime) { - LocalDateTime nextStartTime; - ZonedDateTime now = ZonedDateTime.now(getZoneId()); - LocalDateTime nowLocal = now.toLocalDateTime(); - LocalDate today = now.toLocalDate(); - LocalDateTime currentDayWindowStart = LocalDateTime.of(today, startTime); - LocalDateTime currentDayWindowEnd = LocalDateTime.of(today, endTime); - if (this.currentValidResetCount > 0) { - - nextStartTime = calculateNextTime(referenceTime); - - }else { - if (nowLocal.isBefore(currentDayWindowEnd)) { - nextStartTime = currentDayWindowStart; - } else { - nextStartTime = currentDayWindowStart.plusDays(1); - } - } - - if (nextStartTime.isBefore(currentDayWindowStart)) { - nextStartTime = currentDayWindowStart; - }else if (nextStartTime.isBefore(currentDayWindowEnd)) { - - }else if (nextStartTime.isAfter(currentDayWindowEnd)) { - LocalDate nextDay = now.toLocalDate().plusDays(1); - nextStartTime = LocalDateTime.of(nextDay, startTime); - } - return nextStartTime; - - - } - - - /** - * {@inheritDoc} - * Determines if the current time is within the configured time window. - * Checks if the current time is after the start time and before the end time - * of the current window, and if the condition can still trigger. - * - * @return true if the current time is within the active window and the condition can trigger, - * false otherwise - */ - @Override - public boolean isSatisfied() { - if (getNextTriggerTimeWithPause().isPresent()) { - return isSatisfied(getNextTriggerTimeWithPause().get().toLocalDateTime()); - } - return false; - } - @Override - public boolean isSatisfiedAt(ZonedDateTime triggerTime) { - return isSatisfied(triggerTime.toLocalDateTime()); - - } - - private boolean isSatisfied(LocalDateTime currentStartDateTime) { - if (isPaused()) { - return false; - } - if (!canTriggerAgain()) { - return false; - } - ZonedDateTime now = ZonedDateTime.now(getZoneId()); - LocalDateTime nowLocal = now.toLocalDateTime(); - LocalDate today = now.toLocalDate(); - LocalDate dayBefore = today.minusDays(1); - LocalDateTime currentDayWindowStart = LocalDateTime.of(today, startTime); - LocalDateTime beforeDayWindowStart = LocalDateTime.of( dayBefore, startTime); - LocalDateTime beforeDayWindowEnd = LocalDateTime.of(dayBefore, endTime); - - // For non-daily or interval > 1 day cycles, check against calculated next window - if (currentStartDateTime == null || currentEndDateTime == null) { - - return false; // No more windows in range - - } - if ((currentStartDateTime.isAfter(beforeDayWindowStart) && currentEndDateTime.isBefore(beforeDayWindowEnd))) { - lastValidResetTime = currentDayWindowStart; - this.calculateNextWindow(this.lastValidResetTime); - - } - // Check if window has passed - but don't auto-recalculate - // Let the scheduler decide when to reset the condition - if (nowLocal.isAfter(currentEndDateTime)) { - return false; - } - - // Check if within next window - return nowLocal.isAfter(currentStartDateTime) && nowLocal.isBefore(currentEndDateTime); - - } - - /** - * {@inheritDoc} - * Calculates progress through the current time window as a percentage. - * Returns 0% if outside the window or 0-100% based on how much of the window has elapsed. - * - * @return A percentage from 0-100 indicating progress through the current time window - */ - @Override - public double getProgressPercentage() { - if (!isSatisfied()) { - return 0.0; - } - - // If our window bounds aren't set, we can't calculate progress - if (getNextTriggerTimeWithPause().orElse(null) == null || currentEndDateTime == null) { - log.debug("Unable to calculate progress - window bounds are null"); - return 0.0; - } - - ZonedDateTime now = ZonedDateTime.now(getZoneId()); - LocalDateTime nowLocal = now.toLocalDateTime(); - LocalDateTime currentStartDateTime = getNextTriggerTimeWithPause().get().toLocalDateTime(); - // Calculate total window duration in seconds - long totalDuration = ChronoUnit.SECONDS.between(currentStartDateTime, currentEndDateTime); - if (totalDuration <= 0) { - log.debug("Invalid window duration: {} seconds", totalDuration); - return 0.0; - } - - // Calculate elapsed duration in seconds - long elapsedDuration = ChronoUnit.SECONDS.between(currentStartDateTime, nowLocal); - - // Calculate percentage - cap at 100% - double percentage = Math.min(100.0, (elapsedDuration * 100.0) / totalDuration); - - log.debug("Progress calculation: {}% ({}/{} seconds)", - String.format("%.1f", percentage), - elapsedDuration, - totalDuration); - - return percentage; - } - - /** - * {@inheritDoc} - * Provides a user-friendly description of this time window condition. - * Includes the time range, repeat information, and timezone. - * - * @return A human-readable string describing the time window parameters - */ - @Override - public String getDescription() { - StringBuilder description = new StringBuilder("Time Window: "); - - // Format times - DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm"); - String timeRangeStr = startTime.format(timeFormatter) + " to " + endTime.format(timeFormatter); - description.append(timeRangeStr); - // Add date range information only if not unlimited - if (!hasUnlimitedDateRange()) { - DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - description.append(" ( Between: "); - - if (isUnlimitedStartDate()) { - description.append("No start limit"); - } else { - description.append(startDate.format(dateFormatter)); - } - - description.append(" to "); - - if (isUnlimitedEndDate()) { - description.append("No end limit"); - } else { - description.append(endDate.format(dateFormatter)); - } - - description.append(")"); - } - // Add repeat information - if (repeatCycle != RepeatCycle.ONE_TIME) { - - description.append(" (") - .append(repeatCycle.getDisplayName().replace("X", Integer.toString(repeatIntervalUnit))) - .append(")"); - } - - // Add timezone information for clarity - //description.append(" [").append(getZoneId().getId()).append("]"); - description.append("\n"+super.getDescription()); - return description.toString(); - } - - - /** - * Configures time randomization for this window. - * When enabled, the start and end times will be adjusted by a random - * amount within the specified range each time the condition is reset. - * - * @param useRandomization Whether to enable time randomization - * @param randomizerValue Maximum number of minutes to randomize by (plus or minus) - */ - public void setRandomization(boolean useRandomization, int randomizerValue) { - this.useRandomization = useRandomization; - this.randomizerValue = Math.max(0, randomizerValue); - } - /** - * Configures time randomization for this window. - * When enabled, the start and end times will be adjusted by a random - * amount within the specified range each time the condition is reset. - * - * @param useRandomization Whether to enable time randomization - */ - public void setRandomization(boolean useRandomization) { - this.useRandomization = useRandomization; - this.randomizerValue = 0; - } - - /** - * Sets the randomization value without changing the enabled state. - * - * @param randomizerValue The randomization value to set - */ - public void setRandomizerValue(int randomizerValue) { - this.randomizerValue = Math.max(0, randomizerValue); - } - - /** - * Gets the randomization value. - * - * @return The current randomization value - */ - public int getRandomizerValue() { - return this.randomizerValue; - } - - /** - * Gets the randomization unit that is automatically determined based on the repeat cycle. - * - * @return The current automatically determined randomization unit - */ - public RepeatCycle getRandomizerValueUnit() { - return getAutomaticRandomizerValueUnit(); - } - - - - /** - * Custom deserialization method to initialize transient fields. - * This ensures that the timezone is properly set after deserialization. - * - * @return The properly initialized deserialized object - */ - public Object readResolve() { - // Initialize timezone if needed - if (zoneId == null) { - zoneId = ZoneId.systemDefault(); - } - return this; - } - - /** - * Resets the time window condition with default settings. - * Calculates the next time window based on current time and settings. - * This is a shorthand for reset(false). - */ - public void reset() { - reset(false); - } - - - public void hardReset() { - this.currentValidResetCount = 0; - resume(); - this.lastValidResetTime = LocalDateTime.now(); - this.setNextTriggerTime(null); - - this.currentEndDateTime = null; - this.useRandomization = true; - this.transientNumberOfResetsWithinDailyInterval = 0; - // Initialize next window times based on repeat cycle - calculateNextWindow(this.lastValidResetTime); - - } - /** - * {@inheritDoc} - * Resets the time window condition and calculates the next active window. - * Updates the reset count, applies randomization if enabled, and advances - * the window if necessary based on current time. - * - * @param randomize Whether to apply randomization to window times - */ - @Override - public void reset(boolean randomize) { - // Store current time as the reset reference - log.debug("Last reset time: {}", lastValidResetTime); - this.lastValidResetTime = LocalDateTime.now(); - ZonedDateTime now = ZonedDateTime.now(getZoneId()); - LocalDateTime nowLocal = now.toLocalDateTime(); - // If we are have a current window and we are within the window or after it, we need to force an advance - Optional currentZoneStartDateTime = getNextTriggerTimeWithPause(); - - if (currentZoneStartDateTime ==null || !currentZoneStartDateTime.isPresent()) { - return; - } - LocalDateTime currentStartDateTime =currentZoneStartDateTime.get().toLocalDateTime(); - boolean needsAdvance = currentZoneStartDateTime!=null && currentZoneStartDateTime.isPresent() && nowLocal.isAfter(currentZoneStartDateTime.get().toLocalDateTime()); - // If this the next start window that's passed or any window that needs advancing - if (needsAdvance && canTriggerAgain() ) { - this.currentValidResetCount++; - calculateNextWindow(this.lastValidResetTime); - } - if (nowLocal.isAfter(currentStartDateTime) && nowLocal.isBefore(this.currentEndDateTime)) { - transientNumberOfResetsWithinDailyInterval++; - }else { - transientNumberOfResetsWithinDailyInterval = 0; - } - // Log the new window for debugging - if (currentStartDateTime != null && this.currentEndDateTime != null) { - log.debug("Next window after reset: {} to {}", - DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(currentStartDateTime), - DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(currentEndDateTime)); - } else { - log.debug("No next window available after reset"); - } - } - - /** - * {@inheritDoc} - * Indicates whether this condition uses randomization. - * - * @return true if randomization is enabled, false otherwise - */ - @Override - public boolean isUseRandomization() { - return useRandomization; - } - - /** - * {@inheritDoc} - * Calculates the next time this condition will be satisfied (the start of the next window). - * If already within a window, returns a time slightly in the past to indicate the condition - * is currently satisfied. - * - * @return An Optional containing the time when the next window starts, - * or empty if no future windows are scheduled or the condition cannot trigger again - */ - @Override - public Optional getCurrentTriggerTime() { - if (getNextTriggerTimeWithPause().orElse(null) == null || currentEndDateTime == null || !canTriggerAgain()) { - return Optional.empty(); - } - ZonedDateTime now = ZonedDateTime.now(getZoneId()); - LocalDateTime currentStartDateTime = getNextTriggerTimeWithPause().get().toLocalDateTime(); - // If the condition is already satisfied (we're in the window), return the current time - if (isSatisfied()) { - assert(!currentStartDateTime.isAfter(now.toLocalDateTime()) && !currentEndDateTime.isBefore(now.toLocalDateTime())); - return Optional.of(currentStartDateTime.atZone(getZoneId())); // Slightly in the past to indicate "ready now" - } - - // If our window calculation failed or hasn't been done, calculate it - if (currentStartDateTime == null) { - return Optional.empty(); - }else{ - return Optional.of(currentStartDateTime.atZone(getZoneId())); - } - } - @Override - public boolean canTriggerAgain(){ - - boolean canTrigger = super.canTriggerAgain(); - - // If end date is unlimited, only check parent class logic - if (isUnlimitedEndDate()) { - return canTrigger; - } - - LocalDateTime lastDateTime = LocalDateTime.of( endDate, endTime); - if (canTrigger ) { - ZonedDateTime now = ZonedDateTime.now(getZoneId()); - LocalDateTime nowLocal = now.toLocalDateTime(); - return nowLocal.isBefore(lastDateTime); - } - return canTrigger; - - } - - /** - * {@inheritDoc} - * Generates a detailed string representation of this time window condition. - * Includes configuration, status, window times, randomization settings, - * and trigger count information formatted with visual separators. - * - * @return A multi-line string representation with detailed state information - */ - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm"); - DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - - // Basic information - sb.append("TimeWindowCondition:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Time Range: ").append(startTime.format(timeFormatter)) - .append(" to ").append(endTime.format(timeFormatter)).append("\n"); - sb.append(" │ Date Range: "); - if (isUnlimitedStartDate()) { - sb.append("No start limit"); - } else { - sb.append(startDate.format(dateFormatter)); - } - sb.append(" to "); - if (isUnlimitedEndDate()) { - sb.append("No end limit"); - } else { - sb.append(endDate.format(dateFormatter)); - } - sb.append("\n"); - sb.append(" │ Repeat: ").append(repeatCycle) - .append(", Unit: ").append(repeatIntervalUnit).append("\n"); - sb.append(" │ Timezone: ").append(getZoneId().getId()).append("\n"); - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - sb.append(" │ Satisfied: ").append(isSatisfied()).append("\n"); - sb.append(" │ Paused: ").append(isPaused()).append("\n"); - if (getNextTriggerTimeWithPause().orElse(null) != null && currentEndDateTime != null) { - sb.append(" │ Current Window: ").append(getNextTriggerTimeWithPause().get().toLocalDateTime().format(dateTimeFormatter)) - .append("\n │ To: ").append(currentEndDateTime.format(dateTimeFormatter)).append("\n"); - } else if (getNextTriggerTimeWithPause().orElse(null) != null) { - sb.append(" │ Current Window: ").append(getNextTriggerTimeWithPause().get().toLocalDateTime().format(dateTimeFormatter)) - .append("\n │ To: Not available\n"); - } else { - sb.append(" │ Current Window: Not available\n"); - } - if (isSatisfied()) { - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - } - - // Randomization - sb.append(" ├─ Randomization ────────────────────────────\n"); - sb.append(" │ Randomization: ").append(useRandomization ? "Enabled" : "Disabled").append("\n"); - if (useRandomization) { - RepeatCycle randomUnit = getAutomaticRandomizerValueUnit(); - int maxAllowedRandomization = calculateMaxAllowedRandomization(); - int cappedRandomizerValue = Math.min(randomizerValue, maxAllowedRandomization); - - // Original configured value - sb.append(" │ Random Range: ±").append(randomizerValue).append(" ").append(randomUnit.toString().toLowerCase()).append("\n"); - - // Actual capped value used in calculations - if (cappedRandomizerValue != randomizerValue) { - sb.append(" │ Capped Range: ±").append(cappedRandomizerValue).append(" ").append(randomUnit.toString().toLowerCase()) - .append(" (limited from ").append(randomizerValue).append(")\n"); - } - - // Maximum allowed randomization for this interval - sb.append(" │ Max Allowed: ±").append(maxAllowedRandomization).append(" ").append(randomUnit.toString().toLowerCase()).append("\n"); - - // Show the automatic unit determination - sb.append(" │ Random Unit: ").append(randomUnit.toString().toLowerCase()) - .append(" (auto-determined from ").append(repeatCycle.toString().toLowerCase()).append(" cycle)\n"); - } - - // Tracking info - sb.append(" └─ Tracking ────────────────────────────────\n"); - sb.append(" Reset Count: ").append(currentValidResetCount); - if (this.getMaximumNumberOfRepeats() > 0) { - sb.append("/").append(getMaximumNumberOfRepeats()); - } else { - sb.append(" (unlimited)"); - } - sb.append("\n"); - if (lastValidResetTime != null) { - sb.append(" Last Reset: ").append(lastValidResetTime.format(dateTimeFormatter)).append("\n"); - } - sb.append(" Daily Reset Count: ").append(transientNumberOfResetsWithinDailyInterval).append("\n"); - sb.append(" Can Trigger Again: ").append(canTriggerAgain()).append("\n"); - - // Add pause information - if (isPaused()) { - sb.append(" Paused: Yes\n"); - Duration currentPauseDuration = Duration.between(pauseStartTime, getNow()); - sb.append(" Current Pause Duration: ").append(formatDuration(currentPauseDuration)).append("\n"); - } - if (totalPauseDuration.getSeconds() > 0) { - sb.append(" Total Pause Duration: ").append(formatDuration(totalPauseDuration)).append("\n"); - } - - return sb.toString(); - } - - /** - * Provides a detailed description of the time window condition with status information. - * Includes the window times, repeat cycle, current status, progress, randomization, - * and tracking information in a human-readable format. - * - * @return A detailed multi-line string with current status and configuration details - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm"); - DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - - // Basic description - sb.append("\nTime Window Condition:\ntActive from ").append(startTime.format(timeFormatter)) - .append(" to ").append(endTime.format(timeFormatter)).append("\n"); - - // Repeat cycle - if (repeatCycle == RepeatCycle.ONE_TIME) { - sb.append("Schedule: One time only"); - if (!hasUnlimitedDateRange()) { - sb.append(" ("); - if (isUnlimitedStartDate()) { - sb.append("No start limit"); - } else { - sb.append(startDate.format(dateFormatter)); - } - sb.append(" - "); - if (isUnlimitedEndDate()) { - sb.append("No end limit"); - } else { - sb.append(endDate.format(dateFormatter)); - } - sb.append(")"); - } - sb.append("\n"); - } else { - sb.append("Schedule: Repeats every ").append(repeatIntervalUnit).append(" ") - .append(repeatCycle.toString().toLowerCase()).append("\n"); - if (!hasUnlimitedDateRange()) { - sb.append("Valid period: "); - if (isUnlimitedStartDate()) { - sb.append("No start limit"); - } else { - sb.append(startDate.format(dateFormatter)); - } - sb.append(" - "); - if (isUnlimitedEndDate()) { - sb.append("No end limit"); - } else { - sb.append(endDate.format(dateFormatter)); - } - sb.append("\n"); - } - } - - // Status information - boolean satisfied = isSatisfied(); - sb.append("Status: ").append(satisfied ? "Active (in time window)" : "Inactive (outside time window)").append("\n"); - - // Current window information - ZonedDateTime now = ZonedDateTime.now(getZoneId()); - LocalDateTime nowLocal = now.toLocalDateTime(); - - if (getNextTriggerTimeWithPause() != null && currentEndDateTime != null) { - sb.append("Current window: ").append(getNextTriggerTimeWithPause().get().toLocalDateTime().format(dateTimeFormatter)) - .append(" to ").append(currentEndDateTime.format(dateTimeFormatter)).append("\n"); - - if (nowLocal.isAfter(getNextTriggerTimeWithPause().get().toLocalDateTime()) && nowLocal.isBefore(currentEndDateTime)) { - sb.append("Time remaining: ") - .append(ChronoUnit.MINUTES.between(nowLocal, currentEndDateTime)) - .append(" minutes\n"); - sb.append("Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - } else if (nowLocal.isBefore(getNextTriggerTimeWithPause().get().toLocalDateTime())) { - sb.append("Window starts in: ") - .append(ChronoUnit.MINUTES.between(nowLocal, getNextTriggerTimeWithPause().get().toLocalDateTime())) - .append(" minutes\n"); - } else { - sb.append("Window has passed\n"); - } - } else { - sb.append("No active window available\n"); - } - if (useRandomization) { - sb.append(" │ Random Range: ±").append(randomizerValue); - switch (repeatCycle) { - case MINUTES: - sb.append(" millisec\n"); - break; - case HOURS: - sb.append(" seconds\n"); - break; - case DAYS: - sb.append(" minutes\n"); - break; - case WEEKS: - sb.append(" hours\n"); - break; - default: - break; - } - }else { - sb.append("Randomization: Disabled\n"); - } - - // Reset tracking - sb.append("Reset count: ").append(currentValidResetCount); - if (getMaximumNumberOfRepeats() > 0) { - sb.append("/").append(getMaximumNumberOfRepeats()); - } else { - sb.append(" (unlimited)"); - } - sb.append("\n"); - - if (lastValidResetTime != null) { - sb.append("Last reset: ").append(lastValidResetTime.format(dateTimeFormatter)).append("\n"); - } - - // Timezone information - sb.append("Timezone: ").append(getZoneId().getId()).append("\n"); - - return sb.toString(); - } - - /** - * Called when the condition is resumed. - * Shifts time windows by the pause duration to maintain the same - * relative timing after a pause. - * - * @param pauseDuration The duration of the most recent pause - */ - @Override - protected void onResume(Duration pauseDuration) { - if (isPaused()) { - return; - } - - // Get the original trigger time (since isPaused=false at this point after resume()) - // getNextTriggerTimeWithPause() now returns the original nextTriggerTime without pause adjustments - ZonedDateTime originalTriggerTime = getNextTriggerTimeWithPause().orElse(null); - - if (originalTriggerTime != null) { - // Shift the original trigger time by the pause duration to preserve timing - ZonedDateTime shiftedTriggerTime = originalTriggerTime.plus(pauseDuration); - LocalDateTime shiftedLocalTime = shiftedTriggerTime.toLocalDateTime(); - - // Validate that the shifted time still falls within allowed bounds - boolean isValidShiftedTime = isShiftedTimeWithinBounds(shiftedLocalTime); - - if (isValidShiftedTime) { - // Shifted time is valid, use it - setNextTriggerTime(shiftedTriggerTime); - - // Also shift the current end time by the same duration to maintain window length - if (currentEndDateTime != null) { - LocalDateTime shiftedEndTime = currentEndDateTime.plus(pauseDuration); - // Validate that the shifted end time is also within bounds - if (isShiftedEndTimeWithinBounds(shiftedEndTime)) { - currentEndDateTime = shiftedEndTime; - } else { - // If shifted end time goes out of bounds, recalculate the window - log.warn("Shifted end time {} goes beyond allowed bounds, recalculating window", shiftedEndTime); - calculateNextWindow(getNow().toLocalDateTime()); - return; - } - } - - // Shift the last valid reset time if it exists - if (lastValidResetTime != null) { - lastValidResetTime = lastValidResetTime.plus(pauseDuration); - } - - log.debug("TimeWindowCondition resumed after {}, window shifted by pause duration, new trigger time: {}", - formatDuration(pauseDuration), getNextTriggerTimeWithPause().orElse(null)); - } else { - // Shifted time goes out of bounds, recalculate next valid window - log.warn("Shifted trigger time {} goes beyond allowed bounds, recalculating next valid window", shiftedLocalTime); - calculateNextWindow(getNow().toLocalDateTime()); - } - } else { - // If no trigger time was set, calculate a new window from current time - // This should only happen if the condition was never properly initialized - log.warn("TimeWindowCondition resumed but no trigger time was set, recalculating window"); - calculateNextWindow(getNow().toLocalDateTime()); - } - } - - /** - * Validates that a shifted trigger time is still within the allowed time window and date bounds. - * - * @param shiftedTime The shifted trigger time to validate - * @return true if the shifted time is within bounds, false otherwise - */ - private boolean isShiftedTimeWithinBounds(LocalDateTime shiftedTime) { - // Check date range bounds (if not unlimited) - if (!isUnlimitedStartDate() && shiftedTime.toLocalDate().isBefore(startDate)) { - log.debug("Shifted time {} is before start date {}", shiftedTime, startDate); - return false; - } - - if (!isUnlimitedEndDate()) { - LocalDateTime lastValidDateTime = LocalDateTime.of(endDate, endTime); - if (shiftedTime.isAfter(lastValidDateTime)) { - log.debug("Shifted time {} is after end date/time {}", shiftedTime, lastValidDateTime); - return false; - } - } - - // Check daily time bounds - LocalTime shiftedLocalTime = shiftedTime.toLocalTime(); - - // Handle cross-midnight windows - if (endTime.isBefore(startTime)) { - // Cross-midnight window (e.g., 22:00 to 06:00) - boolean isInFirstPart = !shiftedLocalTime.isBefore(startTime); // >= startTime - boolean isInSecondPart = !shiftedLocalTime.isAfter(endTime); // <= endTime - - if (!(isInFirstPart || isInSecondPart)) { - log.debug("Shifted time {} is outside cross-midnight window {} to {}", - shiftedLocalTime, startTime, endTime); - return false; - } - } else { - // Normal window (e.g., 09:00 to 17:00) - if (shiftedLocalTime.isBefore(startTime) || shiftedLocalTime.isAfter(endTime)) { - log.debug("Shifted time {} is outside time window {} to {}", - shiftedLocalTime, startTime, endTime); - return false; - } - } - - return true; - } - - /** - * Validates that a shifted end time is still within the allowed date bounds. - * - * @param shiftedEndTime The shifted end time to validate - * @return true if the shifted end time is within bounds, false otherwise - */ - private boolean isShiftedEndTimeWithinBounds(LocalDateTime shiftedEndTime) { - // Only need to check date bounds for end time, not daily time bounds - if (!isUnlimitedEndDate()) { - LocalDateTime lastValidDateTime = LocalDateTime.of(endDate, endTime); - if (shiftedEndTime.isAfter(lastValidDateTime)) { - log.debug("Shifted end time {} is after end date/time {}", shiftedEndTime, lastValidDateTime); - return false; - } - } - - return true; - } - - /** - * Gets the estimated time until this time window condition will be satisfied. - * This provides a more accurate estimate by considering the window start time, - * repeat cycles, and current window state. - * - * @return Optional containing the estimated duration until satisfaction, or empty if not determinable - */ - @Override - public Optional getEstimatedTimeWhenIsSatisfied() { - // If the condition is already satisfied (we're in the window), return zero - if (isSatisfied()) { - return Optional.of(Duration.ZERO); - } - - // If we can't trigger again, return empty - if (!canTriggerAgain()) { - return Optional.empty(); - } - - // Get the next trigger time with pause adjustments - Optional triggerTime = getNextTriggerTimeWithPause(); - if (!triggerTime.isPresent()) { - // Fallback to regular getCurrentTriggerTime - triggerTime = getCurrentTriggerTime(); - } - - if (triggerTime.isPresent()) { - ZonedDateTime now = getEffectiveNow(); - Duration duration = Duration.between(now, triggerTime.get()); - - // Apply randomization if enabled to provide a range estimate - if (useRandomization && randomizerValue > 0) { - // Add some uncertainty based on the randomizer value - Duration randomComponent = Duration.of(randomizerValue, - getRandomizerChronoUnit()); - duration = duration.plus(randomComponent.dividedBy(2)); // Add half the random range - } - - // Ensure we don't return negative durations - if (duration.isNegative()) { - return Optional.of(Duration.ZERO); - } - return Optional.of(duration); - } - - return Optional.empty(); - } - - /** - * Helper method to get the ChronoUnit for randomization based on the repeat cycle - */ - private java.time.temporal.ChronoUnit getRandomizerChronoUnit() { - RepeatCycle automaticUnit = getAutomaticRandomizerValueUnit(); - switch (automaticUnit) { - case SECONDS: - return java.time.temporal.ChronoUnit.SECONDS; - case MINUTES: - return java.time.temporal.ChronoUnit.MINUTES; - case HOURS: - return java.time.temporal.ChronoUnit.HOURS; - case DAYS: - return java.time.temporal.ChronoUnit.DAYS; - default: - return java.time.temporal.ChronoUnit.MINUTES; - } - } - - /** - * Automatically determines the appropriate randomization unit based on the repeat cycle. - * This ensures randomization uses sensible granularity relative to the repeat interval. - * - * @return The appropriate RepeatCycle for randomization based on the current repeatCycle - */ - private RepeatCycle getAutomaticRandomizerValueUnit() { - switch (repeatCycle) { - case MINUTES: - return RepeatCycle.SECONDS; // For minute intervals, randomize in seconds - case HOURS: - return RepeatCycle.MINUTES; // For hour intervals, randomize in minutes - case DAYS: - return RepeatCycle.MINUTES; // For day intervals, randomize in minutes - case WEEKS: - return RepeatCycle.HOURS; // For week intervals, randomize in hours - case ONE_TIME: - return RepeatCycle.MINUTES; // For one-time, use minutes as default - default: - return RepeatCycle.MINUTES; // Default fallback to minutes - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/enums/RepeatCycle.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/enums/RepeatCycle.java deleted file mode 100644 index 236559f1440..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/enums/RepeatCycle.java +++ /dev/null @@ -1,41 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.enums; - /** - * Enumeration of supported repeat cycle types - */ -public enum RepeatCycle { - MILLIS("Every X milliseconds"), - SECONDS("Every X seconds"), - MINUTES("Every X minutes"), - HOURS("Every X hours"), - DAYS("Every X days"), - WEEKS("Every X weeks"), - ONE_TIME("One time only"); - - private final String displayName; - - RepeatCycle(String displayName) { - this.displayName = displayName; - } - - public String getDisplayName() { - return displayName; - } - public String unit() { - switch (this) { - case MILLIS: - return "ms"; - case SECONDS: - return "s"; - case MINUTES: - return "min"; - case HOURS: - return "h"; - case DAYS: - return "d"; - case WEEKS: - return "w"; - default: - return ""; - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/DayOfWeekConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/DayOfWeekConditionAdapter.java deleted file mode 100644 index 21c002a3a4c..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/DayOfWeekConditionAdapter.java +++ /dev/null @@ -1,125 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.DayOfWeekCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; - -import java.lang.reflect.Type; -import java.time.DayOfWeek; -import java.time.Duration; -import java.util.EnumSet; -import java.util.Optional; -import java.util.Set; - -/** - * Serializes and deserializes DayOfWeekCondition objects - */ -@Slf4j -public class DayOfWeekConditionAdapter implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(DayOfWeekCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add type information - json.addProperty("type", DayOfWeekCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Serialize active days as an array of day values - JsonArray daysArray = new JsonArray(); - for (DayOfWeek day : src.getActiveDays()) { - daysArray.add(day.getValue()); // getValue() returns 1-7 for MON-SUN - } - data.add("activeDays", daysArray); - data.addProperty("version", src.getVersion()); - data.addProperty("maximumNumberOfRepeats", src.getMaximumNumberOfRepeats()); - data.addProperty("maxRepeatsPerDay", src.getMaxRepeatsPerDay()); - data.addProperty("maxRepeatsPerWeek", src.getMaxRepeatsPerWeek()); - - // Serialize the interval condition if it exists - Optional intervalCondition = src.getIntervalCondition(); - if (intervalCondition.isPresent()) { - // Use a separate serializer for the interval condition - JsonElement intervalJson = context.serialize(intervalCondition.get(), IntervalCondition.class); - data.add("intervalCondition", intervalJson); - } - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public DayOfWeekCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - Set activeDays = EnumSet.noneOf(DayOfWeek.class); - - - if (dataObj.has("version")) { - String version = dataObj.get("version").getAsString(); - if (!version.equals(DayOfWeekCondition.getVersion())) { - throw new JsonParseException("Version mismatch: expected " + DayOfWeekCondition.getVersion() + - ", got " + version); - } - } - // Parse active days - if (dataObj.has("activeDays")) { - JsonArray daysArray = dataObj.getAsJsonArray("activeDays"); - for (JsonElement element : daysArray) { - int dayValue = element.getAsInt(); - // DayOfWeek.of expects 1-7 for MON-SUN - activeDays.add(DayOfWeek.of(dayValue)); - } - } - - // Get maximum number of repeats - long maximumNumberOfRepeats = 0; - if (dataObj.has("maximumNumberOfRepeats")) { - maximumNumberOfRepeats = dataObj.get("maximumNumberOfRepeats").getAsLong(); - } - - // Get maximum number of repeats per day - long maxRepeatsPerDay = 0; - if (dataObj.has("maxRepeatsPerDay")) { - maxRepeatsPerDay = dataObj.get("maxRepeatsPerDay").getAsLong(); - } - - // Get maximum number of repeats per week (new field) - long maxRepeatsPerWeek = 0; - if (dataObj.has("maxRepeatsPerWeek")) { - maxRepeatsPerWeek = dataObj.get("maxRepeatsPerWeek").getAsLong(); - } - - // Create the day of week condition with all limits - DayOfWeekCondition condition = new DayOfWeekCondition(maximumNumberOfRepeats, maxRepeatsPerDay, maxRepeatsPerWeek, activeDays); - - // If there's an interval condition, deserialize and add it - if (dataObj.has("intervalCondition")) { - JsonElement intervalJson = dataObj.get("intervalCondition"); - IntervalCondition intervalCondition = context.deserialize(intervalJson, IntervalCondition.class); - condition.setIntervalCondition(intervalCondition); - } - if (dataObj.has("currentValidResetCount")){ - condition.setCurrentValidResetCount(dataObj.get("currentValidResetCount").getAsLong()); - } - - return condition; - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/DurationAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/DurationAdapter.java deleted file mode 100644 index 18500fce53c..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/DurationAdapter.java +++ /dev/null @@ -1,37 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; - -import java.lang.reflect.Type; -import java.time.Duration; - -/** - * Custom adapter for serializing/deserializing java.time.Duration objects - * This avoids reflection issues with Java modules - */ -@Slf4j -public class DurationAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(Duration src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject jsonObject = new JsonObject(); - jsonObject.addProperty("seconds", src.getSeconds()); - jsonObject.addProperty("nanos", src.getNano()); - return jsonObject; - } - - @Override - public Duration deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - try { - JsonObject jsonObject = json.getAsJsonObject(); - long seconds = jsonObject.get("seconds").getAsLong(); - int nanos = jsonObject.has("nanos") ? jsonObject.get("nanos").getAsInt() : 0; - return Duration.ofSeconds(seconds, nanos); - } catch (Exception e) { - log.error("Error deserializing Duration", e); - return Duration.ZERO; - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/IntervalConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/IntervalConditionAdapter.java deleted file mode 100644 index ede74f5c47a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/IntervalConditionAdapter.java +++ /dev/null @@ -1,258 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; -import java.time.format.DateTimeFormatter; -import java.lang.reflect.Type; -import java.time.Duration; -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.Optional; - -/** - * Serializes and deserializes IntervalCondition objects - */ -@Slf4j -public class IntervalConditionAdapter implements JsonSerializer, JsonDeserializer { - @Override - public JsonElement serialize(IntervalCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - json.addProperty("version", IntervalCondition.getVersion()); - // Add type information - json.addProperty("type", IntervalCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Store interval as seconds for cross-platform compatibility - data.addProperty("intervalSeconds", src.getInterval().getSeconds()); - - // Store min/max interval information if using randomized intervals - if (src.isRandomize()) { - data.addProperty("minIntervalSeconds", src.getMinInterval().getSeconds()); - data.addProperty("maxIntervalSeconds", src.getMaxInterval().getSeconds()); - data.addProperty("isMinMaxRandomized", true); - } - - // Store randomization settings - data.addProperty("randomize", src.isRandomize()); - data.addProperty("randomFactor", src.getRandomFactor()); - data.addProperty("maximumNumberOfRepeats", src.getMaximumNumberOfRepeats()); - data.addProperty("currentValidResetCount", src.getCurrentValidResetCount()); - // Store next trigger time if available - ZonedDateTime nextTrigger = src.getNextTriggerTimeWithPause().orElse(null); - - - if (nextTrigger != null) { - data.addProperty("nextTriggerTimeMillis", nextTrigger.toInstant().toEpochMilli()); - } - - // Serialize initial delay condition if it exists - if (src.getInitialDelayCondition() != null) { - SingleTriggerTimeCondition delayCondition = src.getInitialDelayCondition(); - if (delayCondition.getNextTriggerTimeWithPause().orElse(null) != null) { - data.addProperty("targetTimeMillis", delayCondition.getNextTriggerTimeWithPause().get().toInstant().toEpochMilli()); - } - data.add("initialDelayCondition", context.serialize(delayCondition)); - } - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public IntervalCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - - // Check if this is using the type/data wrapper format - if (jsonObject.has("type") && jsonObject.has("data")) { - jsonObject = jsonObject.getAsJsonObject("data"); - } - - if (jsonObject.has("version")) { - String version = jsonObject.get("version").getAsString(); - if (!IntervalCondition.getVersion().equals(version)) { - log.warn("Version mismatch: expected {}, got {}", IntervalCondition.getVersion(), version); - throw new JsonParseException("Version mismatch"); - } - } - // Parse interval - long intervalSeconds = jsonObject.get("intervalSeconds").getAsLong(); - Duration interval = Duration.ofSeconds(intervalSeconds); - - // Extract initial delay information if present - Long initialDelaySeconds = null; - SingleTriggerTimeCondition intialCondition = null; - if (jsonObject.has("initialDelayCondition")) { - JsonObject initialDelayData = jsonObject.getAsJsonObject("initialDelayCondition"); - intialCondition = context.deserialize(initialDelayData, SingleTriggerTimeCondition.class); - if (initialDelayData.has("targetTimeMillis")) { - long targetTimeMillis = initialDelayData.get("targetTimeMillis").getAsLong(); - - // Calculate the initial delay in seconds from now to target time - long nowMillis = System.currentTimeMillis(); - if (targetTimeMillis < nowMillis) { - // If the target time is in the past, set initial delay to 0 - initialDelaySeconds = 0L; - } else { - // Calculate the delay in milliseconds and convert to seconds - long delayMillis = Math.max(0, targetTimeMillis - nowMillis); - initialDelaySeconds = (Long)(delayMillis / 1000); - } - } - long intitalDelayFromCondition = intialCondition.getNextTriggerTimeWithPause().get().toInstant().toEpochMilli() - System.currentTimeMillis(); - if (intialCondition != null) { - // Format times for better readability - ZonedDateTime targetTime = intialCondition.getNextTriggerTimeWithPause().get(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - String formattedTargetTime = targetTime.format(formatter); - - // Convert milliseconds to duration for proper formatting - Duration delayFromCondition = Duration.ofMillis(Math.max(0, intitalDelayFromCondition)); - String formattedDelay = String.format("%02d:%02d:%02d", - delayFromCondition.toHours(), - delayFromCondition.toMinutesPart(), - delayFromCondition.toSecondsPart() - ); - - // Convert initialDelaySeconds to duration for formatting - Duration initialDelay = Duration.ofSeconds(initialDelaySeconds != null ? initialDelaySeconds : 0); - String formattedInitialDelay = String.format("%02d:%02d:%02d", - initialDelay.toHours(), - initialDelay.toMinutesPart(), - initialDelay.toSecondsPart() - ); - - Optional nextTriggerWithPause = intialCondition.getNextTriggerTimeWithPause(); - String formattedNextTriggerWithPause = nextTriggerWithPause - .map(time -> time.format(formatter)) - .orElse("Not set"); - - // Check if next trigger time is before current time - boolean isBeforeCurrent = nextTriggerWithPause - .map(time -> time.isBefore(ZonedDateTime.now(ZoneId.systemDefault()))) - .orElse(false); - - log.info("\nInitial delay condition: {}\n- Target time: {}\n- Initial delay: {} ({})\n- Delay from condition: {} ({})\n- Next trigger with pause: {}\n- Is before current time: {}\n", - intialCondition.toString(), - formattedTargetTime, - formattedInitialDelay, - initialDelaySeconds + " seconds", - formattedDelay, - (intitalDelayFromCondition / 1000) + " seconds", - formattedNextTriggerWithPause, - isBeforeCurrent - ); - } else { - throw new JsonParseException("Initial delay condition is null"); - } - } - - // Check if this is using min/max randomization - IntervalCondition condition = null; - if (jsonObject.has("isMinMaxRandomized") && jsonObject.get("isMinMaxRandomized").getAsBoolean()) { - long minIntervalSeconds = jsonObject.get("minIntervalSeconds").getAsLong(); - long maxIntervalSeconds = jsonObject.get("maxIntervalSeconds").getAsLong(); - Duration minInterval = Duration.ofSeconds(minIntervalSeconds); - Duration maxInterval = Duration.ofSeconds(maxIntervalSeconds); - - condition = IntervalCondition.createRandomized(minInterval, maxInterval); - - - - } - - - // Parse randomization settings for the traditional approach - boolean randomize = jsonObject.has("randomize") && jsonObject.get("randomize").getAsBoolean(); - double randomFactor = randomize && jsonObject.has("randomFactor") ? - jsonObject.get("randomFactor").getAsDouble() : 0.0; - long maximumNumberOfRepeats = jsonObject.has("maximumNumberOfRepeats") ? - jsonObject.get("maximumNumberOfRepeats").getAsLong() : 0; - if(condition == null) { - if (intialCondition != null) { - // Create a new condition with the initial delay from the initial condition - condition = new IntervalCondition(interval, randomize, randomFactor, maximumNumberOfRepeats, (Long)intialCondition.getDefinedDelay().toSeconds()); - } else if (initialDelaySeconds != null && initialDelaySeconds > 0) { - // Create a new condition with the initial delay - condition = new IntervalCondition(interval, randomize, randomFactor, maximumNumberOfRepeats, initialDelaySeconds); - }else{ - // Create a new condition without initial delay - condition = new IntervalCondition(interval, randomize, randomFactor, maximumNumberOfRepeats); - } - - - } - - - - - if (intialCondition!= null && intialCondition.getNextTriggerTimeWithPause().orElse(null) !=null && intialCondition.getNextTriggerTimeWithPause().get().isBefore(ZonedDateTime.now(ZoneId.systemDefault()))) { - Duration initialDelay = Duration.ofSeconds(intialCondition.getDefinedDelay().toSeconds()); - Duration remaingDuration = Duration.between(intialCondition.getNextTriggerTimeWithPause().get(),ZonedDateTime.now(ZoneId.systemDefault())); - Duration remaingDuration__ = Duration.between(ZonedDateTime.now(ZoneId.systemDefault()),intialCondition.getNextTriggerTimeWithPause().get()); - log.info ("\nInitial delay condition: {} \n- next targeted trigger time {} \n-remaning {} -difference: {}", intialCondition.toString(), - intialCondition.getNextTriggerTimeWithPause().get().toString(),remaingDuration.getSeconds(),remaingDuration__.getSeconds()); - condition = new IntervalCondition( - condition.getInterval(), - condition.getMinInterval(), - condition.getMaxInterval(), - condition.isRandomize(), - condition.getRandomFactor(), - condition.getMaximumNumberOfRepeats(), - remaingDuration__.getSeconds() - ); - } - else if (initialDelaySeconds != null && initialDelaySeconds > 0) { - // Create a new condition with the initial delay - log.info("\nInitial delay condition: {} \n- next targeted trigger time {} \n- initial delay seconds {}, before: {}", - condition.toString(), - condition.getNextTriggerTimeWithPause().orElse(null), - initialDelaySeconds, - condition.getNextTriggerTimeWithPause().orElse(null).isBefore(ZonedDateTime.now(ZoneId.systemDefault())) ? "yes" : "no" - ); - condition = new IntervalCondition( - condition.getInterval(), - condition.getMinInterval(), - condition.getMaxInterval(), - condition.isRandomize(), - condition.getRandomFactor(), - condition.getMaximumNumberOfRepeats(), - initialDelaySeconds - ); - } - if (jsonObject.has("currentValidResetCount")){ - if (jsonObject.get("currentValidResetCount").isJsonNull()) { - condition.setCurrentValidResetCount(0); - }else{ - try { - condition.setCurrentValidResetCount(jsonObject.get("currentValidResetCount").getAsLong()); - } catch (Exception e) { - log.warn("Invalid currentValidResetCount value: {}", jsonObject.get("currentValidResetCount").getAsString()); - } - - } - } - if (jsonObject.has("nextTriggerTimeMillis")) { - long nextTriggerMillis = jsonObject.get("nextTriggerTimeMillis").getAsLong(); - ZonedDateTime nextTrigger = ZonedDateTime.ofInstant( - Instant.ofEpochMilli(nextTriggerMillis), - ZoneId.systemDefault() - ); - ZonedDateTime currentTriggerDateTime = condition.getCurrentTriggerTime().get(); - condition.setNextTriggerTime(nextTrigger); - } - return condition; - - - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/LocalDateAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/LocalDateAdapter.java deleted file mode 100644 index 8e1acb37923..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/LocalDateAdapter.java +++ /dev/null @@ -1,32 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; - -import java.lang.reflect.Type; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; - -/** - * Serializes/deserializes LocalDate with ISO format - */ -@Slf4j -public class LocalDateAdapter implements JsonSerializer, JsonDeserializer { - private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ISO_DATE; - - @Override - public JsonElement serialize(LocalDate src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive(src.format(DATE_FORMAT)); - } - - @Override - public LocalDate deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - try { - return LocalDate.parse(json.getAsString(), DATE_FORMAT); - } catch (Exception e) { - log.warn("Error deserializing LocalDate", e); - return LocalDate.now(); // Default to today if parsing fails - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/LocalTimeAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/LocalTimeAdapter.java deleted file mode 100644 index 69a6f88bcaf..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/LocalTimeAdapter.java +++ /dev/null @@ -1,33 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; - -import java.lang.reflect.Type; -import java.time.LocalTime; -import java.time.format.DateTimeFormatter; - -/** - * Serializes LocalTime as UTC time string and deserializes back to local timezone - */ -@Slf4j -public class LocalTimeAdapter implements JsonSerializer, JsonDeserializer { - private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ISO_TIME; - - @Override - public JsonElement serialize(LocalTime src, Type typeOfSrc, JsonSerializationContext context) { - // Store the time with UTC marker for consistency - return new JsonPrimitive(src.format(TIME_FORMAT)); - } - - @Override - public LocalTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - try { - return LocalTime.parse(json.getAsString(), TIME_FORMAT); - } catch (Exception e) { - log.warn("Error deserializing LocalTime", e); - return LocalTime.of(0, 0); // Default to midnight if parsing fails - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/SingleTriggerTimeConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/SingleTriggerTimeConditionAdapter.java deleted file mode 100644 index 7b96ca09983..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/SingleTriggerTimeConditionAdapter.java +++ /dev/null @@ -1,103 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; - -import java.lang.reflect.Type; -import java.time.Duration; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; - -/** - * Custom serializer/deserializer for SingleTriggerTimeCondition - */ -@Slf4j -public class SingleTriggerTimeConditionAdapter implements JsonSerializer, JsonDeserializer { - private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ISO_TIME; - private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ISO_DATE; - - @Override - public JsonElement serialize(SingleTriggerTimeCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add type information - json.addProperty("type", SingleTriggerTimeCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Get source timezone - ZoneId sourceZone = ZoneId.systemDefault(); - - // Store times in UTC, converting from source timezone - LocalDate today = LocalDate.now(); - - // Convert start time to UTC - ZonedDateTime targetUtc = src.getNextTriggerTimeWithPause().get().withZoneSameInstant(ZoneId.of("UTC")); - data.addProperty("version", src.getVersion()); - data.addProperty("targetTime", targetUtc.toLocalTime().format(TIME_FORMAT)); - data.addProperty("targetDate", targetUtc.toLocalDate().format(DATE_FORMAT)); - - // Mark that these are UTC times for future compatibility - data.addProperty("timeFormat", "UTC"); - // Store trigger state - data.addProperty("maximumNumberOfRepeats", src.getMaximumNumberOfRepeats()); - data.addProperty("currentValidResetCount", src.getCurrentValidResetCount()); - data.addProperty("definedDelay", src.getDefinedDelay().toSeconds()); - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public SingleTriggerTimeCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - - // Get timezone (fallback to system default) - ZoneId zoneId = ZoneId.systemDefault(); - if (dataObj.has("version")) { - if (!dataObj.get("version").getAsString().equals(SingleTriggerTimeCondition.getVersion())) { - throw new JsonParseException("Version mismatch: expected " + SingleTriggerTimeCondition.getVersion() + - ", got " + dataObj.get("version").getAsString()); - } - } - Duration definedDelay = Duration.ofSeconds(0); - if (dataObj.has("definedDelay")){ - definedDelay = Duration.ofSeconds(dataObj.get("definedDelay").getAsLong()); - } - // Parse time values - LocalTime serializedStartTime = LocalTime.parse(dataObj.get("targetTime").getAsString(), TIME_FORMAT); - // Parse date values - LocalDate serializedStartDate = LocalDate.parse(dataObj.get("targetDate").getAsString(), DATE_FORMAT); - // Convert to ZonedDateTime - ZonedDateTime targetZoned = ZonedDateTime.of(serializedStartDate, serializedStartTime, ZoneId.of("UTC")); - ZonedDateTime targetZonedSyDateTime = targetZoned.withZoneSameInstant(zoneId); - // Create condition - SingleTriggerTimeCondition condition = new SingleTriggerTimeCondition(targetZonedSyDateTime , definedDelay, - dataObj.get("maximumNumberOfRepeats").getAsInt()); - if (dataObj.has("currentValidResetCount")){ - condition.setCurrentValidResetCount(dataObj.get("currentValidResetCount").getAsLong()); - } - - return condition; - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/TimeConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/TimeConditionAdapter.java deleted file mode 100644 index 6462e8bb04f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/TimeConditionAdapter.java +++ /dev/null @@ -1,57 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization; -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.DayOfWeekCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition; - -import java.lang.reflect.Type; -import java.util.Map; - -// Then create a new TimeConditionAdapter.java class: -@Slf4j -public class TimeConditionAdapter implements JsonDeserializer { - @Override - public TimeCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - // Handle typed wrapper format - if (jsonObject.has("type") && jsonObject.has("data")) { - String type = jsonObject.get("type").getAsString(); - JsonObject data = jsonObject.getAsJsonObject("data"); - - // Create a new object with the same structure as expected by type-specific deserializers - JsonObject unwrappedJson = new JsonObject(); - for (Map.Entry entry : data.entrySet()) { - unwrappedJson.add(entry.getKey(), entry.getValue()); - } - - // Determine the concrete type from the type field - if (type.endsWith("IntervalCondition")) { - return context.deserialize(unwrappedJson, IntervalCondition.class); - } else if (type.endsWith("SingleTriggerTimeCondition")) { - return context.deserialize(unwrappedJson, SingleTriggerTimeCondition.class); - } else if (type.endsWith("TimeWindowCondition")) { - return context.deserialize(unwrappedJson, TimeWindowCondition.class); - } else if (type.endsWith("DayOfWeekCondition")) { - return context.deserialize(unwrappedJson, DayOfWeekCondition.class); - } - } - - // Legacy format - determine type from properties - if (jsonObject.has("intervalSeconds")) { - return context.deserialize(jsonObject, IntervalCondition.class); - } else if (jsonObject.has("targetTime")) { - return context.deserialize(jsonObject, SingleTriggerTimeCondition.class); - } else if (jsonObject.has("startTime") && jsonObject.has("endTime")) { - return context.deserialize(jsonObject, TimeWindowCondition.class); - } else if (jsonObject.has("activeDays")) { - return context.deserialize(jsonObject, DayOfWeekCondition.class); - } - - throw new JsonParseException("Unknown TimeCondition type"); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/TimeWindowConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/TimeWindowConditionAdapter.java deleted file mode 100644 index e37b3ab7746..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/serialization/TimeWindowConditionAdapter.java +++ /dev/null @@ -1,217 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.enums.RepeatCycle; - -import java.lang.reflect.Type; -import java.time.*; -import java.time.format.DateTimeFormatter; - -/** - * Custom serializer/deserializer for TimeWindowCondition that handles timezone conversion - */ -@Slf4j -public class TimeWindowConditionAdapter implements JsonSerializer, JsonDeserializer { - private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ISO_TIME; - private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ISO_DATE; - - @Override - public JsonElement serialize(TimeWindowCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add type information - json.addProperty("type", TimeWindowCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Get source timezone - ZoneId sourceZone = src.getZoneId() != null ? src.getZoneId() : ZoneId.systemDefault(); - data.addProperty("version", src.getVersion()); - // Store the timezone ID for deserialization - data.addProperty("zoneId", sourceZone.getId()); - - // Store times in UTC, converting from source timezone - LocalDate today = LocalDate.now(); - - // Convert start time to UTC - ZonedDateTime startZoned = ZonedDateTime.of(today, src.getStartTime(), sourceZone); - ZonedDateTime startUtc = startZoned.withZoneSameInstant(ZoneId.of("UTC")); - - // Convert end time to UTC - ZonedDateTime endZoned = ZonedDateTime.of(today, src.getEndTime(), sourceZone); - ZonedDateTime endUtc = endZoned.withZoneSameInstant(ZoneId.of("UTC")); - - // Convert dates to UTC (using noon to avoid DST issues) - ZonedDateTime startDateZoned = ZonedDateTime.of(src.getStartDate(), LocalTime.NOON, sourceZone); - ZonedDateTime startDateUtc = startDateZoned.withZoneSameInstant(ZoneId.of("UTC")); - - ZonedDateTime endDateZoned = ZonedDateTime.of(src.getEndDate(), LocalTime.NOON, sourceZone); - ZonedDateTime endDateUtc = endDateZoned.withZoneSameInstant(ZoneId.of("UTC")); - - // Store UTC times - data.addProperty("startTime", startUtc.toLocalTime().format(TIME_FORMAT)); - data.addProperty("endTime", endUtc.toLocalTime().format(TIME_FORMAT)); - data.addProperty("startDate", startDateUtc.toLocalDate().format(DATE_FORMAT)); - data.addProperty("endDate", endDateUtc.toLocalDate().format(DATE_FORMAT)); - LocalDateTime currentStartDateTime = src.getNextTriggerTimeWithPause().get().toLocalDateTime(); - LocalDateTime currentEndDateTime = src.getCurrentEndDateTime(); - if (currentStartDateTime != null) { - data.addProperty("currentStartDateTime", currentStartDateTime.format(DateTimeFormatter.ISO_DATE_TIME)); - } - if (currentEndDateTime != null) { - data.addProperty("currentEndDateTime", currentEndDateTime.format(DateTimeFormatter.ISO_DATE_TIME)); - } - data.addProperty("transientNumberOfResetsWithinDailyInterval", src.getTransientNumberOfResetsWithinDailyInterval()); - - // Mark that these are UTC times for future compatibility - data.addProperty("timeFormat", "UTC"); - - // Repeat cycle information - data.addProperty("repeatCycle", src.getRepeatCycle().name()); - data.addProperty("repeatInterval", src.getRepeatIntervalUnit()); - - // Randomization settings - data.addProperty("useRandomization", src.isUseRandomization()); - data.addProperty("randomizerValue", src.getRandomizerValue()); - data.addProperty("maximumNumberOfRepeats", src.getMaximumNumberOfRepeats()); - data.addProperty("currentValidResetCount", src.getCurrentValidResetCount()); - - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public TimeWindowCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is a typed format or direct format - JsonObject dataObj; - - - if (jsonObject.has("type") && jsonObject.has("data")) { - dataObj = jsonObject.getAsJsonObject("data"); - } else { - // Legacy format - use the object directly - dataObj = jsonObject; - } - if (dataObj.has("version")) { - if (!dataObj.get("version").getAsString().equals(TimeWindowCondition.getVersion())) { - throw new JsonParseException("Version mismatch: expected " + TimeWindowCondition.getVersion() + - ", got " + dataObj.get("version").getAsString()); - } - } - // Get the target timezone for conversion (default to system) - ZoneId targetZone = ZoneId.systemDefault(); - if (dataObj.has("zoneId")) { - try { - targetZone = ZoneId.of(dataObj.get("zoneId").getAsString()); - } catch (Exception e) { - log.warn("Invalid zoneId in serialized TimeWindowCondition", e); - } - } - - // Check if times are stored in UTC format - boolean isUtcFormat = dataObj.has("timeFormat") && - "UTC".equals(dataObj.get("timeFormat").getAsString()); - - // Parse time values - LocalTime serializedStartTime = LocalTime.parse(dataObj.get("startTime").getAsString(), TIME_FORMAT); - LocalTime serializedEndTime = LocalTime.parse(dataObj.get("endTime").getAsString(), TIME_FORMAT); - - // Parse date values - LocalDate serializedStartDate = LocalDate.parse(dataObj.get("startDate").getAsString(), DATE_FORMAT); - LocalDate serializedEndDate = LocalDate.parse(dataObj.get("endDate").getAsString(), DATE_FORMAT); - - LocalTime startTime; - LocalTime endTime; - LocalDate startDate; - LocalDate endDate; - - if (isUtcFormat) { - // If stored in UTC format, convert back to target timezone - LocalDate today = LocalDate.now(); - - // Convert start time from UTC to target zone - ZonedDateTime startUtc = ZonedDateTime.of(today, serializedStartTime, ZoneId.of("UTC")); - ZonedDateTime startTargetZone = startUtc.withZoneSameInstant(targetZone); - startTime = startTargetZone.toLocalTime(); - - // Convert end time from UTC to target zone - ZonedDateTime endUtc = ZonedDateTime.of(today, serializedEndTime, ZoneId.of("UTC")); - ZonedDateTime endTargetZone = endUtc.withZoneSameInstant(targetZone); - endTime = endTargetZone.toLocalTime(); - - // Convert dates from UTC to target zone - ZonedDateTime startDateUtc = ZonedDateTime.of(serializedStartDate, LocalTime.NOON, ZoneId.of("UTC")); - ZonedDateTime startDateTarget = startDateUtc.withZoneSameInstant(targetZone); - startDate = startDateTarget.toLocalDate(); - - ZonedDateTime endDateUtc = ZonedDateTime.of(serializedEndDate, LocalTime.NOON, ZoneId.of("UTC")); - ZonedDateTime endDateTarget = endDateUtc.withZoneSameInstant(targetZone); - endDate = endDateTarget.toLocalDate(); - } else { - // Legacy format - use times as-is - startTime = serializedStartTime; - endTime = serializedEndTime; - startDate = serializedStartDate; - endDate = serializedEndDate; - } - - // Parse repeat cycle - RepeatCycle repeatCycle = RepeatCycle.valueOf( - dataObj.get("repeatCycle").getAsString()); - int repeatInterval = dataObj.get("repeatInterval").getAsInt(); - long maximumNumberOfRepeats = dataObj.get("maximumNumberOfRepeats").getAsLong(); - // Create the condition with the parsed values - TimeWindowCondition condition = new TimeWindowCondition( - startTime, endTime, startDate, endDate, repeatCycle, repeatInterval, maximumNumberOfRepeats); - - if (dataObj.has("currentStartDateTime") && dataObj.has("currentEndDateTime")) { - LocalDateTime lastCurrentStartDateTime = LocalDateTime.parse(dataObj.get("currentStartDateTime").getAsString()); - LocalDateTime lastCurrentEndDateTime = LocalDateTime.parse(dataObj.get("currentEndDateTime").getAsString()); - // check first if the last current start date time and end date time is in a future - // date time, if so set the current start date time and end date time to the last current start date time and end date time, otherwise set it to the current start date time and end date time - LocalDateTime currentStartDateTime = lastCurrentStartDateTime.isAfter(LocalDateTime.now()) ? lastCurrentStartDateTime : condition.getNextTriggerTimeWithPause().get().toLocalDateTime(); - LocalDateTime currentEndDateTime = lastCurrentEndDateTime.isAfter(LocalDateTime.now()) ? lastCurrentEndDateTime : condition.getCurrentEndDateTime(); - // ensure start date time is before end date time - if (currentStartDateTime.isAfter(currentEndDateTime)) { - throw new JsonParseException("Current start date time is after current end date time"); - } - condition.setNextTriggerTime(currentStartDateTime.atZone(ZoneId.systemDefault())); - condition.setCurrentEndDateTime(currentEndDateTime); - } - - - - if (dataObj.has("currentValidResetCount")){ - condition.setCurrentValidResetCount(dataObj.get("currentValidResetCount").getAsLong()); - } - if (dataObj.has("transientNumberOfResetsWithinDailyInterval")) { - condition.setTransientNumberOfResetsWithinDailyInterval(dataObj.get("transientNumberOfResetsWithinDailyInterval").getAsInt()); - } - - - - // Set timezone - condition.setZoneId(targetZone); - - // Set randomization if present - if (dataObj.has("useRandomization") && dataObj.has("randomizerValue")) { - boolean useRandomization = dataObj.get("useRandomization").getAsBoolean(); - int randomizerValue = dataObj.get("randomizerValue").getAsInt(); - condition.setRandomization(useRandomization); - condition.setRandomizerValue(randomizerValue); - } - return condition; - - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/ui/TimeConditionPanelUtil.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/ui/TimeConditionPanelUtil.java deleted file mode 100644 index 11df242afa3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/ui/TimeConditionPanelUtil.java +++ /dev/null @@ -1,1492 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.ui; -import java.time.ZoneId; -import javax.swing.Box; -import javax.swing.BoxLayout; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.BorderFactory; -import javax.swing.SwingUtilities; -import javax.swing.border.TitledBorder; -import java.awt.BorderLayout; -import java.awt.Component; -import java.awt.Container; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.components.DateRangePanel; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.components.IntervalPickerPanel; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.components.SingleDateTimePickerPanel; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.components.TimeRangePanel; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; -import java.awt.Color; -import java.awt.Dimension; -import java.awt.FlowLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridLayout; -import java.time.DayOfWeek; -import java.time.Duration; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.util.EnumSet; -import java.util.Optional; -import java.util.Set; -import java.time.ZonedDateTime; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JSpinner; -import javax.swing.SpinnerNumberModel; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.DayOfWeekCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.enums.RepeatCycle; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.components.InitialDelayPanel; -@Slf4j -public class TimeConditionPanelUtil { - public static void createIntervalConfigPanel(JPanel panel, GridBagConstraints gbc) { - // Title and initial setup - JLabel titleLabel = new JLabel("Time Interval Configuration:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // Create and add interval picker component - gbc.gridy++; - IntervalPickerPanel intervalPicker = new IntervalPickerPanel(true); - panel.add(intervalPicker, gbc); - - // Add initial delay configuration - gbc.gridy++; - InitialDelayPanel initialDelayPanel = new InitialDelayPanel(); - - panel.add(initialDelayPanel, gbc); - - // Add a helpful description - gbc.gridy++; - JLabel descriptionLabel = new JLabel("Plugin will stop after specified time interval"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Add additional info about randomization - gbc.gridy++; - JLabel randomInfoLabel = new JLabel("Random intervals make your bot behavior less predictable"); - randomInfoLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - randomInfoLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(randomInfoLabel, gbc); - - // Add information about initial delay - gbc.gridy++; - JLabel initialDelayInfoLabel = new JLabel("Initial delay adds waiting time before the first interval trigger"); - initialDelayInfoLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - initialDelayInfoLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(initialDelayInfoLabel, gbc); - - // Store components for later access - panel.putClientProperty("intervalPicker", intervalPicker); - panel.putClientProperty("initialDelayPanel", initialDelayPanel); - // Removed as delayMinutesSpinner is now encapsulated in InitialDelayPanel - // Removed as delaySecondsSpinner is now encapsulated in InitialDelayPanel - } - - /** - * Helper method to validate min and max intervals ensure min <= max - */ - private static void validateMinMaxIntervals( - JSpinner minHoursSpinner, JSpinner minMinutesSpinner, - JSpinner maxHoursSpinner, JSpinner maxMinutesSpinner, - boolean isMinUpdated) { - - int minHours = (Integer) minHoursSpinner.getValue(); - int minMinutes = (Integer) minMinutesSpinner.getValue(); - int maxHours = (Integer) maxHoursSpinner.getValue(); - int maxMinutes = (Integer) maxMinutesSpinner.getValue(); - - int minTotalMinutes = minHours * 60 + minMinutes; - int maxTotalMinutes = maxHours * 60 + maxMinutes; - - if (isMinUpdated) { - // If min was updated and exceeds max, adjust max - if (minTotalMinutes > maxTotalMinutes) { - maxHoursSpinner.setValue(minHours); - maxMinutesSpinner.setValue(minMinutes); - } - } else { - // If max was updated and is less than min, adjust min - if (maxTotalMinutes < minTotalMinutes) { - minHoursSpinner.setValue(maxHours); - minMinutesSpinner.setValue(maxMinutes); - } - } - } - - /** - * Creates an IntervalCondition from the config panel. - * This replaces the createTimeCondition method. - */ - public static IntervalCondition createIntervalCondition(JPanel configPanel) { - IntervalPickerPanel intervalPicker = (IntervalPickerPanel) configPanel.getClientProperty("intervalPicker"); - InitialDelayPanel initialDelayPanel = (InitialDelayPanel) configPanel.getClientProperty("initialDelayPanel"); - if (intervalPicker == null) { - throw new IllegalStateException("Interval picker component not found"); - } - - // Get the interval condition from the picker component - IntervalCondition baseCondition = intervalPicker.createIntervalCondition(); - - // Check if initial delay should be added - if (initialDelayPanel != null && initialDelayPanel.isInitialDelayEnabled()) { - int delayHours = initialDelayPanel.getHours(); - int delayMinutes = initialDelayPanel.getMinutes(); - int delaySeconds = initialDelayPanel.getSeconds(); - int totalDelaySeconds = delayHours * 3600 + delayMinutes * 60 + delaySeconds; - - if (totalDelaySeconds > 0) { - // Create a new condition with the same parameters as the base condition plus the delay - if (baseCondition.isRandomize()) { - // For randomized intervals - return new IntervalCondition( - baseCondition.getInterval(), - baseCondition.getMinInterval(), - baseCondition.getMaxInterval(), - baseCondition.isRandomize(), - baseCondition.getRandomFactor(), - baseCondition.getMaximumNumberOfRepeats(), - (long)totalDelaySeconds - ); - } else { - // For fixed intervals - return new IntervalCondition( - baseCondition.getInterval(), - baseCondition.isRandomize(), - baseCondition.getRandomFactor(), - baseCondition.getMaximumNumberOfRepeats(), - (long)totalDelaySeconds - ); - } - } - } - - return baseCondition; - } - - public static void createTimeWindowConfigPanel(JPanel panel, GridBagConstraints gbc) { - // Section Title - JLabel titleLabel = new JLabel("Time Window Configuration:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // Date Range Configuration with Preset ComboBox - gbc.gridy++; - gbc.gridwidth = 1; - JPanel dateRangeConfigPanel = createDateRangeConfigPanel(); - gbc.gridwidth = 2; - panel.add(dateRangeConfigPanel, gbc); - - // Time Range Configuration with Preset ComboBox - gbc.gridy++; - JPanel timeRangeConfigPanel = createTimeRangeConfigPanel(); - panel.add(timeRangeConfigPanel, gbc); - - // Repeat and Randomization Panel (combined for compactness) - gbc.gridy++; - JPanel optionsPanel = createOptionsPanel(); - panel.add(optionsPanel, gbc); - - // Help text - gbc.gridy++; - JPanel helpPanel = createHelpPanel(); - panel.add(helpPanel, gbc); - - // Store components for later access - DateRangePanel dateRangePanel = (DateRangePanel) dateRangeConfigPanel.getClientProperty("dateRangePanel"); - TimeRangePanel timeRangePanel = (TimeRangePanel) timeRangeConfigPanel.getClientProperty("timeRangePanel"); - @SuppressWarnings("unchecked") - JComboBox repeatComboBox = (JComboBox) optionsPanel.getClientProperty("repeatComboBox"); - JSpinner intervalSpinner = (JSpinner) optionsPanel.getClientProperty("intervalSpinner"); - JCheckBox randomizeCheckBox = (JCheckBox) optionsPanel.getClientProperty("randomizeCheckBox"); - JSpinner randomizeSpinner = (JSpinner) optionsPanel.getClientProperty("randomizeSpinner"); - - panel.putClientProperty("dateRangePanel", dateRangePanel); - panel.putClientProperty("timeRangePanel", timeRangePanel); - panel.putClientProperty("repeatComboBox", repeatComboBox); - panel.putClientProperty("intervalSpinner", intervalSpinner); - panel.putClientProperty("randomizeCheckBox", randomizeCheckBox); - panel.putClientProperty("randomizeSpinner", randomizeSpinner); - } - - /** - * Creates a compact date range configuration panel with preset ComboBox - */ - private static JPanel createDateRangeConfigPanel() { - JPanel mainPanel = new JPanel(new BorderLayout(5, 5)); - mainPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - mainPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Date Range", - TitledBorder.LEFT, - TitledBorder.TOP, - FontManager.getRunescapeSmallFont(), - Color.WHITE)); - - // Preset selection panel - JPanel presetPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - presetPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel presetLabel = new JLabel("Preset:"); - presetLabel.setForeground(Color.WHITE); - presetLabel.setFont(FontManager.getRunescapeSmallFont()); - presetPanel.add(presetLabel); - - String[] datePresets = { - "Unlimited", "Today", "This Week", "This Month", - "Next 7 Days", "Next 30 Days", "Next 90 Days", "Custom" - }; - JComboBox datePresetCombo = new JComboBox<>(datePresets); - datePresetCombo.setSelectedItem("Unlimited"); - datePresetCombo.setFont(FontManager.getRunescapeSmallFont()); - presetPanel.add(datePresetCombo); - - mainPanel.add(presetPanel, BorderLayout.NORTH); - - // Date range panel (initially hidden for preset selections) - DateRangePanel dateRangePanel = new DateRangePanel( - net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition.UNLIMITED_START_DATE, - net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition.UNLIMITED_END_DATE - ); - dateRangePanel.setVisible(false); // Hidden by default for "Unlimited" - mainPanel.add(dateRangePanel, BorderLayout.CENTER); - - // Handle preset selection - datePresetCombo.addActionListener(e -> { - String selected = (String) datePresetCombo.getSelectedItem(); - LocalDate today = LocalDate.now(); - - switch (selected) { - case "Unlimited": - dateRangePanel.setStartDate(net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition.UNLIMITED_START_DATE); - dateRangePanel.setEndDate(net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition.UNLIMITED_END_DATE); - dateRangePanel.setVisible(false); - break; - case "Today": - dateRangePanel.setStartDate(today); - dateRangePanel.setEndDate(today); - dateRangePanel.setVisible(true); - break; - case "This Week": - dateRangePanel.setStartDate(today); - dateRangePanel.setEndDate(today.plusDays(7 - today.getDayOfWeek().getValue())); - dateRangePanel.setVisible(true); - break; - case "This Month": - dateRangePanel.setStartDate(today); - dateRangePanel.setEndDate(today.withDayOfMonth(today.lengthOfMonth())); - dateRangePanel.setVisible(true); - break; - case "Next 7 Days": - dateRangePanel.setStartDate(today); - dateRangePanel.setEndDate(today.plusDays(7)); - dateRangePanel.setVisible(true); - break; - case "Next 30 Days": - dateRangePanel.setStartDate(today); - dateRangePanel.setEndDate(today.plusDays(30)); - dateRangePanel.setVisible(true); - break; - case "Next 90 Days": - dateRangePanel.setStartDate(today); - dateRangePanel.setEndDate(today.plusDays(90)); - dateRangePanel.setVisible(true); - break; - case "Custom": - dateRangePanel.setVisible(true); - break; - } - mainPanel.revalidate(); - mainPanel.repaint(); - }); - - mainPanel.putClientProperty("dateRangePanel", dateRangePanel); - mainPanel.putClientProperty("datePresetCombo", datePresetCombo); - return mainPanel; - } - - /** - * Creates a compact time range configuration panel with preset ComboBox - */ - private static JPanel createTimeRangeConfigPanel() { - JPanel mainPanel = new JPanel(new BorderLayout(5, 5)); - mainPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - mainPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Time Range", - TitledBorder.LEFT, - TitledBorder.TOP, - FontManager.getRunescapeSmallFont(), - Color.WHITE)); - - // Preset selection panel - JPanel presetPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - presetPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel presetLabel = new JLabel("Preset:"); - presetLabel.setForeground(Color.WHITE); - presetLabel.setFont(FontManager.getRunescapeSmallFont()); - presetPanel.add(presetLabel); - - String[] timePresets = { - "All Day", "Business Hours", "Morning", "Afternoon", - "Evening", "Night", "Custom" - }; - JComboBox timePresetCombo = new JComboBox<>(timePresets); - timePresetCombo.setSelectedItem("Business Hours"); - timePresetCombo.setFont(FontManager.getRunescapeSmallFont()); - presetPanel.add(timePresetCombo); - - mainPanel.add(presetPanel, BorderLayout.NORTH); - - // Time range panel (initially hidden for preset selections) - TimeRangePanel timeRangePanel = new TimeRangePanel(LocalTime.of(9, 0), LocalTime.of(17, 0)); - timeRangePanel.setVisible(false); // Hidden by default for "Business Hours" - mainPanel.add(timeRangePanel, BorderLayout.CENTER); - - // Handle preset selection - timePresetCombo.addActionListener(e -> { - String selected = (String) timePresetCombo.getSelectedItem(); - - switch (selected) { - case "All Day": - timeRangePanel.setStartTime(LocalTime.of(0, 0)); - timeRangePanel.setEndTime(LocalTime.of(23, 59)); - timeRangePanel.setVisible(false); - break; - case "Business Hours": - timeRangePanel.setStartTime(LocalTime.of(9, 0)); - timeRangePanel.setEndTime(LocalTime.of(17, 0)); - timeRangePanel.setVisible(false); - break; - case "Morning": - timeRangePanel.setStartTime(LocalTime.of(6, 0)); - timeRangePanel.setEndTime(LocalTime.of(12, 0)); - timeRangePanel.setVisible(false); - break; - case "Afternoon": - timeRangePanel.setStartTime(LocalTime.of(12, 0)); - timeRangePanel.setEndTime(LocalTime.of(18, 0)); - timeRangePanel.setVisible(false); - break; - case "Evening": - timeRangePanel.setStartTime(LocalTime.of(18, 0)); - timeRangePanel.setEndTime(LocalTime.of(22, 0)); - timeRangePanel.setVisible(false); - break; - case "Night": - timeRangePanel.setStartTime(LocalTime.of(22, 0)); - timeRangePanel.setEndTime(LocalTime.of(6, 0)); - timeRangePanel.setVisible(false); - break; - case "Custom": - timeRangePanel.setVisible(true); - break; - } - mainPanel.revalidate(); - mainPanel.repaint(); - }); - - mainPanel.putClientProperty("timeRangePanel", timeRangePanel); - mainPanel.putClientProperty("timePresetCombo", timePresetCombo); - return mainPanel; - } - - /** - * Creates a compact options panel with repeat cycle and randomization controls - */ - private static JPanel createOptionsPanel() { - JPanel mainPanel = new JPanel(new BorderLayout(5, 5)); - mainPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - mainPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Options", - TitledBorder.LEFT, - TitledBorder.TOP, - FontManager.getRunescapeSmallFont(), - Color.WHITE)); - - // Repeat options panel - JPanel repeatPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 2)); - repeatPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel repeatLabel = new JLabel("Repeat:"); - repeatLabel.setForeground(Color.WHITE); - repeatLabel.setFont(FontManager.getRunescapeSmallFont()); - repeatPanel.add(repeatLabel); - - String[] repeatOptions = {"Every Day", "Every X Days", "Every X Hours", "Every X Minutes", "Every X Weeks", "One Time Only"}; - JComboBox repeatComboBox = new JComboBox<>(repeatOptions); - repeatComboBox.setFont(FontManager.getRunescapeSmallFont()); - repeatPanel.add(repeatComboBox); - - JLabel intervalLabel = new JLabel("Interval:"); - intervalLabel.setForeground(Color.WHITE); - intervalLabel.setFont(FontManager.getRunescapeSmallFont()); - repeatPanel.add(intervalLabel); - - SpinnerNumberModel intervalModel = new SpinnerNumberModel(1, 1, 100, 1); - JSpinner intervalSpinner = new JSpinner(intervalModel); - intervalSpinner.setPreferredSize(new Dimension(60, intervalSpinner.getPreferredSize().height)); - intervalSpinner.setEnabled(false); - repeatPanel.add(intervalSpinner); - - // Randomization options panel - JPanel randomPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 2)); - randomPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JCheckBox randomizeCheckBox = new JCheckBox("Randomize"); - randomizeCheckBox.setForeground(Color.WHITE); - randomizeCheckBox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - randomizeCheckBox.setFont(FontManager.getRunescapeSmallFont()); - randomPanel.add(randomizeCheckBox); - - JLabel randomizeAmountLabel = new JLabel("±"); - randomizeAmountLabel.setForeground(Color.WHITE); - randomizeAmountLabel.setFont(FontManager.getRunescapeSmallFont()); - randomPanel.add(randomizeAmountLabel); - - SpinnerNumberModel randomizeModel = new SpinnerNumberModel(3, 1, 15, 1); // Default to 3 minutes, max 15 for default "Every Day" - JSpinner randomizeSpinner = new JSpinner(randomizeModel); - randomizeSpinner.setPreferredSize(new Dimension(50, randomizeSpinner.getPreferredSize().height)); - randomizeSpinner.setEnabled(false); - randomizeSpinner.setToolTipText("Randomization range: ±1 to ±15 minutes
Maximum is 40% of 1 days interval"); - randomPanel.add(randomizeSpinner); - - JLabel randomizerUnitLabel = new JLabel("min"); - randomizerUnitLabel.setForeground(Color.WHITE); - randomizerUnitLabel.setFont(FontManager.getRunescapeSmallFont()); - randomPanel.add(randomizerUnitLabel); - - // Control interactions - repeatComboBox.addActionListener(e -> { - String selected = (String) repeatComboBox.getSelectedItem(); - boolean enableInterval = !selected.equals("Every Day") && !selected.equals("One Time Only"); - intervalSpinner.setEnabled(enableInterval); - - // Update randomizer limits based on selected repeat cycle and interval - updateRandomizerLimits(repeatComboBox, intervalSpinner, randomizeSpinner, randomizerUnitLabel); - }); - - // Update randomizer limits when interval changes - intervalSpinner.addChangeListener(e -> { - updateRandomizerLimits(repeatComboBox, intervalSpinner, randomizeSpinner, randomizerUnitLabel); - }); - - randomizeCheckBox.addActionListener(e -> - randomizeSpinner.setEnabled(randomizeCheckBox.isSelected()) - ); - - // Set initial randomizer limits based on default selection - SwingUtilities.invokeLater(() -> updateRandomizerLimits(repeatComboBox, intervalSpinner, randomizeSpinner, randomizerUnitLabel)); - - // Layout both panels - JPanel combinedPanel = new JPanel(new GridLayout(2, 1, 0, 2)); - combinedPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - combinedPanel.add(repeatPanel); - combinedPanel.add(randomPanel); - - mainPanel.add(combinedPanel, BorderLayout.CENTER); - - // Store components for later access - mainPanel.putClientProperty("repeatComboBox", repeatComboBox); - mainPanel.putClientProperty("intervalSpinner", intervalSpinner); - mainPanel.putClientProperty("randomizeCheckBox", randomizeCheckBox); - mainPanel.putClientProperty("randomizeSpinner", randomizeSpinner); - mainPanel.putClientProperty("randomizerUnitLabel", randomizerUnitLabel); - - return mainPanel; - } - - /** - * Updates the randomizer spinner limits based on the current repeat cycle and interval - */ - private static void updateRandomizerLimits(JComboBox repeatComboBox, JSpinner intervalSpinner, JSpinner randomizeSpinner, JLabel randomizerUnitLabel) { - String selectedOption = (String) repeatComboBox.getSelectedItem(); - int interval = (Integer) intervalSpinner.getValue(); - - // Map the selected option to RepeatCycle - RepeatCycle repeatCycle; - switch (selectedOption) { - case "Every Day": - repeatCycle = RepeatCycle.DAYS; - interval = 1; - break; - case "Every X Days": - repeatCycle = RepeatCycle.DAYS; - break; - case "Every X Hours": - repeatCycle = RepeatCycle.HOURS; - break; - case "Every X Minutes": - repeatCycle = RepeatCycle.MINUTES; - break; - case "Every X Weeks": - repeatCycle = RepeatCycle.WEEKS; - break; - case "One Time Only": - repeatCycle = RepeatCycle.ONE_TIME; - break; - default: - repeatCycle = RepeatCycle.DAYS; - interval = 1; - } - - // Get the automatic randomization unit based on repeat cycle - RepeatCycle randomUnit = getAutomaticRandomizerValueUnit(repeatCycle); - - // Calculate the maximum allowed randomizer value using the new logic - int maxRandomizer = calculateMaxAllowedRandomization(repeatCycle, interval); - - // Ensure minimum valid bounds for SpinnerNumberModel - if (maxRandomizer < 1) { - maxRandomizer = 1; - } - - // Update the spinner model with new limits - SpinnerNumberModel currentModel = (SpinnerNumberModel) randomizeSpinner.getModel(); - int currentValue = currentModel.getNumber().intValue(); - - // Ensure current value is within valid bounds - int validatedCurrentValue = Math.max(1, Math.min(currentValue, maxRandomizer)); - - // Create new model with validated values - SpinnerNumberModel newModel = new SpinnerNumberModel( - validatedCurrentValue, // current value, validated to be within bounds - 1, // minimum - maxRandomizer, // maximum (at least 1) - 1 // step - ); - - randomizeSpinner.setModel(newModel); - - // Update the unit label based on the automatic randomization unit - String unitDisplayName = getRandomizationUnitDisplayName(randomUnit); - randomizerUnitLabel.setText(unitDisplayName); - - // Update tooltip to show the reasoning with correct unit - randomizeSpinner.setToolTipText(String.format( - "Randomization range: Âą1 to Âą%d %s
" + - "Unit: %s (auto-determined from %s cycle)
" + - "Maximum is 40%% of %d %s interval", - maxRandomizer, - unitDisplayName, - randomUnit.toString().toLowerCase(), - repeatCycle.toString().toLowerCase(), - interval, - repeatCycle.toString().toLowerCase() - )); - } - - /** - * Creates a help panel with useful information - */ - private static JPanel createHelpPanel() { - JPanel helpPanel = new JPanel(); - helpPanel.setLayout(new BoxLayout(helpPanel, BoxLayout.Y_AXIS)); - helpPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel descriptionLabel = new JLabel("Plugin will only run during the specified time window"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - descriptionLabel.setAlignmentX(Component.LEFT_ALIGNMENT); - - JLabel crossDayLabel = new JLabel("Note: If start time > end time, window crosses midnight"); - crossDayLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - crossDayLabel.setFont(FontManager.getRunescapeSmallFont()); - crossDayLabel.setAlignmentX(Component.LEFT_ALIGNMENT); - - JLabel timezoneLabel = new JLabel("Timezone: " + ZoneId.systemDefault().getId()); - timezoneLabel.setForeground(Color.YELLOW); - timezoneLabel.setFont(FontManager.getRunescapeSmallFont()); - timezoneLabel.setAlignmentX(Component.LEFT_ALIGNMENT); - - helpPanel.add(descriptionLabel); - helpPanel.add(Box.createVerticalStrut(2)); - helpPanel.add(crossDayLabel); - helpPanel.add(Box.createVerticalStrut(2)); - helpPanel.add(timezoneLabel); - - return helpPanel; - } - - public static TimeWindowCondition createTimeWindowCondition(JPanel configPanel) { - DateRangePanel dateRangePanel = (DateRangePanel) configPanel.getClientProperty("dateRangePanel"); - TimeRangePanel timeRangePanel = (TimeRangePanel) configPanel.getClientProperty("timeRangePanel"); - @SuppressWarnings("unchecked") - JComboBox repeatComboBox = (JComboBox) configPanel.getClientProperty("repeatComboBox"); - JSpinner intervalSpinner = (JSpinner) configPanel.getClientProperty("intervalSpinner"); - JCheckBox randomizeCheckBox = (JCheckBox) configPanel.getClientProperty("randomizeCheckBox"); - JSpinner randomizeSpinner = (JSpinner) configPanel.getClientProperty("randomizeSpinner"); - - if (dateRangePanel == null || timeRangePanel == null) { - throw new IllegalStateException("Time window configuration components not found"); - } - - // Get date values - LocalDate startDate = dateRangePanel.getStartDate(); - LocalDate endDate = dateRangePanel.getEndDate(); - - // Get time values - LocalTime startTime = timeRangePanel.getStartTime(); - LocalTime endTime = timeRangePanel.getEndTime(); - - // Get repeat cycle configuration - String repeatOption = (String) repeatComboBox.getSelectedItem(); - RepeatCycle repeatCycle; - int interval = (Integer) intervalSpinner.getValue(); - long maximumNumberOfRepeats = 0; // Default to infinite repeats - switch (repeatOption) { - case "Every Day": - repeatCycle = RepeatCycle.DAYS; - interval = 1; - break; - case "Every X Days": - repeatCycle = RepeatCycle.DAYS; - break; - case "Every X Hours": - repeatCycle = RepeatCycle.HOURS; - break; - case "Every X Minutes": - repeatCycle = RepeatCycle.MINUTES; - break; - case "Every X Weeks": - repeatCycle = RepeatCycle.WEEKS; - break; - case "One Time Only": - repeatCycle = RepeatCycle.ONE_TIME; - interval = 1; - maximumNumberOfRepeats = 1; - break; - default: - repeatCycle = RepeatCycle.DAYS; - interval = 1; - } - - // Create the condition - TimeWindowCondition condition = new TimeWindowCondition( - startTime, - endTime, - startDate, - endDate, - repeatCycle, - interval, - maximumNumberOfRepeats - - ); - - // Apply randomization if enabled - if (randomizeCheckBox.isSelected()) { - int randomizerValue = (Integer) randomizeSpinner.getValue(); - int maxAllowedRandomizer = calculateMaxAllowedRandomization(condition.getRepeatCycle(), condition.getRepeatIntervalUnit()); - int validatedValue = Math.max(1, Math.min(randomizerValue, maxAllowedRandomizer)); - if (validatedValue != randomizerValue) { - log.warn(" - createTimeWindowCondition - Randomizer value {} is too large for interval {} {}. Capping at maxi {}", - randomizerValue, interval, repeatCycle, maxAllowedRandomizer); - randomizerValue = validatedValue; - randomizeSpinner.setValue(validatedValue); // Update UI to reflect capped value - } - - condition.setRandomization(true); - condition.setRandomizerValue(validatedValue); - // Note: randomization unit is now automatically determined based on repeat cycle - // No need to manually set it anymore - TimeWindow handles this internally - } - - return condition; - } - - - /** - * Creates a panel for configuring SingleTriggerTimeCondition - * Uses the enhanced SingleDateTimePickerPanel component - */ - public static void createSingleTriggerConfigPanel(JPanel panel, GridBagConstraints gbc) { - // Section title - JLabel titleLabel = new JLabel("One-Time Trigger Configuration:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - panel.add(titleLabel, gbc); - - // Create the date/time picker panel - gbc.gridy++; - SingleDateTimePickerPanel dateTimePicker = new SingleDateTimePickerPanel(); - panel.add(dateTimePicker, gbc); - // Description - gbc.gridy++; - JLabel descriptionLabel = new JLabel("Plugin will be triggered once at the specified date and time"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Current timezone info - gbc.gridy++; - JLabel timezoneLabel = new JLabel("Current timezone: " + ZoneId.systemDefault().getId()); - timezoneLabel.setForeground(Color.YELLOW); - timezoneLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(timezoneLabel, gbc); - - // Store components for later access - panel.putClientProperty("dateTimePicker", dateTimePicker); - - } - /** - * Creates a SingleTriggerTimeCondition from the config panel - * Uses the enhanced SingleDateTimePickerPanel component - */ - public static SingleTriggerTimeCondition createSingleTriggerCondition(JPanel configPanel) { - SingleDateTimePickerPanel dateTimePicker = (SingleDateTimePickerPanel) configPanel.getClientProperty("dateTimePicker"); - - if (dateTimePicker == null) { - log.error("Date time picker component not found in panel"); - return null; - } - - // Get the selected date and time as LocalDateTime - LocalDateTime selectedDateTime = dateTimePicker.getDateTime(); - - // Convert to ZonedDateTime using the system default timezone - ZonedDateTime triggerTime = selectedDateTime.atZone(ZoneId.systemDefault()); - - // Create and return the condition - return new SingleTriggerTimeCondition(triggerTime,Duration.ofSeconds(0),1); - } - public static void createDayOfWeekConfigPanel(JPanel panel, GridBagConstraints gbc) { - // Title and initial setup - JLabel titleLabel = new JLabel("Day of Week Configuration:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - panel.add(titleLabel, gbc); - - // Preset options - gbc.gridy++; - JPanel presetPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - presetPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JButton weekdaysButton = new JButton("Weekdays"); - weekdaysButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - weekdaysButton.setForeground(Color.WHITE); - - JButton weekendsButton = new JButton("Weekends"); - weekendsButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - weekendsButton.setForeground(Color.WHITE); - - JButton allDaysButton = new JButton("All Days"); - allDaysButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - allDaysButton.setForeground(Color.WHITE); - - presetPanel.add(weekdaysButton); - presetPanel.add(weekendsButton); - presetPanel.add(allDaysButton); - - panel.add(presetPanel, gbc); - - // Day checkboxes - gbc.gridy++; - JPanel daysPanel = new JPanel(new GridLayout(0, 3)); - daysPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - String[] dayNames = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}; - JCheckBox[] dayCheckboxes = new JCheckBox[7]; - - for (int i = 0; i < dayNames.length; i++) { - dayCheckboxes[i] = new JCheckBox(dayNames[i]); - dayCheckboxes[i].setBackground(ColorScheme.DARKER_GRAY_COLOR); - dayCheckboxes[i].setForeground(Color.WHITE); - daysPanel.add(dayCheckboxes[i]); - } - - // Set up weekdays button - weekdaysButton.addActionListener(e -> { - for (int i = 0; i < 5; i++) { - dayCheckboxes[i].setSelected(true); - } - dayCheckboxes[5].setSelected(false); - dayCheckboxes[6].setSelected(false); - }); - - // Set up weekends button - weekendsButton.addActionListener(e -> { - for (int i = 0; i < 5; i++) { - dayCheckboxes[i].setSelected(false); - } - dayCheckboxes[5].setSelected(true); - dayCheckboxes[6].setSelected(true); - }); - - // Set up all days button - allDaysButton.addActionListener(e -> { - for (JCheckBox checkbox : dayCheckboxes) { - checkbox.setSelected(true); - } - }); - - panel.add(daysPanel, gbc); - - // Add usage limits panel - gbc.gridy++; - JPanel usageLimitsPanel = new JPanel(); - usageLimitsPanel.setLayout(new BoxLayout(usageLimitsPanel, BoxLayout.Y_AXIS)); - usageLimitsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Daily limit panel - JPanel dailyLimitPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - dailyLimitPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel dailyLimitLabel = new JLabel("Max repeats per day:"); - dailyLimitLabel.setForeground(Color.WHITE); - dailyLimitPanel.add(dailyLimitLabel); - - SpinnerNumberModel dailyLimitModel = new SpinnerNumberModel(0, 0, 100, 1); - JSpinner dailyLimitSpinner = new JSpinner(dailyLimitModel); - dailyLimitSpinner.setPreferredSize(new Dimension(70, dailyLimitSpinner.getPreferredSize().height)); - dailyLimitPanel.add(dailyLimitSpinner); - - JLabel dailyUnlimitedLabel = new JLabel("(0 = unlimited)"); - dailyUnlimitedLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - dailyUnlimitedLabel.setFont(FontManager.getRunescapeSmallFont()); - dailyLimitPanel.add(dailyUnlimitedLabel); - - usageLimitsPanel.add(dailyLimitPanel); - - // Weekly limit panel - JPanel weeklyLimitPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - weeklyLimitPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel weeklyLimitLabel = new JLabel("Max repeats per week:"); - weeklyLimitLabel.setForeground(Color.WHITE); - weeklyLimitPanel.add(weeklyLimitLabel); - - SpinnerNumberModel weeklyLimitModel = new SpinnerNumberModel(0, 0, 100, 1); - JSpinner weeklyLimitSpinner = new JSpinner(weeklyLimitModel); - weeklyLimitSpinner.setPreferredSize(new Dimension(70, weeklyLimitSpinner.getPreferredSize().height)); - weeklyLimitPanel.add(weeklyLimitSpinner); - - JLabel weeklyUnlimitedLabel = new JLabel("(0 = unlimited)"); - weeklyUnlimitedLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - weeklyUnlimitedLabel.setFont(FontManager.getRunescapeSmallFont()); - weeklyLimitPanel.add(weeklyUnlimitedLabel); - - usageLimitsPanel.add(weeklyLimitPanel); - - panel.add(usageLimitsPanel, gbc); - - // Add interval configuration using the reusable IntervalPickerPanel - gbc.gridy++; - JPanel intervalOptionPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - intervalOptionPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JCheckBox useIntervalCheckBox = new JCheckBox("Use interval between triggers"); - useIntervalCheckBox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - useIntervalCheckBox.setForeground(Color.WHITE); - intervalOptionPanel.add(useIntervalCheckBox); - - panel.add(intervalOptionPanel, gbc); - - // Add the interval picker panel (initially disabled) - gbc.gridy++; - IntervalPickerPanel intervalPicker = new IntervalPickerPanel(false); // No presets needed - intervalPicker.setEnabled(false); - panel.add(intervalPicker, gbc); - - // Toggle interval picker based on checkbox - useIntervalCheckBox.addActionListener(e -> { - boolean useInterval = useIntervalCheckBox.isSelected(); - intervalPicker.setEnabled(useInterval); - }); - - // Description - gbc.gridy++; - JLabel descriptionLabel = new JLabel("Plugin will only run on selected days of the week"); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Add limits description - gbc.gridy++; - JLabel limitsLabel = new JLabel("Daily/weekly limits prevent excessive usage"); - limitsLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - limitsLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(limitsLabel, gbc); - - // Add interval description - gbc.gridy++; - JLabel intervalDescLabel = new JLabel("Intervals control time between triggers on the same day"); - intervalDescLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - intervalDescLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(intervalDescLabel, gbc); - - // Store components for later access - panel.putClientProperty("dayCheckboxes", dayCheckboxes); - panel.putClientProperty("dailyLimitSpinner", dailyLimitSpinner); - panel.putClientProperty("weeklyLimitSpinner", weeklyLimitSpinner); - panel.putClientProperty("useIntervalCheckBox", useIntervalCheckBox); - panel.putClientProperty("intervalPicker", intervalPicker); -} -public static DayOfWeekCondition createDayOfWeekCondition(JPanel configPanel) { - JCheckBox[] dayCheckboxes = (JCheckBox[]) configPanel.getClientProperty("dayCheckboxes"); - JSpinner dailyLimitSpinner = (JSpinner) configPanel.getClientProperty("dailyLimitSpinner"); - JSpinner weeklyLimitSpinner = (JSpinner) configPanel.getClientProperty("weeklyLimitSpinner"); - JCheckBox useIntervalCheckBox = (JCheckBox) configPanel.getClientProperty("useIntervalCheckBox"); - IntervalPickerPanel intervalPicker = (IntervalPickerPanel) configPanel.getClientProperty("intervalPicker"); - - if (dayCheckboxes == null) { - throw new IllegalStateException("Day of week configuration components not found"); - } - - // Collect the selected days - Set activeDays = EnumSet.noneOf(DayOfWeek.class); - if (dayCheckboxes[0].isSelected()) activeDays.add(DayOfWeek.MONDAY); - if (dayCheckboxes[1].isSelected()) activeDays.add(DayOfWeek.TUESDAY); - if (dayCheckboxes[2].isSelected()) activeDays.add(DayOfWeek.WEDNESDAY); - if (dayCheckboxes[3].isSelected()) activeDays.add(DayOfWeek.THURSDAY); - if (dayCheckboxes[4].isSelected()) activeDays.add(DayOfWeek.FRIDAY); - if (dayCheckboxes[5].isSelected()) activeDays.add(DayOfWeek.SATURDAY); - if (dayCheckboxes[6].isSelected()) activeDays.add(DayOfWeek.SUNDAY); - - // If no days selected, default to all days - if (activeDays.isEmpty()) { - activeDays.add(DayOfWeek.MONDAY); - activeDays.add(DayOfWeek.TUESDAY); - activeDays.add(DayOfWeek.WEDNESDAY); - activeDays.add(DayOfWeek.THURSDAY); - activeDays.add(DayOfWeek.FRIDAY); - activeDays.add(DayOfWeek.SATURDAY); - activeDays.add(DayOfWeek.SUNDAY); - } - - // Get daily and weekly limits - long maxRepeatsPerDay = dailyLimitSpinner != null ? (Integer) dailyLimitSpinner.getValue() : 0; - long maxRepeatsPerWeek = weeklyLimitSpinner != null ? (Integer) weeklyLimitSpinner.getValue() : 0; - - // Create the base condition with appropriate limits - DayOfWeekCondition condition = new DayOfWeekCondition(0, maxRepeatsPerDay, maxRepeatsPerWeek, activeDays); - - // If using interval, add interval condition from the interval picker - if (useIntervalCheckBox != null && useIntervalCheckBox.isSelected() && intervalPicker != null) { - IntervalCondition intervalCondition = intervalPicker.createIntervalCondition(); - condition.setIntervalCondition(intervalCondition); - } - - return condition; -} - - - /** - * Sets up the panel with values from an existing time condition - * - * @param panel The panel containing the UI components - * @param condition The time condition to read values from - */ - public static void setupTimeCondition(JPanel panel, Condition condition) { - if (condition == null) { - return; - } - - if (condition instanceof IntervalCondition) { - setupIntervalCondition(panel, (IntervalCondition) condition); - } else if (condition instanceof TimeWindowCondition) { - setupTimeWindowCondition(panel, (TimeWindowCondition) condition); - } else if (condition instanceof DayOfWeekCondition) { - setupDayOfWeekCondition(panel, (DayOfWeekCondition) condition); - } else if (condition instanceof SingleTriggerTimeCondition) { - setupSingleTriggerCondition(panel, (SingleTriggerTimeCondition) condition); - } - } - - - /** - * Sets up the interval condition panel with values from an existing condition - */ - private static void setupIntervalCondition(JPanel panel, IntervalCondition condition) { - // Get the IntervalPickerPanel component which encapsulates all the interval UI controls - IntervalPickerPanel intervalPicker = (IntervalPickerPanel) panel.getClientProperty("intervalPicker"); - InitialDelayPanel initialDelayPanel = (InitialDelayPanel) panel.getClientProperty("initialDelayPanel"); - if (intervalPicker == null) { - log.error("IntervalPickerPanel component not found for interval condition setup"); - return; // Missing UI components - } - - // Use the IntervalPickerPanel's built-in method to configure itself from the condition - intervalPicker.setIntervalCondition(condition); - - // Set initial delay if it exists - if (condition.getInitialDelayCondition() != null && initialDelayPanel != null) { - Duration definedDelay = condition.getInitialDelayCondition().getDefinedDelay(); - if (definedDelay != null && definedDelay.getSeconds() > 0) { - long definedSeconds = definedDelay.getSeconds(); - - // Set the delay time in the UI - int hours = (int) (definedSeconds / 3600); - int minutes = (int) ((definedSeconds % 3600) / 60); - int seconds = (int) (definedSeconds % 60); - - initialDelayPanel.setEnabled(true); - initialDelayPanel.getHoursSpinner().setValue(hours); - initialDelayPanel.getMinutesSpinner().setValue(minutes); - initialDelayPanel.getSecondsSpinner().setValue(seconds); - } - } - } - - /** - * Sets up the time window condition panel with values from an existing condition - */ - private static void setupTimeWindowCondition(JPanel panel, TimeWindowCondition condition) { - // Get custom components from client properties - DateRangePanel dateRangePanel = (DateRangePanel) panel.getClientProperty("dateRangePanel"); - TimeRangePanel timeRangePanel = (TimeRangePanel) panel.getClientProperty("timeRangePanel"); - @SuppressWarnings("unchecked") - JComboBox repeatComboBox = (JComboBox) panel.getClientProperty("repeatComboBox"); - JSpinner intervalSpinner = (JSpinner) panel.getClientProperty("intervalSpinner"); - JCheckBox randomizeCheckBox = (JCheckBox) panel.getClientProperty("randomizeCheckBox"); - JSpinner randomizeSpinner = (JSpinner) panel.getClientProperty("randomizeSpinner"); - - // Find and update date preset ComboBox - updateDatePresetFromCondition(panel, condition); - - // Find and update time preset ComboBox - updateTimePresetFromCondition(panel, condition); - - // Set date range - if (dateRangePanel != null) { - if (condition.getStartDate() != null) { - dateRangePanel.setStartDate(condition.getStartDate()); - } - if (condition.getEndDate() != null) { - dateRangePanel.setEndDate(condition.getEndDate()); - } - } - - // Set time range - if (timeRangePanel != null) { - timeRangePanel.setStartTime(condition.getStartTime()); - timeRangePanel.setEndTime(condition.getEndTime()); - } - - // Set repeat cycle - if (repeatComboBox != null) { - RepeatCycle cycle = condition.getRepeatCycle(); - int interval = condition.getRepeatIntervalUnit(); - - // Map RepeatCycle enum to combo box options - switch (cycle) { - case DAYS: - repeatComboBox.setSelectedItem(interval == 1 ? "Every Day" : "Every X Days"); - break; - case HOURS: - repeatComboBox.setSelectedItem("Every X Hours"); - break; - case MINUTES: - repeatComboBox.setSelectedItem("Every X Minutes"); - break; - case WEEKS: - repeatComboBox.setSelectedItem("Every X Weeks"); - break; - case ONE_TIME: - repeatComboBox.setSelectedItem("One Time Only"); - break; - case SECONDS: - case MILLIS: - default: - // Fallback for unsupported cycles - repeatComboBox.setSelectedItem("Every Day"); - break; - } - - // Set interval value - if (intervalSpinner != null) { - intervalSpinner.setValue(interval); - intervalSpinner.setEnabled(!cycle.equals(RepeatCycle.DAYS) || interval != 1); - } - } - - // Set randomization - if (randomizeCheckBox != null && randomizeSpinner != null) { - randomizeCheckBox.setSelected(condition.isUseRandomization()); - randomizeSpinner.setEnabled(condition.isUseRandomization()); - - // Get the randomizer unit label for updating - JLabel randomizerUnitLabel = (JLabel) panel.getClientProperty("randomizerUnitLabel"); - - // Get the automatic randomization unit and update the label - RepeatCycle randomUnit = getAutomaticRandomizerValueUnit(condition.getRepeatCycle()); - if (randomizerUnitLabel != null) { - String unitDisplayName = getRandomizationUnitDisplayName(randomUnit); - randomizerUnitLabel.setText(unitDisplayName); - } - - // Validate and set randomizer value using the new logic - int savedRandomizerValue = condition.getRandomizerValue(); - if (savedRandomizerValue > 0) { - // Calculate max allowed for this condition's settings using new logic - int maxAllowedRandomizer = calculateMaxAllowedRandomization(condition.getRepeatCycle(), condition.getRepeatIntervalUnit()); - int validatedValue = Math.max(1, Math.min(savedRandomizerValue, maxAllowedRandomizer)); - - // Ensure maximum is at least 1 - maxAllowedRandomizer = Math.max(1, maxAllowedRandomizer); - - // Update spinner model with proper limits - SpinnerNumberModel newModel = new SpinnerNumberModel( - validatedValue, // current value, validated - 1, // minimum - maxAllowedRandomizer, // maximum - 1 // step - ); - randomizeSpinner.setModel(newModel); - - // Update tooltip with correct unit information - String unitDisplayName = getRandomizationUnitDisplayName(randomUnit); - randomizeSpinner.setToolTipText(String.format( - "Randomization range: Âą1 to Âą%d %s
" + - "Unit: %s (auto-determined from %s cycle)
" + - "Maximum is 40%% of %d %s interval", - maxAllowedRandomizer, - unitDisplayName, - randomUnit.toString().toLowerCase(), - condition.getRepeatCycle().toString().toLowerCase(), - condition.getRepeatIntervalUnit(), - condition.getRepeatCycle().toString().toLowerCase() - )); - if ( savedRandomizerValue != validatedValue) { - log.warn("Randomizer value {} was too large for {}x{} interval. Capped at {} - maximum {}", - savedRandomizerValue, condition.getRepeatIntervalUnit(), condition.getRepeatCycle(), validatedValue,maxAllowedRandomizer); - } - } else { - // Set default value if no randomization value is set - int maxAllowedRandomizer = calculateMaxAllowedRandomization(condition.getRepeatCycle(), - condition.getRepeatIntervalUnit()); - int defaultValue = Math.max(1, Math.min(3, maxAllowedRandomizer)); - - // Ensure maximum is at least 1 - maxAllowedRandomizer = Math.max(1, maxAllowedRandomizer); - - // Update spinner model with proper limits - SpinnerNumberModel newModel = new SpinnerNumberModel( - defaultValue, // default value - 1, // minimum - maxAllowedRandomizer, // maximum - 1 // step - ); - randomizeSpinner.setModel(newModel); - - // Update tooltip with correct unit information - String unitDisplayName = getRandomizationUnitDisplayName(randomUnit); - randomizeSpinner.setToolTipText(String.format( - "Randomization range: Âą1 to Âą%d %s
" + - "Unit: %s (auto-determined from %s cycle)
" + - "Maximum is 40%% of %d %s interval", - maxAllowedRandomizer, - unitDisplayName, - randomUnit.toString().toLowerCase(), - condition.getRepeatCycle().toString().toLowerCase(), - condition.getRepeatIntervalUnit(), - condition.getRepeatCycle().toString().toLowerCase() - )); - } - } - } - - /** - * Updates the date preset ComboBox based on the condition's date range - */ - private static void updateDatePresetFromCondition(JPanel panel, TimeWindowCondition condition) { - // Try to find the date preset ComboBox in the panel hierarchy - JComboBox datePresetCombo = findDatePresetComboBox(panel); - if (datePresetCombo == null) return; - - LocalDate startDate = condition.getStartDate(); - LocalDate endDate = condition.getEndDate(); - LocalDate today = LocalDate.now(); - - // Check if it matches any preset - if (condition.hasUnlimitedDateRange()) { - datePresetCombo.setSelectedItem("Unlimited"); - } else if (startDate.equals(today) && endDate.equals(today)) { - datePresetCombo.setSelectedItem("Today"); - } else if (startDate.equals(today) && endDate.equals(today.plusDays(7 - today.getDayOfWeek().getValue()))) { - datePresetCombo.setSelectedItem("This Week"); - } else if (startDate.equals(today) && endDate.equals(today.withDayOfMonth(today.lengthOfMonth()))) { - datePresetCombo.setSelectedItem("This Month"); - } else if (startDate.equals(today) && endDate.equals(today.plusDays(7))) { - datePresetCombo.setSelectedItem("Next 7 Days"); - } else if (startDate.equals(today) && endDate.equals(today.plusDays(30))) { - datePresetCombo.setSelectedItem("Next 30 Days"); - } else if (startDate.equals(today) && endDate.equals(today.plusDays(90))) { - datePresetCombo.setSelectedItem("Next 90 Days"); - } else { - datePresetCombo.setSelectedItem("Custom"); - } - } - - /** - * Updates the time preset ComboBox based on the condition's time range - */ - private static void updateTimePresetFromCondition(JPanel panel, TimeWindowCondition condition) { - // Try to find the time preset ComboBox in the panel hierarchy - JComboBox timePresetCombo = findTimePresetComboBox(panel); - if (timePresetCombo == null) return; - - LocalTime startTime = condition.getStartTime(); - LocalTime endTime = condition.getEndTime(); - - // Check if it matches any preset - if (startTime.equals(LocalTime.of(0, 0)) && endTime.equals(LocalTime.of(23, 59))) { - timePresetCombo.setSelectedItem("All Day"); - } else if (startTime.equals(LocalTime.of(9, 0)) && endTime.equals(LocalTime.of(17, 0))) { - timePresetCombo.setSelectedItem("Business Hours"); - } else if (startTime.equals(LocalTime.of(6, 0)) && endTime.equals(LocalTime.of(12, 0))) { - timePresetCombo.setSelectedItem("Morning"); - } else if (startTime.equals(LocalTime.of(12, 0)) && endTime.equals(LocalTime.of(18, 0))) { - timePresetCombo.setSelectedItem("Afternoon"); - } else if (startTime.equals(LocalTime.of(18, 0)) && endTime.equals(LocalTime.of(22, 0))) { - timePresetCombo.setSelectedItem("Evening"); - } else if (startTime.equals(LocalTime.of(22, 0)) && endTime.equals(LocalTime.of(6, 0))) { - timePresetCombo.setSelectedItem("Night"); - } else { - timePresetCombo.setSelectedItem("Custom"); - } - } - - /** - * Recursively searches for the date preset ComboBox in the panel hierarchy - */ - private static JComboBox findDatePresetComboBox(Container container) { - for (Component component : container.getComponents()) { - if (component instanceof JPanel) { - JPanel panel = (JPanel) component; - Object datePresetCombo = panel.getClientProperty("datePresetCombo"); - if (datePresetCombo instanceof JComboBox) { - @SuppressWarnings("unchecked") - JComboBox comboBox = (JComboBox) datePresetCombo; - return comboBox; - } - // Recursively search in child panels - JComboBox found = findDatePresetComboBox(panel); - if (found != null) return found; - } - } - return null; - } - - /** - * Recursively searches for the time preset ComboBox in the panel hierarchy - */ - private static JComboBox findTimePresetComboBox(Container container) { - for (Component component : container.getComponents()) { - if (component instanceof JPanel) { - JPanel panel = (JPanel) component; - Object timePresetCombo = panel.getClientProperty("timePresetCombo"); - if (timePresetCombo instanceof JComboBox) { - @SuppressWarnings("unchecked") - JComboBox comboBox = (JComboBox) timePresetCombo; - return comboBox; - } - // Recursively search in child panels - JComboBox found = findTimePresetComboBox(panel); - if (found != null) return found; - } - } - return null; - } - - /** - * Sets up the day of week condition panel with values from an existing condition - */ - private static void setupDayOfWeekCondition(JPanel panel, DayOfWeekCondition condition) { - JCheckBox[] dayCheckboxes = (JCheckBox[]) panel.getClientProperty("dayCheckboxes"); - JSpinner dailyLimitSpinner = (JSpinner) panel.getClientProperty("dailyLimitSpinner"); - JSpinner weeklyLimitSpinner = (JSpinner) panel.getClientProperty("weeklyLimitSpinner"); - JCheckBox useIntervalCheckBox = (JCheckBox) panel.getClientProperty("useIntervalCheckBox"); - IntervalPickerPanel intervalPicker = (IntervalPickerPanel) panel.getClientProperty("intervalPicker"); - - if (dayCheckboxes != null) { - Set activeDays = condition.getActiveDays(); - - // Map DayOfWeek enum values to checkbox indices (0 = Monday) - if (activeDays.contains(DayOfWeek.MONDAY)) dayCheckboxes[0].setSelected(true); - if (activeDays.contains(DayOfWeek.TUESDAY)) dayCheckboxes[1].setSelected(true); - if (activeDays.contains(DayOfWeek.WEDNESDAY)) dayCheckboxes[2].setSelected(true); - if (activeDays.contains(DayOfWeek.THURSDAY)) dayCheckboxes[3].setSelected(true); - if (activeDays.contains(DayOfWeek.FRIDAY)) dayCheckboxes[4].setSelected(true); - if (activeDays.contains(DayOfWeek.SATURDAY)) dayCheckboxes[5].setSelected(true); - if (activeDays.contains(DayOfWeek.SUNDAY)) dayCheckboxes[6].setSelected(true); - } - - // Set daily and weekly limits - if (dailyLimitSpinner != null) { - dailyLimitSpinner.setValue((int)condition.getMaxRepeatsPerDay()); - } - - if (weeklyLimitSpinner != null) { - weeklyLimitSpinner.setValue((int)condition.getMaxRepeatsPerWeek()); - } - - // Handle interval condition if present - Optional intervalConditionOpt = condition.getIntervalCondition(); - if (intervalConditionOpt.isPresent() && useIntervalCheckBox != null && intervalPicker != null) { - // Enable the interval checkbox - useIntervalCheckBox.setSelected(true); - intervalPicker.setEnabled(true); - - // Configure the interval picker with the condition - intervalPicker.setIntervalCondition(intervalConditionOpt.get()); - } - - // Refresh panel layout - panel.revalidate(); - panel.repaint(); - } - - /** - * Sets up the single trigger condition panel with values from an existing condition - */ - private static void setupSingleTriggerCondition(JPanel panel, SingleTriggerTimeCondition condition) { - SingleDateTimePickerPanel dateTimePicker = (SingleDateTimePickerPanel) panel.getClientProperty("dateTimePicker"); - if (dateTimePicker != null) { - // Convert ZonedDateTime to LocalDateTime - dateTimePicker.setDateTime(condition.getNextTriggerTimeWithPause().get().toLocalDateTime()); - } - - } - - - - /** - * Gets the automatic randomization unit based on repeat cycle (mirrors TimeWindowCondition logic) - */ - private static RepeatCycle getAutomaticRandomizerValueUnit(RepeatCycle repeatCycle) { - switch (repeatCycle) { - case MINUTES: - return RepeatCycle.SECONDS; // For minute intervals, randomize in seconds - case HOURS: - return RepeatCycle.MINUTES; // For hour intervals, randomize in minutes - case DAYS: - return RepeatCycle.MINUTES; // For day intervals, randomize in minutes - case WEEKS: - return RepeatCycle.HOURS; // For week intervals, randomize in hours - case ONE_TIME: - return RepeatCycle.MINUTES; // For one-time, use minutes as default - default: - return RepeatCycle.MINUTES; // Default fallback to minutes - } - } - - /** - * Converts an interval value from one unit to another (mirrors TimeWindowCondition logic) - */ - public static long convertToRandomizationUnit(int value, RepeatCycle fromUnit, RepeatCycle toUnit) { - // Convert to seconds first, then to target unit - long totalSeconds; - switch (fromUnit) { - case MINUTES: - totalSeconds = value * 60L; - break; - case HOURS: - totalSeconds = value * 3600L; - break; - case DAYS: - totalSeconds = value * 86400L; - break; - case WEEKS: - totalSeconds = value * 604800L; - break; - default: - totalSeconds = value; - break; - } - - // Convert from seconds to target unit - switch (toUnit) { - case SECONDS: - return totalSeconds; - case MINUTES: - return totalSeconds / 60L; - case HOURS: - return totalSeconds / 3600L; - default: - return totalSeconds / 60L; // Default to minutes - } - } - - /** - * Calculates the maximum allowed randomization value (mirrors TimeWindowCondition logic) - */ - public static int calculateMaxAllowedRandomization(RepeatCycle repeatCycle, int interval) { - RepeatCycle randomUnit = getAutomaticRandomizerValueUnit(repeatCycle); - - // Calculate total interval in the randomization unit - long totalIntervalInRandomUnit; - switch (repeatCycle) { - case MINUTES: - totalIntervalInRandomUnit = convertToRandomizationUnit(interval, RepeatCycle.MINUTES, randomUnit); - break; - case HOURS: - totalIntervalInRandomUnit = convertToRandomizationUnit(interval, RepeatCycle.HOURS, randomUnit); - break; - case DAYS: - totalIntervalInRandomUnit = convertToRandomizationUnit(interval, RepeatCycle.DAYS, randomUnit); - break; - case WEEKS: - totalIntervalInRandomUnit = convertToRandomizationUnit(interval, RepeatCycle.WEEKS, randomUnit); - break; - case ONE_TIME: - // For one-time, allow up to 1 hour of randomization - return randomUnit == RepeatCycle.HOURS ? 1 : - randomUnit == RepeatCycle.MINUTES ? 60 : - randomUnit == RepeatCycle.SECONDS ? 3600 : 15; - default: - return 15; // Default fallback - } - // Allow randomization up to 40% of the total interval, but apply sensible caps - int maxRandomization = (int) Math.min(totalIntervalInRandomUnit * 0.4, totalIntervalInRandomUnit / 2); - - // Apply caps based on randomization unit to prevent excessive randomization - switch (randomUnit) { - case SECONDS: - return Math.min(maxRandomization, 3600); // Max 1 hour in seconds - case MINUTES: - return Math.min(maxRandomization, 720); // Max 12 hours in minutes - case HOURS: - return Math.min(maxRandomization, 48); // Max 2 days in hours - default: - return Math.min(maxRandomization, 60); // Default to 1 hour equivalent - } - } - - /** - * Gets the display name for the randomization unit - */ - private static String getRandomizationUnitDisplayName(RepeatCycle randomUnit) { - switch (randomUnit) { - case SECONDS: - return "sec"; - case MINUTES: - return "min"; - case HOURS: - return "hr"; - default: - return "min"; - } - } - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/util/TimeConditionUtil.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/util/TimeConditionUtil.java deleted file mode 100644 index 9d7e7fbfe08..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/time/util/TimeConditionUtil.java +++ /dev/null @@ -1,730 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.time.util; - -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.OrCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.DayOfWeekCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.enums.RepeatCycle; - -import java.time.DayOfWeek; -import java.time.Duration; -import java.time.LocalDate; -import java.time.LocalTime; -import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; -import java.util.EnumSet; - -import lombok.Getter; -import lombok.Setter; - - -/** - * Utility class providing factory methods for creating time-based conditions - * for the plugin scheduler system. These methods create start conditions with - * configurable parameters and appropriate caps. - */ -public final class TimeConditionUtil { - - // Maximum durations to prevent excessive sessions - private static final Duration MAX_WEEKDAY_SESSION = Duration.ofHours(4); - private static final Duration MAX_WEEKEND_SESSION = Duration.ofHours(6); - private static final int MAX_DAILY_REPEATS = 5; - private static final int MAX_WEEKLY_REPEATS = 20; - - // Private constructor to prevent instantiation - private TimeConditionUtil() {} - - /** - * Creates a day of week condition with configurable daily limits - * - * @param maxRepeatsPerDay Maximum number of times to trigger per day (capped at 5) - * @param days The days on which the condition should be active - * @return A DayOfWeekCondition with the specified settings - */ - public static DayOfWeekCondition createDailyLimitedCondition(int maxRepeatsPerDay, DayOfWeek... days) { - // Cap daily repeats at the maximum value - int cappedRepeats = Math.min(maxRepeatsPerDay, MAX_DAILY_REPEATS); - return new DayOfWeekCondition(0, cappedRepeats, days); - } - - /** - * Creates a day of week condition with configurable weekly limits - * - * @param maxRepeatsPerWeek Maximum number of times to trigger per week (capped at 20) - * @param days The days on which the condition should be active - * @return A DayOfWeekCondition with the specified settings - */ - public static DayOfWeekCondition createWeeklyLimitedCondition(int maxRepeatsPerWeek, DayOfWeek... days) { - // Cap weekly repeats at the maximum value - int cappedRepeats = Math.min(maxRepeatsPerWeek, MAX_WEEKLY_REPEATS); - return new DayOfWeekCondition(0, 0, cappedRepeats, days); - } - - /** - * Creates a day of week condition with both daily and weekly limits - * - * @param maxRepeatsPerDay Maximum number of times to trigger per day (capped at 5) - * @param maxRepeatsPerWeek Maximum number of times to trigger per week (capped at 20) - * @param days The days on which the condition should be active - * @return A DayOfWeekCondition with the specified settings - */ - public static DayOfWeekCondition createDailyAndWeeklyLimitedCondition( - int maxRepeatsPerDay, int maxRepeatsPerWeek, DayOfWeek... days) { - // Cap daily and weekly repeats at the maximum values - int cappedDailyRepeats = Math.min(maxRepeatsPerDay, MAX_DAILY_REPEATS); - int cappedWeeklyRepeats = Math.min(maxRepeatsPerWeek, MAX_WEEKLY_REPEATS); - return new DayOfWeekCondition(0, cappedDailyRepeats, cappedWeeklyRepeats, days); - } - - /** - * Creates a combined condition for running on weekdays with a specified session duration - * - * @param sessionHours Duration of each session in hours (capped at 4 hours for weekdays) - * @param maxRepeatsPerDay Maximum repeats per day (optional, defaults to 1) - * @return A combined condition for weekday play - */ - public static DayOfWeekCondition createWeekdaySessionCondition(float sessionHours, int maxRepeatsPerDay) { - // Cap the session duration - float cappedHours = Math.min(sessionHours, MAX_WEEKDAY_SESSION.toHours()); - int cappedRepeats = Math.min(maxRepeatsPerDay, MAX_DAILY_REPEATS); - - // Calculate minutes portion for partial hours - int hours = (int)cappedHours; - int minutes = (int)((cappedHours - hours) * 60); - - // Create the interval condition - IntervalCondition sessionDuration = new IntervalCondition( - Duration.ofHours(hours).plusMinutes(minutes)); - - // Create day of week condition for weekdays - DayOfWeekCondition weekdays = new DayOfWeekCondition( - 0, cappedRepeats, - DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, DayOfWeek.FRIDAY); - weekdays.setIntervalCondition(sessionDuration); - return weekdays; - } - - /** - * Creates a combined condition for running on weekdays with a specified session duration - * Uses default of 1 repeat per day - * - * @param sessionHours Duration of each session in hours (capped at 4 hours for weekdays) - * @return A combined condition for weekday play - */ - public static DayOfWeekCondition createWeekdaySessionCondition(float sessionHours) { - return createWeekdaySessionCondition(sessionHours, 1); - } - - /** - * Creates a combined condition for running on weekends with a specified session duration - * - * @param sessionHours Duration of each session in hours (capped at 6 hours for weekends) - * @param maxRepeatsPerDay Maximum repeats per day (optional, defaults to 2) - * @return A combined condition for weekend play - */ - public static DayOfWeekCondition createWeekendSessionCondition(float sessionHours, int maxRepeatsPerDay) { - // Cap the session duration - float cappedHours = Math.min(sessionHours, MAX_WEEKEND_SESSION.toHours()); - int cappedRepeats = Math.min(maxRepeatsPerDay, MAX_DAILY_REPEATS); - - // Calculate minutes portion for partial hours - int hours = (int)cappedHours; - int minutes = (int)((cappedHours - hours) * 60); - - // Create the interval condition - IntervalCondition sessionDuration = new IntervalCondition( - Duration.ofHours(hours).plusMinutes(minutes)); - - // Create day of week condition for weekends - DayOfWeekCondition weekends = new DayOfWeekCondition( - 0, cappedRepeats, - DayOfWeek.SATURDAY, DayOfWeek.SUNDAY); - weekends.setIntervalCondition(sessionDuration); - - - return weekends; - } - - /** - * Creates a combined condition for running on weekends with a specified session duration - * Uses default of 2 repeats per day - * - * @param sessionHours Duration of each session in hours (capped at 6 hours for weekends) - * @return A combined condition for weekend play - */ - public static DayOfWeekCondition createWeekendSessionCondition(float sessionHours) { - return createWeekendSessionCondition(sessionHours, 2); - } - - /** - * Creates a condition for randomized session durations on specified days - * - * @param minSessionHours Minimum session hours - * @param maxSessionHours Maximum session hours (capped based on weekday/weekend) - * @param maxRepeatsPerDay Maximum repeats per day (capped at 5) - * @param days The days on which the condition should be active - * @return A combined condition with randomized session length - */ - public static DayOfWeekCondition createRandomizedSessionCondition( - float minSessionHours, float maxSessionHours, int maxRepeatsPerDay, DayOfWeek... days) { - - // Determine if the days contain only weekends, only weekdays, or mixed - boolean hasWeekend = false; - boolean hasWeekday = false; - - for (DayOfWeek day : days) { - if (day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY) { - hasWeekend = true; - } else { - hasWeekday = true; - } - } - - // Apply appropriate caps based on day type - float cappedMaxHours; - if (hasWeekend && !hasWeekday) { - // Weekend-only cap - cappedMaxHours = Math.min(maxSessionHours, MAX_WEEKEND_SESSION.toHours()); - } else { - // Apply weekday cap (more restrictive) if any weekday is included - cappedMaxHours = Math.min(maxSessionHours, MAX_WEEKDAY_SESSION.toHours()); - } - - // Ensure min <= max - float cappedMinHours = Math.min(minSessionHours, cappedMaxHours); - - // Create min/max durations - int minHours = (int)cappedMinHours; - int minMinutes = (int)((cappedMinHours - minHours) * 60); - Duration minDuration = Duration.ofHours(minHours).plusMinutes(minMinutes); - - int maxHours = (int)cappedMaxHours; - int maxMinutes = (int)((cappedMaxHours - maxHours) * 60); - Duration maxDuration = Duration.ofHours(maxHours).plusMinutes(maxMinutes); - - // Create randomized interval condition - IntervalCondition intervalCondition = IntervalCondition.createRandomized( - minDuration, maxDuration); - - // Create day of week condition - int cappedRepeats = Math.min(maxRepeatsPerDay, MAX_DAILY_REPEATS); - DayOfWeekCondition dayCondition = new DayOfWeekCondition(0, cappedRepeats, days); - dayCondition.setIntervalCondition(intervalCondition); - - - return dayCondition; - } - - /** - * Creates a balanced weekly schedule with different session durations for different days - * and an overall weekly limit. - * - * @param weekdaySessionHours Session duration for weekdays (capped at 4 hours) - * @param weekendSessionHours Session duration for weekends (capped at 6 hours) - * @param weekdayRepeatsPerDay Maximum repeats per weekday (capped at 3) - * @param weekendRepeatsPerDay Maximum repeats per weekend day (capped at 4) - * @param weeklyLimit Overall weekly limit (capped at 20) - * @return A condition that combines all these restrictions - */ - public static AndCondition createBalancedWeeklySchedule( - float weekdaySessionHours, float weekendSessionHours, - int weekdayRepeatsPerDay, int weekendRepeatsPerDay, int weeklyLimit) { - - // Cap input values - float cappedWeekdayHours = Math.min(weekdaySessionHours, MAX_WEEKDAY_SESSION.toHours()); - float cappedWeekendHours = Math.min(weekendSessionHours, MAX_WEEKEND_SESSION.toHours()); - int cappedWeekdayRepeats = Math.min(weekdayRepeatsPerDay, 3); // Stricter cap for weekdays - int cappedWeekendRepeats = Math.min(weekendRepeatsPerDay, 4); // Looser cap for weekends - int cappedWeeklyLimit = Math.min(weeklyLimit, MAX_WEEKLY_REPEATS); - - // Convert hours to durations - int weekdayHours = (int)cappedWeekdayHours; - int weekdayMinutes = (int)((cappedWeekdayHours - weekdayHours) * 60); - Duration weekdayDuration = Duration.ofHours(weekdayHours).plusMinutes(weekdayMinutes); - - int weekendHours = (int)cappedWeekendHours; - int weekendMinutes = (int)((cappedWeekendHours - weekendHours) * 60); - Duration weekendDuration = Duration.ofHours(weekendHours).plusMinutes(weekendMinutes); - - // Create weekday condition - DayOfWeekCondition weekdays = new DayOfWeekCondition( - 0, cappedWeekdayRepeats, 0, - DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, DayOfWeek.FRIDAY); - - IntervalCondition weekdayInterval = new IntervalCondition(weekdayDuration); - weekdays.setIntervalCondition(weekdayInterval); - - - // Create weekend condition - DayOfWeekCondition weekends = new DayOfWeekCondition( - 0, cappedWeekendRepeats, 0, - DayOfWeek.SATURDAY, DayOfWeek.SUNDAY); - - IntervalCondition weekendInterval = new IntervalCondition(weekendDuration); - weekends.setIntervalCondition(weekendInterval); - - - - - // Create OR condition to combine weekday and weekend options - OrCondition dayOptions = new OrCondition(); - dayOptions.addCondition(weekdays); - dayOptions.addCondition(weekends); - - // Create weekly limit condition that applies to all days - DayOfWeekCondition weeklyLimitCondition = new DayOfWeekCondition( - 0, 0, cappedWeeklyLimit, - DayOfWeek.values()); - - // Combine day options with weekly limit - AndCondition finalCondition = new AndCondition(); - finalCondition.addCondition(weeklyLimitCondition); - finalCondition.addCondition(dayOptions); - - return finalCondition; - } - - /** - * Creates a scheduled time window condition based on time of day. - * - * @param startHour Starting hour (0-23) - * @param startMinute Starting minute (0-59) - * @param endHour Ending hour (0-23) - * @param endMinute Ending minute (0-59) - * @param days Days of week to apply this schedule - * @return A combined time window and day of week condition - */ - public static AndCondition createTimeWindowSchedule( - int startHour, int startMinute, int endHour, int endMinute, DayOfWeek... days) { - - // Validate and cap time values - startHour = Math.min(Math.max(startHour, 0), 23); - startMinute = Math.min(Math.max(startMinute, 0), 59); - endHour = Math.min(Math.max(endHour, 0), 23); - endMinute = Math.min(Math.max(endMinute, 0), 59); - - // Create time window condition - TimeWindowCondition timeWindow = new TimeWindowCondition( - LocalTime.of(startHour, startMinute), - LocalTime.of(endHour, endMinute), - LocalDate.now(), - LocalDate.now().plus(1, ChronoUnit.YEARS), - null, 1, 0); - - // Create day of week condition - DayOfWeekCondition dayCondition = new DayOfWeekCondition(0, days); - - // Combine conditions - AndCondition condition = new AndCondition(); - condition.addCondition(dayCondition); - condition.addCondition(timeWindow); - - return condition; - } - - /** - * Creates a randomized time window schedule. - * - * @param baseStartHour Base starting hour (0-23) - * @param baseStartMinute Base starting minute (0-59) - * @param baseEndHour Base ending hour (0-23) - * @param baseEndMinute Base ending minute (0-59) - * @param randomizerValue Amount to randomize times by (±minutes) - * @param days Days of week to apply this schedule - * @return A combined randomized time window and day of week condition - */ - public static AndCondition createRandomizedTimeWindowSchedule( - int baseStartHour, int baseStartMinute, int baseEndHour, int baseEndMinute, - int randomizerValue, DayOfWeek... days) { - - // Validate and cap time values - baseStartHour = Math.min(Math.max(baseStartHour, 0), 23); - baseStartMinute = Math.min(Math.max(baseStartMinute, 0), 59); - baseEndHour = Math.min(Math.max(baseEndHour, 0), 23); - baseEndMinute = Math.min(Math.max(baseEndMinute, 0), 59); - randomizerValue = Math.min(Math.max(randomizerValue, 0), 60); - - // Create time window condition - TimeWindowCondition timeWindow = new TimeWindowCondition( - LocalTime.of(baseStartHour, baseStartMinute), - LocalTime.of(baseEndHour, baseEndMinute), - LocalDate.now(), - LocalDate.now().plus(1, ChronoUnit.YEARS), - null, 1, 0); - - // Set randomization if requested - if (randomizerValue > 0) { - timeWindow.setRandomization(true); - } - - // Create day of week condition - DayOfWeekCondition dayCondition = new DayOfWeekCondition(0, days); - - // Combine conditions - AndCondition condition = new AndCondition(); - condition.addCondition(dayCondition); - condition.addCondition(timeWindow); - - return condition; - } - - /** - * Creates a humanized play schedule that mimics natural human gaming patterns - * with appropriate limits. - * - * @param weekdayMaxHours Maximum session hours for weekdays - * @param weekendMaxHours Maximum session hours for weekends - * @param weeklyMaxRepeats Maximum weekly repeats overall - * @return A realistic human-like play schedule - */ - public static AndCondition createHumanizedPlaySchedule( - float weekdayMaxHours, float weekendMaxHours, int weeklyMaxRepeats) { - - // Cap input values - float cappedWeekdayHours = Math.min(weekdayMaxHours, MAX_WEEKDAY_SESSION.toHours()); - float cappedWeekendHours = Math.min(weekendMaxHours, MAX_WEEKEND_SESSION.toHours()); - int cappedWeeklyRepeats = Math.min(weeklyMaxRepeats, MAX_WEEKLY_REPEATS); - - // Calculate min duration as ~60% of max - float weekdayMinHours = cappedWeekdayHours * 0.6f; - float weekendMinHours = cappedWeekendHours * 0.6f; - - // Convert to Duration objects - int weekdayMinH = (int)weekdayMinHours; - int weekdayMinM = (int)((weekdayMinHours - weekdayMinH) * 60); - Duration weekdayMinDuration = Duration.ofHours(weekdayMinH).plusMinutes(weekdayMinM); - - int weekdayMaxH = (int)cappedWeekdayHours; - int weekdayMaxM = (int)((cappedWeekdayHours - weekdayMaxH) * 60); - Duration weekdayMaxDuration = Duration.ofHours(weekdayMaxH).plusMinutes(weekdayMaxM); - - int weekendMinH = (int)weekendMinHours; - int weekendMinM = (int)((weekendMinHours - weekendMinH) * 60); - Duration weekendMinDuration = Duration.ofHours(weekendMinH).plusMinutes(weekendMinM); - - int weekendMaxH = (int)cappedWeekendHours; - int weekendMaxM = (int)((cappedWeekendHours - weekendMaxH) * 60); - Duration weekendMaxDuration = Duration.ofHours(weekendMaxH).plusMinutes(weekendMaxM); - - // Monday/Wednesday/Friday: 1 session per day, shorter - DayOfWeekCondition mwfDays = new DayOfWeekCondition( - 0, 1, 0, // 1 per day, no specific weekly limit - DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY - ); - - IntervalCondition mwfSession = IntervalCondition.createRandomized( - weekdayMinDuration, weekdayMaxDuration - ); - mwfDays.setIntervalCondition(mwfSession); - - - - - // Tuesday/Thursday: 2 sessions per day, even shorter - DayOfWeekCondition ttDays = new DayOfWeekCondition( - 0, 2, 0, // 2 per day, no specific weekly limit - DayOfWeek.TUESDAY, DayOfWeek.THURSDAY - ); - - // Make TT sessions ~75% of MWF sessions - Duration ttMinDuration = Duration.ofMillis((long)(weekdayMinDuration.toMillis() * 0.75)); - Duration ttMaxDuration = Duration.ofMillis((long)(weekdayMaxDuration.toMillis() * 0.75)); - - IntervalCondition ttSession = IntervalCondition.createRandomized( - ttMinDuration, ttMaxDuration - ); - ttDays .setIntervalCondition(ttSession); - - - - - // Weekend: 2-3 sessions per day, longer - DayOfWeekCondition weekendDays = new DayOfWeekCondition( - 0, 3, 0, // Up to 3 per day - DayOfWeek.SATURDAY, DayOfWeek.SUNDAY - ); - - IntervalCondition weekendSession = IntervalCondition.createRandomized( - weekendMinDuration, weekendMaxDuration - ); - - weekendDays.setIntervalCondition(weekendSession); - - - // Create an OR condition for the day schedules - OrCondition daySchedules = new OrCondition(); - daySchedules.addCondition(mwfDays); - daySchedules.addCondition(ttDays); - daySchedules.addCondition(weekendDays); - - // Apply weekly limit - DayOfWeekCondition weeklyLimit = new DayOfWeekCondition( - 0, 0, cappedWeeklyRepeats, EnumSet.allOf(DayOfWeek.class) - ); - - // Combine with the weekly limit - AndCondition finalSchedule = new AndCondition(); - finalSchedule.addCondition(weeklyLimit); - finalSchedule.addCondition(daySchedules); - - return finalSchedule; - } - - /** - * Creates a "work-life balance" schedule that simulates a player with a daytime job, - * playing evenings on weekdays and more on weekends. - * - * @param weekdayMaxHours Maximum session hours for weekdays - * @param weekendMaxHours Maximum session hours for weekends - * @param weeklyMaxRepeats Maximum weekly repeats overall - * @return A realistic work-life balance schedule - */ - public static Condition createWorkLifeBalanceSchedule( - float weekdayMaxHours, float weekendMaxHours, int weeklyMaxRepeats) { - - // Cap input values - float cappedWeekdayHours = Math.min(weekdayMaxHours, MAX_WEEKDAY_SESSION.toHours()); - float cappedWeekendHours = Math.min(weekendMaxHours, MAX_WEEKEND_SESSION.toHours()); - int cappedWeeklyRepeats = Math.min(weeklyMaxRepeats, MAX_WEEKLY_REPEATS); - - // Calculate min duration as ~70% of max for more predictable evening sessions - float weekdayMinHours = cappedWeekdayHours * 0.7f; - float weekendMinHours = cappedWeekendHours * 0.6f; // More variation on weekends - - // Convert to Duration objects - int weekdayMinH = (int)weekdayMinHours; - int weekdayMinM = (int)((weekdayMinHours - weekdayMinH) * 60); - Duration weekdayMinDuration = Duration.ofHours(weekdayMinH).plusMinutes(weekdayMinM); - - int weekdayMaxH = (int)cappedWeekdayHours; - int weekdayMaxM = (int)((cappedWeekdayHours - weekdayMaxH) * 60); - Duration weekdayMaxDuration = Duration.ofHours(weekdayMaxH).plusMinutes(weekdayMaxM); - - int weekendMinH = (int)weekendMinHours; - int weekendMinM = (int)((weekendMinHours - weekendMinH) * 60); - Duration weekendMinDuration = Duration.ofHours(weekendMinH).plusMinutes(weekendMinM); - - int weekendMaxH = (int)cappedWeekendHours; - int weekendMaxM = (int)((cappedWeekendHours - weekendMaxH) * 60); - Duration weekendMaxDuration = Duration.ofHours(weekendMaxH).plusMinutes(weekendMaxM); - - // Weekday schedule (Mon-Fri) with evening hours - TimeWindowCondition eveningHours = new TimeWindowCondition( - LocalTime.of(18, 0), // 6:00 PM - LocalTime.of(23, 0), // 11:00 PM - LocalDate.now(), - LocalDate.now().plusYears(1), - null, 1, 0 - ); - eveningHours.setRandomization(true, 30); // Randomize by ±30 minutes - - // MWF - lighter play (1 session) - DayOfWeekCondition mwfDays = new DayOfWeekCondition(0, 1, 0, - DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY - ); - - // TT - slightly more play (2 sessions) - DayOfWeekCondition ttDays = new DayOfWeekCondition(0, 2, 0, - DayOfWeek.TUESDAY, DayOfWeek.THURSDAY - ); - - // Weekday session duration - IntervalCondition weekdaySession = IntervalCondition.createRandomized( - weekdayMinDuration, weekdayMaxDuration - ); - - // Combine MWF schedule - AndCondition mwfSchedule = new AndCondition(); - mwfSchedule.addCondition(mwfDays); - mwfSchedule.addCondition(eveningHours); - mwfSchedule.addCondition(weekdaySession); - - // Combine TT schedule - AndCondition ttSchedule = new AndCondition(); - ttSchedule.addCondition(ttDays); - ttSchedule.addCondition(eveningHours); - ttSchedule.addCondition(weekdaySession); - - // Weekend schedule (Sat-Sun) - DayOfWeekCondition weekendDays = new DayOfWeekCondition(0, 3, 0, - DayOfWeek.SATURDAY, DayOfWeek.SUNDAY - ); - - // Weekend has more flexible hours - TimeWindowCondition flexibleHours = new TimeWindowCondition( - LocalTime.of(10, 0), // 10:00 AM - LocalTime.of(23, 59), // 11:59 PM - LocalDate.now(), - LocalDate.now().plusYears(1), - null, 1, 0 - ); - flexibleHours.setRandomization(true, 60); // Randomize by ±60 minutes - - // Weekend session duration - IntervalCondition weekendSession = IntervalCondition.createRandomized( - weekendMinDuration, weekendMaxDuration - ); - - // Combine weekend schedule - AndCondition weekendSchedule = new AndCondition(); - weekendSchedule.addCondition(weekendDays); - weekendSchedule.addCondition(flexibleHours); - weekendSchedule.addCondition(weekendSession); - - // Combine all daily schedules with OR - OrCondition allDaySchedules = new OrCondition(); - allDaySchedules.addCondition(mwfSchedule); - allDaySchedules.addCondition(ttSchedule); - allDaySchedules.addCondition(weekendSchedule); - - // Apply weekly limit - DayOfWeekCondition weeklyLimit = new DayOfWeekCondition( - 0, 0, cappedWeeklyRepeats, EnumSet.allOf(DayOfWeek.class) - ); - - // Apply weekly limit to the combined schedule - AndCondition finalSchedule = new AndCondition(); - finalSchedule.addCondition(weeklyLimit); - finalSchedule.addCondition(allDaySchedules); - - return finalSchedule; - } - - /** - * Creates a condition that runs all day (from midnight to midnight) - * - * @param startDate The start date of the condition - * @param endDate The end date of the condition - * @param repeatCycle The repeat cycle type - * @param repeatIntervalUnit The interval between repetitions - * @return A TimeWindowCondition configured to run all day - */ - public static TimeWindowCondition createAllDayTimeWindow( - LocalDate startDate, - LocalDate endDate, - RepeatCycle repeatCycle, - int repeatIntervalUnit) { - return new TimeWindowCondition( - LocalTime.of(0, 0), - LocalTime.of(23, 59), - startDate, - endDate, - repeatCycle, - repeatIntervalUnit, - 0 // unlimited - ); - } - - /** - * Creates a condition that runs from the start of the day until a specific time - * - * @param endTime The time when the window should end - * @param startDate The start date of the condition - * @param endDate The end date of the condition - * @param repeatCycle The repeat cycle type - * @param repeatIntervalUnit The interval between repetitions - * @return A TimeWindowCondition configured to run from midnight to the specified end time - */ - public static TimeWindowCondition createStartOfDayTimeWindow( - LocalTime endTime, - LocalDate startDate, - LocalDate endDate, - RepeatCycle repeatCycle, - int repeatIntervalUnit) { - return new TimeWindowCondition( - LocalTime.of(0, 0), - endTime, - startDate, - endDate, - repeatCycle, - repeatIntervalUnit, - 0 // unlimited - ); - } - - /** - * Creates a condition that runs from a specific time until the end of the day - * - * @param startTime The time when the window should start - * @param startDate The start date of the condition - * @param endDate The end date of the condition - * @param repeatCycle The repeat cycle type - * @param repeatIntervalUnit The interval between repetitions - * @return A TimeWindowCondition configured to run from the specified start time until midnight - */ - public static TimeWindowCondition createEndOfDayTimeWindow( - LocalTime startTime, - LocalDate startDate, - LocalDate endDate, - RepeatCycle repeatCycle, - int repeatIntervalUnit) { - return new TimeWindowCondition( - startTime, - LocalTime.of(23, 59), - startDate, - endDate, - repeatCycle, - repeatIntervalUnit, - 0 // unlimited - ); - } - - /** - * Provides diagnostic information about an AndCondition, explaining whether - * each component is satisfied and why. - * - * @param condition The AndCondition to diagnose - * @return A detailed diagnostic report - */ - public static String diagnoseCombinedCondition(AndCondition condition) { - StringBuilder sb = new StringBuilder(); - sb.append("Combined condition status: ").append(condition.isSatisfied() ? "SATISFIED" : "NOT SATISFIED").append("\n"); - sb.append("Analyzing individual conditions:\n"); - - for (int i = 0; i < condition.getConditions().size(); i++) { - Condition subCondition = condition.getConditions().get(i); - boolean satisfied = subCondition.isSatisfied(); - - sb.append(i + 1).append(". "); - sb.append(subCondition.getClass().getSimpleName()).append(": "); - sb.append(satisfied ? "SATISFIED" : "NOT SATISFIED").append("\n"); - sb.append(" - ").append(subCondition.getDescription().replace("\n", "\n - ")).append("\n"); - - if (!satisfied) { - // Special handling for different condition types - if (subCondition instanceof DayOfWeekCondition) { - DayOfWeekCondition dayCondition = (DayOfWeekCondition) subCondition; - sb.append(" - Today is not an active day or has reached daily/weekly limit\n"); - sb.append(" - Current day usage: ").append( - dayCondition.getResetCountForDate(LocalDate.now())).append("\n"); - sb.append(" - Daily limit reached: ").append(dayCondition.isDailyLimitReached()).append("\n"); - sb.append(" - Current week usage: ").append(dayCondition.getCurrentWeekResetCount()).append("\n"); - sb.append(" - Weekly limit reached: ").append(dayCondition.isWeeklyLimitReached()).append("\n"); - - // Show next trigger day - dayCondition.getCurrentTriggerTime().ifPresent(time -> - sb.append(" - Next active day: ").append(time.toLocalDate()).append("\n")); - } - else if (subCondition instanceof IntervalCondition) { - IntervalCondition intervalCondition = (IntervalCondition) subCondition; - sb.append(" - Interval not yet elapsed\n"); - intervalCondition.getCurrentTriggerTime().ifPresent(time -> - sb.append(" - Next trigger time: ").append(time).append("\n")); - } - else if (subCondition instanceof TimeWindowCondition) { - TimeWindowCondition timeWindow = (TimeWindowCondition) subCondition; - sb.append(" - Outside of configured time window\n"); - sb.append(" - Current time window: ") - .append(timeWindow.getStartTime()).append(" - ") - .append(timeWindow.getEndTime()).append("\n"); - } - } - } - - return sb.toString(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/ConditionConfigPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/ConditionConfigPanel.java deleted file mode 100644 index dffb133cb81..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/ConditionConfigPanel.java +++ /dev/null @@ -1,2883 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.ui; - -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.FlowLayout; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.GridLayout; -import java.awt.Insets; - -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Consumer; - -import javax.swing.BorderFactory; -import javax.swing.Box; -import javax.swing.BoxLayout; -import javax.swing.DefaultListCellRenderer; -import javax.swing.DefaultListModel; -import javax.swing.JButton; - -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JMenuItem; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JPopupMenu; -import javax.swing.JScrollPane; -import javax.swing.JSeparator; - -import javax.swing.JSplitPane; -import javax.swing.JTabbedPane; - -import javax.swing.JTree; -import javax.swing.ListSelectionModel; - -import javax.swing.SwingConstants; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import javax.swing.event.PopupMenuEvent; -import javax.swing.event.PopupMenuListener; -import javax.swing.tree.DefaultMutableTreeNode; -import javax.swing.tree.DefaultTreeModel; -import javax.swing.tree.TreeNode; -import javax.swing.tree.TreePath; -import javax.swing.tree.TreeSelectionModel; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionManager; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.AreaCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.LocationCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.PositionCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.RegionCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.ui.LocationConditionUtil; -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.resource.BankItemCountCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.GatheredResourceCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.InventoryItemCountCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.LootItemCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ProcessItemCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ui.ResourceConditionPanelUtil; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.SkillLevelCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.SkillXpCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.ui.SkillConditionPanelUtil; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.DayOfWeekCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.ui.TimeConditionPanelUtil; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ui.callback.ConditionUpdateCallback; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.util.SchedulerUIUtils; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ui.renderer.ConditionTreeCellRenderer; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -// Import the utility class -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ui.util.ConditionConfigPanelUtil; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.VarbitCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.ui.VarbitConditionPanelUtil; - -@Slf4j -public class ConditionConfigPanel extends JPanel { - public static final Color BRAND_BLUE = new Color(25, 130, 196); - private final JComboBox conditionCategoryComboBox; - private final JComboBox conditionTypeComboBox; - private JPanel configPanel; - - private ConditionTreeCellRenderer conditionTreeCellRenderer; - - // Tree visualization components - private DefaultMutableTreeNode rootNode; - private DefaultTreeModel treeModel; - private JTree conditionTree; - private JSplitPane splitPane; - - // Condition list components - private DefaultListModel conditionListModel; - private JList conditionList; - - - // callback system - private ConditionUpdateCallback conditionUpdateCallback; - - - - - private PluginScheduleEntry selectScheduledPlugin; - // UI Controls - private JButton resetButton; - private JButton resetUserConditionsButton; - - private JButton editButton; - private JButton addButton; - private JButton removeButton; - private JButton negateButton; - private JButton convertToAndButton; - private JButton convertToOrButton; - private JButton ungroupButton; - private JPanel titlePanel; - private JLabel titleLabel; - private final boolean stopConditionPanel; - private boolean[] updatingSelectionFlag = new boolean[1]; - List lastRefreshConditions = new CopyOnWriteArrayList<>(); - - public ConditionConfigPanel(boolean stopConditionPanel) { - this.stopConditionPanel = stopConditionPanel; - setLayout(new BorderLayout()); - setBorder(BorderFactory.createTitledBorder( - BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(1, 1, 1, 1, ColorScheme.DARK_GRAY_COLOR), - BorderFactory.createEmptyBorder(5, 5, 5, 5) - ), - stopConditionPanel ? "Stop Conditions" : "Start Conditions", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Initialize title panel - titlePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - titlePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - titlePanel.setName("titlePanel"); - - titleLabel = new JLabel("No plugin selected"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeBoldFont()); - titlePanel.add(titleLabel); - - // Initialize reset buttons - initializeResetButton(); - initializeResetUserConditionsButton(); - - // Create a panel for the top buttons, aligned to the right - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - buttonPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - buttonPanel.add(resetUserConditionsButton); - buttonPanel.add(Box.createHorizontalStrut(5)); - buttonPanel.add(resetButton); - - // Add the title and buttons to the top panel - JPanel topPanel = new JPanel(new BorderLayout()); - topPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - topPanel.add(titlePanel, BorderLayout.WEST); - topPanel.add(buttonPanel, BorderLayout.EAST); - - add(topPanel, BorderLayout.NORTH); - - // Define condition categories based on ConditionType enum - String[] conditionCategories = new String[]{ - "Time", - "Skill", - "Resource", - "Location", - "Varbit" // Added Varbit condition category - }; - - conditionCategoryComboBox = new JComboBox<>(conditionCategories); - - // Initialize with empty condition types - will be populated based on category - conditionTypeComboBox = new JComboBox<>(); - - // Set initial condition types based on first category - updateConditionTypes(conditionCategories[0]); - - // Create split pane for main content - splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); - splitPane.setResizeWeight(0.6); // Give more space to the top components - splitPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Initialize condition list - JPanel listPanel = createConditionListPanel(); - - // Initialize condition tree - JPanel treePanel = createLogicalTreePanel(); - - // Create a panel for the list and tree components - JPanel conditionsPanel = new JPanel(new BorderLayout()); - conditionsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Remove split pane and just use the tree panel directly - conditionsPanel.add(treePanel, BorderLayout.CENTER); - - // If you want to keep the list panel in the code but hidden for now, - // you can add this commented code: - // JSplitPane conditionsSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); - // conditionsSplitPane.setTopComponent(treePanel); - // conditionsSplitPane.setBottomComponent(listPanel); - // conditionsSplitPane.setResizeWeight(0.9); // Give almost all space to the tree - // conditionsSplitPane.setBorder(null); - // conditionsSplitPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - // conditionsPanel.add(conditionsSplitPane, BorderLayout.CENTER); - - // Create the condition config panel - JPanel addConditionPanel = createAddConditionPanel(); - - // Add both panels to the main split pane - splitPane.setTopComponent(conditionsPanel); - splitPane.setBottomComponent(addConditionPanel); - - add(splitPane, BorderLayout.CENTER); - - // Initialize the config panel - updateConfigPanel(); - - // Set up tree and list selection synchronization - fixSelectionPersistence(false); - } - - /** - * Updates the condition types dropdown based on the selected category - */ - private void updateConditionTypes(String category) { - conditionTypeComboBox.removeAllItems(); - - if ("Time".equals(category)) { - if (stopConditionPanel) { - // Stop conditions - conditionTypeComboBox.addItem("Time Duration"); - conditionTypeComboBox.addItem("Time Window"); - conditionTypeComboBox.addItem("Not In Time Window"); - conditionTypeComboBox.addItem("Day of Week"); - conditionTypeComboBox.addItem("Specific Time"); - } else { - // Start conditions - conditionTypeComboBox.addItem("Time Interval"); - conditionTypeComboBox.addItem("Time Window"); - conditionTypeComboBox.addItem("Outside Time Window"); - conditionTypeComboBox.addItem("Day of Week"); - conditionTypeComboBox.addItem("Specific Time"); - } - } else if ("Skill".equals(category)) { - if (stopConditionPanel) { - // Stop conditions - conditionTypeComboBox.addItem("Skill Level"); - conditionTypeComboBox.addItem("Skill XP Goal"); - } else { - // Start conditions - conditionTypeComboBox.addItem("Skill Level Required"); - } - } else if ("Resource".equals(category)) { - if (stopConditionPanel) { - // Stop conditions - conditionTypeComboBox.addItem("Item Collection"); - conditionTypeComboBox.addItem("Process Items"); - conditionTypeComboBox.addItem("Gather Resources"); - } else { - // Start conditions - conditionTypeComboBox.addItem("Item Required"); - conditionTypeComboBox.addItem("Inventory Item Count"); - } - } else if ("Location".equals(category)) { - conditionTypeComboBox.addItem("Position"); - conditionTypeComboBox.addItem("Area"); - conditionTypeComboBox.addItem("Region"); - } else if ("Varbit".equals(category)) { - // Varbit conditions with improved naming - conditionTypeComboBox.addItem("Collection Log - Bosses"); - conditionTypeComboBox.addItem("Collection Log - Minigames"); - //conditionTypeComboBox.addItem("General Varbit Condition"); Not yet implemented - } - } - /** - * Fixes selection persistence in the tree and list view with improved event blocking - */ - private void fixSelectionPersistence( boolean syncWithList) { - if(! syncWithList){ - // Create a tree selection listener that only updates button states - conditionTree.addTreeSelectionListener(e -> { - if (!updatingSelectionFlag[0]) { - updateLogicalButtonStates(); - // Update the condition editor when a condition is selected - updateConditionPanelForSelectedNode(); - } - }); - return; - } - // Store in a class field to allow other methods to access it - // Create a tree selection listener that doesn't trigger when programmatically updating - conditionTree.addTreeSelectionListener(e -> { - if (updatingSelectionFlag[0]) return; - - updateLogicalButtonStates(); - // Update the condition editor when a condition is selected - updateConditionPanelForSelectedNode(); - - // Sync with list - only if there's a valid selection - DefaultMutableTreeNode node = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (node != null && node.getUserObject() instanceof Condition) { - Condition condition = (Condition) node.getUserObject(); - int index = getCurrentConditions().indexOf(condition); - if (index >= 0) { - try { - updatingSelectionFlag[0] = true; - conditionList.setSelectedIndex(index); - } finally { - updatingSelectionFlag[0] = false; - } - } - } - }); - - // Create a list selection listener that doesn't trigger when programmatically updating - conditionList.addListSelectionListener(e -> { - if (e.getValueIsAdjusting() || updatingSelectionFlag[0]) return; - - int index = conditionList.getSelectedIndex(); - if (index >= 0 && index < getCurrentConditions().size()) { - try { - updatingSelectionFlag[0] = true; - selectNodeForCondition(getCurrentConditions().get(index)); - } finally { - updatingSelectionFlag[0] = false; - } - } - }); - } - /** - * Gets the current conditions from the selected plugin - * @return List of conditions, or empty list if no plugin selected - */ - private List getCurrentConditions() { - if (selectScheduledPlugin == null) { - return new ArrayList<>(); - }else if (this.stopConditionPanel && selectScheduledPlugin.getStopConditionManager() != null) { - return selectScheduledPlugin.getStopConditions(); - }else if (!this.stopConditionPanel && selectScheduledPlugin.getStartConditionManager() != null) { - return selectScheduledPlugin.getStartConditions(); - } - return new ArrayList<>(); - } - - /** - * Checks if the current plugin has user-defined conditions - * @return true if there are user-defined conditions, false otherwise - */ - private boolean hasUserDefinedConditions() { - if (selectScheduledPlugin == null) { - return false; - } - - ConditionManager manager = getConditionManger(); - if (manager == null) { - return false; - } - - return !manager.getUserConditions().isEmpty(); - } - - /** - * Checks if conditions can be edited based on plugin state - * @return true if conditions can be edited, false if editing should be disabled - */ - private boolean canEditConditions() { - if (selectScheduledPlugin == null) { - return false; - } - - // For stop conditions, disable editing when plugin is running - if (stopConditionPanel && selectScheduledPlugin.isRunning()) { - JOptionPane.showMessageDialog(this, - "Cannot edit stop conditions while the plugin is running.\n" + - "Please wait for the plugin to finish or stop it manually.", - "Plugin Running", - JOptionPane.WARNING_MESSAGE); - return false; - } - - return true; - } - // Side-effect-free variant for enablement checks - private boolean isEditAllowedNoDialog() { - if (selectScheduledPlugin == null) { - return false; - } - return !(stopConditionPanel && selectScheduledPlugin.isRunning()); - } - - - - /** - * Refreshes the UI to display the current plugin conditions - * while preserving selection and expansion state - */ - private void refreshDisplay() { - if (selectScheduledPlugin == null) { - log.debug("refreshDisplay: No plugin selected, skipping refresh"); - return; - } - - List currentConditions = getCurrentConditions(); - log.debug("refreshDisplay: Found {} conditions in plugin", currentConditions.size()); - - // Store both list and tree selection states with better debugging - int selectedListIndex = conditionList.getSelectedIndex(); - log.debug("refreshDisplay: Current list selection index: {}", selectedListIndex); - - // list selection tracking - Condition selectedListCondition = null; - if (selectedListIndex >= 0 && selectedListIndex < currentConditions.size()) { - selectedListCondition = currentConditions.get(selectedListIndex); - log.debug("refreshDisplay: List selection mapped to condition: {}", - selectedListCondition.getDescription()); - } - - // Remember tree selection with better logging - Set selectedTreeConditions = new HashSet<>(); - TreePath[] selectedTreePaths = conditionTree.getSelectionPaths(); - if (selectedTreePaths != null && selectedTreePaths.length > 0) { - log.debug("refreshDisplay: Found {} selected tree paths", selectedTreePaths.length); - for (TreePath path : selectedTreePaths) { - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - if (node != null && node.getUserObject() instanceof Condition) { - Condition condition = (Condition) node.getUserObject(); - selectedTreeConditions.add(condition); - log.debug("refreshDisplay: Added selected tree condition: {}", - condition.getDescription()); - } - } - } else { - log.debug("refreshDisplay: No tree paths selected"); - } - - // robust expansion state tracking with better debugging - Set expandedConditions = new HashSet<>(); - Map expandedPathMap = new HashMap<>(); // Store path for easier restoration - - // First check if root node exists - if (rootNode != null) { - TreePath rootPath = new TreePath(rootNode.getPath()); - log.debug("refreshDisplay: Getting expanded nodes from root path: {}", rootPath); - - Enumeration expandedPaths = conditionTree.getExpandedDescendants(rootPath); - if (expandedPaths != null && expandedPaths.hasMoreElements()) { - log.debug("refreshDisplay: Found expanded paths"); - int expandedCount = 0; - while (expandedPaths.hasMoreElements()) { - TreePath path = expandedPaths.nextElement(); - expandedCount++; - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - if (node != null && node.getUserObject() instanceof Condition) { - Condition condition = (Condition) node.getUserObject(); - expandedConditions.add(condition); - expandedPathMap.put(condition, path); - log.debug("refreshDisplay: Added expanded condition: {}", - condition.getDescription()); - } - } - log.debug("refreshDisplay: Found {} expanded paths, {} are conditions", - expandedCount, expandedConditions.size()); - } else { - log.debug("refreshDisplay: No expanded paths found"); - } - } else { - log.debug("refreshDisplay: Root node is null, can't get expanded paths"); - } - - // Flag to track update types needed - boolean needsStructureUpdate = false; // Complete rebuild needed - boolean needsTextUpdate = false; // Just text needs refreshing - - // Check if structure has changed - if (lastRefreshConditions.size() != currentConditions.size()) { - log.debug("refreshDisplay: Condition count changed from {} to {}, structure update needed", - lastRefreshConditions.size(), currentConditions.size()); - needsStructureUpdate = true; - } else { - // Check if conditions have changed or reordered - for (int i = 0; i < lastRefreshConditions.size(); i++) { - if (!lastRefreshConditions.get(i).equals(currentConditions.get(i))) { - log.debug("refreshDisplay: Condition at index {} changed, structure update needed", i); - needsStructureUpdate = true; - break; - } - } - - // If structure unchanged, check if descriptions need updating - if (!needsStructureUpdate) { - for (int i = 0; i < currentConditions.size(); i++) { - String existingDesc = conditionListModel.getElementAt(i); - String newDesc = descriptionForCondition(currentConditions.get(i)); - if (!existingDesc.equals(newDesc)) { - log.debug("refreshDisplay: Description at index {} changed from '{}' to '{}', text update needed", - i, existingDesc, newDesc); - needsTextUpdate = true; - break; - } - } - } - } - - // Use a flag to prevent selection events during refresh - updatingSelectionFlag[0] = true; - log.debug("refreshDisplay: Setting updatingSelectionFlag to prevent event feedback"); - - try { - // Case 1: Full structure update needed - if (needsStructureUpdate) { - log.debug("refreshDisplay: Performing full structure update"); - lastRefreshConditions = new CopyOnWriteArrayList<>(currentConditions); - - // Update list model - conditionListModel.clear(); - for (Condition condition : currentConditions) { - conditionListModel.addElement(descriptionForCondition(condition)); - } - - // Update tree - updateTreeFromConditions(); - - // Expand all category nodes by default - for (int i = 0; i < conditionTree.getRowCount(); i++) { - TreePath path = conditionTree.getPathForRow(i); - if (path == null) continue; - - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - if (node.getUserObject() instanceof String) { - log.debug("refreshDisplay: Auto-expanding category node: {}", node.getUserObject()); - conditionTree.expandPath(path); - } - } - - // Restore expansion state for condition nodes - if (!expandedConditions.isEmpty()) { - log.debug("refreshDisplay: Restoring {} expanded conditions", expandedConditions.size()); - - for (int i = 0; i < conditionTree.getRowCount(); i++) { - TreePath path = conditionTree.getPathForRow(i); - if (path == null) continue; - - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - if (node.getUserObject() instanceof Condition) { - Condition condition = (Condition) node.getUserObject(); - if (expandedConditions.contains(condition)) { - log.debug("refreshDisplay: Expanding condition node: {}", condition.getDescription()); - conditionTree.expandPath(path); - } - } - } - } else { - log.debug("refreshDisplay: No expanded conditions to restore"); - } - - // Restore selection state - if (!selectedTreeConditions.isEmpty()) { - log.debug("refreshDisplay: Restoring {} selected tree conditions", selectedTreeConditions.size()); - List pathsToSelect = new ArrayList<>(); - - // Find paths to all selected conditions - for (int i = 0; i < conditionTree.getRowCount(); i++) { - TreePath path = conditionTree.getPathForRow(i); - if (path == null) continue; - - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - if (node.getUserObject() instanceof Condition) { - Condition condition = (Condition) node.getUserObject(); - if (selectedTreeConditions.contains(condition)) { - pathsToSelect.add(path); - log.debug("refreshDisplay: Found path for selected condition: {}", - condition.getDescription()); - } - } - } - - if (!pathsToSelect.isEmpty()) { - log.debug("refreshDisplay: Setting {} tree selection paths", pathsToSelect.size()); - conditionTree.setSelectionPaths(pathsToSelect.toArray(new TreePath[0])); - } else { - log.debug("refreshDisplay: Could not find any paths for selected conditions"); - } - } else { - log.debug("refreshDisplay: No tree selections to restore"); - } - - // Restore list selection - if (selectedListCondition != null) { - int newIndex = currentConditions.indexOf(selectedListCondition); - if (newIndex >= 0) { - log.debug("refreshDisplay: Restoring list selection to index {}", newIndex); - conditionList.setSelectedIndex(newIndex); - } else { - log.debug("refreshDisplay: Could not find list selection in current conditions"); - } - } - } - // Case 2: Only text descriptions need updating - else if (needsTextUpdate) { - log.debug("refreshDisplay: Performing text-only update"); - - // Update just the text in the list model without rebuilding - for (int i = 0; i < currentConditions.size(); i++) { - String newDesc = descriptionForCondition(currentConditions.get(i)); - if (!conditionListModel.getElementAt(i).equals(newDesc)) { - log.debug("refreshDisplay: Updating description at index {} to '{}'", i, newDesc); - conditionListModel.setElementAt(newDesc, i); - } - } - - // Update tree nodes' text by forcing renderer refresh without rebuilding - log.debug("refreshDisplay: Repainting tree to refresh node text"); - conditionTree.repaint(); - } else { - log.debug("refreshDisplay: No updates needed"); - } - } finally { - // Re-enable selection events - updatingSelectionFlag[0] = false; - log.debug("refreshDisplay: Resetting updatingSelectionFlag to allow events"); - } - if (this.conditionTreeCellRenderer != null ){ - this.conditionTreeCellRenderer.setIsActive(selectScheduledPlugin.isRunning()); - } - } - - /** - * Helper method to get consistent description for a condition - */ - private String descriptionForCondition(Condition condition) { - // Check if this is a plugin-defined condition - boolean isPluginDefined = false; - - if (this.selectScheduledPlugin!= null && getConditionManger() != null) { - - isPluginDefined = getConditionManger().isPluginDefinedCondition(condition); - } - - - // Add with appropriate tag for plugin-defined conditions - String description = condition.getDescription(); - if (isPluginDefined) { - description = "[Plugin] " + description; - } - - return description; - } - /** - * Updates the panel when a new plugin is selected - * - * @param selectedPlugin The newly selected plugin, or null if selection cleared - */ - public void setSelectScheduledPlugin(PluginScheduleEntry selectedPlugin) { - if (selectedPlugin == this.selectScheduledPlugin) { - return; - }else{ - if (Microbot.isDebug()){ - log.info("setSelectScheduledPlugin: Changing selected plugin from {} to {} - reload list and tree", - this.selectScheduledPlugin==null ? "null": this.selectScheduledPlugin.getCleanName() , selectedPlugin==null ? "null" : selectedPlugin.getCleanName()); - } - } - - // Store the selected plugin - this.selectScheduledPlugin = selectedPlugin; - - // Enable/disable controls based on whether a plugin is selected - boolean hasPlugin = (selectedPlugin != null); - boolean pluginRunning = hasPlugin && selectedPlugin.isRunning(); - boolean isEditingStopConditions = stopConditionPanel; - boolean shouldDisableStopConditionEditing = isEditingStopConditions && pluginRunning; - - resetButton.setEnabled(hasPlugin); - resetUserConditionsButton.setEnabled(hasPlugin && hasUserDefinedConditions()); - - // Disable stop condition editing when plugin is running - editButton.setEnabled(hasPlugin && !shouldDisableStopConditionEditing); - if(addButton != null) { - // Only enable add button if conditions can be edited - addButton.setEnabled(hasPlugin && !shouldDisableStopConditionEditing); - } - if (removeButton != null) { - removeButton.setEnabled(hasPlugin && !shouldDisableStopConditionEditing); - } - conditionTypeComboBox.setEnabled(hasPlugin && !shouldDisableStopConditionEditing); - conditionList.setEnabled(hasPlugin); - conditionTree.setEnabled(hasPlugin); - - // Update logical operation buttons based on edit restrictions - updateLogicalButtonStates(); - - // Update the plugin name display - setScheduledPluginNameLabel(); - - // If a plugin is selected, load its conditions - if (hasPlugin) { - // Set the logic type combo box based on the plugin's condition manager - // Safely obtain the plugin's stop-condition manager - ConditionManager conditionManager; - if (stopConditionPanel){ - conditionManager = selectedPlugin.getStopConditionManager();// has sPlugin checks if null already - }else{ - conditionManager = selectedPlugin.getStartConditionManager(); - } - boolean requireAll = conditionManager != null && conditionManager.requiresAll(); - // Load conditions using the guarded manager - if (conditionManager != null) { - loadConditions(conditionManager.getConditions(), requireAll); - } else { - // Fallback to empty if no manager - loadConditions(new ArrayList<>(), requireAll); - } - } else { - // Clear conditions if no plugin selected - loadConditions(new ArrayList<>(), true); - } - - // Update the tree and list displays - refreshDisplay(); - } - - - private JPanel createConditionListPanel() { - JPanel panel = new JPanel(new BorderLayout()); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - panel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Condition List", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), // Changed to bold font - Color.WHITE - )); - - conditionListModel = new DefaultListModel<>(); - conditionList = new JList<>(conditionListModel); - conditionList.setCellRenderer(new DefaultListCellRenderer() { - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, - boolean isSelected, boolean cellHasFocus) { - Component c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - - List currentConditions = getCurrentConditions(); - if (index >= 0 && index < currentConditions.size()) { - Condition condition = currentConditions.get(index); - - boolean isConditionRelevant = false; - - // Check if this condition is relevant based on plugin state - if (selectScheduledPlugin != null) { - if (stopConditionPanel) { - // Stop conditions are relevant when plugin is running - isConditionRelevant = selectScheduledPlugin.isRunning(); - } else { - // Start conditions are relevant when plugin is enabled but not started - isConditionRelevant = selectScheduledPlugin.isEnabled() && !selectScheduledPlugin.isRunning(); - } - } - - // Check if condition is satisfied - boolean isConditionSatisfied = condition.isSatisfied(); - - // Apply appropriate styling - if (!isSelected) { - // Plugin-defined conditions get blue color - if (selectScheduledPlugin != null && - getConditionManger() != null && - getConditionManger().isPluginDefinedCondition(condition)) { - - setForeground(new Color(0, 128, 255)); // Blue for plugin conditions - setFont(getFont().deriveFont(Font.ITALIC)); // Italic for plugin conditions - } else if (isConditionRelevant) { - // Relevant conditions - color based on satisfied status - if (isConditionSatisfied) { - setForeground(new Color(0, 180, 0)); // Green for satisfied - } else { - setForeground(new Color(220, 60, 60)); // Red for unsatisfied - } - } else { - // Non-relevant conditions shown in gray - setForeground(new Color(150, 150, 150)); - } - } - - // Add visual indicators for condition state - String text = (String) value; - String prefix = ""; - - // Relevance indicator - if (isConditionRelevant) { - prefix += "⚡ "; - } - - // Status indicator for relevant conditions - if (isConditionRelevant) { - if (isConditionSatisfied) { - prefix += "✓ "; - } else { - prefix += "✗ "; - } - } - - setText(prefix + text); - - // Enhanced tooltip - StringBuilder tooltip = new StringBuilder(""); - tooltip.append(text).append("
"); - - if (isConditionRelevant) { - if (isConditionSatisfied) { - tooltip.append("Condition is satisfied"); - } else { - tooltip.append("Condition is not satisfied"); - } - - if (condition instanceof TimeCondition) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - ZonedDateTime triggerTime = ((TimeCondition)condition).getCurrentTriggerTime().orElse(null); - if (triggerTime != null) { - tooltip.append("
Trigger time: ").append(triggerTime.format(formatter)); - } - } - } - - tooltip.append(""); - setToolTipText(tooltip.toString()); - } - - return c; - } - }); - conditionList.setBackground(ColorScheme.DARKER_GRAY_COLOR); - conditionList.setForeground(Color.WHITE); - conditionList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - /*conditionList.addListSelectionListener(e -> { - if (!e.getValueIsAdjusting()) { - int index = conditionList.getSelectedIndex(); - if (index >= 0 && index < getCurrentConditions().size()) { - // Select corresponding node in tree - selectNodeForCondition(getCurrentConditions().get(index)); - } - } - });*/ - - JScrollPane scrollPane = new JScrollPane(conditionList); - scrollPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - scrollPane.setBorder(new EmptyBorder(5, 5, 5, 5)); - scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); - scrollPane.getVerticalScrollBar().setUnitIncrement(16); // For smoother scrolling - - panel.add(scrollPane, BorderLayout.CENTER); - return panel; - } - /** - * Creates the add condition panel with condition type selector and controls - */ - private JPanel createAddConditionPanel() { - // Create a panel with border to clearly separate this section - JPanel panel = new JPanel(new BorderLayout()); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - panel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Condition Editor", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - - // Create a main content panel that will be scrollable - JPanel contentPanel = new JPanel(new BorderLayout()); - contentPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Add condition type selector with a more descriptive label - JPanel selectorPanel = new JPanel(new BorderLayout(5, 0)); - selectorPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - selectorPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); - - // Create a panel for both dropdowns - JPanel dropdownsPanel = new JPanel(new GridLayout(2, 2, 5, 5)); - dropdownsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Add category selector - JLabel categoryLabel = new JLabel("Condition Category:"); - categoryLabel.setForeground(Color.WHITE); - categoryLabel.setFont(FontManager.getRunescapeSmallFont()); - dropdownsPanel.add(categoryLabel); - - // Style the category combobox - SchedulerUIUtils.styleComboBox(conditionCategoryComboBox); - conditionCategoryComboBox.addActionListener(e -> { - String selectedCategory = (String) conditionCategoryComboBox.getSelectedItem(); - if (selectedCategory != null) { - updateConditionTypes(selectedCategory); - updateConfigPanel(); - } - }); - dropdownsPanel.add(conditionCategoryComboBox); - - // Add type selector - JLabel typeLabel = new JLabel("Condition Type:"); - typeLabel.setForeground(Color.WHITE); - typeLabel.setFont(FontManager.getRunescapeSmallFont()); - dropdownsPanel.add(typeLabel); - - // Style the type combobox - SchedulerUIUtils.styleComboBox(conditionTypeComboBox); - conditionTypeComboBox.addActionListener(e -> updateConfigPanel()); - dropdownsPanel.add(conditionTypeComboBox); - - selectorPanel.add(dropdownsPanel, BorderLayout.CENTER); - - // Config panel with scroll pane for better visibility - configPanel = new JPanel(new BorderLayout()); - configPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - configPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); - - JScrollPane configScrollPane = new JScrollPane(configPanel); - configScrollPane.setBorder(BorderFactory.createEmptyBorder()); - configScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - configScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - configScrollPane.getVerticalScrollBar().setUnitIncrement(16); - - // Button panel with improved spacing - JPanel buttonPanel = new JPanel(new GridLayout(1, 2, 5, 0)); - buttonPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - buttonPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); - - this.editButton = ConditionConfigPanelUtil.createButton("Add", ColorScheme.PROGRESS_COMPLETE_COLOR); - editButton.addActionListener(e -> { - if ("Apply Changes".equals(editButton.getText())) { - editSelectedCondition(); - } else { - addCurrentCondition(); - } - }); - buttonPanel.add(editButton); - - this.removeButton = ConditionConfigPanelUtil.createButton("Remove", ColorScheme.PROGRESS_ERROR_COLOR); - removeButton.addActionListener(e -> removeSelectedCondition()); - buttonPanel.add(removeButton); - - // Add components to content panel - contentPanel.add(selectorPanel, BorderLayout.NORTH); - contentPanel.add(configScrollPane, BorderLayout.CENTER); - contentPanel.add(buttonPanel, BorderLayout.SOUTH); - - // Add content panel to main panel - panel.add(contentPanel, BorderLayout.CENTER); - - return panel; - } - /** - * Creates the logical condition tree panel with controls - */ - private JPanel createLogicalTreePanel() { - // Use the utility method instead of duplicating code - JPanel panel = ConditionConfigPanelUtil.createTitledPanel("Condition Structure"); - panel.setLayout(new BorderLayout()); - - // Initialize tree - initializeConditionTree(panel); - - // Add logical operations toolbar - JPanel logicalOpPanel = createLogicalOperationsToolbar(); - panel.add(logicalOpPanel, BorderLayout.SOUTH); - - return panel; -} - - private JPanel createLogicalOperationsToolbar() { - // Create the panel - JPanel logicalOpPanel = new JPanel(); - logicalOpPanel.setLayout(new BoxLayout(logicalOpPanel, BoxLayout.X_AXIS)); - logicalOpPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); - logicalOpPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Group operations section - JButton createAndButton = ConditionConfigPanelUtil.createButton("Group as AND", ColorScheme.BRAND_ORANGE); - createAndButton.setToolTipText("Group selected conditions with AND logic"); - createAndButton.addActionListener(e -> { - if (canEditConditions()) { - createLogicalGroup(true); - } - }); - - JButton createOrButton = ConditionConfigPanelUtil.createButton("Group as OR", BRAND_BLUE); - createOrButton.setToolTipText("Group selected conditions with OR logic"); - createOrButton.addActionListener(e -> { - if (canEditConditions()) { - createLogicalGroup(false); - } - }); - - // Negation button - JButton negateButton = ConditionConfigPanelUtil.createButton("Negate", new Color(220, 50, 50)); - negateButton.setToolTipText("Negate the selected condition (toggle NOT)"); - negateButton.addActionListener(e -> { - if (canEditConditions()) { - negateSelectedCondition(); - } - }); - - - // Convert operation buttons - JButton convertToAndButton = ConditionConfigPanelUtil.createButton("Convert to AND", ColorScheme.BRAND_ORANGE); - convertToAndButton.setToolTipText("Convert selected logical group to AND type"); - convertToAndButton.addActionListener(e -> { - if (canEditConditions()) { - convertLogicalType(true); - } - }); - - - JButton convertToOrButton = ConditionConfigPanelUtil.createButton("Convert to OR", BRAND_BLUE); - convertToOrButton.setToolTipText("Convert selected logical group to OR type"); - convertToOrButton.addActionListener(e -> { - if (canEditConditions()) { - convertLogicalType(false); - } - }); - - - // Ungroup button - JButton ungroupButton = ConditionConfigPanelUtil.createButton("Ungroup", ColorScheme.LIGHT_GRAY_COLOR); - ungroupButton.setToolTipText("Remove the logical group but keep its conditions"); - ungroupButton.addActionListener(e -> { - if (canEditConditions()) { - ungroupSelectedLogical(); - } - }); - - - // Add buttons to panel with separators - logicalOpPanel.add(createAndButton); - logicalOpPanel.add(Box.createHorizontalStrut(5)); - logicalOpPanel.add(createOrButton); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(new JSeparator(SwingConstants.VERTICAL)); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(negateButton); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(new JSeparator(SwingConstants.VERTICAL)); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(convertToAndButton); - logicalOpPanel.add(Box.createHorizontalStrut(5)); - logicalOpPanel.add(convertToOrButton); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(new JSeparator(SwingConstants.VERTICAL)); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(ungroupButton); - - // Store references to buttons that need context-sensitive enabling - this.negateButton = negateButton; - this.convertToAndButton = convertToAndButton; - this.convertToOrButton = convertToOrButton; - this.ungroupButton = ungroupButton; - - // Initial state - disable all by default - negateButton.setEnabled(false); - convertToAndButton.setEnabled(false); - convertToOrButton.setEnabled(false); - ungroupButton.setEnabled(false); - - return logicalOpPanel; - } - - private void initializeConditionTree(JPanel panel) { - rootNode = new DefaultMutableTreeNode("Conditions"); - treeModel = new DefaultTreeModel(rootNode); - conditionTree = new JTree(treeModel); - conditionTree.setRootVisible(false); - conditionTree.setShowsRootHandles(true); - this.conditionTreeCellRenderer = new ConditionTreeCellRenderer(getConditionManger(),this.stopConditionPanel); - conditionTree.setCellRenderer(this.conditionTreeCellRenderer); - - conditionTree.getSelectionModel().setSelectionMode( - TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); // Enable multi-select - - // Add popup menu for right-click operations - JPopupMenu popupMenu = createTreePopupMenu(); - conditionTree.setComponentPopupMenu(popupMenu); - - // Add selection listener to update button states and handle selection synchronization - fixSelectionPersistence(false); - - // Create scroll pane with the utility method - JScrollPane treeScrollPane = ConditionConfigPanelUtil.createScrollPane(conditionTree); - treeScrollPane.setPreferredSize(new Dimension(400, 300)); - - panel.add(treeScrollPane, BorderLayout.CENTER); - } - - - - private void updateConfigPanel() { - configPanel.removeAll(); - - // Create a main panel with GridBagLayout for flexibility - JPanel panel = new JPanel(new GridBagLayout()); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 0; - gbc.anchor = GridBagConstraints.WEST; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.weightx = 1.0; - gbc.insets = new Insets(5, 5, 5, 5); - - // Get selected category and type - String selectedCategory = (String) conditionCategoryComboBox.getSelectedItem(); - String selectedType = (String) conditionTypeComboBox.getSelectedItem(); - - if (selectedCategory == null || selectedType == null) { - // No selection, show empty panel - configPanel.add(panel, BorderLayout.NORTH); - return; - } - - // Add condition type header - JLabel typeHeaderLabel = new JLabel("Configure " + selectedType + " Condition"); - typeHeaderLabel.setForeground(Color.WHITE); - typeHeaderLabel.setFont(FontManager.getRunescapeBoldFont()); - panel.add(typeHeaderLabel, gbc); - gbc.gridy++; - - // Add a separator for visual clarity - JSeparator separator = new JSeparator(); - separator.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - panel.add(separator, gbc); - gbc.gridy++; - - // Create the appropriate config panel based on the selected category and type - if ("Location".equals(selectedCategory)) { - // Use LocationConditionUtil for location-based conditions - LocationConditionUtil.createLocationConditionPanel(panel, gbc); - - // After creating the location panel, copy the location-specific client properties from the inner panel to configPanel for later access - JTabbedPane locationTabbedPane = (JTabbedPane) panel.getClientProperty("locationTabbedPane"); - if (locationTabbedPane != null) { - configPanel.putClientProperty("locationTabbedPane", locationTabbedPane); - - // Auto-select the appropriate tab based on the selected condition type - switch (selectedType) { - case "Position": - locationTabbedPane.setSelectedIndex(0); // Position tab - break; - case "Area": - locationTabbedPane.setSelectedIndex(1); // Area tab - break; - case "Region": - locationTabbedPane.setSelectedIndex(2); // Region tab - break; - } - // Copy all location-related properties from the tabbed pane panels - for (int i = 0; i < locationTabbedPane.getTabCount(); i++) { - JPanel tabPanel = (JPanel) locationTabbedPane.getComponentAt(i); - // Copy client properties from tab panels to configPanel for universal access - if (tabPanel.getClientProperty("positionXSpinner") != null) { - configPanel.putClientProperty("positionXSpinner", tabPanel.getClientProperty("positionXSpinner")); - configPanel.putClientProperty("positionYSpinner", tabPanel.getClientProperty("positionYSpinner")); - configPanel.putClientProperty("positionPlaneSpinner", tabPanel.getClientProperty("positionPlaneSpinner")); - configPanel.putClientProperty("positionDistanceSpinner", tabPanel.getClientProperty("positionDistanceSpinner")); - configPanel.putClientProperty("positionNameField", tabPanel.getClientProperty("positionNameField")); - } - if (tabPanel.getClientProperty("areaX1Spinner") != null) { - configPanel.putClientProperty("areaX1Spinner", tabPanel.getClientProperty("areaX1Spinner")); - configPanel.putClientProperty("areaY1Spinner", tabPanel.getClientProperty("areaY1Spinner")); - configPanel.putClientProperty("areaX2Spinner", tabPanel.getClientProperty("areaX2Spinner")); - configPanel.putClientProperty("areaY2Spinner", tabPanel.getClientProperty("areaY2Spinner")); - configPanel.putClientProperty("areaPlaneSpinner", tabPanel.getClientProperty("areaPlaneSpinner")); - configPanel.putClientProperty("areaNameField", tabPanel.getClientProperty("areaNameField")); - } - if (tabPanel.getClientProperty("regionIdsField") != null) { - configPanel.putClientProperty("regionIdsField", tabPanel.getClientProperty("regionIdsField")); - configPanel.putClientProperty("regionNameField", tabPanel.getClientProperty("regionNameField")); - } - } - } - } else if ("Varbit".equals(selectedCategory)) { - // Use VarbitConditionPanelUtil for varbit-based conditions - switch (selectedType) { - case "Collection Log - Minigames": - VarbitConditionPanelUtil.createMinigameVarbitPanel(panel, gbc); - break; - case "Collection Log - Bosses": - VarbitConditionPanelUtil.createBossVarbitPanel(panel, gbc); - break; - } - } else if (stopConditionPanel) { - switch (selectedType) { - case "Time Duration": - TimeConditionPanelUtil.createIntervalConfigPanel(panel, gbc); - break; - case "Time Window": - TimeConditionPanelUtil.createTimeWindowConfigPanel(panel, gbc); - break; - case "Not In Time Window": - TimeConditionPanelUtil.createTimeWindowConfigPanel(panel, gbc); - // Store whether we want inside or outside the window - configPanel.putClientProperty("withInWindow", false); - break; - case "Day of Week": - TimeConditionPanelUtil.createDayOfWeekConfigPanel(panel, gbc); - break; - case "Specific Time": - TimeConditionPanelUtil.createSingleTriggerConfigPanel(panel, gbc); - break; - case "Skill Level": - SkillConditionPanelUtil.createSkillLevelConfigPanel(panel, gbc, true); - break; - case "Skill XP Goal": - SkillConditionPanelUtil.createSkillXpConfigPanel(panel, gbc, panel); - break; - case "Item Collection": - ResourceConditionPanelUtil.createItemConfigPanel(panel, gbc, panel, true); - break; - case "Process Items": - //ResourceConditionPanelUtil.createProcessItemConfigPanel(panel, gbc, panel); - break; - case "Gather Resources": - //ResourceConditionPanelUtil.createGatheredResourceConditionPanel(panel, gbc, panel); - break; - default: - JLabel notImplementedLabel = new JLabel("This condition type is not yet implemented: " + selectedType); - notImplementedLabel.setForeground(Color.RED); - panel.add(notImplementedLabel, gbc); - break; - } - } else { - switch (selectedType) { - case "Time Interval": - TimeConditionPanelUtil.createIntervalConfigPanel(panel, gbc); - break; - case "Time Window": - TimeConditionPanelUtil.createTimeWindowConfigPanel(panel, gbc); - break; - case "Outside Time Window": - TimeConditionPanelUtil.createTimeWindowConfigPanel(panel, gbc); - break; - case "Day of Week": - TimeConditionPanelUtil.createDayOfWeekConfigPanel(panel, gbc); - break; - case "Specific Time": - TimeConditionPanelUtil.createSingleTriggerConfigPanel(panel, gbc); - break; - case "Skill Level Required": - SkillConditionPanelUtil.createSkillLevelConfigPanel(panel, gbc, false); - break; - case "Item Required": - ResourceConditionPanelUtil.createInventoryItemCountPanel(panel, gbc); - break; - default: - JLabel notImplementedLabel = new JLabel("This Start condition type is not yet implemented"); - notImplementedLabel.setForeground(Color.RED); - panel.add(notImplementedLabel, gbc); - break; - } - } - - // Add the panel to the config panel - configPanel.add(panel, BorderLayout.NORTH); - - // Add a filler panel to push everything to the top - JPanel fillerPanel = new JPanel(); - fillerPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - configPanel.add(fillerPanel, BorderLayout.CENTER); - - configPanel.revalidate(); - configPanel.repaint(); - configPanel.putClientProperty("localConditionPanel", panel); - } - - /** - * Sets the condition update callback interface - * This is the preferred way to handle condition updates - * - * @param callback The callback interface to be called when conditions are updated - */ - public void setConditionUpdateCallback(ConditionUpdateCallback callback) { - this.conditionUpdateCallback = callback; - } - - - - - - private void editSelectedCondition() { - // Get the selected node from the tree (same as updateConditionPanelForSelectedNode) - DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (selectedNode == null || !(selectedNode.getUserObject() instanceof Condition)) { - log.warn("editSelectedCondition: No condition selected or invalid selection"); - return; - } - - // Check if conditions can be edited - if (!canEditConditions()) { - return; - } - - // Get the selected condition from the tree node - Condition oldCondition = (Condition) selectedNode.getUserObject(); - - // Skip logical conditions as they don't have direct UI editors - if (oldCondition instanceof LogicalCondition || oldCondition instanceof NotCondition) { - log.warn("editSelectedCondition: Cannot edit logical condition: {}", oldCondition.getDescription()); - JOptionPane.showMessageDialog(this, - "Logical conditions (AND/OR/NOT) cannot be edited directly. Edit their child conditions instead.", - "Cannot Edit Logical Condition", - JOptionPane.INFORMATION_MESSAGE); - return; - } - - // Check if this is a plugin-defined condition - ConditionManager manager = getConditionManger(); - if (manager != null && manager.isPluginDefinedCondition(oldCondition)) { - JOptionPane.showMessageDialog(this, - "This condition is defined by the plugin and cannot be edited.", - "Plugin Condition", - JOptionPane.WARNING_MESSAGE); - return; - } - - // Find the logical condition that contains our target condition - LogicalCondition parentLogical = manager.findContainingLogical(oldCondition); - if (parentLogical == null) { - log.warn("editSelectedCondition: Could not find parent logical for condition: {}", oldCondition.getDescription()); - parentLogical = manager.getUserLogicalCondition(); - } - - log.info("editSelectedCondition: Editing condition: {} from parent: {}", - oldCondition.getDescription(), parentLogical.getDescription()); - - // Create the new condition from the current UI state - - try { - Condition newCondition = getCurrentComboboxCondition(); - - if (newCondition == null) { - log.warn("editSelectedCondition: Failed to create new condition"); - return; - } - - log.info("editSelectedCondition: Created new condition: {}", newCondition.getDescription()); - - // Remove the old condition - boolean removed = manager.removeFromLogicalStructure(parentLogical, oldCondition); - - if (!removed) { - log.warn("editSelectedCondition: Failed to remove old condition from parent"); - return; - } - - // Add the new condition to the parent logical - manager.addConditionToLogical(newCondition, parentLogical); - - // Update UI - updateTreeFromConditions(); - refreshDisplay(); - - // Notify listeners - notifyConditionUpdate(); - - // Select the newly edited condition - selectNodeForCondition(newCondition); - - // Save changes - saveConditionsToScheduledPlugin(); - - } catch (Exception e) { - log.error("editSelectedCondition: Error editing condition", e); - JOptionPane.showMessageDialog(this, - "Error editing condition: " + e.getMessage(), - "Edit Failed", - JOptionPane.ERROR_MESSAGE); - } - } - - - private void loadConditions(List conditionList, boolean requireAll) { - boolean needsUpdate = false; - for (Condition condition : conditionList) { - if (! lastRefreshConditions.contains(condition)){ - needsUpdate = true; - } - } - conditionListModel.clear(); - - if (conditionList != null) { - for (Condition condition : conditionList) { - // Check if this is a plugin-defined condition - boolean isPluginDefined = false; - - if (selectScheduledPlugin != null && getConditionManger() != null) { - isPluginDefined = getConditionManger().isPluginDefinedCondition(condition); - } - - - // Add with appropriate tag for plugin-defined conditions - String description = condition.getDescription(); - if (isPluginDefined) { - description = "[Plugin] " + description; - } - - conditionListModel.addElement(description); - } - } - - updateTreeFromConditions(); - } - /** - * Updates the tree from conditions while preserving selection and expansion state - */ - private void updateTreeFromConditions() { - // Store selected conditions and expanded state before rebuilding - Set selectedConditions = new HashSet<>(); - Set expandedConditions = new HashSet<>(); - - // Remember selected conditions - TreePath[] selectedPaths = conditionTree.getSelectionPaths(); - if (selectedPaths != null) { - for (TreePath path : selectedPaths) { - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - if (node.getUserObject() instanceof Condition) { - selectedConditions.add((Condition) node.getUserObject()); - } - } - } - - // Remember expanded nodes - Enumeration expandedPaths = conditionTree.getExpandedDescendants(new TreePath(rootNode.getPath())); - if (expandedPaths != null) { - while (expandedPaths.hasMoreElements()) { - TreePath path = expandedPaths.nextElement(); - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - if (node.getUserObject() instanceof Condition) { - expandedConditions.add((Condition) node.getUserObject()); - } - } - } - - // Clear and rebuild tree - rootNode.removeAllChildren(); - - if (selectScheduledPlugin == null) { - treeModel.nodeStructureChanged(rootNode); - return; - } - - // Build the tree from the condition manager - ConditionManager manager = getConditionManger(); - if (Microbot.isDebug()){log.info ("updateTreeFromConditions: Building tree for plugin {} with {} conditions", - selectScheduledPlugin.getCleanName(), manager.getConditions().size());} - // If there is a plugin condition, show plugin and user sections separately - if (manager.getPluginCondition() != null && !manager.getPluginCondition().getConditions().isEmpty()) { - // Add plugin section - DefaultMutableTreeNode pluginNode = new DefaultMutableTreeNode("Plugin Conditions"); - rootNode.add(pluginNode); - buildConditionTree(pluginNode, manager.getPluginCondition()); - - // Add user section if it has conditions - if (manager.getUserLogicalCondition() != null && - !manager.getUserLogicalCondition().getConditions().isEmpty()) { - - DefaultMutableTreeNode userNode = new DefaultMutableTreeNode("User Conditions"); - rootNode.add(userNode); - buildConditionTree(userNode, manager.getUserLogicalCondition()); - } - } - // Otherwise just build from the root logical or flat conditions - else if (manager.getUserLogicalCondition() != null) { - LogicalCondition rootLogical = manager.getUserLogicalCondition(); - - // For the root logical, show its children directly if it matches the selected type - buildConditionTree(rootNode, rootLogical); - - } - else { - // This handles the case where we have flat conditions without logical structure - for (Condition condition : getCurrentConditions()) { - buildConditionTree(rootNode, condition); - } - } - - // Update tree model - treeModel.nodeStructureChanged(rootNode); - - // First expand all nodes that were previously expanded - for (int i = 0; i < conditionTree.getRowCount(); i++) { - TreePath path = conditionTree.getPathForRow(i); - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - - if (node.getUserObject() instanceof Condition) { - Condition condition = (Condition) node.getUserObject(); - if (expandedConditions.contains(condition)) { - conditionTree.expandPath(path); - } - } else if (node.getUserObject() instanceof String) { - // Always expand category headers - conditionTree.expandPath(path); - } - } - - // Then restore selection - List pathsToSelect = new ArrayList<>(); - for (int i = 0; i < conditionTree.getRowCount(); i++) { - TreePath path = conditionTree.getPathForRow(i); - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - - if (node.getUserObject() instanceof Condition) { - Condition condition = (Condition) node.getUserObject(); - if (selectedConditions.contains(condition)) { - pathsToSelect.add(path); - } - } - } - - if (!pathsToSelect.isEmpty()) { - conditionTree.setSelectionPaths(pathsToSelect.toArray(new TreePath[0])); - } - this.conditionTreeCellRenderer.setIsActive(selectScheduledPlugin.isEnabled() && selectScheduledPlugin.isRunning()); - } - - private void buildConditionTree(DefaultMutableTreeNode parent, Condition condition) { - if (condition instanceof AndCondition) { - AndCondition andCondition = (AndCondition) condition; - DefaultMutableTreeNode andNode = new DefaultMutableTreeNode(andCondition); - parent.add(andNode); - - for (Condition child : andCondition.getConditions()) { - buildConditionTree(andNode, child); - } - } else if (condition instanceof OrCondition) { - OrCondition orCondition = (OrCondition) condition; - DefaultMutableTreeNode orNode = new DefaultMutableTreeNode(orCondition); - parent.add(orNode); - - for (Condition child : orCondition.getConditions()) { - buildConditionTree(orNode, child); - } - } else if (condition instanceof NotCondition) { - NotCondition notCondition = (NotCondition) condition; - DefaultMutableTreeNode notNode = new DefaultMutableTreeNode(notCondition); - parent.add(notNode); - - buildConditionTree(notNode, notCondition.getCondition()); - } else { - // Add leaf condition - parent.add(new DefaultMutableTreeNode(condition)); - } - } - /** - * Recursively searches for a tree node containing the specified condition - */ - private DefaultMutableTreeNode findTreeNodeForCondition(DefaultMutableTreeNode parent, Condition target) { - // Check if this node contains our target - if (parent.getUserObject() == target) { - return parent; - } - - // Check all children - for (int i = 0; i < parent.getChildCount(); i++) { - DefaultMutableTreeNode child = (DefaultMutableTreeNode) parent.getChildAt(i); - DefaultMutableTreeNode result = findTreeNodeForCondition(child, target); - if (result != null) { - return result; - } - } - - return null; - } - private void groupSelectedWithLogical(LogicalCondition logicalCondition) { - DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (selectedNode == null || selectedNode == rootNode) { - return; - } - - // Get the parent node - DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) selectedNode.getParent(); - if (parentNode == null) { - return; - } - - // Get the selected condition - Object userObject = selectedNode.getUserObject(); - if (!(userObject instanceof Condition)) { - return; - } - - Condition condition = (Condition) userObject; - - // Remove the condition from its current position - int index = getCurrentConditions().indexOf(condition); - if (index >= 0) { - getCurrentConditions().remove(index); - conditionListModel.remove(index); - } - - // Add it to the new logical condition - logicalCondition.addCondition(condition); - - // Add the logical condition to the list - getCurrentConditions().add(logicalCondition); - conditionListModel.addElement(logicalCondition.getDescription()); - - updateTreeFromConditions(); - notifyConditionUpdate(); - } - private void negateSelected() { - DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (selectedNode == null || selectedNode == rootNode) { - return; - } - - // Get the selected condition - Object userObject = selectedNode.getUserObject(); - if (!(userObject instanceof Condition)) { - return; - } - - Condition condition = (Condition) userObject; - - // Remove the condition from its current position - int index = getCurrentConditions().indexOf(condition); - if (index >= 0) { - getCurrentConditions().remove(index); - conditionListModel.remove(index); - } - - // Create a NOT condition - NotCondition notCondition = new NotCondition(condition); - - // Add the NOT condition to the list - getCurrentConditions().add(notCondition); - conditionListModel.addElement(notCondition.getDescription()); - - updateTreeFromConditions(); - notifyConditionUpdate(); - } - /** - * Removes the selected condition, properly handling nested logic conditions - */ - private void removeSelectedFromTree() { - DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (selectedNode == null || selectedNode == rootNode) { - return; - } - - // Get the selected condition - Object userObject = selectedNode.getUserObject(); - if (!(userObject instanceof Condition)) { - return; - } - - Condition condition = (Condition) userObject; - - // Check if this is a plugin-defined condition that shouldn't be removed - if (selectScheduledPlugin != null && - getConditionManger().isPluginDefinedCondition(condition)) { - JOptionPane.showMessageDialog(this, - "This condition is defined by the plugin and cannot be removed.", - "Plugin Condition", - JOptionPane.WARNING_MESSAGE); - return; - } - - - - // Get parent logical condition if any - DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) selectedNode.getParent(); - if (parentNode != rootNode && parentNode.getUserObject() instanceof LogicalCondition) { - LogicalCondition parentLogical = (LogicalCondition) parentNode.getUserObject(); - parentLogical.removeCondition(condition); - - // If logical condition is now empty and not the root, remove it too - if (parentLogical.getConditions().isEmpty() && - parentNode.getParent() != rootNode && - parentNode.getParent() instanceof DefaultMutableTreeNode) { - - DefaultMutableTreeNode grandparentNode = (DefaultMutableTreeNode) parentNode.getParent(); - if (grandparentNode.getUserObject() instanceof LogicalCondition) { - LogicalCondition grandparentLogical = (LogicalCondition) grandparentNode.getUserObject(); - grandparentLogical.removeCondition(parentLogical); - } - } - } else { - // Direct removal from condition manager - getConditionManger().removeCondition(condition); - } - - updateTreeFromConditions(); - notifyConditionUpdate(); - } - /** - * Selects a tree node corresponding to the condition, preserving expansion state - */ - private void selectNodeForCondition(Condition condition) { - if (condition == null) { - log.debug("selectNodeForCondition: Cannot select null condition"); - return; - } - - log.debug("selectNodeForCondition: Attempting to select condition: {}", condition.getDescription()); - - // Store current expansion state - Set expandedPaths = new HashSet<>(); - Enumeration expanded = conditionTree.getExpandedDescendants(new TreePath(rootNode.getPath())); - if (expanded != null) { - while (expanded.hasMoreElements()) { - expandedPaths.add(expanded.nextElement()); - } - log.debug("selectNodeForCondition: Saved {} expanded paths", expandedPaths.size()); - } else { - log.debug("selectNodeForCondition: No expanded paths to save"); - } - - // Find the node corresponding to the condition - DefaultMutableTreeNode targetNode = null; - Enumeration e = rootNode.breadthFirstEnumeration(); - while (e.hasMoreElements()) { - DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.nextElement(); - if (node.getUserObject() == condition) { - targetNode = node; - break; - } - } - - if (targetNode != null) { - log.debug("selectNodeForCondition: Found node for condition: {}", condition.getDescription()); - TreePath path = new TreePath(targetNode.getPath()); - - // Make all parent nodes visible - expanding the path as needed - TreePath parentPath = path.getParentPath(); - if (parentPath != null) { - log.debug("selectNodeForCondition: Expanding parent path"); - conditionTree.expandPath(parentPath); - } - - // Set selection - log.debug("selectNodeForCondition: Setting selection path"); - conditionTree.setSelectionPath(path); - - // Ensure the selected node is visible - log.debug("selectNodeForCondition: Scrolling to make path visible"); - conditionTree.scrollPathToVisible(path); - - // Restore previously expanded paths - log.debug("selectNodeForCondition: Restoring {} expanded paths", expandedPaths.size()); - for (TreePath expandedPath : expandedPaths) { - conditionTree.expandPath(expandedPath); - } - } else { - log.debug("selectNodeForCondition: Could not find node for condition: {}", condition.getDescription()); - } - } - - - private void initializeResetButton() { - resetButton = ConditionConfigPanelUtil.createButton("Reset All Conditions", ColorScheme.PROGRESS_ERROR_COLOR); - - resetButton.addActionListener(e -> { - if (selectScheduledPlugin == null) return; - - int option = JOptionPane.showConfirmDialog(this, - "Are you sure you want to reset all " + (stopConditionPanel ? "stop" : "start") + " conditions?\n" + - "This will remove both user-defined and plugin-defined conditions.", - "Reset All Conditions", - JOptionPane.YES_NO_OPTION, - JOptionPane.WARNING_MESSAGE); - - if (option == JOptionPane.YES_OPTION) { - // Clear all conditions from the condition manager - if (conditionUpdateCallback != null) { - conditionUpdateCallback.onConditionsReset(selectScheduledPlugin, stopConditionPanel); - } - - // Refresh the display - refreshDisplay(); - } - }); - } - - private void initializeResetUserConditionsButton() { - resetUserConditionsButton = ConditionConfigPanelUtil.createButton("Reset User Conditions", new Color(180, 120, 0)); - resetUserConditionsButton.setToolTipText("Reset only user-defined conditions (preserves plugin conditions)"); - - resetUserConditionsButton.addActionListener(e -> { - if (selectScheduledPlugin == null) return; - - if (!hasUserDefinedConditions()) { - JOptionPane.showMessageDialog(this, - "No user-defined conditions to reset.", - "No User Conditions", - JOptionPane.INFORMATION_MESSAGE); - return; - } - - int option = JOptionPane.showConfirmDialog(this, - "Are you sure you want to reset only user-defined " + (stopConditionPanel ? "stop" : "start") + " conditions?\n" + - "Plugin-defined conditions will be preserved.", - "Reset User Conditions", - JOptionPane.YES_NO_OPTION, - JOptionPane.WARNING_MESSAGE); - - if (option == JOptionPane.YES_OPTION) { - // Clear only user conditions from the condition manager - ConditionManager manager = getConditionManger(); - if (manager != null) { - manager.clearUserConditions(); - - // Save changes - if (conditionUpdateCallback != null) { - conditionUpdateCallback.onConditionsUpdated( - manager.getUserLogicalCondition(), - selectScheduledPlugin, - stopConditionPanel - ); - } - } - - // Refresh the display - refreshDisplay(); - - JOptionPane.showMessageDialog(this, - "User-defined conditions have been reset.\n" + - "Plugin-defined conditions remain unchanged.", - "Reset Complete", - JOptionPane.INFORMATION_MESSAGE); - } - }); - } - - private void saveConditionsToScheduledPlugin() { - if (selectScheduledPlugin == null) return; - // Save to config - setScheduledPluginNameLabel(); // Update label - } - /** - * Updates the title label with the selected plugin name using color for better visibility - */ - private void setScheduledPluginNameLabel() { - if (selectScheduledPlugin != null) { - String pluginName = selectScheduledPlugin.getCleanName(); - boolean isRunning = selectScheduledPlugin.isRunning(); - boolean isEnabled = selectScheduledPlugin.isEnabled(); - - titleLabel.setText(ConditionConfigPanelUtil.formatPluginTitle(isRunning, isEnabled, pluginName)); - } else { - titleLabel.setText(ConditionConfigPanelUtil.formatPluginTitle(false, false, null)); - } - } - - /** - * Notifies any external components of condition changes - * and ensures changes are saved to the config. - */ - private void notifyConditionUpdate() { - if (selectScheduledPlugin == null) { - return; - } - - // Get the logical condition structure - LogicalCondition logicalCondition = getConditionManger().getUserLogicalCondition(); - - // Call the new callback if registered - if (conditionUpdateCallback != null) { - conditionUpdateCallback.onConditionsUpdated( - logicalCondition, - selectScheduledPlugin, - stopConditionPanel - ); - } - - - } - /** - * Refreshes the condition list and tree if conditions have changed in the selected plugin. - * This should be called periodically to keep the UI in sync with the plugin state. - * - * @return true if conditions were refreshed, false if no changes were detected - */ - public boolean refreshConditions() { - if (selectScheduledPlugin == null) { - return false; - } - - refreshDisplay(); - return true; - } - private Condition getCurrentComboboxCondition( ) { - JPanel localConfigPanel = (JPanel) configPanel.getClientProperty("localConditionPanel"); - if (localConfigPanel == null) { - log.debug("No config panel found"); - return null; - } - - String selectedCategory = (String) conditionCategoryComboBox.getSelectedItem(); - String selectedType = (String) conditionTypeComboBox.getSelectedItem(); - - if (selectedCategory == null || selectedType == null) { - log.debug("Selected category or type is null"); - return null; - } - - Condition condition = null; - try { - // Create appropriate condition based on the type - if ("Location".equals(selectedCategory)) { - condition = LocationConditionUtil.createLocationCondition(localConfigPanel); - } else if ("Varbit".equals(selectedCategory)) { - - condition = VarbitConditionPanelUtil.createVarbitCondition(localConfigPanel); - } else if (stopConditionPanel) { - // Stop conditions - switch (selectedType) { - case "Time Duration": - condition = TimeConditionPanelUtil.createIntervalCondition(localConfigPanel); - break; - case "Time Window": - condition = TimeConditionPanelUtil.createTimeWindowCondition(localConfigPanel); - break; - case "Not In Time Window": - condition = TimeConditionPanelUtil.createTimeWindowCondition(localConfigPanel); - break; - case "Day of Week": - condition = TimeConditionPanelUtil.createDayOfWeekCondition(localConfigPanel); - break; - case "Specific Time": - condition = TimeConditionPanelUtil.createSingleTriggerCondition(localConfigPanel); - break; - case "Skill Level": - condition = SkillConditionPanelUtil.createSkillLevelCondition(localConfigPanel); - break; - case "Skill XP Goal": - condition = SkillConditionPanelUtil.createSkillXpCondition(localConfigPanel); - break; - - case "Item Collection": - condition = ResourceConditionPanelUtil.createItemCondition(localConfigPanel); - break; - case "Process Items": - condition = ResourceConditionPanelUtil.createProcessItemCondition(localConfigPanel); - break; - case "Gather Resources": - condition = ResourceConditionPanelUtil.createGatheredResourceCondition(localConfigPanel); - break; - case "Inventory Item Count"://TODO these are not working right now. have to update the logic-> change these here to gaathered items - condition = ResourceConditionPanelUtil.createInventoryItemCountCondition(localConfigPanel); - break; - default: - JOptionPane.showMessageDialog(this, "Condition type not implemented", "Error", JOptionPane.ERROR_MESSAGE); - return null; - } - - } else { - switch (selectedType) { - case "Time Interval": - condition = TimeConditionPanelUtil.createIntervalCondition(localConfigPanel); - break; - case "Time Window": - condition = TimeConditionPanelUtil.createTimeWindowCondition(localConfigPanel); - break; - case "Outside Time Window": - condition = TimeConditionPanelUtil.createTimeWindowCondition(localConfigPanel); - break; - case "Day of Week": - condition = TimeConditionPanelUtil.createDayOfWeekCondition(localConfigPanel); - break; - case "Specific Time": - condition = TimeConditionPanelUtil.createSingleTriggerCondition(localConfigPanel); - break; - case "Skill Level Required": - condition = SkillConditionPanelUtil.createSkillLevelCondition(localConfigPanel); - break; - case "Item Required": - //condition = ResourceConditionPanelUtil.createItemRequiredCondition(localConfigPanel); - condition = ResourceConditionPanelUtil.createInventoryItemCountCondition(localConfigPanel); - break; - default: - JOptionPane.showMessageDialog(this, "Condition type not implemented", "Error", JOptionPane.ERROR_MESSAGE); - return null; - } - } - } catch (Exception e) { - JOptionPane.showMessageDialog(this, "Error creating condition: " + e.getMessage()); - } - return condition; - } - - private void addCurrentCondition() { - if (selectScheduledPlugin == null) { - JOptionPane.showMessageDialog(this, "No plugin selected", "Error", JOptionPane.ERROR_MESSAGE); - return; - } - - // Check if conditions can be edited - if (!canEditConditions()) { - return; - } - - Condition condition = getCurrentComboboxCondition(); - - if (condition != null) { - addConditionToPlugin(condition); - refreshDisplay(); - } else { - JOptionPane.showMessageDialog(this, - "Failed to create condition. Check your inputs.", - "Error", JOptionPane.ERROR_MESSAGE); - } - - - } - private ConditionManager getConditionManger(){ - if (selectScheduledPlugin == null) { - return null; - } - if (stopConditionPanel){ - return selectScheduledPlugin.getStopConditionManager(); - }else{ - return selectScheduledPlugin.getStartConditionManager(); - } - } - /** - * Finds the logical condition that should be the target for adding a new condition - * based on the current tree selection - */ - private LogicalCondition findTargetLogicalForAddition() { - if (stopConditionPanel){ - ConditionManager manager = getConditionManger(); - } - if (selectScheduledPlugin == null) { - return null; - } - - DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (selectedNode == null) { - // No selection, use root logical - return getConditionManger().getUserLogicalCondition(); - } - - Object userObject = selectedNode.getUserObject(); - - // If selected node is a logical condition, use it directly - if (userObject instanceof LogicalCondition) { - // Check if this is a plugin-defined condition - if (getConditionManger().isPluginDefinedCondition((LogicalCondition)userObject)) { - return getConditionManger().getUserLogicalCondition(); - } - return (LogicalCondition) userObject; - } - - // If selected node is a regular condition, find its parent logical - if (userObject instanceof Condition && - selectedNode.getParent() != null && - selectedNode.getParent() != rootNode) { - - DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) selectedNode.getParent(); - if (parentNode.getUserObject() instanceof LogicalCondition) { - // Check if this is a plugin-defined condition - - if (getConditionManger().isPluginDefinedCondition((LogicalCondition)parentNode.getUserObject())) { - return getConditionManger().getUserLogicalCondition(); - } - return (LogicalCondition) parentNode.getUserObject(); - } - } - - // Default to user logical condition - return getConditionManger().getUserLogicalCondition(); - } - - /** - * Adds a condition to the appropriate logical structure based on selection - */ - private void addConditionToPlugin(Condition condition) { - if (selectScheduledPlugin == null || condition == null) { - log.error("addConditionToPlugin: Cannot add condition - plugin or condition is null"); - return; - } - - log.info("addConditionToPlugin: Adding condition: {}", condition.getDescription()); - - ConditionManager manager = getConditionManger(); - if (manager == null) { - log.error("addConditionToPlugin: Condition manager is null"); - return; - } - - // Find target logical condition based on selection - LogicalCondition targetLogical = findTargetLogicalForAddition(); - if (targetLogical == null) { - log.error("addConditionToPlugin: Target logical is null"); - return; - } - - log.info("addConditionToPlugin: Using target logical: {}", targetLogical.getDescription()); - - // Add the condition - manager.addConditionToLogical(condition, targetLogical); - - log.info("addConditionToPlugin: Condition added to manager"); - - // Update UI - updateTreeFromConditions(); - - // Notify listeners - notifyConditionUpdate(); - - // Select the newly added condition - selectNodeForCondition(condition); - } - - /** - * Removes the selected condition from the logical structure - */ - private void removeSelectedCondition() { - DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (selectedNode == null || selectedNode == rootNode) { - log.warn("No condition selected for removal"); - return; - } - - // Check if conditions can be edited - if (!canEditConditions()) { - return; - } - - Object userObject = selectedNode.getUserObject(); - if (!(userObject instanceof Condition)) { - log.warn("Selected node is not a condition"); - return; - } - - Condition condition = (Condition) userObject; - ConditionManager manager = getConditionManger(); - - // Check if this is a plugin-defined condition - if (manager.isPluginDefinedCondition(condition)) { - JOptionPane.showMessageDialog(this, - "This condition is defined by the plugin and cannot be removed.", - "Plugin Condition", - JOptionPane.WARNING_MESSAGE); - return; - } - - // Remove the condition from its logical structure - boolean removed = manager.removeCondition(condition); - - if (!removed) { - log.warn("Failed to remove condition: {}", condition.getDescription()); - } - - // Update UI - updateTreeFromConditions(); - notifyConditionUpdate(); - } - - - - /** - * Finds the common parent logical condition for a set of tree nodes - */ - private LogicalCondition findCommonParent(DefaultMutableTreeNode[] nodes) { - if (nodes.length == 0) { - return null; - } - - // Get the parent of the first node - DefaultMutableTreeNode firstParent = (DefaultMutableTreeNode) nodes[0].getParent(); - if (firstParent == null || firstParent == rootNode) { - return getConditionManger().getUserLogicalCondition(); - } - - if (!(firstParent.getUserObject() instanceof LogicalCondition)) { - return null; - } - - LogicalCondition parentLogical = (LogicalCondition) firstParent.getUserObject(); - - // Check if all nodes have the same parent - for (int i = 1; i < nodes.length; i++) { - DefaultMutableTreeNode parent = (DefaultMutableTreeNode) nodes[i].getParent(); - if (parent != firstParent) { - return null; - } - } - - return parentLogical; - } - - /** - * Negates the selected condition - */ - private void negateSelectedCondition() { - DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (selectedNode == null || !(selectedNode.getUserObject() instanceof Condition)) { - return; - } - - Condition selectedCondition = (Condition) selectedNode.getUserObject(); - - // Use the utility method for negation - boolean success = ConditionConfigPanelUtil.negateCondition( - selectedCondition, - getConditionManger(), - this - ); - - if (success) { - // Update UI - updateTreeFromConditions(); - notifyConditionUpdate(); - } - } - - - - /** - * Updates button states based on current selection - */ - private void updateLogicalButtonStates() { - TreePath[] selectionPaths = conditionTree.getSelectionPaths(); - - // Check if we should disable editing for stop conditions when plugin is running - boolean pluginRunning = selectScheduledPlugin != null && selectScheduledPlugin.isRunning(); - boolean isEditingStopConditions = stopConditionPanel; - boolean shouldDisableStopConditionEditing = isEditingStopConditions && pluginRunning; - - // Default state - all operations disabled - boolean canNegate = false; - boolean canConvertToAnd = false; - boolean canConvertToOr = false; - boolean canUngroup = false; - - // If we have a single selection - if (selectionPaths != null && selectionPaths.length == 1) { - DefaultMutableTreeNode node = (DefaultMutableTreeNode) selectionPaths[0].getLastPathComponent(); - Object userObject = node.getUserObject(); - - if (userObject instanceof Condition) { - Condition condition = (Condition) userObject; - - // Check if it's a plugin-defined condition (can't modify these) - boolean isPluginDefined = selectScheduledPlugin != null && - getConditionManger() != null && - getConditionManger().isPluginDefinedCondition(condition); - - // Can negate any condition that isn't plugin-defined and editing is allowed - canNegate = !isPluginDefined && !shouldDisableStopConditionEditing; - - // Can convert logical conditions (AND, OR) if they're not plugin-defined and editing is allowed - if (condition instanceof LogicalCondition && !isPluginDefined && !shouldDisableStopConditionEditing) { - // Can convert AND to OR - canConvertToOr = condition instanceof AndCondition; - - // Can convert OR to AND - canConvertToAnd = condition instanceof OrCondition; - - // Can ungroup any logical that has a parent and isn't plugin-defined - canUngroup = node.getParent() != null && node.getParent() != rootNode; - } - } - } - // If we have multiple selections, only enable group operations if editing is allowed - else if (selectionPaths != null && selectionPaths.length > 1 && !shouldDisableStopConditionEditing) { - // Group operations are handled separately - don't enable other operations - } - - // Update button states - if (negateButton != null) negateButton.setEnabled(canNegate); - if (convertToAndButton != null) convertToAndButton.setEnabled(canConvertToAnd); - if (convertToOrButton != null) convertToOrButton.setEnabled(canConvertToOr); - if (ungroupButton != null) ungroupButton.setEnabled(canUngroup); - } - - /** - * Creates a popup menu for the condition tree with improved visibility - */ - private JPopupMenu createTreePopupMenu() { - JPopupMenu menu = new JPopupMenu(); - menu.setBackground(new Color(30, 30, 35)); - menu.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(new Color(80, 80, 90), 1), - BorderFactory.createEmptyBorder(2, 2, 2, 2) - )); - - // Negate option - JMenuItem negateItem = new JMenuItem("Negate"); - negateItem.setIcon(ConditionConfigPanelUtil.getResourceIcon("not-equal.png", 16, 16)); - negateItem.addActionListener(e -> negateSelectedCondition()); - styleMenuItem(negateItem, new Color(220, 50, 50)); - - // Group options with icons and improved styling - JMenuItem groupAndItem = new JMenuItem("Group as AND"); - groupAndItem.setIcon(ConditionConfigPanelUtil.getResourceIcon("logic-gate-and.png", 16, 16)); - groupAndItem.addActionListener(e -> createLogicalGroup(true)); - styleMenuItem(groupAndItem, ColorScheme.BRAND_ORANGE); - - JMenuItem groupOrItem = new JMenuItem("Group as OR"); - groupOrItem.setIcon(ConditionConfigPanelUtil.getResourceIcon("logic-gate-or.png", 16, 16)); - groupOrItem.addActionListener(e -> createLogicalGroup(false)); - styleMenuItem(groupOrItem, BRAND_BLUE); - - // Convert options with visual distinction - JMenuItem convertToAndItem = new JMenuItem("Convert to AND"); - convertToAndItem.addActionListener(e -> convertLogicalType(true)); - styleMenuItem(convertToAndItem, ColorScheme.BRAND_ORANGE); - - JMenuItem convertToOrItem = new JMenuItem("Convert to OR"); - convertToOrItem.addActionListener(e -> convertLogicalType(false)); - styleMenuItem(convertToOrItem, BRAND_BLUE); - - // Ungroup option - JMenuItem ungroupItem = new JMenuItem("Ungroup"); - ungroupItem.setIcon(ConditionConfigPanelUtil.getResourceIcon("ungroup.png", 16, 16)); - ungroupItem.addActionListener(e -> ungroupSelectedLogical()); - styleMenuItem(ungroupItem, Color.LIGHT_GRAY); - - // Remove option with warning color - JMenuItem removeItem = new JMenuItem("Remove"); - removeItem.setIcon(ConditionConfigPanelUtil.getResourceIcon("delete.png", 16, 16)); - removeItem.addActionListener(e -> removeSelectedCondition()); - styleMenuItem(removeItem, new Color(220, 50, 50)); - - // Add all items with clear separators and sections - menu.add(negateItem); - menu.addSeparator(); - - // Group operations section with header - JLabel groupHeader = new JLabel("Group Operations"); - groupHeader.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - groupHeader.setBorder(BorderFactory.createEmptyBorder(3, 5, 3, 0)); - groupHeader.setForeground(Color.LIGHT_GRAY); - menu.add(groupHeader); - menu.add(groupAndItem); - menu.add(groupOrItem); - - // Conversion operations section - menu.addSeparator(); - JLabel convertHeader = new JLabel("Convert Operations"); - convertHeader.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - convertHeader.setBorder(BorderFactory.createEmptyBorder(3, 5, 3, 0)); - convertHeader.setForeground(Color.LIGHT_GRAY); - menu.add(convertHeader); - menu.add(convertToAndItem); - menu.add(convertToOrItem); - menu.add(ungroupItem); - - // Remove operation section - menu.addSeparator(); - menu.add(removeItem); - - // Use a custom PopupMenuListener to add visual cues for available operations - menu.addPopupMenuListener(new PopupMenuListener() { - @Override - public void popupMenuWillBecomeVisible(PopupMenuEvent e) { - DefaultMutableTreeNode[] selectedNodes = getSelectedConditionNodes(); - DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - - if (selectedNode == null) { - return; - } - - boolean isLogical = selectedNode != null && selectedNode.getUserObject() instanceof LogicalCondition; - boolean isAnd = isLogical && selectedNode.getUserObject() instanceof AndCondition; - boolean isPluginDefined = selectedNode != null && - selectedNode.getUserObject() instanceof Condition && - getConditionManger() != null && - getConditionManger() - .isPluginDefinedCondition((Condition)selectedNode.getUserObject()); - - // Check if editing is allowed for the current plugin state - boolean canEdit = isEditAllowedNoDialog(); - - // Enable/disable with visual indicators for context awareness - configureMenuItem(negateItem, selectedNode != null && !isLogical && !isPluginDefined && canEdit); - configureMenuItem(groupAndItem, selectedNodes.length >= 2 && !isPluginDefined && canEdit); - configureMenuItem(groupOrItem, selectedNodes.length >= 2 && !isPluginDefined && canEdit); - configureMenuItem(convertToAndItem, isLogical && !isAnd && !isPluginDefined && canEdit); - configureMenuItem(convertToOrItem, isLogical && isAnd && !isPluginDefined && canEdit); - configureMenuItem(ungroupItem, isLogical && selectedNode.getParent() != rootNode && !isPluginDefined && canEdit); - configureMenuItem(removeItem, selectedNode != null && !isPluginDefined && canEdit); - - // Set headers visible only if their sections have enabled items - boolean hasGroupOperations = groupAndItem.isEnabled() || groupOrItem.isEnabled(); - groupHeader.setVisible(hasGroupOperations); - - boolean hasConvertOperations = convertToAndItem.isEnabled() || - convertToOrItem.isEnabled() || - ungroupItem.isEnabled(); - convertHeader.setVisible(hasConvertOperations); - } - - @Override - public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {} - - @Override - public void popupMenuCanceled(PopupMenuEvent e) {} - }); - - return menu; - } - - /** - * Helper method to configure menu items with visual indicators - */ - private void configureMenuItem(JMenuItem item, boolean enabled) { - // Get the accent color that was stored during styling - Color accentColor = (Color) item.getClientProperty("accentColor"); - - // Set the enabled state - item.setEnabled(enabled); - - // Apply different visual styling based on availability - if (enabled) { - // For enabled items: dark background, accent-colored text and border - item.setBackground(new Color(45, 45, 45)); - item.setForeground(accentColor != null ? accentColor : Color.WHITE); - item.setOpaque(true); - item.setBorderPainted(true); - - // Add a subtle left border to indicate availability - item.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(0, 3, 0, 0, accentColor != null ? accentColor : Color.LIGHT_GRAY), - BorderFactory.createEmptyBorder(6, 8, 6, 8) - )); - - // Add bold font for available options - item.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - } else { - // For disabled items: transparent background, dimmed text - item.setBackground(new Color(35, 35, 35)); - item.setForeground(new Color(150, 150, 150, 160)); - item.setOpaque(true); - item.setBorderPainted(false); - - // Regular padding for disabled items - item.setBorder(BorderFactory.createEmptyBorder(6, 11, 6, 8)); - - // Normal font for disabled options - item.setFont(FontManager.getRunescapeSmallFont()); - } - } - - /** - * Gets an array of tree nodes representing the selected conditions - */ - private DefaultMutableTreeNode[] getSelectedConditionNodes() { - TreePath[] paths = conditionTree.getSelectionPaths(); - if (paths == null) { - return new DefaultMutableTreeNode[0]; - } - - List nodes = new ArrayList<>(); - for (TreePath path : paths) { - DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); - if (node != rootNode && node.getUserObject() instanceof Condition) { - nodes.add(node); - } - } - - return nodes.toArray(new DefaultMutableTreeNode[0]); - } - - /** - * Converts a logical group from one type to another (AND <-> OR) - */ - private void convertLogicalType(boolean toAnd) { - DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (selectedNode == null || !(selectedNode.getUserObject() instanceof LogicalCondition)) { - return; - } - - LogicalCondition logicalCondition = (LogicalCondition) selectedNode.getUserObject(); - - // Use the utility method for conversion - boolean success = ConditionConfigPanelUtil.convertLogicalType( - logicalCondition, - toAnd, - getConditionManger(), - this - ); - - if (success) { - // Update UI - updateTreeFromConditions(); - selectNodeForCondition(toAnd ? - getConditionManger().getUserLogicalCondition() : - getConditionManger().getUserLogicalCondition()); - notifyConditionUpdate(); - } -} - - /** - * Removes a logical group but keeps its conditions - */ - private void ungroupSelectedLogical() { - DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (selectedNode == null || !(selectedNode.getUserObject() instanceof LogicalCondition)) { - return; - } - - // Don't allow ungrouping the root logical - if (selectedNode.getParent() == rootNode) { - JOptionPane.showMessageDialog(this, - "Cannot ungroup the root logical condition.", - "Operation Not Allowed", - JOptionPane.WARNING_MESSAGE); - return; - } - - LogicalCondition logicalToUngroup = (LogicalCondition) selectedNode.getUserObject(); - - // Check if this is a plugin-defined logical group - if (selectScheduledPlugin != null && - selectScheduledPlugin.getStopConditionManager().isPluginDefinedCondition(logicalToUngroup)) { - - JOptionPane.showMessageDialog(this, - "Cannot ungroup plugin-defined condition groups. These conditions are protected.", - "Protected Conditions", - JOptionPane.WARNING_MESSAGE); - return; - } - DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) selectedNode.getParent(); - - // Only proceed if parent is a logical condition - if (!(parentNode.getUserObject() instanceof LogicalCondition)) { - return; - } - - LogicalCondition parentLogical = (LogicalCondition) parentNode.getUserObject(); - - // Find position in parent - int index = parentLogical.getConditions().indexOf(logicalToUngroup); - if (index < 0) { - return; - } - - // Remove the logical from its parent - parentLogical.getConditions().remove(index); - - // Add all of its conditions to the parent at the same position - int currentIndex = index; - for (Condition condition : new ArrayList<>(logicalToUngroup.getConditions())) { - parentLogical.addConditionAt(currentIndex++, condition); - } - - // Update UI - updateTreeFromConditions(); - notifyConditionUpdate(); - } - - /** - * Creates a new logical group from the selected conditions - */ - private void createLogicalGroup(boolean isAnd) { - DefaultMutableTreeNode[] selectedNodes = getSelectedConditionNodes(); - if (selectedNodes.length < 2) { - JOptionPane.showMessageDialog(this, - "Please select at least two conditions to group", - "Selection Required", - JOptionPane.INFORMATION_MESSAGE); - return; - } - - // Verify all nodes have the same parent - DefaultMutableTreeNode firstParent = (DefaultMutableTreeNode) selectedNodes[0].getParent(); - for (int i = 1; i < selectedNodes.length; i++) { - if (selectedNodes[i].getParent() != firstParent) { - JOptionPane.showMessageDialog(this, - "All conditions must have the same parent to group them", - "Invalid Selection", - JOptionPane.WARNING_MESSAGE); - return; - } - } - // Check for plugin-defined conditions - for (DefaultMutableTreeNode node : selectedNodes) { - if (node.getUserObject() instanceof Condition) { - Condition condition = (Condition) node.getUserObject(); - // Don't allow modifying plugin-defined conditions - if (selectScheduledPlugin != null && - getConditionManger().isPluginDefinedCondition(condition)) { - - JOptionPane.showMessageDialog(this, - "Cannot group plugin-defined conditions. These conditions are protected.", - "Protected Conditions", - JOptionPane.WARNING_MESSAGE); - return; - } - } - } - // Create new logical condition - LogicalCondition newLogical = isAnd ? new AndCondition() : new OrCondition(); - - // Determine parent logical - LogicalCondition parentLogical; - if (firstParent == rootNode) { - parentLogical = getConditionManger().getUserLogicalCondition(); - } else if (firstParent.getUserObject() instanceof LogicalCondition) { - parentLogical = (LogicalCondition) firstParent.getUserObject(); - } else { - JOptionPane.showMessageDialog(this, - "Cannot determine parent logical group", - "Operation Failed", - JOptionPane.WARNING_MESSAGE); - return; - } - - // Collect all selected conditions - List conditionsToGroup = new ArrayList<>(); - for (DefaultMutableTreeNode node : selectedNodes) { - if (node.getUserObject() instanceof Condition) { - conditionsToGroup.add((Condition) node.getUserObject()); - } - } - - // Remove conditions from parent - for (Condition condition : conditionsToGroup) { - parentLogical.getConditions().remove(condition); - } - - // Add conditions to new logical - for (Condition condition : conditionsToGroup) { - newLogical.addCondition(condition); - } - - // Add new logical to parent - parentLogical.addCondition(newLogical); - - // Update UI - updateTreeFromConditions(); - selectNodeForCondition(newLogical); - notifyConditionUpdate(); - } - - /** - * Updates the condition editor panel when a condition is selected in the tree - */ - private void updateConditionPanelForSelectedNode() { - DefaultMutableTreeNode node = (DefaultMutableTreeNode) conditionTree.getLastSelectedPathComponent(); - if (node == null || !(node.getUserObject() instanceof Condition)) { - // Reset edit button text when no valid condition is selected - editButton.setText("Add"); - return; - } - - Condition condition = (Condition) node.getUserObject(); - if (condition instanceof LogicalCondition || condition instanceof NotCondition) { - // Reset edit button text for logical conditions since they can't be edited - editButton.setText("Add"); - return; // Skip logical conditions as they don't have direct UI editors - } - - try { - updatingSelectionFlag[0] = true; - - // Determine condition type and setup appropriate UI - if (condition instanceof VarbitCondition) { - conditionCategoryComboBox.setSelectedItem("Varbit"); - updateConditionTypes("Varbit"); - // Select a valid Varbit UI type present in the model - boolean varbitTypeSet = false; - for (int i = 0; i < conditionTypeComboBox.getItemCount(); i++) { - String item = conditionTypeComboBox.getItemAt(i); - if ("Collection Log - Bosses".equals(item) || "Collection Log - Minigames".equals(item)) { - conditionTypeComboBox.setSelectedIndex(i); - varbitTypeSet = true; - break; - } - } - if (!varbitTypeSet && conditionTypeComboBox.getItemCount() > 0) { - conditionTypeComboBox.setSelectedIndex(0); - } - - updateConfigPanel(); - JPanel localConfigPanel = (JPanel) configPanel.getClientProperty("localConditionPanel"); - if (localConfigPanel != null) { - VarbitConditionPanelUtil.setupVarbitCondition(localConfigPanel, (VarbitCondition) condition); - } - } - else if (condition instanceof LocationCondition) { - conditionCategoryComboBox.setSelectedItem("Location"); - updateConditionTypes("Location"); - - if (condition instanceof PositionCondition) { - conditionTypeComboBox.setSelectedItem("Position"); - } else if (condition instanceof AreaCondition) { - conditionTypeComboBox.setSelectedItem("Area"); - } else if (condition instanceof RegionCondition) { - conditionTypeComboBox.setSelectedItem("Region"); - } - - updateConfigPanel(); - JPanel localConfigPanel = (JPanel) configPanel.getClientProperty("localConditionPanel"); - if (localConfigPanel != null) { - LocationConditionUtil.setupLocationCondition(localConfigPanel, condition); - } - } - else if (condition instanceof SkillLevelCondition || condition instanceof SkillXpCondition) { - conditionCategoryComboBox.setSelectedItem("Skill"); - updateConditionTypes("Skill"); - - if (condition instanceof SkillLevelCondition) { - if (stopConditionPanel) { - conditionTypeComboBox.setSelectedItem("Skill Level"); - } else { - conditionTypeComboBox.setSelectedItem("Skill Level Required"); - } - } else if (condition instanceof SkillXpCondition) { - conditionTypeComboBox.setSelectedItem("Skill XP Goal"); - } - - updateConfigPanel(); - JPanel localConfigPanel = (JPanel) configPanel.getClientProperty("localConditionPanel"); - if (localConfigPanel != null) { - SkillConditionPanelUtil.setupSkillCondition(localConfigPanel, condition); - } - } - else if (condition instanceof TimeCondition) { - conditionCategoryComboBox.setSelectedItem("Time"); - updateConditionTypes("Time"); - - if (condition instanceof IntervalCondition) { - conditionTypeComboBox.setSelectedItem(stopConditionPanel ? "Time Duration" : "Time Interval"); - } else if (condition instanceof TimeWindowCondition) { - TimeWindowCondition windowCondition = (TimeWindowCondition) condition; - conditionTypeComboBox.setSelectedItem("Time Window"); - } else if (condition instanceof NotCondition && - ((NotCondition) condition).getCondition() instanceof TimeWindowCondition) { - // This is a negated time window condition - conditionTypeComboBox.setSelectedItem(stopConditionPanel ? "Not In Time Window" : "Outside Time Window"); - } else if (condition instanceof SingleTriggerTimeCondition) { - conditionTypeComboBox.setSelectedItem("Specific Time"); - } else if (condition instanceof DayOfWeekCondition) { - conditionTypeComboBox.setSelectedItem("Day of Week"); - } - - updateConfigPanel(); - JPanel localConfigPanel = (JPanel) configPanel.getClientProperty("localConditionPanel"); - if (localConfigPanel != null) { - TimeConditionPanelUtil.setupTimeCondition(localConfigPanel, condition); - } - } - else if (condition instanceof InventoryItemCountCondition || - condition instanceof BankItemCountCondition || - condition instanceof LootItemCondition || - condition instanceof ProcessItemCondition || - condition instanceof GatheredResourceCondition - || condition instanceof AndCondition|| condition instanceof OrCondition) { - Condition baseResourceCondition = condition; - if (condition instanceof AndCondition || condition instanceof OrCondition) { - //check if all conditions are resource conditions, and from the same type - baseResourceCondition = (Condition) ((LogicalCondition)condition).getConditions().get(0); - for (Condition c : ((LogicalCondition) condition).getConditions()) { - //check if c is of the same typ as first condition - if (!c.getClass().equals(baseResourceCondition.getClass())) { - //not all from the same type ? - return; - } - } - } - conditionCategoryComboBox.setSelectedItem("Resource"); - updateConditionTypes("Resource"); - - if (baseResourceCondition instanceof InventoryItemCountCondition) { - conditionTypeComboBox.setSelectedItem(stopConditionPanel ? "Item Collection" : "Item Required"); - } else if (baseResourceCondition instanceof ProcessItemCondition) { - conditionTypeComboBox.setSelectedItem("Process Items"); - } else if (baseResourceCondition instanceof GatheredResourceCondition) { - conditionTypeComboBox.setSelectedItem("Gather Resources"); - } else if (baseResourceCondition instanceof LootItemCondition) { - conditionTypeComboBox.setSelectedItem("Loot Items"); - } - - updateConfigPanel(); - JPanel localConfigPanel = (JPanel) configPanel.getClientProperty("localConditionPanel"); - if (localConfigPanel != null) { - ResourceConditionPanelUtil.setupResourceCondition(localConfigPanel, condition); - } - } - - // Update edit button state - editButton.setText("Apply Changes"); - } finally { - updatingSelectionFlag[0] = false; - } - } - /** - * Styles a menu item with consistent fonts, borders and hover effects - */ - private void styleMenuItem(JMenuItem item, Color accentColor) { - item.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - item.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8)); - item.setBackground(new Color(35, 35, 35)); - - // Store the accent color as a client property for use in configureMenuItem - item.putClientProperty("accentColor", accentColor); - - // Add hover effect using MouseListener - item.addMouseListener(new MouseAdapter() { - @Override - public void mouseEntered(MouseEvent e) { - if (item.isEnabled()) { - // Highlight with slightly lighter background on hover - item.setBackground(new Color(55, 55, 55)); - - // Make text brighter on hover - Color currentColor = item.getForeground(); - item.putClientProperty("originalForeground", currentColor); - - // Create a brighter version of the accent color - int r = Math.min(255, (int)(currentColor.getRed() * 1.2)); - int g = Math.min(255, (int)(currentColor.getGreen() * 1.2)); - int b = Math.min(255, (int)(currentColor.getBlue() * 1.2)); - item.setForeground(new Color(r, g, b)); - - // Add a stronger border on hover - if (accentColor != null) { - item.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(0, 4, 0, 0, accentColor), - BorderFactory.createEmptyBorder(6, 7, 6, 8) - )); - } - } - } - - @Override - public void mouseExited(MouseEvent e) { - if (item.isEnabled()) { - // Restore original background - item.setBackground(new Color(45, 45, 45)); - - // Restore original foreground - Color originalColor = (Color)item.getClientProperty("originalForeground"); - if (originalColor != null) { - item.setForeground(originalColor); - } - - // Restore original border - if (accentColor != null) { - item.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(0, 3, 0, 0, accentColor), - BorderFactory.createEmptyBorder(6, 8, 6, 8) - )); - } - } - } - }); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/callback/ConditionUpdateCallback.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/callback/ConditionUpdateCallback.java deleted file mode 100644 index 186eef3b41b..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/callback/ConditionUpdateCallback.java +++ /dev/null @@ -1,53 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.ui.callback; - -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LogicalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; - -import java.io.File; - -/** - * Callback interface for condition updates in the scheduler system. - *

- * This interface defines methods that are called when conditions are updated or reset - * in the UI. Implementing classes can respond to these events by saving the updated - * conditions to the appropriate location (config or file). - *

- * This approach provides a flexible way for different components of the system to be - * notified about condition changes without needing to know the details of how and - * where conditions are saved. - */ -public interface ConditionUpdateCallback { - - /** - * Called when conditions are updated in the UI. - * This version saves to the default configuration. - * - * @param logicalCondition The updated logical condition structure - * @param plugin The plugin entry whose conditions are being updated - * @param isStopCondition True if these are stop conditions, false for start conditions - */ - void onConditionsUpdated(LogicalCondition logicalCondition, - PluginScheduleEntry plugin, - boolean isStopCondition); - - /** - * Called when conditions are updated in the UI with a specific file destination. - * - * @param logicalCondition The updated logical condition structure - * @param plugin The plugin entry whose conditions are being updated - * @param isStopCondition True if these are stop conditions, false for start conditions - * @param saveFile The file to save the conditions to, or null to use default config - */ - void onConditionsUpdated(LogicalCondition logicalCondition, - PluginScheduleEntry plugin, - boolean isStopCondition, - File saveFile); - - /** - * Called when conditions are reset in the UI. - * - * @param plugin The plugin entry whose conditions are being reset - * @param isStopCondition True if these are stop conditions, false for start conditions - */ - void onConditionsReset(PluginScheduleEntry plugin, boolean isStopCondition); -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/renderer/ConditionTreeCellRenderer.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/renderer/ConditionTreeCellRenderer.java deleted file mode 100644 index 39a0feb5333..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/renderer/ConditionTreeCellRenderer.java +++ /dev/null @@ -1,290 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.ui.renderer; - -import javax.swing.ImageIcon; -import javax.swing.JTree; -import javax.swing.UIManager; -import javax.swing.tree.DefaultMutableTreeNode; -import javax.swing.tree.DefaultTreeCellRenderer; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionManager; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.AreaCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.PositionCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.RegionCondition; -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.condition.logical.NotCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.OrCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.LootItemCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.SkillCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.DayOfWeekCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ui.util.ConditionConfigPanelUtil; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; - -import java.awt.Component; -import java.awt.Color; -import java.awt.Font; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; - -import javax.swing.Icon; - -// Condition tree cell renderer -public class ConditionTreeCellRenderer extends DefaultTreeCellRenderer { - - private static final Color PLUGIN_CONDITION_COLOR = new Color(0, 128, 255); // Blue for plugin conditions - private static final Color USER_CONDITION_COLOR = Color.WHITE; // White for user conditions - private static final Color SATISFIED_COLOR = new Color(0, 180, 0); // Bright green for satisfied conditions - private static final Color NOT_SATISFIED_COLOR = new Color(220, 60, 60); // Bright red for unsatisfied conditions - private static final Color RELEVANT_CONDITION_COLOR = new Color(255, 215, 0); // Gold for relevant conditions - private static final Color INACTIVE_CONDITION_COLOR = new Color(150, 150, 150); // Gray for inactive conditions - - private final ConditionManager conditionManager; - private final boolean isStopConditionRenderer; - private boolean isActive = true; - - public ConditionTreeCellRenderer(ConditionManager conditionManager, boolean isStopConditionRenderer) { - this.conditionManager = conditionManager; - this.isStopConditionRenderer = isStopConditionRenderer; - } - public void setIsActive(boolean isActive) { - this.isActive = isActive; - } - - @Override - public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, - boolean leaf, int row, boolean hasFocus) { - super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); - // Check if node is null first - if (value == null) { - setText("null"); - return this; - } - - // Safely cast to DefaultMutableTreeNode - if (!(value instanceof DefaultMutableTreeNode)) { - return this; - } - DefaultMutableTreeNode node = (DefaultMutableTreeNode) value; - Object userObject = node.getUserObject(); - - // Default styling - setFont(getFont().deriveFont(Font.PLAIN)); - - - - // Determine relevance: - // - For start conditions: relevant when plugin is enabled but not started - // - For stop conditions: relevant when plugin is running - boolean conditionsAreRelevant = isStopConditionRenderer ? isActive : !isActive; - - - if (userObject instanceof LogicalCondition) { - LogicalCondition logicalCondition = (LogicalCondition) userObject; - - // Show condition counts and progress percentage - int total = logicalCondition.getTotalConditionCount(); - int met = logicalCondition.getMetConditionCount(); - double progress = logicalCondition.getProgressPercentage(); - - // Check if this is a plugin-defined logical condition - boolean isPluginDefined = conditionManager != null && - conditionManager.isPluginDefinedCondition(logicalCondition); - - // Determine icon based on logical type - if (logicalCondition instanceof AndCondition) { - setIcon(getConditionTypeIcon(logicalCondition)); - setFont(getFont().deriveFont(isPluginDefined ? Font.BOLD | Font.ITALIC : Font.BOLD)); - } else if (logicalCondition instanceof OrCondition) { - setIcon(getConditionTypeIcon(logicalCondition)); - setFont(getFont().deriveFont(isPluginDefined ? Font.BOLD | Font.ITALIC : Font.BOLD)); - } else { - // Use appropriate icon based on condition type - setIcon(getConditionTypeIcon(logicalCondition)); - } - - // Color based on condition status and relevance - if (!conditionsAreRelevant) { - // If conditions aren't relevant, show in gray - setForeground(INACTIVE_CONDITION_COLOR); - } else if (logicalCondition.isSatisfied()) { - setForeground(SATISFIED_COLOR); // Green for satisfied conditions - } else { - setForeground(NOT_SATISFIED_COLOR); // Red for unsatisfied conditions - } - - // Show progress info with more detailed formatting - String text = logicalCondition.getDescription(); - if (total > 0) { - text += String.format(" [%d/%d met, %.0f%%]", met, total, progress); - } - - if (isPluginDefined) { - text = "📌 " + text; - } - - // Add a visual indicator for relevance - if (conditionsAreRelevant) { - text = "⚡ " + text; - } - - // Handle newlines in text by replacing them with spaces for tree display - text = text.replace('\n', ' ').replace('\r', ' '); - // Collapse multiple spaces into single spaces - text = text.replaceAll("\\s+", " ").trim(); - - setText(text); - - // For tooltips, use the new HTML formatting - setToolTipText(logicalCondition.getHtmlDescription(200)); - } else if (userObject instanceof NotCondition) { - NotCondition notCondition = (NotCondition) userObject; - setIcon(ConditionConfigPanelUtil.getResourceIcon("not-equal.png")); - - // Adjust color based on relevance - if (!conditionsAreRelevant) { - setForeground(INACTIVE_CONDITION_COLOR); - } else { - setForeground(new Color(210, 40, 40)); // Red for NOT - } - - String text = notCondition.getDescription(); - if (conditionsAreRelevant) { - text = "⚡ " + text; - } - - // Handle newlines in text by replacing them with spaces for tree display - text = text.replace('\n', ' ').replace('\r', ' '); - // Collapse multiple spaces into single spaces - text = text.replaceAll("\\s+", " ").trim(); - - setText(text); - } else if (userObject instanceof Condition) { - Condition condition = (Condition) userObject; - - // Show progress for the condition - String text = condition.getDescription(); - double progress = condition.getProgressPercentage(); - - if (progress > 0 && progress < 100) { - text += String.format(" (%.0f%%)", progress); - } - - // Use appropriate icon based on condition type - setIcon(getConditionTypeIcon(condition)); - - // Color based on condition status and relevance - if (!conditionsAreRelevant) { - // If conditions aren't relevant, show in gray - setForeground(INACTIVE_CONDITION_COLOR); - } else if (condition.isSatisfied()) { - setForeground(SATISFIED_COLOR); // Green for satisfied conditions - } else { - setForeground(NOT_SATISFIED_COLOR); // Red for unsatisfied conditions - } - - // Visual indicator for plugin-defined conditions - if (conditionManager != null && conditionManager.isPluginDefinedCondition(condition)) { - setFont(getFont().deriveFont(Font.ITALIC)); - text = "📌 " + text; - } - - // Add a visual indicator for relevance - if (conditionsAreRelevant) { - text = "⚡ " + text; - } - - // Handle newlines in text by replacing them with spaces for tree display - text = text.replace('\n', ' ').replace('\r', ' '); - // Collapse multiple spaces into single spaces - text = text.replaceAll("\\s+", " ").trim(); - - setText(text); - - // Enhanced tooltip with status information - StringBuilder tooltip = new StringBuilder(); - tooltip.append("").append(condition.getDescription()).append("
"); - tooltip.append(condition.isSatisfied() ? - "Satisfied" : - "Not satisfied"); - - if (condition instanceof TimeCondition) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - ZonedDateTime triggerTime = ((TimeCondition)condition).getCurrentTriggerTime().orElse(null); - if (triggerTime != null) { - tooltip.append("
Trigger time: ").append(triggerTime.format(formatter)); - } - } - - tooltip.append(""); - setToolTipText(tooltip.toString()); - } else if (userObject instanceof String) { - // Section headers (Plugin/User Conditions) - setFont(getFont().deriveFont(Font.BOLD)); - setIcon(null); - setForeground(Color.YELLOW); - } - - // If selected, keep our custom foreground color but change background - if (sel) { - // Keep the foreground color we've set above, but use a selection background that works with it - setBackground(new Color(60, 60, 60)); // Dark gray selection background - setBorderSelectionColor(new Color(100, 100, 100)); // Darker border for selection - } - - return this; - } - - /** - * Gets an appropriate icon for the condition type - */ - private Icon getConditionTypeIcon(Condition condition) { - if (condition instanceof SkillCondition) { - SkillCondition skillCondition = (SkillCondition) condition; - Icon skillIcon = skillCondition.getSkillIcon(); - - if (skillIcon != null) { - // Ensure icon is properly sized to 24x24 - if (skillIcon instanceof ImageIcon) { - ImageIcon imageIcon = (ImageIcon) skillIcon; - if (imageIcon.getIconWidth() != 24 || imageIcon.getIconHeight() != 24) { - // Rescale if not already the right size - return ConditionConfigPanelUtil.getResourceIcon("skill_icon.png"); - } - } - return skillIcon; - } - return ConditionConfigPanelUtil.getResourceIcon("skill_icon.png"); - } else if (condition instanceof TimeCondition) { - if (condition instanceof IntervalCondition) { - return ConditionConfigPanelUtil.getResourceIcon("clock.png"); - }else if (condition instanceof TimeWindowCondition) { - return ConditionConfigPanelUtil.getResourceIcon("calendar-icon.png"); - } - return ConditionConfigPanelUtil.getResourceIcon("clock.png"); - } else if (condition instanceof TimeWindowCondition) { - return ConditionConfigPanelUtil.getResourceIcon("calendar_icon.png"); - } else if (condition instanceof DayOfWeekCondition) { - return ConditionConfigPanelUtil.getResourceIcon("day_icon.png"); - } else if (condition instanceof AndCondition) { - return ConditionConfigPanelUtil.getResourceIcon("logic-gate-and.png"); - } else if (condition instanceof OrCondition) { - return ConditionConfigPanelUtil.getResourceIcon("logic-gate-or.png"); - }else if (condition instanceof LootItemCondition) { - return ConditionConfigPanelUtil.getResourceIcon("loot_icon.png"); - }else if (condition instanceof AreaCondition) { - return ConditionConfigPanelUtil.getResourceIcon("area_map.png"); - }else if (condition instanceof RegionCondition) { - return ConditionConfigPanelUtil.getResourceIcon("region.png"); - }else if (condition instanceof PositionCondition) { - return ConditionConfigPanelUtil.getResourceIcon("position.png"); - }else if (condition instanceof LockCondition) { - return ConditionConfigPanelUtil.getResourceIcon("padlock.png"); - } - - return UIManager.getIcon("Tree.leafIcon"); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/util/ConditionConfigPanelUtil.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/util/ConditionConfigPanelUtil.java deleted file mode 100644 index eed7183897f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/ui/util/ConditionConfigPanelUtil.java +++ /dev/null @@ -1,868 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.ui.util; - -import java.awt.Color; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.Graphics2D; -import java.awt.GridBagConstraints; -import java.awt.Image; -import java.awt.Insets; -import java.awt.RenderingHints; -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.net.URL; - -import javax.imageio.ImageIO; -import javax.swing.BorderFactory; -import javax.swing.Box; -import javax.swing.BoxLayout; -import javax.swing.Icon; -import javax.swing.ImageIcon; -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JComponent; -import javax.swing.JDialog; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JSeparator; -import javax.swing.SwingConstants; -import javax.swing.UIManager; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; - -import java.awt.BorderLayout; -import java.awt.FlowLayout; -import javax.swing.JTree; -import javax.swing.tree.DefaultMutableTreeNode; -import javax.swing.tree.DefaultTreeModel; -import javax.swing.tree.TreeSelectionModel; - -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ui.renderer.ConditionTreeCellRenderer; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionManager; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionManager; -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.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -/** - * Utility class for common methods used in the ConditionConfigPanel - */ -@Slf4j -public class ConditionConfigPanelUtil { - - /** - * Creates a styled button with consistent appearance - * - * @param text The button text - * @param color The background color - * @return A styled JButton - */ - public static JButton createButton(String text, Color color) { - JButton button = new JButton(text); - button.setFont(FontManager.getRunescapeSmallFont()); - button.setBackground(color); - button.setForeground(Color.WHITE); - button.setFocusPainted(false); - button.setBorderPainted(false); - button.setFont(button.getFont().deriveFont(Font.BOLD)); - button.setMargin(new Insets(5, 10, 5, 10)); - button.setBorder(new EmptyBorder(5, 10, 5, 10)); - return button; - } - - /** - * Creates a button with neutral appearance for common actions - * - * @param text The button text - * @return A styled JButton with neutral appearance - */ - public static JButton createNeutralButton(String text) { - return createButton(text, new Color(60, 60, 60)); - } - - /** - * Creates a button with positive/success appearance - * - * @param text The button text - * @return A styled JButton with positive appearance - */ - public static JButton createPositiveButton(String text) { - return createButton(text, new Color(0, 160, 0)); - } - - /** - * Creates a button with negative/danger appearance - * - * @param text The button text - * @return A styled JButton with negative appearance - */ - public static JButton createNegativeButton(String text) { - return createButton(text, new Color(180, 60, 60)); - } - - /** - * Creates a standard titled section panel for consistent visual style - * - * @param title The title for the panel - * @return A JPanel with titled border - */ - public static JPanel createTitledPanel(String title) { - JPanel panel = new JPanel(); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - panel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - title, - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - return panel; - } - - /** - * Creates a scrollable panel with consistent styling - * - * @param component The component to make scrollable - * @return A JScrollPane containing the component - */ - public static JScrollPane createScrollPane(Component component) { - JScrollPane scrollPane = new JScrollPane(component); - scrollPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - scrollPane.setBorder(new EmptyBorder(5, 5, 5, 5)); - scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); - scrollPane.getVerticalScrollBar().setUnitIncrement(16); // For smoother scrolling - return scrollPane; - } - - /** - * Styles a JComboBox for consistent appearance - * - * @param comboBox The JComboBox to style - */ - public static void styleComboBox(JComboBox comboBox) { - comboBox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - comboBox.setForeground(Color.WHITE); - comboBox.setFocusable(false); - comboBox.setFont(FontManager.getRunescapeSmallFont()); - } - - /** - * Creates a styled label with consistent appearance - * - * @param text The label text - * @return A styled JLabel - */ - public static JLabel createLabel(String text) { - JLabel label = new JLabel(text); - label.setForeground(Color.WHITE); - label.setFont(FontManager.getRunescapeSmallFont()); - return label; - } - - /** - * Creates a styled header label with larger, bold font - * - * @param text The header text - * @return A styled header JLabel - */ - public static JLabel createHeaderLabel(String text) { - JLabel label = new JLabel(text); - label.setForeground(Color.WHITE); - label.setFont(FontManager.getRunescapeBoldFont()); - return label; - } - - /** - * Adds a labeled separator to a panel at the given grid position - * - * @param panel The panel to add the separator to - * @param gbc The GridBagConstraints to use - * @param text The text for the separator (can be null for plain separator) - */ - public static void addSeparator(JPanel panel, GridBagConstraints gbc, String text) { - int originalGridy = gbc.gridy; - - if (text != null && !text.isEmpty()) { - JLabel label = createHeaderLabel(text); - gbc.anchor = GridBagConstraints.WEST; - gbc.fill = GridBagConstraints.NONE; - panel.add(label, gbc); - gbc.gridy++; - } - - JSeparator separator = new JSeparator(SwingConstants.HORIZONTAL); - separator.setForeground(ColorScheme.MEDIUM_GRAY_COLOR); - separator.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.anchor = GridBagConstraints.CENTER; - gbc.insets = new Insets(text != null ? 2 : 10, 5, 10, 5); - panel.add(separator, gbc); - - // Restore original insets - gbc.insets = new Insets(5, 5, 5, 5); - gbc.gridy++; - } - - /** - * Sets consistent padding for a component - * - * @param component The component to pad - * @param padding The padding size in pixels - */ - public static void setPadding(JComponent component, int padding) { - component.setBorder(BorderFactory.createEmptyBorder(padding, padding, padding, padding)); - } - - /** - * Sets consistent maximum width for a component while keeping its preferred height - * - * @param component The component to constrain - * @param maxWidth The maximum width in pixels - */ - public static void setMaxWidth(JComponent component, int maxWidth) { - Dimension prefSize = component.getPreferredSize(); - component.setMaximumSize(new Dimension(maxWidth, prefSize.height)); - } - - /** - * Loads and scales an icon from resources - * - * @param name The resource name/path - * @param width The desired width - * @param height The desired height - * @return An Icon, or the default leaf icon if loading fails - */ - public static Icon getResourceIcon(String name, int width, int height) { - try { - URL resourceUrl = ConditionConfigPanelUtil.class.getResource( - "/net/runelite/client/plugins/microbot/pluginscheduler/" + name); - - if (resourceUrl == null) { - log.warn("Resource not found: /net/runelite/client/plugins/microbot/pluginscheduler/" + name); - return UIManager.getIcon("Tree.leafIcon"); - } - - BufferedImage originalImage = ImageIO.read(resourceUrl); - - if (originalImage == null) { - log.warn("Could not load resource: " + name); - return UIManager.getIcon("Tree.leafIcon"); - } - - if (originalImage.getWidth() == width && originalImage.getHeight() == height) { - return new ImageIcon(originalImage); - } - - // Scale the image to desired size - BufferedImage scaledImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2d = scaledImage.createGraphics(); - - // Use high quality scaling - g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - g2d.drawImage(originalImage, 0, 0, width, height, null); - g2d.dispose(); - - return new ImageIcon(scaledImage); - } catch (IOException e) { - log.warn("Failed to load icon: " + name, e); - // Fallback to default icon - return UIManager.getIcon("Tree.leafIcon"); - } - } - - /** - * Convenience method for default 24x24 icons - * - * @param name The resource name - * @return An Icon sized 24x24 - */ - public static Icon getResourceIcon(String name) { - return getResourceIcon(name, 24, 24); - } - - /** - * Shows a confirmation dialog with consistent styling - * - * @param parentComponent The parent component for the dialog - * @param message The message to display - * @param title The dialog title - * @return true if user confirms, false otherwise - */ - public static boolean showConfirmDialog(Component parentComponent, String message, String title) { - // Create custom button text - Object[] options = {"Yes", "No"}; - - // Create styled option pane - JOptionPane optionPane = new JOptionPane( - message, - JOptionPane.QUESTION_MESSAGE, - JOptionPane.YES_NO_OPTION, - null, // No custom icon - options, - options[1] // Default to "No" for safety - ); - - // Create and configure dialog - JDialog dialog = optionPane.createDialog(parentComponent, title); - dialog.setVisible(true); - - // Get the result (returns the selected value or null if closed) - Object selectedValue = optionPane.getValue(); - - // Check if user selected "Yes" - return selectedValue != null && selectedValue.equals(options[0]); - } - - /** - * Shows an error dialog with consistent styling - * - * @param parentComponent The parent component for the dialog - * @param message The error message to display - * @param title The dialog title - */ - public static void showErrorDialog(Component parentComponent, String message, String title) { - JOptionPane.showMessageDialog( - parentComponent, - message, - title, - JOptionPane.ERROR_MESSAGE - ); - } - - /** - * Shows a warning dialog with consistent styling - * - * @param parentComponent The parent component for the dialog - * @param message The warning message to display - * @param title The dialog title - */ - public static void showWarningDialog(Component parentComponent, String message, String title) { - JOptionPane.showMessageDialog( - parentComponent, - message, - title, - JOptionPane.WARNING_MESSAGE - ); - } - - /** - * Shows an information dialog with consistent styling - * - * @param parentComponent The parent component for the dialog - * @param message The information message to display - * @param title The dialog title - */ - public static void showInfoDialog(Component parentComponent, String message, String title) { - JOptionPane.showMessageDialog( - parentComponent, - message, - title, - JOptionPane.INFORMATION_MESSAGE - ); - } - - /** - * Creates a logical operations toolbar with buttons for manipulating logical conditions - * - * @param createAndAction The action to perform when creating an AND group - * @param createOrAction The action to perform when creating an OR group - * @param negateAction The action to perform when negating a condition - * @param convertToAndAction The action to perform when converting to AND - * @param convertToOrAction The action to perform when converting to OR - * @param ungroupAction The action to perform when ungrouping a logical condition - * @return A panel containing the logical operations buttons - */ - public static JPanel createLogicalOperationsToolbar( - Runnable createAndAction, - Runnable createOrAction, - Runnable negateAction, - Runnable convertToAndAction, - Runnable convertToOrAction, - Runnable ungroupAction) { - - JPanel logicalOpPanel = new JPanel(); - logicalOpPanel.setLayout(new BoxLayout(logicalOpPanel, BoxLayout.X_AXIS)); - logicalOpPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); - logicalOpPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Group operations section - JButton createAndButton = createButton("Group as AND", ColorScheme.BRAND_ORANGE); - createAndButton.setToolTipText("Group selected conditions with AND logic"); - createAndButton.addActionListener(e -> createAndAction.run()); - - JButton createOrButton = createButton("Group as OR", new Color(25, 130, 196)); - createOrButton.setToolTipText("Group selected conditions with OR logic"); - createOrButton.addActionListener(e -> createOrAction.run()); - - // Negation button - JButton negateButton = createButton("Negate", new Color(220, 50, 50)); - negateButton.setToolTipText("Negate the selected condition (toggle NOT)"); - negateButton.addActionListener(e -> negateAction.run()); - - // Convert operation buttons - JButton convertToAndButton = createButton("Convert to AND", ColorScheme.BRAND_ORANGE); - convertToAndButton.setToolTipText("Convert selected logical group to AND type"); - convertToAndButton.addActionListener(e -> convertToAndAction.run()); - - JButton convertToOrButton = createButton("Convert to OR", new Color(25, 130, 196)); - convertToOrButton.setToolTipText("Convert selected logical group to OR type"); - convertToOrButton.addActionListener(e -> convertToOrAction.run()); - - // Ungroup button - JButton ungroupButton = createButton("Ungroup", ColorScheme.LIGHT_GRAY_COLOR); - ungroupButton.setToolTipText("Remove the logical group but keep its conditions"); - ungroupButton.addActionListener(e -> ungroupAction.run()); - - // Add buttons to panel with separators - logicalOpPanel.add(createAndButton); - logicalOpPanel.add(Box.createHorizontalStrut(5)); - logicalOpPanel.add(createOrButton); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(new JSeparator(SwingConstants.VERTICAL)); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(negateButton); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(new JSeparator(SwingConstants.VERTICAL)); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(convertToAndButton); - logicalOpPanel.add(Box.createHorizontalStrut(5)); - logicalOpPanel.add(convertToOrButton); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(new JSeparator(SwingConstants.VERTICAL)); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(ungroupButton); - - return logicalOpPanel; - } - - /** - * Creates a logical condition operations panel that handles button state management - * - * @param panel The panel to add the toolbar to - * @param negateButtonRef Reference to store the negate button - * @param convertToAndButtonRef Reference to store the convert to AND button - * @param convertToOrButtonRef Reference to store the convert to OR button - * @param ungroupButtonRef Reference to store the ungroup button - * @param createAndAction The action to perform when creating an AND group - * @param createOrAction The action to perform when creating an OR group - * @param negateAction The action to perform when negating a condition - * @param convertToAndAction The action to perform when converting to AND - * @param convertToOrAction The action to perform when converting to OR - * @param ungroupAction The action to perform when ungrouping a logical condition - */ - public static void addLogicalOperationsToolbar( - JPanel panel, - JButton[] negateButtonRef, - JButton[] convertToAndButtonRef, - JButton[] convertToOrButtonRef, - JButton[] ungroupButtonRef, - Runnable createAndAction, - Runnable createOrAction, - Runnable negateAction, - Runnable convertToAndAction, - Runnable convertToOrAction, - Runnable ungroupAction) { - - JPanel logicalOpPanel = new JPanel(); - logicalOpPanel.setLayout(new BoxLayout(logicalOpPanel, BoxLayout.X_AXIS)); - logicalOpPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); - logicalOpPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Group operations section - JButton createAndButton = createButton("Group as AND", ColorScheme.BRAND_ORANGE); - createAndButton.setToolTipText("Group selected conditions with AND logic"); - createAndButton.addActionListener(e -> createAndAction.run()); - - JButton createOrButton = createButton("Group as OR", new Color(25, 130, 196)); - createOrButton.setToolTipText("Group selected conditions with OR logic"); - createOrButton.addActionListener(e -> createOrAction.run()); - - // Negation button - JButton negateButton = createButton("Negate", new Color(220, 50, 50)); - negateButton.setToolTipText("Negate the selected condition (toggle NOT)"); - negateButton.addActionListener(e -> negateAction.run()); - if (negateButtonRef != null && negateButtonRef.length > 0) { - negateButtonRef[0] = negateButton; - } - - // Convert operation buttons - JButton convertToAndButton = createButton("Convert to AND", ColorScheme.BRAND_ORANGE); - convertToAndButton.setToolTipText("Convert selected logical group to AND type"); - convertToAndButton.addActionListener(e -> convertToAndAction.run()); - if (convertToAndButtonRef != null && convertToAndButtonRef.length > 0) { - convertToAndButtonRef[0] = convertToAndButton; - } - - JButton convertToOrButton = createButton("Convert to OR", new Color(25, 130, 196)); - convertToOrButton.setToolTipText("Convert selected logical group to OR type"); - convertToOrButton.addActionListener(e -> convertToOrAction.run()); - if (convertToOrButtonRef != null && convertToOrButtonRef.length > 0) { - convertToOrButtonRef[0] = convertToOrButton; - } - - // Ungroup button - JButton ungroupButton = createButton("Ungroup", ColorScheme.LIGHT_GRAY_COLOR); - ungroupButton.setToolTipText("Remove the logical group but keep its conditions"); - ungroupButton.addActionListener(e -> ungroupAction.run()); - if (ungroupButtonRef != null && ungroupButtonRef.length > 0) { - ungroupButtonRef[0] = ungroupButton; - } - - // Add buttons to panel with separators - logicalOpPanel.add(createAndButton); - logicalOpPanel.add(Box.createHorizontalStrut(5)); - logicalOpPanel.add(createOrButton); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(new JSeparator(SwingConstants.VERTICAL)); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(negateButton); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(new JSeparator(SwingConstants.VERTICAL)); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(convertToAndButton); - logicalOpPanel.add(Box.createHorizontalStrut(5)); - logicalOpPanel.add(convertToOrButton); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(new JSeparator(SwingConstants.VERTICAL)); - logicalOpPanel.add(Box.createHorizontalStrut(10)); - logicalOpPanel.add(ungroupButton); - - // Initialize button states - negateButton.setEnabled(false); - convertToAndButton.setEnabled(false); - convertToOrButton.setEnabled(false); - ungroupButton.setEnabled(false); - - panel.add(logicalOpPanel, "North"); - } - - /** - * Creates a condition manipulation panel with add, edit, and remove buttons - * - * @param addAction The action to perform when adding a condition - * @param editAction The action to perform when editing a condition - * @param removeAction The action to perform when removing a condition - * @return A panel containing the condition manipulation buttons - */ - public static JPanel createConditionManipulationPanel( - Runnable addAction, - Runnable editAction, - Runnable removeAction) { - - JPanel buttonPanel = new JPanel(); - buttonPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - buttonPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); - - JButton addButton = createButton("Add", ColorScheme.PROGRESS_COMPLETE_COLOR); - addButton.addActionListener(e -> addAction.run()); - buttonPanel.add(addButton); - - JButton editButton = createButton("Edit", ColorScheme.BRAND_ORANGE); - editButton.addActionListener(e -> editAction.run()); - buttonPanel.add(editButton); - - JButton removeButton = createButton("Remove", ColorScheme.PROGRESS_ERROR_COLOR); - removeButton.addActionListener(e -> removeAction.run()); - buttonPanel.add(removeButton); - - return buttonPanel; - } - - /** - * Safely negates a condition within its parent logical condition - * - * @param condition The condition to negate - * @param conditionManager The condition manager - * @param parentComponent The parent component for error dialogs - * @return true if negation was successful, false otherwise - */ - public static boolean negateCondition(Condition condition, ConditionManager conditionManager, Component parentComponent) { - // Don't allow negating null conditions - if (condition == null) { - return false; - } - - // Check if this is a plugin-defined condition that shouldn't be modified - if (conditionManager != null && conditionManager.isPluginDefinedCondition(condition)) { - showWarningDialog( - parentComponent, - "Cannot negate plugin-defined conditions. These conditions are protected.", - "Protected Conditions" - ); - return false; - } - - // Find parent logical condition - LogicalCondition parentLogical = conditionManager.findContainingLogical(condition); - if (parentLogical == null) { - showWarningDialog( - parentComponent, - "Could not determine which logical group contains this condition", - "Operation Failed" - ); - return false; - } - - // Toggle NOT status - int index = parentLogical.getConditions().indexOf(condition); - if (index < 0) { - return false; - } - - // If condition is already a NOT, unwrap it - if (condition instanceof NotCondition) { - NotCondition notCondition = (NotCondition) condition; - Condition innerCondition = notCondition.getCondition(); - - // Replace NOT with its inner condition - parentLogical.getConditions().remove(index); - parentLogical.addConditionAt(index, innerCondition); - } - // Otherwise, wrap it in a NOT - else { - // Create NOT condition - NotCondition notCondition = new NotCondition(condition); - - // Replace original with NOT version - parentLogical.getConditions().remove(index); - parentLogical.addConditionAt(index, notCondition); - } - - return true; - } - - /** - * Converts a logical condition from one type to another - * - * @param logical The logical condition to convert - * @param toAnd true to convert to AND, false to convert to OR - * @param conditionManager The condition manager - * @param parentComponent The parent component for error dialogs - * @return true if conversion was successful, false otherwise - */ - public static boolean convertLogicalType( - LogicalCondition logical, - boolean toAnd, - ConditionManager conditionManager, - Component parentComponent) { - - // Skip if already the desired type - if ((toAnd && logical instanceof AndCondition) || - (!toAnd && logical instanceof OrCondition)) { - return false; - } - - // Check if this is a plugin-defined logical group - if (conditionManager != null && conditionManager.isPluginDefinedCondition(logical)) { - showWarningDialog( - parentComponent, - "Cannot modify plugin-defined condition groups. These conditions are protected.", - "Protected Conditions" - ); - return false; - } - - // Create new logical of the desired type - LogicalCondition newLogical = toAnd ? new AndCondition() : new OrCondition(); - - // Transfer all conditions to the new logical - for (Condition condition : logical.getConditions()) { - newLogical.addCondition(condition); - } - - // Find parent logical - LogicalCondition parentLogical = conditionManager.findContainingLogical(logical); - - if (parentLogical == logical) { - // This is the root logical - replace in condition manager - if (toAnd) { - conditionManager.setUserLogicalCondition((AndCondition) newLogical); - } else { - conditionManager.setUserLogicalCondition((OrCondition) newLogical); - } - } else if (parentLogical != null) { - // Replace in parent - int index = parentLogical.getConditions().indexOf(logical); - if (index >= 0) { - parentLogical.getConditions().remove(index); - parentLogical.addConditionAt(index, newLogical); - } - } else { - // Couldn't find parent - showWarningDialog( - parentComponent, - "Couldn't find the parent logical condition for this group.", - "Operation Failed" - ); - return false; - } - - return true; - } - - /** - * Creates a condition tree panel with full functionality - * - * @param rootNode The root node for the tree - * @param treeModel The tree model - * @param conditionTree The tree component - * @param conditionManager The condition manager for rendering - * @return A panel containing the condition tree with scrolling - */ - public static JPanel createConditionTreePanel( - DefaultMutableTreeNode rootNode, - DefaultTreeModel treeModel, - JTree conditionTree, - ConditionManager conditionManager,boolean isStopConditionRenderer) { - - // Create the panel with border - JPanel panel = createTitledPanel("Condition Structure"); - panel.setLayout(new BorderLayout()); - - // Initialize tree if not already done - if (rootNode == null) { - rootNode = new DefaultMutableTreeNode("Conditions"); - } - - if (treeModel == null) { - treeModel = new DefaultTreeModel(rootNode); - } - - if (conditionTree == null) { - conditionTree = new JTree(treeModel); - conditionTree.setRootVisible(false); - conditionTree.setShowsRootHandles(true); - - // Set up tree cell renderer - conditionTree.setCellRenderer(new ConditionTreeCellRenderer(conditionManager,isStopConditionRenderer)); - - // Set up tree selection mode - conditionTree.getSelectionModel().setSelectionMode( - TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); // Enable multi-select - } - - // Create scroll pane for the tree - JScrollPane treeScrollPane = createScrollPane(conditionTree); - treeScrollPane.setPreferredSize(new Dimension(400, 300)); - - // Add to panel - panel.add(treeScrollPane, BorderLayout.CENTER); - - return panel; - } - - /** - * Creates a title panel with plugin name display - * - * @param isRunning Whether the plugin is running - * @param isEnabled Whether the plugin is enabled - * @param pluginName The name of the plugin - * @return A panel with the title display - */ - public static JPanel createTitlePanel(boolean isRunning, boolean isEnabled, String pluginName) { - JPanel titlePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - titlePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - titlePanel.setName("titlePanel"); - - // Create and format the title label - JLabel titleLabel = new JLabel(formatPluginTitle(isRunning, isEnabled, pluginName)); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeBoldFont()); - - titlePanel.add(titleLabel); - - return titlePanel; - } - - /** - * Formats the plugin title with appropriate HTML styling - * - * @param isRunning Whether the plugin is running - * @param isEnabled Whether the plugin is enabled - * @param pluginName The name of the plugin - * @return Formatted HTML title string - */ - public static String formatPluginTitle(boolean isRunning, boolean isEnabled, String pluginName) { - if (pluginName == null || pluginName.isEmpty()) { - return "No plugin selected"; - } - - // Apply color based on plugin state - String colorHex; - if (isEnabled) { - if (isRunning) { - // Running plugin - bright green - colorHex = "#4CAF50"; - } else { - // Enabled but not running - blue - colorHex = "#2196F3"; - } - } else { - // Disabled plugin - orange/amber - colorHex = "#FFC107"; - } - - // Format with HTML for color and bold styling - return "" + - pluginName + ""; - } - - /** - * Creates a top control panel with title and buttons - * - * @param titlePanel The title panel to include - * @param saveAction The action to perform when saving - * @param loadAction The action to perform when loading - * @param resetAction The action to perform when resetting - * @return A panel with title and control buttons - */ - public static JPanel createTopControlPanel( - JPanel titlePanel, - Runnable saveAction, - Runnable loadAction, - Runnable resetAction) { - - // Create button panel with right alignment - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - buttonPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Create control buttons - JButton loadButton = createButton("Load Current Conditions", ColorScheme.PROGRESS_COMPLETE_COLOR); - loadButton.addActionListener(e -> loadAction.run()); - - JButton saveButton = createButton("Save Conditions", ColorScheme.PROGRESS_COMPLETE_COLOR); - saveButton.addActionListener(e -> saveAction.run()); - - JButton resetButton = createButton("Reset Conditions", ColorScheme.PROGRESS_ERROR_COLOR); - resetButton.addActionListener(e -> resetAction.run()); - - // Add buttons to panel - buttonPanel.add(loadButton); - buttonPanel.add(saveButton); - buttonPanel.add(resetButton); - - // Create main top panel - JPanel topPanel = new JPanel(new BorderLayout()); - topPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - topPanel.add(titlePanel, BorderLayout.WEST); - topPanel.add(buttonPanel, BorderLayout.EAST); - - return topPanel; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/VarbitCondition.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/VarbitCondition.java deleted file mode 100644 index 96601eb41d9..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/VarbitCondition.java +++ /dev/null @@ -1,559 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit; - -import java.time.ZonedDateTime; -import java.util.Optional; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.events.VarbitChanged; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionType; -import net.runelite.client.plugins.microbot.util.math.Rs2Random; - -/** - * A condition that checks the current value of a Varbit or VarPlayer. - * This can be used to track game state like quest progress, minigame scores, - * collection log completions, etc. - */ -@Slf4j -@EqualsAndHashCode(callSuper = false) -public class VarbitCondition implements Condition { - - public static String getVersion() { - return "0.0.1"; - } - /** - * Defines the different types of variables that can be tracked - */ - public enum VarType { - VARBIT, - VARPLAYER - } - - /** - * Comparison operators for the varbit value - */ - public enum ComparisonOperator { - EQUALS("equals"), - NOT_EQUALS("not equals"), - GREATER_THAN("greater than"), - GREATER_THAN_OR_EQUALS("greater than or equals"), - LESS_THAN("less than"), - LESS_THAN_OR_EQUALS("less than or equals"); - - private final String displayName; - - ComparisonOperator(String displayName) { - this.displayName = displayName; - } - - public String getDisplayName() { - return displayName; - } - - @Override - public String toString() { - return displayName; - } - } - - @Getter private final String name; - @Getter private final VarType varType; - @Getter private final int varId; - @Getter private final int targetValue; - @Getter private final ComparisonOperator operator; - - - @Getter private final boolean relative; - @Getter private final boolean randomized; - @Getter private final int targetValueMin; - @Getter private final int targetValueMax; - - @Getter @Setter private transient int currentValue; - @Getter private transient volatile boolean satisfied; - @Getter private transient int startValue; - @Getter private transient int effectiveTargetValue; - - /** - * Creates a new VarbitCondition with absolute target value - * - * @param name A human-readable name for this condition - * @param varType Whether this is tracking a Varbit or VarPlayer variable - * @param varId The ID of the variable to track - * @param targetValue The target value to compare against - * @param operator The comparison operator to use - */ - public VarbitCondition(String name, VarType varType, int varId, int targetValue, ComparisonOperator operator) { - this.name = name; - this.varType = varType; - this.varId = varId; - this.targetValue = targetValue; - this.operator = operator; - this.relative = false; - this.randomized = false; - this.targetValueMin = targetValue; - this.targetValueMax = targetValue; - this.effectiveTargetValue = targetValue; - - // Initialize current value and starting value - updateCurrentValue(); - this.startValue = this.currentValue; - this.satisfied = checkSatisfied(); - } - - /** - * Creates a new VarbitCondition with absolute target value and randomization range - * - * @param name A human-readable name for this condition - * @param varType Whether this is tracking a Varbit or VarPlayer variable - * @param varId The ID of the variable to track - * @param targetValueMin The minimum target value to compare against - * @param targetValueMax The maximum target value to compare against - * @param operator The comparison operator to use - */ - public VarbitCondition(String name, VarType varType, int varId, int targetValueMin, int targetValueMax, ComparisonOperator operator) { - this.name = name; - this.varType = varType; - this.varId = varId; - this.targetValueMin = targetValueMin; - this.targetValueMax = targetValueMax; - this.targetValue = Rs2Random.between(targetValueMin, targetValueMax); - this.operator = operator; - this.relative = false; - this.randomized = true; - this.effectiveTargetValue = this.targetValue; - - // Initialize current value and starting value - updateCurrentValue(); - this.startValue = this.currentValue; - this.satisfied = checkSatisfied(); - } - - /** - * Creates a new VarbitCondition with relative target value - * - * @param name A human-readable name for this condition - * @param varType Whether this is tracking a Varbit or VarPlayer variable - * @param varId The ID of the variable to track - * @param targetValue The target value delta to compare against - * @param operator The comparison operator to use - * @param relative Whether this is a relative target value - */ - public VarbitCondition(String name, VarType varType, int varId, int targetValue, ComparisonOperator operator, boolean relative) { - this.name = name; - this.varType = varType; - this.varId = varId; - this.targetValue = targetValue; - this.operator = operator; - this.relative = relative; - this.randomized = false; - this.targetValueMin = targetValue; - this.targetValueMax = targetValue; - - // Initialize current value and starting value - updateCurrentValue(); - this.startValue = this.currentValue; - - // Calculate effective target value for relative mode - if (relative) { - calculateEffectiveTargetValue(); - } else { - this.effectiveTargetValue = targetValue; - } - - this.satisfied = checkSatisfied(); - } - - /** - * Creates a new VarbitCondition with relative target value and randomization range - * - * @param name A human-readable name for this condition - * @param varType Whether this is tracking a Varbit or VarPlayer variable - * @param varId The ID of the variable to track - * @param targetValueMin The minimum target value delta to compare against - * @param targetValueMax The maximum target value delta to compare against - * @param operator The comparison operator to use - * @param relative Whether this is a relative target value - */ - public VarbitCondition(String name, VarType varType, int varId, int targetValueMin, int targetValueMax, ComparisonOperator operator, boolean relative) { - this.name = name; - this.varType = varType; - this.varId = varId; - this.targetValueMin = targetValueMin; - this.targetValueMax = targetValueMax; - this.targetValue = Rs2Random.between(targetValueMin, targetValueMax); - this.operator = operator; - this.relative = relative; - this.randomized = true; - - // Initialize current value and starting value - updateCurrentValue(); - this.startValue = this.currentValue; - - // Calculate effective target value for relative mode - - calculateEffectiveTargetValue(); - - - - - this.satisfied = checkSatisfied(); - } - - /** - * Calculate the effective target value based on the starting value and target delta - */ - private void calculateEffectiveTargetValue() { - if (relative) { - switch (operator) { - case EQUALS: - case GREATER_THAN: - case GREATER_THAN_OR_EQUALS: - this.effectiveTargetValue = startValue + targetValue; - break; - case LESS_THAN: - case LESS_THAN_OR_EQUALS: - this.effectiveTargetValue = Math.max(0, startValue - targetValue); - break; - case NOT_EQUALS: - this.effectiveTargetValue = startValue; // Not straightforward for NOT_EQUALS, use start value - break; - default: - this.effectiveTargetValue = targetValue; - } - } else { - this.effectiveTargetValue = targetValue; - } - } - - /** - * Create a VarbitCondition with relative target value - */ - public static VarbitCondition createRelative(String name, VarType varType, int varId, int targetValue, ComparisonOperator operator) { - return new VarbitCondition(name, varType, varId, targetValue, operator, true); - } - - /** - * Create a VarbitCondition with randomized relative target value - */ - public static VarbitCondition createRelativeRandomized(String name, VarType varType, int varId, int targetValueMin, int targetValueMax, ComparisonOperator operator) { - return new VarbitCondition(name, varType, varId, targetValueMin, targetValueMax, operator, true); - } - - /** - * Create a VarbitCondition with randomized absolute target value - */ - public static VarbitCondition createRandomized(String name, VarType varType, int varId, int targetValueMin, int targetValueMax, ComparisonOperator operator) { - return new VarbitCondition(name, varType, varId, targetValueMin, targetValueMax, operator); - } - - /** - * Updates the current value from the game - */ - private void updateCurrentValue() { - try { - if (Microbot.isLoggedIn()){ - if (varType == VarType.VARBIT) { - this.currentValue = Microbot.getVarbitValue(varId); - } else { - this.currentValue = Microbot.getVarbitPlayerValue(varId); - } - }else{ - this.currentValue = -1; - } - } catch (Exception e) { - log.error("Error getting current value for " + varType + " " + varId, e); - this.currentValue = -1; - } - } - - /** - * Checks if the condition is satisfied based on the current value and operator - */ - private boolean checkSatisfied() { - if ( this.startValue ==-1) { - updateCurrentValue(); - this.startValue = this.currentValue; - if (relative) { - calculateEffectiveTargetValue(); - } else { - this.effectiveTargetValue = targetValue; - } - if (this.startValue == -1) { - return false; // Not logged in or error getting value - } - } - int compareValue = relative ? effectiveTargetValue : targetValue; - - switch (operator) { - case EQUALS: - return currentValue == compareValue; - case NOT_EQUALS: - return currentValue != compareValue; - case GREATER_THAN: - return currentValue > compareValue; - case GREATER_THAN_OR_EQUALS: - return currentValue >= compareValue; - case LESS_THAN: - return currentValue < compareValue; - case LESS_THAN_OR_EQUALS: - return currentValue <= compareValue; - default: - return false; - } - } - - /** - * Get the value change since the condition was created - */ - public int getValueChange() { - return currentValue - startValue; - } - - /** - * Get the value needed to reach the target - */ - public int getValueNeeded() { - if (!relative) { - return 0; - } - - switch (operator) { - case EQUALS: - return effectiveTargetValue - currentValue; - case GREATER_THAN: - case GREATER_THAN_OR_EQUALS: - return Math.max(0, effectiveTargetValue - currentValue); - case LESS_THAN: - case LESS_THAN_OR_EQUALS: - return Math.max(0, currentValue - effectiveTargetValue); - case NOT_EQUALS: - default: - return 0; - } - } - - /** - * Called when a varbit changes - */ - @Override - public void onVarbitChanged(VarbitChanged event) { - boolean oldSatisfied = this.satisfied; - updateCurrentValue(); - this.satisfied = checkSatisfied(); - - // Log when the condition changes state - if (oldSatisfied != this.satisfied) { - log.debug("VarbitCondition '{}' changed state: {} -> {}", - name, oldSatisfied, this.satisfied); - } - } - - @Override - public boolean isSatisfied() { - updateCurrentValue(); - this.satisfied = checkSatisfied(); - return this.satisfied; - } - - @Override - public String getDescription() { - String varTypeDisplay = varType.toString().toLowerCase(); - updateCurrentValue(); - - StringBuilder description = new StringBuilder(name); - description.append(" (").append(varTypeDisplay).append(" ID: ").append(varId); - description.append(", Name: ").append(this.name); - description.append(", Operate: ").append(this.operator.getDisplayName()); - // Show randomization range if applicable - if (randomized) { - description.append(", random "); - } - - // Show relative or absolute mode - if (relative) { - if (operator == ComparisonOperator.EQUALS) { - description.append(", change by ").append(targetValue); - } else { - description.append(", ").append(operator.getDisplayName()) - .append(" change of ").append(targetValue); - } - if (this.startValue ==-1) { - description.append(", starting value unknown"); - } else { - description.append(", starting ").append(startValue); - } - // Add current progress for relative mode - description.append(", changed ").append(getValueChange()); - - int valueNeeded = getValueNeeded(); - if (valueNeeded > 0) { - description.append(", need ").append(valueNeeded).append(" more"); - } - } else { - description.append(", ").append(operator.getDisplayName()) - .append(" ").append(targetValue); - } - if (currentValue != -1) { - description.append(", current ").append(currentValue); - }else{ - description.append(", current value unknown"); - } - description.append(")"); - return description.toString(); - } - - @Override - public String getDetailedDescription() { - updateCurrentValue(); - StringBuilder desc = new StringBuilder(); - - desc.append("VarbitCondition: ").append(name).append("\n") - .append("Type: ").append(varType.toString()).append("\n") - .append("ID: ").append(varId).append("\n") - .append("Mode: ").append(relative ? "Relative" : "Absolute").append("\n"); - - if (randomized) { - desc.append("Target range: ").append(targetValueMin).append("-").append(targetValueMax).append("\n"); - } - - if (relative) { - desc.append("Target change: ").append(targetValue).append("\n") - .append("Starting value: ").append(startValue).append("\n") - .append("Current value: ").append(currentValue).append("\n") - .append("Value change: ").append(getValueChange()).append("\n") - .append("Effective target: ").append(effectiveTargetValue).append("\n"); - } else { - desc.append("Target value: ").append(targetValue).append("\n") - .append("Current value: ").append(currentValue).append("\n"); - } - - desc.append("Operator: ").append(operator.getDisplayName()).append("\n") - .append("Satisfied: ").append(isSatisfied() ? "Yes" : "No").append("\n") - .append("Progress: ").append(String.format("%.1f%%", getProgressPercentage())); - - return desc.toString(); - } - - @Override - public ConditionType getType() { - return ConditionType.VARBIT; - } - - @Override - public void reset(boolean randomize) { - updateCurrentValue(); - - // Reset starting value - this.startValue = this.currentValue; - - // Randomize target if needed - if (randomize && randomized) { - int newTarget = Rs2Random.between(targetValueMin, targetValueMax); - - // Use reflection to update the final field (not ideal but necessary for this design) - try { - java.lang.reflect.Field targetField = VarbitCondition.class.getDeclaredField("targetValue"); - targetField.setAccessible(true); - - // Remove final modifier - java.lang.reflect.Field modifiersField = java.lang.reflect.Field.class.getDeclaredField("modifiers"); - modifiersField.setAccessible(true); - modifiersField.setInt(targetField, targetField.getModifiers() & ~java.lang.reflect.Modifier.FINAL); - - // Set new value - targetField.set(this, newTarget); - } catch (Exception e) { - log.error("Error updating target value", e); - } - } - - // Recalculate effective target for relative mode - if (relative) { - calculateEffectiveTargetValue(); - } - - this.satisfied = checkSatisfied(); - } - - @Override - public Optional getCurrentTriggerTime() { - return Condition.super.getCurrentTriggerTime(); - } - - @Override - public double getProgressPercentage() { - updateCurrentValue(); - - // For binary conditions (equals/not equals), return either 0 or 100 - if (operator == ComparisonOperator.EQUALS || operator == ComparisonOperator.NOT_EQUALS) { - return isSatisfied() ? 100.0 : 0.0; - } - - // For relative mode with increase operators - if (relative && (operator == ComparisonOperator.GREATER_THAN || - operator == ComparisonOperator.GREATER_THAN_OR_EQUALS)) { - int change = getValueChange(); - if (change >= targetValue) { - return 100.0; - } - return targetValue > 0 ? Math.min(100.0, (change * 100.0) / targetValue) : 0.0; - } - - // For relative mode with decrease operators - if (relative && (operator == ComparisonOperator.LESS_THAN || - operator == ComparisonOperator.LESS_THAN_OR_EQUALS)) { - int change = startValue - currentValue; - if (change >= targetValue) { - return 100.0; - } - return targetValue > 0 ? Math.min(100.0, (change * 100.0) / targetValue) : 0.0; - } - - // For absolute mode comparisons - int compareValue = relative ? effectiveTargetValue : targetValue; - double progress = 0.0; - - switch (operator) { - case GREATER_THAN: - case GREATER_THAN_OR_EQUALS: - if (currentValue >= compareValue) { - progress = 100.0; - } else if (compareValue > 0) { - progress = Math.min(100.0, (currentValue * 100.0) / compareValue); - } - break; - - case LESS_THAN: - case LESS_THAN_OR_EQUALS: - // For "less than" we show progress if we're below the target - if (currentValue <= compareValue) { - progress = 100.0; - } else if (currentValue > 0) { - // Inverse progress - as we get closer to the target - progress = Math.min(100.0, (compareValue * 100.0) / currentValue); - } - break; - - default: - progress = isSatisfied() ? 100.0 : 0.0; - } - - return progress; - } - - @Override - public void pause() { - // Default implementation for VarbitCondition - no specific pause behavior needed - // Varbit conditions are event-based and don't track timing or accumulative state - } - - @Override - public void resume() { - // Default implementation for VarbitCondition - no specific resume behavior needed - // Varbit conditions are event-based and don't track timing or accumulative state - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/VarbitUtil.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/VarbitUtil.java deleted file mode 100644 index 1898569bb82..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/VarbitUtil.java +++ /dev/null @@ -1,249 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.gameval.VarbitID; -import net.runelite.api.gameval.VarPlayerID; - -import java.lang.reflect.Field; -import java.util.*; -import java.util.stream.Collectors; - -/** - * Utility class for game variables (Varbits and VarPlayers) - */ -@Slf4j -public class VarbitUtil { - // Cache of constant names and values - private static Map varbitConstantMap = null; - private static Map varPlayerConstantMap = null; - - // Categories for organizing varbits - private static final Map> varbitCategories = new HashMap<>(); - private static final String[] CATEGORY_NAMES = { - "Quests", - "Skills", - "Minigames", - "Bosses", - "Diaries", - "Combat Achievements", - "Features", - "Items", - "Other" - }; - - /** - * Initializes the constant maps for Varbits and VarPlayer if not already initialized - */ - public static synchronized void initConstantMaps() { - if (varbitConstantMap == null) { - varbitConstantMap = new HashMap<>(); - for (Field field : VarbitID.class.getDeclaredFields()) { - if (field.getType() == int.class && java.lang.reflect.Modifier.isStatic(field.getModifiers())) { - try { - int value = field.getInt(null); - varbitConstantMap.put(value, field.getName()); - } catch (IllegalAccessException e) { - log.error("Error accessing field", e); - } - } - } - } - - if (varPlayerConstantMap == null) { - varPlayerConstantMap = new HashMap<>(); - - // Process VarPlayerID class - for (Field field : VarPlayerID.class.getDeclaredFields()) { - if (field.getType() == int.class && java.lang.reflect.Modifier.isStatic(field.getModifiers())) { - try { - int value = field.getInt(null); - varPlayerConstantMap.put(value, field.getName()); - } catch (IllegalAccessException e) { - log.error("Error accessing field", e); - } - } - } - } - } - - /** - * Gets the constant name for a given varbit/varplayer ID - */ - public static String getConstantNameForId(boolean isVarbit, int id) { - initConstantMaps(); - Map map = isVarbit ? varbitConstantMap : varPlayerConstantMap; - return map.get(id); - } - - /** - * Helper class to hold a varbit/varplayer entry - */ - public static class VarEntry { - public final int id; - public final String name; - - public VarEntry(int id, String name) { - this.id = id; - this.name = name; - } - } - - /** - * Gets a list of predefined Varbit options from the Varbits enum - * - * @return List of formatted Varbit options - */ - public static List getVarbitOptions() { - List options = new ArrayList<>(); - options.add("Select Varbit"); - - try { - // Initialize constant maps if needed - initConstantMaps(); - - // Add all varbit options - for (Map.Entry entry : varbitConstantMap.entrySet()) { - int id = entry.getKey(); - String name = formatConstantName(entry.getValue()); - - // Add formatted option: "Name (ID)" - options.add(name + " (" + id + ")"); - } - - // Sort options alphabetically after the first "Select Varbit" item - if (options.size() > 1) { - List sortedOptions = new ArrayList<>(options.subList(1, options.size())); - Collections.sort(sortedOptions); - options = new ArrayList<>(); - options.add("Select Varbit"); - options.addAll(sortedOptions); - } - - } catch (Exception e) { - log.error("Error getting Varbit options", e); - } - - return options; - } - - /** - * Formats a constant name for better readability - * - * @param name The raw constant name - * @return Formatted name - */ - public static String formatConstantName(String name) { - name = name.replace('_', ' ').toLowerCase(); - - // Capitalize words for better readability - StringBuilder sb = new StringBuilder(); - boolean capitalizeNext = true; - for (char c : name.toCharArray()) { - if (Character.isWhitespace(c)) { - capitalizeNext = true; - sb.append(c); - } else if (capitalizeNext) { - sb.append(Character.toUpperCase(c)); - capitalizeNext = false; - } else { - sb.append(c); - } - } - - return sb.toString(); - } - - /** - * Organizes varbits into meaningful categories - */ - public static void initializeVarbitCategories() { - // Initialize the varbit constant map if needed - initConstantMaps(); - - // Create category lists - for (String category : CATEGORY_NAMES) { - varbitCategories.put(category, new ArrayList<>()); - } - - // Populate categories based on name matching - for (Map.Entry entry : varbitConstantMap.entrySet()) { - int id = entry.getKey(); - String name = entry.getValue(); - - String formattedName = formatConstantName(name); - String lowerName = name.toLowerCase(); - VarEntry varEntry = new VarEntry(id, formattedName); - - // First handle COLLECTION prefixed varbits as they have clear categorization - if (name.contains("COLLECTION")) { - if (name.contains("_BOSSES_") || name.contains("_RAIDS_")) { - varbitCategories.get("Bosses").add(varEntry); - continue; - } else if (name.contains("_MINIGAMES_")) { - varbitCategories.get("Minigames").add(varEntry); - continue; - } - } - - // Check for specific other collections that might fit in our categories - if (name.contains("SLAYER_") && (name.contains("_TASKS_COMPLETED") || name.contains("_POINTS"))) { - varbitCategories.get("Skills").add(varEntry); - continue; - } - if (name.contains("CA_") && (name.contains("_TOTAL_TASKS"))) { - varbitCategories.get("Combat Achievements").add(varEntry); - continue; - } - - // Achievement name - if (name.contains("_DIARY_") && name.contains("_COMPLETE")) { - varbitCategories.get("Diaries").add(varEntry); - continue; - } - } - } - - /** - * Gets the category names - * - * @return Array of category names - */ - public static String[] getCategoryNames() { - return CATEGORY_NAMES; - } - - /** - * Gets the varbit entries for a specific category - * - * @param category The category name - * @return List of VarEntry objects for the category - */ - public static List getVarbitEntriesByCategory(String category) { - // Initialize categories if needed - if (varbitCategories.isEmpty()) { - initializeVarbitCategories(); - } - - return varbitCategories.getOrDefault(category, new ArrayList<>()); - } - - /** - * Gets all varbit entries - * - * @return Map of all varbit entries - */ - public static Map getAllVarbitEntries() { - initConstantMaps(); - return new HashMap<>(varbitConstantMap); - } - - /** - * Gets all varplayer entries - * - * @return Map of all varplayer entries - */ - public static Map getAllVarPlayerEntries() { - initConstantMaps(); - return new HashMap<>(varPlayerConstantMap); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/serialization/VarbitConditionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/serialization/VarbitConditionAdapter.java deleted file mode 100644 index fe9a543c3e3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/serialization/VarbitConditionAdapter.java +++ /dev/null @@ -1,135 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.serialization; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.VarbitCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.VarbitCondition.VarType; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.VarbitCondition.ComparisonOperator; - -import java.lang.reflect.Type; - -/** - * Serializes and deserializes VarbitCondition objects - */ -@Slf4j -public class VarbitConditionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(VarbitCondition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - // Add type information - json.addProperty("type", VarbitCondition.class.getName()); - - // Create data object - JsonObject data = new JsonObject(); - - // Store basic properties - data.addProperty("name", src.getName()); - data.addProperty("version", src.getVersion()); - data.addProperty("varType", src.getVarType().toString()); - data.addProperty("varId", src.getVarId()); - data.addProperty("operator", src.getOperator().name()); // Use name() instead of toString() - - // Store target value information - data.addProperty("targetValue", src.getTargetValue()); - data.addProperty("relative", src.isRelative()); - data.addProperty("randomized", src.isRandomized()); - - // Store randomization range if using randomization - if (src.isRandomized()) { - data.addProperty("targetValueMin", src.getTargetValueMin()); - data.addProperty("targetValueMax", src.getTargetValueMax()); - } - - // Add data to wrapper - json.add("data", data); - - return json; - } - - @Override - public VarbitCondition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if this is using the type/data wrapper format - if (jsonObject.has("type") && jsonObject.has("data")) { - jsonObject = jsonObject.getAsJsonObject("data"); - } - - if (jsonObject.has("version")) { - // Parse basic properties - String version = jsonObject.get("version").getAsString(); - if (!version.equals(VarbitCondition.getVersion())) { - - throw new JsonParseException("Version mismatch: expected " + VarbitCondition.getVersion() + - ", got " + version); - } - } - - String name = jsonObject.get("name").getAsString(); - VarType varType = VarType.valueOf(jsonObject.get("varType").getAsString()); - int varId = jsonObject.get("varId").getAsInt(); - - // Get operator - handle both name and display name formats for backward compatibility - String operatorStr = jsonObject.get("operator").getAsString(); - ComparisonOperator operator; - - try { - // Try parsing as enum name first (new format) - operator = ComparisonOperator.valueOf(operatorStr); - } catch (IllegalArgumentException e) { - // If that fails, try matching by display name (old format) - operator = getOperatorByDisplayName(operatorStr); - if (operator == null) { - // If all parsing fails, default to EQUALS - log.warn("Unknown operator '{}', defaulting to EQUALS", operatorStr); - operator = ComparisonOperator.EQUALS; - } - } - - boolean relative = jsonObject.has("relative") && jsonObject.get("relative").getAsBoolean(); - - // Check if this is using randomization - boolean randomized = jsonObject.has("randomized") && jsonObject.get("randomized").getAsBoolean(); - - if (randomized) { - int targetValueMin = jsonObject.get("targetValueMin").getAsInt(); - int targetValueMax = jsonObject.get("targetValueMax").getAsInt(); - - // Create with randomization - if (relative) { - return VarbitCondition.createRelativeRandomized(name, varType, varId, - targetValueMin, targetValueMax, operator); - } else { - return VarbitCondition.createRandomized(name, varType, varId, - targetValueMin, targetValueMax, operator); - } - } else { - // Regular non-randomized condition - int targetValue = jsonObject.get("targetValue").getAsInt(); - - if (relative) { - return VarbitCondition.createRelative(name, varType, varId, targetValue, operator); - } else { - return new VarbitCondition(name, varType, varId, targetValue, operator); - } - } - - } - - /** - * Helper method to get an operator by its display name - * Used for backward compatibility with old serialized data - */ - private ComparisonOperator getOperatorByDisplayName(String displayName) { - for (ComparisonOperator op : ComparisonOperator.values()) { - if (op.getDisplayName().equalsIgnoreCase(displayName)) { - return op; - } - } - return null; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/ui/VarbitConditionPanelUtil.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/ui/VarbitConditionPanelUtil.java deleted file mode 100644 index 55cbe16c44f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/condition/varbit/ui/VarbitConditionPanelUtil.java +++ /dev/null @@ -1,940 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.ui; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.VarbitCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.VarbitUtil; - -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import java.awt.*; - -import java.util.*; -import java.util.List; - - -/** - * Utility class for creating VarbitCondition configuration UI panels. - */ -@Slf4j -public class VarbitConditionPanelUtil { - - /** - * Callback interface for variable selection - */ - private interface VarSelectCallback { - void onVarSelected(int id); - } - - /** - * Creates a panel for configuring VarbitCondition - * - * @param panel The panel to add components to - * @param gbc GridBagConstraints for layout - * @param specificCategory Optional specific category to restrict selection to (null for general panel) - */ - public static void createVarbitConditionPanel(JPanel panel, GridBagConstraints gbc, String specificCategory) { - // Main label - String titleText = specificCategory == null - ? "Varbit Condition:" - : "Collection Log - " + specificCategory + " Condition:"; - - JLabel titleLabel = new JLabel(titleText); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - panel.add(titleLabel, gbc); - - // Create var type selector (disabled if category is specified) - gbc.gridy++; - JPanel varTypePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - varTypePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel varTypeLabel = new JLabel("Variable Type:"); - varTypeLabel.setForeground(Color.WHITE); - varTypePanel.add(varTypeLabel); - - String[] varTypes = specificCategory != null ? new String[]{"Varbit"} : new String[]{"Varbit", "VarPlayer"}; - JComboBox varTypeComboBox = new JComboBox<>(varTypes); - varTypeComboBox.setPreferredSize(new Dimension(120, varTypeComboBox.getPreferredSize().height)); - varTypeComboBox.setEnabled(specificCategory == null); // Only enable if no specific category - varTypePanel.add(varTypeComboBox); - - panel.add(varTypePanel, gbc); - - // Add mode selection (relative vs absolute) - gbc.gridy++; - JPanel modePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - modePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel modeLabel = new JLabel("Mode:"); - modeLabel.setForeground(Color.WHITE); - modePanel.add(modeLabel); - - JRadioButton absoluteButton = new JRadioButton("Absolute Value"); - absoluteButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - absoluteButton.setForeground(Color.WHITE); - absoluteButton.setSelected(true); // Default to absolute mode - absoluteButton.setToolTipText("Track a specific target value"); - - JRadioButton relativeButton = new JRadioButton("Relative Change"); - relativeButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - relativeButton.setForeground(Color.WHITE); - relativeButton.setToolTipText("Track changes from the current value"); - - ButtonGroup modeGroup = new ButtonGroup(); - modeGroup.add(absoluteButton); - modeGroup.add(relativeButton); - - modePanel.add(absoluteButton); - modePanel.add(relativeButton); - panel.add(modePanel, gbc); - - // Create ID input based on if we have a specific category - JTextField idField = new JTextField(8); - - // Initialize variables that will hold references to the category comboboxes - JComboBox generalCategoryComboBox = null; - JComboBox entriesComboBox = null; - - if (specificCategory == null) { - // For general panel, show category selector and direct ID input - gbc.gridy++; - JPanel categoryPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - categoryPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel categoryLabel = new JLabel("Category:"); - categoryLabel.setForeground(Color.WHITE); - categoryPanel.add(categoryLabel); - - // Create the category selection combobox - generalCategoryComboBox = new JComboBox<>(VarbitUtil.getCategoryNames()); - generalCategoryComboBox.setPreferredSize(new Dimension(150, generalCategoryComboBox.getPreferredSize().height)); - categoryPanel.add(generalCategoryComboBox); - - panel.add(categoryPanel, gbc); - - // Create ID input panel with dropdown for common Varbits - gbc.gridy++; - JPanel idPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - idPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel idLabel = new JLabel("Variable ID:"); - idLabel.setForeground(Color.WHITE); - idPanel.add(idLabel); - - // Numeric field for direct ID input - idField.setToolTipText("Enter the Varbit ID or VarPlayer ID directly"); - idPanel.add(idField); - - // Add lookup button - JButton lookupButton = new JButton("Lookup"); - lookupButton.setBackground(ColorScheme.BRAND_ORANGE); - lookupButton.setForeground(Color.WHITE); - lookupButton.addActionListener(e -> { - showVarLookupDialog(panel, varTypeComboBox.getSelectedItem().equals("Varbit"), id -> { - idField.setText(String.valueOf(id)); - }); - }); - idPanel.add(lookupButton); - - // Add dropdown for entries from selected category - List varbitOptions = new ArrayList<>(); - varbitOptions.add("Select Varbit"); - entriesComboBox = new JComboBox<>(varbitOptions.toArray(new String[0])); - entriesComboBox.setPreferredSize(new Dimension(200, entriesComboBox.getPreferredSize().height)); - entriesComboBox.setToolTipText("Select a predefined Varbit"); - - // Create a final reference to the entries combobox for use in the lambda - final JComboBox finalEntriesComboBox = entriesComboBox; - - entriesComboBox.addActionListener(e -> { - if (finalEntriesComboBox.getSelectedIndex() > 0) { - String selected = (String) finalEntriesComboBox.getSelectedItem(); - if (selected != null && !selected.isEmpty()) { - int openParen = selected.indexOf('('); - int closeParen = selected.indexOf(')'); - if (openParen >= 0 && closeParen > openParen) { - String idStr = selected.substring(openParen + 1, closeParen).trim(); - idField.setText(idStr); - } - } - } - }); - - // Create a final reference to the category combobox for use in the lambda - final JComboBox finalCategoryComboBox = generalCategoryComboBox; - final JComboBox finalEntriesComboBox2 = entriesComboBox; - - // Update the varbit combobox when category changes - generalCategoryComboBox.addActionListener(e -> { - String category = (String) finalCategoryComboBox.getSelectedItem(); - updateVarbitComboBoxByCategory(finalEntriesComboBox2, category); - }); - - idPanel.add(entriesComboBox); - - // Add a name label that shows the constant name if available - JLabel constantNameLabel = new JLabel(""); - constantNameLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - idPanel.add(constantNameLabel); - - // Update constant name when varId changes - idField.getDocument().addDocumentListener(new DocumentListener() { - private void update() { - try { - int id = Integer.parseInt(idField.getText().trim()); - boolean isVarbit = varTypeComboBox.getSelectedItem().equals("Varbit"); - String name = VarbitUtil.getConstantNameForId(isVarbit, id); - if (name != null && !name.isEmpty()) { - constantNameLabel.setText(name); - } else { - constantNameLabel.setText("(Unknown ID)"); - } - } catch (NumberFormatException ex) { - constantNameLabel.setText(""); - } - } - - @Override - public void insertUpdate(DocumentEvent e) { update(); } - - @Override - public void removeUpdate(DocumentEvent e) { update(); } - - @Override - public void changedUpdate(DocumentEvent e) { update(); } - }); - - panel.add(idPanel, gbc); - - // Store components for later - panel.putClientProperty("varbitConstantNameLabel", constantNameLabel); - } else { - // For category-specific panel (Boss/Minigame), show only dropdown - gbc.gridy++; - JPanel idPanel = new JPanel(new BorderLayout()); - idPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - String labelText = "Select " + (specificCategory.equals("Bosses") ? "Boss:" : "Minigame:"); - JLabel idLabel = new JLabel(labelText); - idLabel.setForeground(Color.WHITE); - idPanel.add(idLabel, BorderLayout.WEST); - - // Create a hidden field to store the selected ID - idField.setVisible(false); - - // Initialize the varbit categories if needed - VarbitUtil.initializeVarbitCategories(); - - // Populate the combobox with entries from the specific category - DefaultComboBoxModel entriesModel = new DefaultComboBoxModel<>(); - entriesModel.addElement("Select a " + (specificCategory.equals("Bosses") ? "Boss" : "Minigame") + "..."); - - List categoryEntries = new ArrayList<>(); - for (Map.Entry entry : VarbitUtil.getAllVarbitEntries().entrySet()) { - if (specificCategory.equals("Bosses")) { - if (entry.getValue().startsWith("COLLECTION_BOSSES_") || - entry.getValue().startsWith("COLLECTION_RAIDS_")) { - String formattedName = VarbitUtil.formatConstantName(entry.getValue()); - categoryEntries.add(new VarbitUtil.VarEntry(entry.getKey(), formattedName)); - } - } else if (specificCategory.equals("Minigames")) { - if (entry.getValue().startsWith("COLLECTION_MINIGAMES_")) { - String formattedName = VarbitUtil.formatConstantName(entry.getValue()); - categoryEntries.add(new VarbitUtil.VarEntry(entry.getKey(), formattedName)); - } - } - } - - // Sort entries by name - categoryEntries.sort(Comparator.comparing(e -> e.name)); - - // Add each entry to the combo box model - for (VarbitUtil.VarEntry entry : categoryEntries) { - entriesModel.addElement(entry.name + " (" + entry.id + ")"); - } - - entriesComboBox = new JComboBox<>(entriesModel); - entriesComboBox.setPreferredSize(new Dimension(350, entriesComboBox.getPreferredSize().height)); - entriesComboBox.setToolTipText("Select a " + specificCategory.toLowerCase() + " from the Collection Log"); - - // Create a final reference to the combobox for use in the lambda - final JComboBox finalEntriesComboBox = entriesComboBox; - - // Update the hidden field when selection changes - entriesComboBox.addActionListener(e -> { - if (finalEntriesComboBox.getSelectedIndex() > 0) { - String selected = (String) finalEntriesComboBox.getSelectedItem(); - if (selected != null && !selected.isEmpty()) { - int openParen = selected.indexOf('('); - int closeParen = selected.indexOf(')'); - if (openParen >= 0 && closeParen > openParen) { - String idStr = selected.substring(openParen + 1, closeParen); - idField.setText(idStr); - } - } - } - }); - - idPanel.add(entriesComboBox, BorderLayout.CENTER); - panel.add(idPanel, gbc); - } - - // Create comparison operator selector - gbc.gridy++; - JPanel operatorPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - operatorPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel operatorLabel = new JLabel("Comparison:"); - operatorLabel.setForeground(Color.WHITE); - operatorPanel.add(operatorLabel); - - // Get operator names from enum - String[] operators = new String[VarbitCondition.ComparisonOperator.values().length]; - for (int i = 0; i < VarbitCondition.ComparisonOperator.values().length; i++) { - operators[i] = VarbitCondition.ComparisonOperator.values()[i].getDisplayName(); - } - - JComboBox operatorComboBox = new JComboBox<>(operators); - operatorComboBox.setPreferredSize(new Dimension(150, operatorComboBox.getPreferredSize().height)); - operatorPanel.add(operatorComboBox); - - panel.add(operatorPanel, gbc); - - // Create target value input with randomization option - gbc.gridy++; - JPanel targetValuePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - targetValuePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel targetValueLabel = new JLabel("Target Value:"); - targetValueLabel.setForeground(Color.WHITE); - targetValuePanel.add(targetValueLabel); - - SpinnerNumberModel targetValueModel = new SpinnerNumberModel(1, 0, Integer.MAX_VALUE, 1); - JSpinner targetValueSpinner = new JSpinner(targetValueModel); - targetValuePanel.add(targetValueSpinner); - - JCheckBox randomizeCheckBox = new JCheckBox("Randomize"); - randomizeCheckBox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - randomizeCheckBox.setForeground(Color.WHITE); - targetValuePanel.add(randomizeCheckBox); - - panel.add(targetValuePanel, gbc); - - // Min/Max panel for randomization - gbc.gridy++; - JPanel minMaxPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - minMaxPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel minValueLabel = new JLabel("Min Value:"); - minValueLabel.setForeground(Color.WHITE); - minMaxPanel.add(minValueLabel); - - SpinnerNumberModel minValueModel = new SpinnerNumberModel(0, 0, Integer.MAX_VALUE, 1); - JSpinner minValueSpinner = new JSpinner(minValueModel); - minMaxPanel.add(minValueSpinner); - - JLabel maxValueLabel = new JLabel("Max Value:"); - maxValueLabel.setForeground(Color.WHITE); - minMaxPanel.add(maxValueLabel); - - SpinnerNumberModel maxValueModel = new SpinnerNumberModel(10, 0, Integer.MAX_VALUE, 1); - JSpinner maxValueSpinner = new JSpinner(maxValueModel); - minMaxPanel.add(maxValueSpinner); - - minMaxPanel.setVisible(false); // Initially hidden - panel.add(minMaxPanel, gbc); - - // Set up randomize checkbox behavior - randomizeCheckBox.addChangeListener(e -> { - minMaxPanel.setVisible(randomizeCheckBox.isSelected()); - targetValueSpinner.setEnabled(!randomizeCheckBox.isSelected()); - - if (randomizeCheckBox.isSelected()) { - int value = (Integer) targetValueSpinner.getValue(); - - // Set reasonable min/max values based on current target - minValueSpinner.setValue(Math.max(0, value - 5)); - maxValueSpinner.setValue(value + 5); - } - - panel.revalidate(); - panel.repaint(); - }); - - // Set up min/max validation - minValueSpinner.addChangeListener(e -> { - int min = (Integer) minValueSpinner.getValue(); - int max = (Integer) maxValueSpinner.getValue(); - - if (min > max) { - maxValueSpinner.setValue(min); - } - }); - - maxValueSpinner.addChangeListener(e -> { - int min = (Integer) minValueSpinner.getValue(); - int max = (Integer) maxValueSpinner.getValue(); - - if (max < min) { - minValueSpinner.setValue(max); - } - }); - - // Current value display to help the user - gbc.gridy++; - JPanel currentValuePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - currentValuePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel currentValueLabel = new JLabel("Current Value:"); - currentValueLabel.setForeground(Color.WHITE); - currentValuePanel.add(currentValueLabel); - - JLabel currentValueDisplay = new JLabel("--"); - currentValueDisplay.setForeground(Color.YELLOW); - currentValuePanel.add(currentValueDisplay); - - JButton checkValueButton = new JButton("Check Current Value"); - checkValueButton.addActionListener(e -> { - try { - String idText = idField.getText().trim(); - if (idText.isEmpty()) { - if (specificCategory != null) { - currentValueDisplay.setText("Please select a " + - (specificCategory.equals("Bosses") ? "boss" : "minigame") + " first"); - } else { - currentValueDisplay.setText("Please enter a valid ID"); - } - return; - } - - int varId = Integer.parseInt(idText); - boolean isVarbit = varTypeComboBox.getSelectedItem().equals("Varbit"); - int value; - - if (isVarbit) { - value = Microbot.getVarbitValue(varId); - } else { - value = Microbot.getVarbitPlayerValue(varId); - } - - currentValueDisplay.setText(String.valueOf(value)); - - // If relative mode is enabled, update the description to show the potential target - if (relativeButton.isSelected()) { - int targetValue = (Integer) targetValueSpinner.getValue(); - String operator = (String) operatorComboBox.getSelectedItem(); - - if (operator.contains("greater")) { - currentValueDisplay.setText(value + " (target would be " + (value + targetValue) + ")"); - } else if (operator.contains("less")) { - currentValueDisplay.setText(value + " (target would be " + (value - targetValue) + ")"); - } else { - currentValueDisplay.setText(value + " (target would be " + (value + targetValue) + ")"); - } - } - } catch (NumberFormatException ex) { - currentValueDisplay.setText("Invalid ID"); - } catch (Exception ex) { - currentValueDisplay.setText("Error: " + ex.getMessage()); - } - }); - currentValuePanel.add(checkValueButton); - - panel.add(currentValuePanel, gbc); - - // Update target value label based on selected mode - relativeButton.addActionListener(e -> { - targetValueLabel.setText("Value Change:"); - }); - - absoluteButton.addActionListener(e -> { - targetValueLabel.setText("Target Value:"); - }); - - // Add a helpful description - gbc.gridy++; - String descriptionText; - if (specificCategory == null) { - descriptionText = "Varbits and VarPlayers are game values that track states like quest progress,
minigame scores, etc. Great for tracking game completion objectives."; - } else if (specificCategory.equals("Bosses")) { - descriptionText = "Collection Log Boss varbits track your boss kills and achievements.
Generally, values of 1 indicate completion or a kill count achievement."; - } else { - descriptionText = "Collection Log Minigame varbits track your progress in minigames.
Generally, values of 1 indicate completion or a kill count achievement."; - } - - JLabel descriptionLabel = new JLabel(descriptionText); - descriptionLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - descriptionLabel.setFont(FontManager.getRunescapeSmallFont()); - panel.add(descriptionLabel, gbc); - - // Store components for later access using unified naming scheme - panel.putClientProperty("varbitTypeComboBox", varTypeComboBox); - panel.putClientProperty("varbitIdField", idField); - panel.putClientProperty("varbitOperatorComboBox", operatorComboBox); - panel.putClientProperty("varbitTargetValueSpinner", targetValueSpinner); - panel.putClientProperty("varbitCategoryEntriesComboBox", entriesComboBox); - - // Store the category selector only if we're in the general panel - if (specificCategory == null && generalCategoryComboBox != null) { - panel.putClientProperty("varbitCategoryComboBox", generalCategoryComboBox); - } else if (specificCategory != null) { - // Store the specific category as a string property - panel.putClientProperty("varbitSpecificCategory", specificCategory); - } - - panel.putClientProperty("varbitRelativeMode", relativeButton); - panel.putClientProperty("varbitAbsoluteMode", absoluteButton); - panel.putClientProperty("varbitCurrentValueDisplay", currentValueDisplay); - panel.putClientProperty("varbitRandomize", randomizeCheckBox); - panel.putClientProperty("varbitMinValueSpinner", minValueSpinner); - panel.putClientProperty("varbitMaxValueSpinner", maxValueSpinner); - panel.putClientProperty("varbitMinMaxPanel", minMaxPanel); - } - - /** - * Creates a panel for configuring general VarbitCondition - * - * @param panel The panel to add components to - * @param gbc GridBagConstraints for layout - */ - public static void createVarbitConditionPanel(JPanel panel, GridBagConstraints gbc) { - createVarbitConditionPanel(panel, gbc, null); - } - - /** - * Creates a panel specifically for minigame-related varbit conditions - * - * @param panel The panel to add components to - * @param gbc GridBagConstraints for layout - */ - public static void createMinigameVarbitPanel(JPanel panel, GridBagConstraints gbc) { - createVarbitConditionPanel(panel, gbc, "Minigames"); - } - - /** - * Creates a panel specifically for boss-related varbit conditions - * - * @param panel The panel to add components to - * @param gbc GridBagConstraints for layout - */ - public static void createBossVarbitPanel(JPanel panel, GridBagConstraints gbc) { - createVarbitConditionPanel(panel, gbc, "Bosses"); - } - - // ... rest of the code remains unchanged ... - - /** - * Updates the varbit combo box to show only items from a specific category - * - * @param comboBox The combo box to update - * @param category The category to filter by - */ - private static void updateVarbitComboBoxByCategory(JComboBox comboBox, String category) { - comboBox.removeAllItems(); - comboBox.addItem("Select Varbit"); - - List entries = VarbitUtil.getVarbitEntriesByCategory(category); - - if (category.equals("Other") && entries.isEmpty()) { - // For "Other" category, include all entries if not specifically categorized - Map varbitConstantMap = VarbitUtil.getAllVarbitEntries(); - for (Map.Entry entry : varbitConstantMap.entrySet()) { - String formattedName = VarbitUtil.formatConstantName(entry.getValue()); - comboBox.addItem(formattedName + " (" + entry.getKey() + ")"); - } - } else { - // Sort entries by name - entries.sort(Comparator.comparing(e -> e.name)); - - // Add all entries from the selected category - for (VarbitUtil.VarEntry entry : entries) { - comboBox.addItem(entry.name + " (" + entry.id + ")"); - } - } - } - - - /** - * Shows a dialog to select from known varbits or varplayers - */ - private static void showVarLookupDialog(Component parent, boolean isVarbit, VarSelectCallback callback) { - // Create a dialog for variable selection - JDialog dialog = new JDialog(SwingUtilities.getWindowAncestor(parent), - (isVarbit ? "Select Varbit" : "Select VarPlayer"), Dialog.ModalityType.APPLICATION_MODAL); - dialog.setLayout(new BorderLayout()); - dialog.setSize(600, 500); - dialog.setLocationRelativeTo(parent); - - JPanel contentPanel = new JPanel(new BorderLayout()); - contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - contentPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Add category filter dropdown - JPanel filterPanel = new JPanel(new BorderLayout()); - filterPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - filterPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0)); - - JPanel categoryPanel = new JPanel(new BorderLayout()); - categoryPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - categoryPanel.add(new JLabel("Category:"), BorderLayout.WEST); - - JComboBox categoryComboBox = new JComboBox<>( - new String[]{"All Categories", "Collection Log", "Bosses", "Minigames", "Quests", "Skills", "Diaries", "Features", "Items", "Other"} - ); - categoryPanel.add(categoryComboBox, BorderLayout.CENTER); - - // Search field - JPanel searchPanel = new JPanel(new BorderLayout()); - searchPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - searchPanel.setBorder(BorderFactory.createEmptyBorder(5, 0, 0, 0)); - searchPanel.add(new JLabel("Search:"), BorderLayout.WEST); - - JTextField searchField = new JTextField(); - searchField.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - searchField.setForeground(Color.WHITE); - searchPanel.add(searchField, BorderLayout.CENTER); - - filterPanel.add(categoryPanel, BorderLayout.NORTH); - filterPanel.add(searchPanel, BorderLayout.SOUTH); - - // List model and list - DefaultListModel varListModel = new DefaultListModel<>(); - - // Get entries from VarbitUtil - Map constantMap = isVarbit ? VarbitUtil.getAllVarbitEntries() : VarbitUtil.getAllVarPlayerEntries(); - for (Map.Entry entry : constantMap.entrySet()) { - varListModel.addElement(new VarbitUtil.VarEntry(entry.getKey(), VarbitUtil.formatConstantName(entry.getValue()))); - } - - JList varList = new JList<>(varListModel); - varList.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - varList.setForeground(Color.WHITE); - varList.setCellRenderer(new DefaultListCellRenderer() { - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, - boolean isSelected, boolean cellHasFocus) { - super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - if (value instanceof VarbitUtil.VarEntry) { - VarbitUtil.VarEntry entry = (VarbitUtil.VarEntry) value; - setText(entry.name + " (" + entry.id + ")"); - } - return this; - } - }); - - JScrollPane scrollPane = new JScrollPane(varList); - scrollPane.getVerticalScrollBar().setUnitIncrement(16); - - // Filter the list when search text changes or category changes - DocumentListener searchListener = new DocumentListener() { - @Override - public void insertUpdate(DocumentEvent e) { filterList(); } - - @Override - public void removeUpdate(DocumentEvent e) { filterList(); } - - @Override - public void changedUpdate(DocumentEvent e) { filterList(); } - - private void filterList() { - String searchText = searchField.getText().toLowerCase(); - String category = (String) categoryComboBox.getSelectedItem(); - DefaultListModel filteredModel = new DefaultListModel<>(); - - Map constantMap = isVarbit ? VarbitUtil.getAllVarbitEntries() : VarbitUtil.getAllVarPlayerEntries(); - List filteredEntries = new ArrayList<>(); - - if ("Collection Log".equals(category)) { - // For collection log category - for (Map.Entry entry : constantMap.entrySet()) { - if (entry.getValue().contains("COLLECTION")) { - String formatted = VarbitUtil.formatConstantName(entry.getValue()); - if (formatted.toLowerCase().contains(searchText)) { - filteredEntries.add(new VarbitUtil.VarEntry(entry.getKey(), formatted)); - } - } - } - } else if (!"All Categories".equals(category)) { - // For other specific categories, use the VarbitUtil's categorization - List categoryEntries = VarbitUtil.getVarbitEntriesByCategory(category); - for (VarbitUtil.VarEntry entry : categoryEntries) { - if (entry.name.toLowerCase().contains(searchText)) { - filteredEntries.add(entry); - } - } - } else { - // For "All Categories", search all entries - for (Map.Entry entry : constantMap.entrySet()) { - String formatted = VarbitUtil.formatConstantName(entry.getValue()); - if (formatted.toLowerCase().contains(searchText) || - entry.getValue().toLowerCase().contains(searchText) || - String.valueOf(entry.getKey()).contains(searchText)) { - filteredEntries.add(new VarbitUtil.VarEntry(entry.getKey(), formatted)); - } - } - } - - // Sort entries by name - filteredEntries.sort(Comparator.comparing(e -> e.name)); - - for (VarbitUtil.VarEntry entry : filteredEntries) { - filteredModel.addElement(entry); - } - - varList.setModel(filteredModel); - } - }; - - searchField.getDocument().addDocumentListener(searchListener); - categoryComboBox.addActionListener(e -> searchListener.changedUpdate(null)); - - // Buttons panel - JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - buttonsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JButton selectButton = new JButton("Select"); - selectButton.setBackground(ColorScheme.BRAND_ORANGE); - selectButton.setForeground(Color.WHITE); - selectButton.addActionListener(e -> { - VarbitUtil.VarEntry selected = varList.getSelectedValue(); - if (selected != null) { - callback.onVarSelected(selected.id); - dialog.dispose(); - } - }); - - JButton cancelButton = new JButton("Cancel"); - cancelButton.setBackground(ColorScheme.LIGHT_GRAY_COLOR); - cancelButton.setForeground(Color.BLACK); - cancelButton.addActionListener(e -> dialog.dispose()); - - buttonsPanel.add(cancelButton); - buttonsPanel.add(selectButton); - - // Double-click to select - varList.addMouseListener(new java.awt.event.MouseAdapter() { - @Override - public void mouseClicked(java.awt.event.MouseEvent e) { - if (e.getClickCount() == 2) { - VarbitUtil.VarEntry selected = varList.getSelectedValue(); - if (selected != null) { - callback.onVarSelected(selected.id); - dialog.dispose(); - } - } - } - }); - - // Add everything to dialog - contentPanel.add(filterPanel, BorderLayout.NORTH); - contentPanel.add(scrollPane, BorderLayout.CENTER); - contentPanel.add(buttonsPanel, BorderLayout.SOUTH); - - dialog.add(contentPanel); - dialog.setVisible(true); - } - - - - /** - * Custom DocumentListener that can be toggled between auto-updating and manual modes - */ - private static class EditableDocumentListener implements DocumentListener { - private boolean userEditing = false; - private final Runnable updateAction; - - public EditableDocumentListener(Runnable updateAction) { - this.updateAction = updateAction; - } - - public void setUserEditing(boolean editing) { - this.userEditing = editing; - } - - @Override - public void insertUpdate(DocumentEvent e) { - if (!userEditing) { - // Use invokeLater to avoid mutating during notification - SwingUtilities.invokeLater(updateAction); - } - } - - @Override - public void removeUpdate(DocumentEvent e) { - if (!userEditing) { - // Use invokeLater to avoid mutating during notification - SwingUtilities.invokeLater(updateAction); - } - } - - @Override - public void changedUpdate(DocumentEvent e) { - if (!userEditing) { - // Use invokeLater to avoid mutating during notification - SwingUtilities.invokeLater(updateAction); - } - } - } - - /** - * Sets up the varbit condition panel with values from an existing condition - * - * @param panel The panel containing the UI components - * @param condition The varbit condition to read values from - */ - public static void setupVarbitCondition(JPanel panel, VarbitCondition condition) { - if (condition == null) return; - - // Get UI components - JComboBox varTypeComboBox = (JComboBox) panel.getClientProperty("varbitTypeComboBox"); - JTextField idField = (JTextField) panel.getClientProperty("varbitIdField"); - JComboBox operatorComboBox = (JComboBox) panel.getClientProperty("varbitOperatorComboBox"); - JSpinner targetValueSpinner = (JSpinner) panel.getClientProperty("varbitTargetValueSpinner"); - - JRadioButton relativeMode = (JRadioButton) panel.getClientProperty("varbitRelativeMode"); - JRadioButton absoluteMode = (JRadioButton) panel.getClientProperty("varbitAbsoluteMode"); - JLabel currentValueDisplay = (JLabel) panel.getClientProperty("varbitCurrentValueDisplay"); - JCheckBox randomizeCheckBox = (JCheckBox) panel.getClientProperty("varbitRandomize"); - JSpinner minValueSpinner = (JSpinner) panel.getClientProperty("varbitMinValueSpinner"); - JSpinner maxValueSpinner = (JSpinner) panel.getClientProperty("varbitMaxValueSpinner"); - JPanel minMaxPanel = (JPanel) panel.getClientProperty("varbitMinMaxPanel"); - JComboBox categoryEntriesComboBox = (JComboBox) panel.getClientProperty("varbitCategoryEntriesComboBox"); - - if (varTypeComboBox == null || idField == null || operatorComboBox == null || targetValueSpinner == null || - relativeMode == null || absoluteMode == null) { - return; - } - - int varId = condition.getVarId(); - idField.setText(String.valueOf(varId)); - - // Set var type - varTypeComboBox.setSelectedItem(condition.getVarType() == VarbitCondition.VarType.VARBIT ? "Varbit" : "VarPlayer"); - - // Set operator - operatorComboBox.setSelectedItem(condition.getOperator().getDisplayName()); - - // Set mode - relativeMode.setSelected(condition.isRelative()); - absoluteMode.setSelected(!condition.isRelative()); - - // Set randomization - randomizeCheckBox.setSelected(condition.isRandomized()); - if (condition.isRandomized()) { - minValueSpinner.setValue(condition.getTargetValueMin()); - maxValueSpinner.setValue(condition.getTargetValueMax()); - minMaxPanel.setVisible(true); - targetValueSpinner.setEnabled(false); - } else { - targetValueSpinner.setValue(condition.getTargetValue()); - minMaxPanel.setVisible(false); - } - - // Set current value - currentValueDisplay.setText(String.valueOf(condition.getCurrentValue())); - - // If we have a category-specific panel, try to select the matching entry - String specificCategory = (String) panel.getClientProperty("varbitSpecificCategory"); - if (specificCategory != null && categoryEntriesComboBox != null) { - // Get the varbit name if available - String varbitName = VarbitUtil.getConstantNameForId( - condition.getVarType() == VarbitCondition.VarType.VARBIT, varId); - - // Try to find and select the matching entry in the dropdown - if (varbitName != null) { - for (int i = 0; i < categoryEntriesComboBox.getItemCount(); i++) { - String item = categoryEntriesComboBox.getItemAt(i); - if (item.contains("(" + varId + ")")) { - categoryEntriesComboBox.setSelectedIndex(i); - break; - } - } - } - } - } - - /** - * Creates a VarbitCondition from the panel configuration - * - * @param panel The panel containing the configuration - * @return A new VarbitCondition - */ - public static VarbitCondition createVarbitCondition(JPanel panel) { - // Get all required UI components using getClientProperty - JComboBox varTypeComboBox = (JComboBox) panel.getClientProperty("varbitTypeComboBox"); - JTextField idField = (JTextField) panel.getClientProperty("varbitIdField"); - JComboBox operatorComboBox = (JComboBox) panel.getClientProperty("varbitOperatorComboBox"); - JSpinner targetValueSpinner = (JSpinner) panel.getClientProperty("varbitTargetValueSpinner"); - JRadioButton relativeMode = (JRadioButton) panel.getClientProperty("varbitRelativeMode"); - JCheckBox randomizeCheckBox = (JCheckBox) panel.getClientProperty("varbitRandomize"); - JSpinner minValueSpinner = (JSpinner) panel.getClientProperty("varbitMinValueSpinner"); - JSpinner maxValueSpinner = (JSpinner) panel.getClientProperty("varbitMaxValueSpinner"); - - // Get the category entries dropdown (exists in both general and specific panels) - JComboBox categoryEntriesComboBox = (JComboBox) panel.getClientProperty("varbitCategoryEntriesComboBox"); - - // Check if we have the core required components - if (varTypeComboBox == null || idField == null || operatorComboBox == null || - targetValueSpinner == null || relativeMode == null || randomizeCheckBox == null) { - throw new IllegalStateException("Missing required UI components for Varbit condition"); - } - - // Check for category-specific panels - String specificCategory = (String) panel.getClientProperty("varbitSpecificCategory"); - if (specificCategory != null && categoryEntriesComboBox != null) { - // For category-specific panels, ensure we have a selection in the dropdown - if (categoryEntriesComboBox.getSelectedIndex() <= 0) { - throw new IllegalArgumentException("Please select a " + - (specificCategory.equals("Bosses") ? "boss" : "minigame")); - } - } - - // Get values from UI components - String name = varTypeComboBox.getSelectedItem().toString(); - VarbitCondition.VarType varType = varTypeComboBox.getSelectedItem().equals("Varbit") - ? VarbitCondition.VarType.VARBIT - : VarbitCondition.VarType.VARPLAYER; - - // Parse var ID - int varId; - try { - varId = Integer.parseInt(idField.getText().trim()); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid variable ID: " + idField.getText()); - } - - // Get comparison operator - get enum value by ordinal - int operatorIndex = operatorComboBox.getSelectedIndex(); - VarbitCondition.ComparisonOperator operator = VarbitCondition.ComparisonOperator.values()[operatorIndex]; - - // Get target value(s) - int targetValue = (Integer) targetValueSpinner.getValue(); - boolean isRelative = relativeMode.isSelected(); - - // Create condition based on configuration - if (randomizeCheckBox.isSelected()) { - int minValue = (Integer) minValueSpinner.getValue(); - int maxValue = (Integer) maxValueSpinner.getValue(); - - if (isRelative) { - return VarbitCondition.createRelativeRandomized(name, varType, varId, minValue, maxValue, operator); - } else { - return VarbitCondition.createRandomized(name, varType, varId, minValue, maxValue, operator); - } - } else { - if (isRelative) { - return VarbitCondition.createRelative(name, varType, varId, targetValue, operator); - } else { - return new VarbitCondition(name, varType, varId, targetValue, operator); - } - } - } - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ScheduleEntryConfigManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ScheduleEntryConfigManager.java deleted file mode 100644 index 626a6557b40..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ScheduleEntryConfigManager.java +++ /dev/null @@ -1,612 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.config; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.config.ConfigDescriptor; -import net.runelite.client.config.ConfigGroup; -import net.runelite.client.config.ConfigItemDescriptor; -import net.runelite.client.config.ConfigSectionDescriptor; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.api.SchedulablePlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.config.ui.ScheduleEntryConfigManagerPanel; -import java.util.Collection; -import java.util.Optional; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import javax.swing.JPanel; - - -/** - * Utility class for managing plugin configuration interactions from PluginScheduleEntry - */ -@Slf4j -public class ScheduleEntryConfigManager { - private ConfigDescriptor initialConfigPluginDescriptor; // Initial configuration from plugin (now final) - private ConfigDescriptor configScheduleEntryDescriptor; - private Supplier configPluginDescriptorProvider; - private JPanel configPanel; - - - public void setConfigPluginDescriptor(ConfigDescriptor configPluginDescriptor) { - if (configScheduleEntryDescriptor == null) { - log.info("Setting configScheduleEntryDescriptor to configPluginDescriptor"); - this.configScheduleEntryDescriptor = configPluginDescriptor; - } - // Cannot modify initialConfigPluginDescriptor as it's final now - } - - /** - * Sets a provider function that returns the current ConfigDescriptor for a plugin - * This is more flexible than directly storing the descriptor as it can get updated values - * - * @param provider A supplier function that returns the current ConfigDescriptor - */ - public void setConfigPluginDescriptorProvider(Supplier provider) { - this.configPluginDescriptorProvider = provider; - } - - /** - * Sets a provider function that returns the current ConfigDescriptor for a SchedulablePlugin - * - * @param plugin The SchedulablePlugin that provides the ConfigDescriptor - */ - public void setConfigPluginDescriptorProvider(SchedulablePlugin plugin) { - if (plugin != null) { - this.configPluginDescriptorProvider = plugin::getConfigDescriptor; - this.initialConfigPluginDescriptor = plugin.getConfigDescriptor(); - // Initialize with the current value - ConfigDescriptor descriptor = plugin.getConfigDescriptor(); - if (descriptor != null) { - // Only update schedule entry descriptor, not the initial descriptor - this.configScheduleEntryDescriptor = descriptor; - } - } - } - - /** - * Gets the current ConfigDescriptor from the provider if available, - * otherwise returns the stored descriptor - * - * @return The current ConfigDescriptor from the provider or stored value - */ - public ConfigDescriptor getCurrentPluginConfigDescriptor() { - if (configPluginDescriptorProvider != null) { - ConfigDescriptor current = configPluginDescriptorProvider.get(); - if (current != null) { - return current; - } - } - return initialConfigPluginDescriptor; - } - - //private final JPanel configPanel; - /** - * Constructs a new configuration manager for a specific plugin - * - * @param configPluginDescriptor The config descriptor from the plugin - */ - public ScheduleEntryConfigManager() { - this.initialConfigPluginDescriptor = null; - } - /** - * Constructs a new configuration manager for a specific plugin - * - * @param configPluginDescriptor The config descriptor from the plugin - */ - public ScheduleEntryConfigManager(ConfigDescriptor configPluginDescriptor) { - this.initialConfigPluginDescriptor = configPluginDescriptor; - //this.configGroup = configPluginDescriptor != null ? configPluginDescriptor.getGroup().value() : null; - // Initialize with the plugin's config descriptor - this.configScheduleEntryDescriptor = configPluginDescriptor; - } - - /** - * Constructs a new configuration manager with a provider function - * - * @param provider A supplier function that returns the current ConfigDescriptor - */ - public ScheduleEntryConfigManager(Supplier provider) { - this.configPluginDescriptorProvider = provider; - ConfigDescriptor initialDescriptor = provider.get(); - this.initialConfigPluginDescriptor = initialDescriptor; - if (initialDescriptor != null) { - this.configScheduleEntryDescriptor = initialDescriptor; - } - } - - /** - * Constructs a new configuration manager for a specific SchedulablePlugin - * - * @param plugin The SchedulablePlugin that provides the ConfigDescriptor - */ - public ScheduleEntryConfigManager(SchedulablePlugin plugin) { - if (plugin != null) { - this.configPluginDescriptorProvider = plugin::getConfigDescriptor; - ConfigDescriptor initialDescriptor = plugin.getConfigDescriptor(); - this.initialConfigPluginDescriptor = initialDescriptor; - if (initialDescriptor != null) { - this.configScheduleEntryDescriptor = initialDescriptor; - } - } else { - this.initialConfigPluginDescriptor = null; - } - } - - /** - * Finds a config item descriptor by its name - * - * @param name The display name of the config item - * @return Optional containing the config item descriptor if found - */ - public Optional findConfigItemByName(String name) { - if (configScheduleEntryDescriptor == null) { - return Optional.empty(); - } - - Collection items = configScheduleEntryDescriptor.getItems(); - return items.stream() - .filter(item -> item.name().equals(name)) - .findFirst(); - } - - /** - * Finds a config item descriptor by its key name - * - * @param keyName The key name of the config item - * @return Optional containing the config item descriptor if found - */ - public Optional findConfigItemByKeyName(String keyName) { - if (configScheduleEntryDescriptor == null) { - return Optional.empty(); - } - - Collection items = configScheduleEntryDescriptor.getItems(); - return items.stream() - .filter(item -> item.key().equals(keyName)) - .findFirst(); - } - - /** - * Gets the configuration value for a specific key - * - * @param keyName The key name of the config item - * @param type The type of the config value - * @return The configuration value, or null if not found - */ - public T getConfiguration(String keyName, Class type) { - if (getConfigGroup() == null) { - return null; - } - return Microbot.getConfigManager().getConfiguration(getConfigGroup(), keyName, type); - } - - /** - * Sets the configuration value for a specific key - * - * @param keyName The key name of the config item - * @param value The value to set - */ - public void setConfiguration(String keyName, T value) { - if (getConfigGroup() == null) { - return; - } - - Microbot.getConfigManager().setConfiguration(getConfigGroup(), keyName, value); - } - - /** - * Gets the boolean value of a config item - * - * @param keyName The key name of the config item - * @return The boolean value, or false if the item doesn't exist or is not a boolean - */ - public boolean getBooleanValue(String keyName) { - Boolean value = getConfiguration(keyName, Boolean.class); - return value != null && value; - } - - /** - * Gets the string value of a config item - * - * @param keyName The key name of the config item - * @return The string value, or null if not found - */ - public String getStringValue(String keyName) { - return getConfiguration(keyName, String.class); - } - - /** - * Gets the integer value of a config item - * - * @param keyName The key name of the config item - * @return The integer value, or null if not found - */ - public Integer getIntegerValue(String keyName) { - return getConfiguration(keyName, Integer.class); - } - - /** - * Sets the schedule mode of the plugin. - * This is a utility function to indicate whether a plugin is currently being managed - * by the scheduler. - * - * @param isActive Whether the plugin is actively being scheduled - */ - public void setScheduleMode(boolean isActive) { - setConfiguration("scheduleMode", isActive); - } - - /** - * Gets the current schedule mode of the plugin - * - * @return Whether the plugin is in schedule mode - */ - public boolean isInScheduleMode() { - return getBooleanValue("scheduleMode"); - } - - /** - * Gets the configuration group name - * - * @return The configuration group name - */ - public String getConfigGroup() { - - - if (initialConfigPluginDescriptor == null) { - return configScheduleEntryDescriptor != null ? configScheduleEntryDescriptor.getGroup().value() : null; - } - return initialConfigPluginDescriptor != null ? initialConfigPluginDescriptor.getGroup().value() : null; - } - - - /** - * Gets the current schedule entry's configuration descriptor - * - * @return The current schedule entry's configuration descriptor - */ - public ConfigDescriptor getConfigScheduleEntryDescriptor() { - return configScheduleEntryDescriptor; - } - - /** - * Sets the current schedule entry's configuration descriptor - * - * @param configScheduleEntryDescriptor The configuration descriptor to set - */ - public void setConfigScheduleEntryDescriptor(ConfigDescriptor configScheduleEntryDescriptor) { - this.configScheduleEntryDescriptor = configScheduleEntryDescriptor; - } - - /** - * Gets all configuration items from the current schedule entry's config descriptor - * - * @return Collection of config item descriptors - */ - public Collection getAllConfigItems() { - if (configScheduleEntryDescriptor == null) { - return java.util.Collections.emptyList(); - } - - return configScheduleEntryDescriptor.getItems(); - } - - /** - * Applies the saved schedule entry configuration to the current plugin - * This should be called when starting a scheduled plugin to use its custom config - */ - public void applyScheduleEntryConfig() { - if (configScheduleEntryDescriptor == null || initialConfigPluginDescriptor == null) { - return; - } - - // Apply configuration values from the schedule entry config - for (ConfigItemDescriptor item : configScheduleEntryDescriptor.getItems()) { - // Find corresponding item in plugin config - initialConfigPluginDescriptor.getItems().stream() - .filter(pluginItem -> pluginItem.key().equals(item.key())) - .findFirst() - .ifPresent(pluginItem -> { - // Copy configuration value from schedule entry to plugin - try { - Object value = Microbot.getConfigManager().getConfiguration( - configScheduleEntryDescriptor.getGroup().value(), - item.key(), - item.getType() - ); - - if (value != null) { - Microbot.getConfigManager().setConfiguration( - initialConfigPluginDescriptor.getGroup().value(), - pluginItem.key(), - value - ); - } - } catch (Exception e) { - log.error("Failed to apply config item " + item.key(), e); - } - }); - } - } - - - - /** - * Applies the original plugin configuration to reset the plugin to default settings - * This should be called when stopping a scheduled plugin to reset its configuration - * Renamed from applyPluginConfig to applyInitialPluginConfig - */ - public void applyInitialPluginConfig() { - // Use the initial config descriptor directly instead of getting from provider - ConfigDescriptor descriptorToApply = initialConfigPluginDescriptor; - - if (descriptorToApply == null) { - log.warn("No initial config descriptor available to apply plugin config"); - return; - } - - // Apply configuration values from the original plugin config descriptor - for (ConfigItemDescriptor item : descriptorToApply.getItems()) { - try { - Object value = Microbot.getConfigManager().getConfiguration( - descriptorToApply.getGroup().value(), - item.key(), - item.getType() - ); - - if (value != null) { - Microbot.getConfigManager().setConfiguration( - descriptorToApply.getGroup().value(), - item.key(), - value - ); - } - } catch (Exception e) { - log.error("Failed to apply original config item " + item.key(), e); - } - } - - log.debug("Applied initial plugin configuration"); - } - - /** - * Gets or creates a configuration panel for the current schedule entry config - * - * @return JPanel containing configuration controls, or null if no configuration is available - */ - public JPanel getConfigPanel() { - if (configScheduleEntryDescriptor == null) { - return null; - } - - // Return cached panel if it exists - if (configPanel != null) { - return configPanel; - } - - try { - // Create a new panel using the ScheduleEntryConfigManagerPanel class - configPanel = new ScheduleEntryConfigManagerPanel(Microbot.getConfigManager(), configScheduleEntryDescriptor); - return configPanel; - } catch (Exception e) { - log.error("Error creating config panel", e); - return null; - } - } - - /** - * Refreshes the configuration panel if it exists. - * Call this when the configScheduleEntryDescriptor has been updated. - */ - public void refreshConfigPanel() { - if (configPanel != null) { - configPanel = new ScheduleEntryConfigManagerPanel(Microbot.getConfigManager(), configScheduleEntryDescriptor); - } - } - - /** - * Returns whether there is configuration available for this plugin - * - * @return true if configuration is available, false otherwise - */ - public boolean hasConfiguration() { - return configScheduleEntryDescriptor != null && - !configScheduleEntryDescriptor.getItems().isEmpty(); - } - - /** - * Logs the current ConfigDescriptor for debugging purposes - */ - public void logConfigDescriptor() { - log.debug("Plugin ConfigDescriptor: \n{}", getPluginConfigDescriptorString()); - log.debug("Schedule Entry ConfigDescriptor: \n{}", getScheduleEntryConfigDescriptorString()); - } - - /** - * Returns a string representation of the plugin ConfigDescriptor - * - * @return A readable string representation of the plugin ConfigDescriptor - */ - public String getPluginConfigDescriptorString() { - return configDescriptorToString(initialConfigPluginDescriptor); - } - - /** - * Returns a string representation of the schedule entry ConfigDescriptor - * - * @return A readable string representation of the schedule entry ConfigDescriptor - */ - public String getScheduleEntryConfigDescriptorString() { - return configDescriptorToString(configScheduleEntryDescriptor); - } - - /** - * Converts a ConfigDescriptor to a readable string representation - * - * @param descriptor The ConfigDescriptor to convert - * @return A string representation of the ConfigDescriptor - */ - private String configDescriptorToString(ConfigDescriptor descriptor) { - if (descriptor == null) { - return "null"; - } - - StringBuilder sb = new StringBuilder(); - sb.append("ConfigDescriptor{"); - - // Add group info - sb.append("group=").append(configGroupToString(descriptor.getGroup())); - - // Add sections - sb.append(", sections=["); - if (descriptor.getSections() != null) { - sb.append(descriptor.getSections().stream() - .map(this::configSectionDescriptorToString) - .collect(Collectors.joining(", "))); - } - sb.append("]"); - - // Add items - sb.append(", items=["); - if (descriptor.getItems() != null) { - sb.append(descriptor.getItems().stream() - .map(this::configItemDescriptorToString) - .collect(Collectors.joining(", "))); - } - sb.append("]"); - - // Add information if present - if (descriptor.getInformation() != null) { - sb.append(", information='").append(descriptor.getInformation().value()).append("'"); - } - - sb.append("}"); - return sb.toString(); - } - - /** - * Converts a ConfigGroup to a readable string representation - * - * @param group The ConfigGroup to convert - * @return A string representation of the ConfigGroup - */ - private String configGroupToString(ConfigGroup group) { - if (group == null) { - return "null"; - } - - return "'" + group.value() + "'"; - } - - /** - * Converts a ConfigSectionDescriptor to a readable string representation - * - * @param section The ConfigSectionDescriptor to convert - * @return A string representation of the ConfigSectionDescriptor - */ - private String configSectionDescriptorToString(ConfigSectionDescriptor section) { - if (section == null) { - return "null"; - } - - StringBuilder sb = new StringBuilder(); - sb.append("Section{"); - sb.append("key='").append(section.key()).append("'"); - sb.append(", name='").append(section.name()).append("'"); - sb.append(", position=").append(section.position()); - - if (section.getSection() != null) { - sb.append(", description='").append(section.getSection().description()).append("'"); - sb.append(", closedByDefault=").append(section.getSection().closedByDefault()); - } - - sb.append("}"); - return sb.toString(); - } - - /** - * Converts a ConfigItemDescriptor to a readable string representation - * - * @param item The ConfigItemDescriptor to convert - * @return A string representation of the ConfigItemDescriptor - */ - private String configItemDescriptorToString(ConfigItemDescriptor item) { - if (item == null) { - return "null"; - } - - StringBuilder sb = new StringBuilder(); - sb.append("Item{"); - sb.append("key='").append(item.key()).append("'"); - - if (item.getItem() != null) { - sb.append(", name='").append(item.getItem().name()).append("'"); - sb.append(", description='").append(item.getItem().description()).append("'"); - sb.append(", position=").append(item.getItem().position()); - - if (!item.getItem().section().isEmpty()) { - sb.append(", section='").append(item.getItem().section()).append("'"); - } - - if (item.getItem().hidden()) { - sb.append(", hidden=true"); - } - - if (item.getItem().secret()) { - sb.append(", secret=true"); - } - - if (!item.getItem().warning().isEmpty()) { - sb.append(", warning='").append(item.getItem().warning()).append("'"); - } - } - - if (item.getType() != null) { - sb.append(", type=").append(item.getType().getTypeName()); - } - - if (item.getRange() != null) { - sb.append(", range=[min=").append(item.getRange().min()); - sb.append(", max=").append(item.getRange().max()); - sb.append("]"); - } - - if (item.getAlpha() != null) { - sb.append(", hasAlpha=true"); - } - - if (item.getUnits() != null) { - sb.append(", units='").append(item.getUnits().value()).append("'"); - } - - sb.append("}"); - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("ScheduleEntryConfigManager{"); - - sb.append("configGroup='").append(getConfigGroup()).append("'"); - - if (configPluginDescriptorProvider != null) { - sb.append(", hasConfigProvider=true"); - } else { - sb.append(", hasConfigProvider=false"); - } - - if (initialConfigPluginDescriptor != null) { - sb.append(", pluginConfig=").append(getPluginConfigDescriptorString()); - } else { - sb.append(", pluginConfig=null"); - } - - if (configScheduleEntryDescriptor != null) { - sb.append(", scheduleEntryConfig=").append(getScheduleEntryConfigDescriptorString()); - } else { - sb.append(", scheduleEntryConfig=null"); - } - - sb.append("}"); - return sb.toString(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ui/HotkeyButton.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ui/HotkeyButton.java deleted file mode 100644 index a8f7240c1f0..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ui/HotkeyButton.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2018 Abex - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package net.runelite.client.plugins.microbot.pluginscheduler.config.ui; - -import java.awt.event.KeyAdapter; -import java.awt.event.KeyEvent; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import javax.swing.JButton; -import lombok.Getter; -import net.runelite.client.config.Keybind; -import net.runelite.client.config.ModifierlessKeybind; -import net.runelite.client.ui.FontManager; - -class HotkeyButton extends JButton -{ - @Getter - private Keybind value; - - public HotkeyButton(Keybind value, boolean modifierless) - { - // Disable focus traversal keys such as tab to allow tab key to be bound - setFocusTraversalKeysEnabled(false); - setFont(FontManager.getDefaultFont().deriveFont(12.f)); - setValue(value); - addMouseListener(new MouseAdapter() - { - @Override - public void mouseReleased(MouseEvent e) - { - // Mouse buttons other than button1 don't give focus - if (e.getButton() == MouseEvent.BUTTON1) - { - // We have to use a mouse adapter instead of an action listener so the press action key (space) can be bound - setValue(Keybind.NOT_SET); - } - } - }); - - addKeyListener(new KeyAdapter() - { - @Override - public void keyPressed(KeyEvent e) - { - if (modifierless) - { - setValue(new ModifierlessKeybind(e)); - } - else - { - setValue(new Keybind(e)); - } - } - }); - } - - public void setValue(Keybind value) - { - if (value == null) - { - value = Keybind.NOT_SET; - } - - this.value = value; - setText(value.toString()); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ui/ScheduleEntryConfigManagerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ui/ScheduleEntryConfigManagerPanel.java deleted file mode 100644 index d38708f47b3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/config/ui/ScheduleEntryConfigManagerPanel.java +++ /dev/null @@ -1,619 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.config.ui; - -import com.google.common.base.MoreObjects; -import com.google.common.base.Strings; -import com.google.common.collect.ComparisonChain; -import com.google.common.collect.Sets; -import com.google.common.primitives.Ints; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.config.*; -import net.runelite.client.plugins.microbot.Microbot; - -import net.runelite.client.plugins.microbot.MicrobotPlugin; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.DynamicGridLayout; -import net.runelite.client.ui.FontManager; - -import net.runelite.client.ui.UnitFormatterFactory; -import net.runelite.client.ui.components.ColorJButton; -import net.runelite.client.ui.components.TitleCaseListCellRenderer; -import net.runelite.client.ui.components.colorpicker.ColorPickerManager; -import net.runelite.client.ui.components.colorpicker.RuneliteColorPicker; -import net.runelite.client.util.ColorUtil; -import net.runelite.client.util.ImageUtil; -import net.runelite.client.util.SwingUtil; -import net.runelite.client.util.Text; -import org.apache.commons.lang3.ArrayUtils; - -import javax.inject.Inject; - -import javax.swing.*; -import javax.swing.border.CompoundBorder; -import javax.swing.border.EmptyBorder; -import javax.swing.border.LineBorder; -import javax.swing.border.MatteBorder; -import javax.swing.event.ChangeListener; -import javax.swing.text.JTextComponent; -import java.awt.*; -import java.awt.event.*; -import java.awt.image.BufferedImage; -import java.lang.reflect.ParameterizedType; -import java.util.List; -import java.util.*; - - -@Slf4j -public class ScheduleEntryConfigManagerPanel extends JPanel { - private static final int BORDER_OFFSET = 5; - private static final int PANEL_WIDTH = 220; - private static final int SPINNER_FIELD_WIDTH = 6; - private final JPanel mainPanel; - @Getter - private final ConfigDescriptor configDescriptor; - private final ConfigManager configManager; - private static final Map sectionExpandStates = new HashMap<>(); - private static final ImageIcon SECTION_EXPAND_ICON; - private static final ImageIcon SECTION_RETRACT_ICON; - static final ImageIcon CONFIG_ICON; - static final ImageIcon BACK_ICON; - private final TitleCaseListCellRenderer listCellRenderer = new TitleCaseListCellRenderer(); - @Inject - private ColorPickerManager colorPickerManager; - //@Inject - //private final Provider notificationPanelProvider; - //private final JPanel configPanel; - static - { - final BufferedImage backIcon = ImageUtil.loadImageResource(MicrobotPlugin.class, "config_back_icon.png"); - BACK_ICON = new ImageIcon(backIcon); - - BufferedImage sectionRetractIcon = ImageUtil.loadImageResource(MicrobotPlugin.class, "/util/arrow_right.png"); - sectionRetractIcon = ImageUtil.luminanceOffset(sectionRetractIcon, -121); - SECTION_EXPAND_ICON = new ImageIcon(sectionRetractIcon); - final BufferedImage sectionExpandIcon = ImageUtil.rotateImage(sectionRetractIcon, Math.PI / 2); - SECTION_RETRACT_ICON = new ImageIcon(sectionExpandIcon); - BufferedImage configIcon = ImageUtil.loadImageResource(MicrobotPlugin.class, "config_edit_icon.png"); - CONFIG_ICON = new ImageIcon(configIcon); - } - /** - * Constructs a new configuration manager for a specific plugin - * - * @param configDescriptor The config descriptor from the plugin - */ - public ScheduleEntryConfigManagerPanel(ConfigManager configManager, ConfigDescriptor configDescriptor) { - this.configManager = configManager; - this.configDescriptor = configDescriptor; - mainPanel = new JPanel(); - mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); - rebuild(); - //mainPanel.setBorder(new EmptyBorder(8, 10, 10, 10)); - //mainPanel.setLayout(new DynamicGridLayout(0, 1, 0, 5)); - //mainPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - } - - private void toggleSection(ConfigSectionDescriptor csd, JButton button, JPanel contents) - { - boolean newState = !contents.isVisible(); - contents.setVisible(newState); - button.setIcon(newState ? SECTION_RETRACT_ICON : SECTION_EXPAND_ICON); - button.setToolTipText(newState ? "Retract" : "Expand"); - sectionExpandStates.put(csd, newState); - SwingUtilities.invokeLater(contents::revalidate); - } - private void rebuild() - { - mainPanel.removeAll(); - - - ConfigDescriptor cd = getConfigDescriptor(); - - final Map sectionWidgets = new HashMap<>(); - final Map topLevelPanels = new TreeMap<>((a, b) -> - ComparisonChain.start() - .compare(a.position(), b.position()) - .compare(a.name(), b.name()) - .result()); - - if (cd.getInformation() != null) { - buildInformationPanel(cd.getInformation()); - } - - for (ConfigSectionDescriptor csd : cd.getSections()) - { - ConfigSection cs = csd.getSection(); - final boolean isOpen = sectionExpandStates.getOrDefault(csd, !cs.closedByDefault()); - - final JPanel section = new JPanel(); - section.setLayout(new BoxLayout(section, BoxLayout.Y_AXIS)); - section.setMinimumSize(new Dimension(PANEL_WIDTH, 0)); - - final JPanel sectionHeader = new JPanel(); - sectionHeader.setLayout(new BorderLayout()); - sectionHeader.setMinimumSize(new Dimension(PANEL_WIDTH, 0)); - // For whatever reason, the header extends out by a single pixel when closed. Adding a single pixel of - // border on the right only affects the width when closed, fixing the issue. - sectionHeader.setBorder(new CompoundBorder( - new MatteBorder(0, 0, 1, 0, ColorScheme.MEDIUM_GRAY_COLOR), - new EmptyBorder(0, 0, 3, 1))); - section.add(sectionHeader, BorderLayout.NORTH); - - final JButton sectionToggle = new JButton(isOpen ? SECTION_RETRACT_ICON : SECTION_EXPAND_ICON); - sectionToggle.setPreferredSize(new Dimension(18, 0)); - sectionToggle.setBorder(new EmptyBorder(0, 0, 0, 5)); - sectionToggle.setToolTipText(isOpen ? "Retract" : "Expand"); - SwingUtil.removeButtonDecorations(sectionToggle); - sectionHeader.add(sectionToggle, BorderLayout.WEST); - - String name = cs.name(); - final JLabel sectionName = new JLabel(name); - sectionName.setForeground(ColorScheme.BRAND_ORANGE); - sectionName.setFont(FontManager.getRunescapeBoldFont()); - sectionName.setToolTipText("" + name + ":
" + cs.description() + ""); - sectionHeader.add(sectionName, BorderLayout.CENTER); - - final JPanel sectionContents = new JPanel(); - sectionContents.setLayout(new DynamicGridLayout(0, 1, 0, 5)); - sectionContents.setMinimumSize(new Dimension(PANEL_WIDTH, 0)); - sectionContents.setBorder(new CompoundBorder( - new MatteBorder(0, 0, 1, 0, ColorScheme.MEDIUM_GRAY_COLOR), - new EmptyBorder(BORDER_OFFSET, 0, BORDER_OFFSET, 0))); - sectionContents.setVisible(isOpen); - section.add(sectionContents, BorderLayout.SOUTH); - - // Add listeners to each part of the header so that it's easier to toggle them - final MouseAdapter adapter = new MouseAdapter() - { - @Override - public void mouseClicked(MouseEvent e) - { - toggleSection(csd, sectionToggle, sectionContents); - } - }; - sectionToggle.addActionListener(actionEvent -> toggleSection(csd, sectionToggle, sectionContents)); - sectionName.addMouseListener(adapter); - sectionHeader.addMouseListener(adapter); - - sectionWidgets.put(csd.getKey(), sectionContents); - - topLevelPanels.put(csd, section); - } - - for (ConfigItemDescriptor cid : cd.getItems()) - { - if (cid.getItem().hidden()) - { - continue; - } - - JPanel item = new JPanel(); - item.setLayout(new BorderLayout()); - item.setMinimumSize(new Dimension(PANEL_WIDTH, 0)); - String name = cid.getItem().name(); - JLabel configEntryName = new JLabel(name); - configEntryName.setForeground(Color.WHITE); - String description = cid.getItem().description(); - if (!"".equals(description)) - { - configEntryName.setToolTipText("" + name + ":
" + description + ""); - } - - item.add(configEntryName, BorderLayout.CENTER); - - if (cid.getType() == boolean.class) - { - item.add(createCheckbox(cd, cid), BorderLayout.EAST); - } - else if (cid.getType() == int.class) - { - item.add(createIntSpinner(cd, cid), BorderLayout.EAST); - } - else if (cid.getType() == double.class) - { - item.add(createDoubleSpinner(cd, cid), BorderLayout.EAST); - } - else if (cid.getType() == String.class) - { - item.add(createTextField(cd, cid), BorderLayout.SOUTH); - } - else if (cid.getType() == Color.class) - { - item.add(createColorPicker(cd, cid), BorderLayout.EAST); - } - else if (cid.getType() == Dimension.class) - { - item.add(createDimension(cd, cid), BorderLayout.EAST); - } - else if (cid.getType() instanceof Class && ((Class) cid.getType()).isEnum()) - { - item.add(createComboBox(cd, cid), BorderLayout.EAST); - } - else if (cid.getType() == Keybind.class || cid.getType() == ModifierlessKeybind.class) - { - item.add(createKeybind(cd, cid), BorderLayout.EAST); - } - else if (cid.getType() == Notification.class) - { - //item.add(createNotification(cd, cid), BorderLayout.EAST); - } - else if (cid.getType() instanceof ParameterizedType) - { - ParameterizedType parameterizedType = (ParameterizedType) cid.getType(); - if (parameterizedType.getRawType() == Set.class) - { - item.add(createList(cd, cid), BorderLayout.EAST); - } - } - - JPanel section = sectionWidgets.get(cid.getItem().section()); - if (section == null) - { - topLevelPanels.put(cid, item); - } - else - { - section.add(item); - } - } - - topLevelPanels.values().forEach(mainPanel::add); - - - revalidate(); - } - - - - private void buildInformationPanel(ConfigInformation ci) { - // Create the main panel (similar to a Bootstrap panel) - JPanel panel = new JPanel(); - panel.setLayout(new BorderLayout()); - panel.setBorder(new CompoundBorder( - new EmptyBorder(10, 10, 10, 10), // Outer padding - new LineBorder(Color.GRAY, 1) // Border around the panel - )); - - // Create the body/content panel - JPanel bodyPanel = new JPanel(); - bodyPanel.setLayout(new BoxLayout(bodyPanel, BoxLayout.Y_AXIS)); // Vertical alignment - bodyPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); // Padding inside the body - bodyPanel.setBackground(new Color(0, 142, 255, 50)); - JLabel bodyLabel1 = new JLabel("" + ci.value() + ""); - bodyLabel1.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); - bodyPanel.add(bodyLabel1); - bodyPanel.add(Box.createRigidArea(new Dimension(0, 5))); // Spacer between components - - panel.add(bodyPanel, BorderLayout.CENTER); - - mainPanel.add(panel); - } - - private JCheckBox createCheckbox(ConfigDescriptor cd, ConfigItemDescriptor cid) - { - ConfigManager configManager = Microbot.getConfigManager(); - if (configManager == null) - { - return new JCheckBox(); - } - JCheckBox checkbox = new JCheckBox(); - checkbox.setSelected(Boolean.parseBoolean(configManager.getConfiguration(cd.getGroup().value(), cid.getItem().keyName()))); - checkbox.addActionListener(ae -> changeConfiguration(checkbox, cd, cid)); - return checkbox; - } - - private JSpinner createIntSpinner(ConfigDescriptor cd, ConfigItemDescriptor cid) - { - ConfigManager configManager = Microbot.getConfigManager(); - if (configManager == null) - { - return new JSpinner(); - } - int value = MoreObjects.firstNonNull(configManager.getConfiguration(cd.getGroup().value(), cid.getItem().keyName(), int.class), 0); - - Range range = cid.getRange(); - int min = 0, max = Integer.MAX_VALUE; - if (range != null) - { - min = range.min(); - max = range.max(); - } - - // Config may previously have been out of range - value = Ints.constrainToRange(value, min, max); - - SpinnerModel model = new SpinnerNumberModel(value, min, max, 1); - JSpinner spinner = new JSpinner(model); - Component editor = spinner.getEditor(); - JFormattedTextField spinnerTextField = ((JSpinner.DefaultEditor) editor).getTextField(); - spinnerTextField.setColumns(SPINNER_FIELD_WIDTH); - spinner.addChangeListener(ce -> changeConfiguration(spinner, cd, cid)); - - Units units = cid.getUnits(); - if (units != null) - { - // The existing DefaultFormatterFactory with a NumberEditorFormatter. Its model is the same SpinnerModel above. - JFormattedTextField.AbstractFormatterFactory delegate = spinnerTextField.getFormatterFactory(); - spinnerTextField.setFormatterFactory(new UnitFormatterFactory(delegate, units.value())); - } - - return spinner; - } - - private JSpinner createDoubleSpinner(ConfigDescriptor cd, ConfigItemDescriptor cid) - { - double value = MoreObjects.firstNonNull(configManager.getConfiguration(cd.getGroup().value(), cid.getItem().keyName(), double.class), 0d); - - SpinnerModel model = new SpinnerNumberModel(value, 0, Double.MAX_VALUE, 0.1); - JSpinner spinner = new JSpinner(model); - Component editor = spinner.getEditor(); - JFormattedTextField spinnerTextField = ((JSpinner.DefaultEditor) editor).getTextField(); - spinnerTextField.setColumns(SPINNER_FIELD_WIDTH); - spinner.addChangeListener(ce -> changeConfiguration(spinner, cd, cid)); - - Units units = cid.getUnits(); - if (units != null) - { - // The existing DefaultFormatterFactory with a NumberEditorFormatter. Its model is the same SpinnerModel above. - JFormattedTextField.AbstractFormatterFactory delegate = spinnerTextField.getFormatterFactory(); - spinnerTextField.setFormatterFactory(new UnitFormatterFactory(delegate, units.value())); - } - - return spinner; - } - - private JTextComponent createTextField(ConfigDescriptor cd, ConfigItemDescriptor cid) - { - JTextComponent textField; - - if (cid.getItem().secret()) - { - textField = new JPasswordField(); - } - else - { - final JTextArea textArea = new JTextArea(); - textArea.setLineWrap(true); - textArea.setWrapStyleWord(true); - textField = textArea; - } - - textField.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); - textField.setText(configManager.getConfiguration(cd.getGroup().value(), cid.getItem().keyName())); - - textField.addFocusListener(new FocusAdapter() - { - @Override - public void focusLost(FocusEvent e) - { - changeConfiguration(textField, cd, cid); - } - }); - - return textField; - } - - private ColorJButton createColorPicker(ConfigDescriptor cd, ConfigItemDescriptor cid) - { - Color existing = configManager.getConfiguration(cd.getGroup().value(), cid.getItem().keyName(), Color.class); - - ColorJButton colorPickerBtn; - - boolean alphaHidden = cid.getAlpha() == null; - - if (existing == null) - { - colorPickerBtn = new ColorJButton("Pick a color", Color.BLACK); - } - else - { - String colorHex = "#" + (alphaHidden ? ColorUtil.colorToHexCode(existing) : ColorUtil.colorToAlphaHexCode(existing)).toUpperCase(); - colorPickerBtn = new ColorJButton(colorHex, existing); - } - - colorPickerBtn.setFocusable(false); - colorPickerBtn.addMouseListener(new MouseAdapter() - { - @Override - public void mouseClicked(MouseEvent e) - { - RuneliteColorPicker colorPicker = colorPickerManager.create( - ScheduleEntryConfigManagerPanel.this, - colorPickerBtn.getColor(), - cid.getItem().name(), - alphaHidden); - colorPicker.setLocationRelativeTo(colorPickerBtn); - colorPicker.setOnColorChange(c -> - { - colorPickerBtn.setColor(c); - colorPickerBtn.setText("#" + (alphaHidden ? ColorUtil.colorToHexCode(c) : ColorUtil.colorToAlphaHexCode(c)).toUpperCase()); - }); - colorPicker.setOnClose(c -> changeConfiguration(colorPicker, cd, cid)); - colorPicker.setVisible(true); - } - }); - - return colorPickerBtn; - } - - private JPanel createDimension(ConfigDescriptor cd, ConfigItemDescriptor cid) - { - - - JPanel dimensionPanel = new JPanel(); - dimensionPanel.setLayout(new BorderLayout()); - - Dimension dimension = MoreObjects.firstNonNull(configManager.getConfiguration(cd.getGroup().value(), cid.getItem().keyName(), Dimension.class), new Dimension()); - int width = dimension.width; - int height = dimension.height; - - SpinnerModel widthModel = new SpinnerNumberModel(width, 0, Integer.MAX_VALUE, 1); - JSpinner widthSpinner = new JSpinner(widthModel); - Component widthEditor = widthSpinner.getEditor(); - JFormattedTextField widthSpinnerTextField = ((JSpinner.DefaultEditor) widthEditor).getTextField(); - widthSpinnerTextField.setColumns(4); - - SpinnerModel heightModel = new SpinnerNumberModel(height, 0, Integer.MAX_VALUE, 1); - JSpinner heightSpinner = new JSpinner(heightModel); - Component heightEditor = heightSpinner.getEditor(); - JFormattedTextField heightSpinnerTextField = ((JSpinner.DefaultEditor) heightEditor).getTextField(); - heightSpinnerTextField.setColumns(4); - - ChangeListener listener = e -> - configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), widthSpinner.getValue() + "x" + heightSpinner.getValue()); - - widthSpinner.addChangeListener(listener); - heightSpinner.addChangeListener(listener); - - dimensionPanel.add(widthSpinner, BorderLayout.WEST); - dimensionPanel.add(new JLabel(" x "), BorderLayout.CENTER); - dimensionPanel.add(heightSpinner, BorderLayout.EAST); - - return dimensionPanel; - } - - private JComboBox> createComboBox(ConfigDescriptor cd, ConfigItemDescriptor cid) - { - Class type = (Class) cid.getType(); - - JComboBox> box = new JComboBox>(type.getEnumConstants()); // NOPMD: UseDiamondOperator - // set renderer prior to calling box.getPreferredSize(), since it will invoke the renderer - // to build components for each combobox element in order to compute the display size of the - // combobox - box.setRenderer(listCellRenderer); - box.setPreferredSize(new Dimension(box.getPreferredSize().width, 22)); - - try - { - Enum selectedItem = Enum.valueOf(type, configManager.getConfiguration(cd.getGroup().value(), cid.getItem().keyName())); - box.setSelectedItem(selectedItem); - box.setToolTipText(Text.titleCase(selectedItem)); - } - catch (IllegalArgumentException ex) - { - log.debug("invalid selected item", ex); - } - box.addItemListener(e -> - { - if (e.getStateChange() == ItemEvent.SELECTED) - { - changeConfiguration(box, cd, cid); - box.setToolTipText(Text.titleCase((Enum) box.getSelectedItem())); - } - }); - - return box; - } - - private HotkeyButton createKeybind(ConfigDescriptor cd, ConfigItemDescriptor cid) - { - Keybind startingValue = configManager.getConfiguration(cd.getGroup().value(), - cid.getItem().keyName(), - (Class) cid.getType()); - - HotkeyButton button = new HotkeyButton(startingValue, cid.getType() == ModifierlessKeybind.class); - - button.addFocusListener(new FocusAdapter() - { - @Override - public void focusLost(FocusEvent e) - { - changeConfiguration(button, cd, cid); - } - }); - - return button; - } - - - - private JList> createList(ConfigDescriptor cd, ConfigItemDescriptor cid) - { - ParameterizedType parameterizedType = (ParameterizedType) cid.getType(); - Class type = (Class) parameterizedType.getActualTypeArguments()[0]; - Set set = configManager.getConfiguration(cd.getGroup().value(), null, - cid.getItem().keyName(), parameterizedType); - - JList> list = new JList>(type.getEnumConstants()); // NOPMD: UseDiamondOperator - list.setCellRenderer(listCellRenderer); - list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); - list.setLayoutOrientation(JList.VERTICAL); - list.setSelectedIndices( - MoreObjects.firstNonNull(set, Collections.emptySet()) - .stream() - .mapToInt(e -> ArrayUtils.indexOf(type.getEnumConstants(), e)) - .toArray()); - list.addFocusListener(new FocusAdapter() - { - @Override - public void focusLost(FocusEvent e) - { - changeConfiguration(list, cd, cid); - } - }); - - return list; - } - - private void changeConfiguration(Component component, ConfigDescriptor cd, ConfigItemDescriptor cid) - { - ConfigManager configManager = Microbot.getConfigManager(); - if (configManager == null) - { - return; - } - - final ConfigItem configItem = cid.getItem(); - - if (!Strings.isNullOrEmpty(configItem.warning())) - { - final int result = JOptionPane.showOptionDialog(component, configItem.warning(), - "Are you sure?", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, - null, new String[]{"Yes", "No"}, "No"); - - if (result != JOptionPane.YES_OPTION) - { - rebuild(); - return; - } - } - - if (component instanceof JCheckBox) - { - JCheckBox checkbox = (JCheckBox) component; - configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), "" + checkbox.isSelected()); - } - else if (component instanceof JSpinner) - { - JSpinner spinner = (JSpinner) component; - configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), "" + spinner.getValue()); - } - else if (component instanceof JTextComponent) - { - JTextComponent textField = (JTextComponent) component; - configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), textField.getText()); - } - else if (component instanceof RuneliteColorPicker) - { - RuneliteColorPicker colorPicker = (RuneliteColorPicker) component; - configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), colorPicker.getSelectedColor().getRGB() + ""); - } - else if (component instanceof JComboBox) - { - JComboBox jComboBox = (JComboBox) component; - configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), ((Enum) jComboBox.getSelectedItem()).name()); - } - else if (component instanceof HotkeyButton) - { - HotkeyButton hotkeyButton = (HotkeyButton) component; - configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), hotkeyButton.getValue()); - } - else if (component instanceof JList) - { - JList list = (JList) component; - List selectedValues = list.getSelectedValuesList(); - - configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), Sets.newHashSet(selectedValues)); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/ExecutionResult.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/ExecutionResult.java deleted file mode 100644 index 6b6d5c20b4a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/ExecutionResult.java +++ /dev/null @@ -1,93 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.event; - -/** - * Enum representing the granular success states for plugin execution events. - * This provides more detailed reporting of execution outcomes beyond simple success/failure. - */ -public enum ExecutionResult { - /** - * Plugin execution completed successfully without any issues. - * The plugin accomplished its intended task and is ready for normal scheduling. - */ - SUCCESS("Success", "Plugin completed successfully", true, false), - - /** - * Plugin encountered a recoverable issue but can potentially run again. - * Examples: temporary resource unavailability, network timeouts, minor game state issues. - * The plugin schedule entry remains enabled but the failure is tracked. - */ - SOFT_FAILURE("Soft Failure", "Plugin failed but can retry", false, true), - - /** - * Plugin encountered a critical failure that prevents future execution. - * Examples: invalid configuration, missing dependencies, critical errors. - * The plugin schedule entry should be disabled after this result. - */ - HARD_FAILURE("Hard Failure", "Plugin failed critically", false, false); - - private final String displayName; - private final String description; - private final boolean isSuccess; - private final boolean canRetry; - - ExecutionResult(String displayName, String description, boolean isSuccess, boolean canRetry) { - this.displayName = displayName; - this.description = description; - this.isSuccess = isSuccess; - this.canRetry = canRetry; - } - - /** - * @return Human-readable display name for this result - */ - public String getDisplayName() { - return displayName; - } - - /** - * @return Detailed description of what this result means - */ - public String getDescription() { - return description; - } - - /** - * @return true if this represents a successful execution - */ - public boolean isSuccess() { - return isSuccess; - } - - /** - * @return true if the plugin can be retried after this result - */ - public boolean canRetry() { - return canRetry; - } - - /** - * @return true if this represents any kind of failure (soft or hard) - */ - public boolean isFailure() { - return !isSuccess; - } - - /** - * @return true if this is a soft failure that allows retries - */ - public boolean isSoftFailure() { - return this == SOFT_FAILURE; - } - - /** - * @return true if this is a hard failure that prevents retries - */ - public boolean isHardFailure() { - return this == HARD_FAILURE; - } - - @Override - public String toString() { - return displayName; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryMainTaskFinishedEvent.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryMainTaskFinishedEvent.java deleted file mode 100644 index 4e642a1c9dd..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryMainTaskFinishedEvent.java +++ /dev/null @@ -1,55 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.event; - -import java.time.ZonedDateTime; - -import lombok.Getter; -import net.runelite.client.plugins.Plugin; - -/** - * Event fired when a plugin has completed its work and is ready to be stopped - * This is different from ScheduledStopEvent as it represents a plugin self-reporting - * that it has finished its task rather than the scheduler determining it should be stopped - * due to conditions. - */ -@Getter -public class PluginScheduleEntryMainTaskFinishedEvent { - private final Plugin plugin; - private final ZonedDateTime finishDateTime; - private final String reason; - private final ExecutionResult result; - - /** - * Creates a new plugin finished event - * - * @param plugin The plugin that has finished - * @param finishDateTime The time when the plugin finished - * @param reason A description of why the plugin finished - * @param result The execution result (SUCCESS, SOFT_FAILURE, or HARD_FAILURE) - */ - public PluginScheduleEntryMainTaskFinishedEvent(Plugin plugin, ZonedDateTime finishDateTime, String reason, ExecutionResult result) { - this.plugin = plugin; - this.finishDateTime = finishDateTime; - this.reason = reason; - this.result = result; - } - - /** - * Creates a new plugin finished event with current time - * - * @param plugin The plugin that has finished - * @param reason A description of why the plugin finished - * @param result The execution result (SUCCESS, SOFT_FAILURE, or HARD_FAILURE) - */ - public PluginScheduleEntryMainTaskFinishedEvent(Plugin plugin, String reason, ExecutionResult result) { - this(plugin, ZonedDateTime.now(), reason, result); - } - - /** - * @deprecated Use {@link #getResult()} instead for more granular result information - * @return true if the result indicates success - */ - @Deprecated - public boolean isSuccess() { - return result.isSuccess(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPostScheduleTaskEvent.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPostScheduleTaskEvent.java deleted file mode 100644 index 70c1d4fad69..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPostScheduleTaskEvent.java +++ /dev/null @@ -1,38 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.event; - -import java.time.ZonedDateTime; - -import lombok.Getter; -import net.runelite.client.plugins.Plugin; - -/** - * Event fired when a plugin should start its post-schedule tasks. - * This is sent by the scheduler to plugins that implement SchedulablePlugin to initiate - * their post-schedule task execution after the plugin has been stopped. - */ -@Getter -public class PluginScheduleEntryPostScheduleTaskEvent { - private final Plugin plugin; - private final ZonedDateTime stopDateTime; - - /** - * Creates a new plugin post-schedule task event - * - * @param plugin The plugin that should start post-schedule tasks - * @param stopDateTime The time when the plugin was stopped - */ - public PluginScheduleEntryPostScheduleTaskEvent(Plugin plugin, ZonedDateTime stopDateTime) { - this.plugin = plugin; - this.stopDateTime = stopDateTime; - } - - /** - * Creates a new plugin post-schedule task event with current time - * - * @param plugin The plugin that should start post-schedule tasks - * @param wasSuccessful Whether the plugin run was successful - */ - public PluginScheduleEntryPostScheduleTaskEvent(Plugin plugin) { - this(plugin, ZonedDateTime.now()); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPostScheduleTaskFinishedEvent.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPostScheduleTaskFinishedEvent.java deleted file mode 100644 index 9ef4e79c4fc..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPostScheduleTaskFinishedEvent.java +++ /dev/null @@ -1,53 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.event; - -import java.time.ZonedDateTime; - -import lombok.Getter; -import net.runelite.client.plugins.Plugin; - -/** - * Event fired when a plugin's post-schedule tasks have finished. - * This is sent by plugins back to the scheduler to indicate completion of post-schedule tasks. - */ -@Getter -public class PluginScheduleEntryPostScheduleTaskFinishedEvent { - private final Plugin plugin; - private final ZonedDateTime finishDateTime; - private final ExecutionResult result; - private final String message; - - /** - * Creates a new plugin post-schedule task finished event - * - * @param plugin The plugin that finished post-schedule tasks - * @param finishDateTime The time when the plugin finished - * @param result The execution result (SUCCESS, SOFT_FAILURE, or HARD_FAILURE) - * @param message Optional message describing the completion - */ - public PluginScheduleEntryPostScheduleTaskFinishedEvent(Plugin plugin, ZonedDateTime finishDateTime, ExecutionResult result, String message) { - this.plugin = plugin; - this.finishDateTime = finishDateTime; - this.result = result; - this.message = message; - } - - /** - * Creates a new plugin post-schedule task finished event with current time - * - * @param plugin The plugin that finished post-schedule tasks - * @param result The execution result (SUCCESS, SOFT_FAILURE, or HARD_FAILURE) - * @param message Optional message describing the completion - */ - public PluginScheduleEntryPostScheduleTaskFinishedEvent(Plugin plugin, ExecutionResult result, String message) { - this(plugin, ZonedDateTime.now(), result, message); - } - - /** - * @deprecated Use {@link #getResult()} instead for more granular result information - * @return true if the result indicates success - */ - @Deprecated - public boolean isSuccess() { - return result.isSuccess(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPreScheduleTaskEvent.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPreScheduleTaskEvent.java deleted file mode 100644 index ad73c7aa5fd..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPreScheduleTaskEvent.java +++ /dev/null @@ -1,41 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.event; - -import java.time.ZonedDateTime; - -import lombok.Getter; -import net.runelite.client.plugins.Plugin; - -/** - * Event fired when a plugin should start its pre-schedule tasks. - * This is sent by the scheduler to plugins that implement SchedulablePlugin to initiate - * their pre-schedule task execution. - */ -@Getter -public class PluginScheduleEntryPreScheduleTaskEvent { - private final Plugin plugin; - private final ZonedDateTime startDateTime; - private final boolean isSchedulerControlled; - - /** - * Creates a new plugin pre-schedule task event - * - * @param plugin The plugin that should start pre-schedule tasks - * @param startDateTime The time when the plugin should start - * @param isSchedulerControlled Whether this plugin is under scheduler control - */ - public PluginScheduleEntryPreScheduleTaskEvent(Plugin plugin, ZonedDateTime startDateTime, boolean isSchedulerControlled) { - this.plugin = plugin; - this.startDateTime = startDateTime; - this.isSchedulerControlled = isSchedulerControlled; - } - - /** - * Creates a new plugin pre-schedule task event with current time - * - * @param plugin The plugin that should start pre-schedule tasks - * @param isSchedulerControlled Whether this plugin is under scheduler control - */ - public PluginScheduleEntryPreScheduleTaskEvent(Plugin plugin, boolean isSchedulerControlled) { - this(plugin, ZonedDateTime.now(), isSchedulerControlled); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPreScheduleTaskFinishedEvent.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPreScheduleTaskFinishedEvent.java deleted file mode 100644 index cc1bc3f66a3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntryPreScheduleTaskFinishedEvent.java +++ /dev/null @@ -1,53 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.event; - -import java.time.ZonedDateTime; - -import lombok.Getter; -import net.runelite.client.plugins.Plugin; - -/** - * Event fired when a plugin's pre-schedule tasks have finished. - * This is sent by plugins back to the scheduler to indicate completion of pre-schedule tasks. - */ -@Getter -public class PluginScheduleEntryPreScheduleTaskFinishedEvent { - private final Plugin plugin; - private final ZonedDateTime finishDateTime; - private final ExecutionResult result; - private final String message; - - /** - * Creates a new plugin pre-schedule task finished event - * - * @param plugin The plugin that finished pre-schedule tasks - * @param finishDateTime The time when the plugin finished - * @param result The execution result (SUCCESS, SOFT_FAILURE, or HARD_FAILURE) - * @param message Optional message describing the completion - */ - public PluginScheduleEntryPreScheduleTaskFinishedEvent(Plugin plugin, ZonedDateTime finishDateTime, ExecutionResult result, String message) { - this.plugin = plugin; - this.finishDateTime = finishDateTime; - this.result = result; - this.message = message; - } - - /** - * Creates a new plugin pre-schedule task finished event with current time - * - * @param plugin The plugin that finished pre-schedule tasks - * @param result The execution result (SUCCESS, SOFT_FAILURE, or HARD_FAILURE) - * @param message Optional message describing the completion - */ - public PluginScheduleEntryPreScheduleTaskFinishedEvent(Plugin plugin, ExecutionResult result, String message) { - this(plugin, ZonedDateTime.now(), result, message); - } - - /** - * @deprecated Use {@link #getResult()} instead for more granular result information - * @return true if the result indicates success - */ - @Deprecated - public boolean isSuccess() { - return result.isSuccess(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntrySoftStopEvent.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntrySoftStopEvent.java deleted file mode 100644 index f5becec3780..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/event/PluginScheduleEntrySoftStopEvent.java +++ /dev/null @@ -1,45 +0,0 @@ - -// DEPRECATED: This event class is deprecated and will be removed in a future release. -// Use PluginScheduleEntryPostScheduleTaskEvent instead for post-schedule task signaling. -// (See PluginScheduleEntryPostScheduleTaskEvent.java) -// -// This class remains only for backward compatibility and migration purposes. -// Please update all usages to the new event class as soon as possible. - -// TODO: Remove this class after migration to PluginScheduleEntryPostScheduleTaskEvent is complete. -// (Scheduled for removal in the next major version.) -// -// Replacement: PluginScheduleEntryPostScheduleTaskEvent -// -// --- -// Original Javadoc below: -// -// Event fired when a plugin should start its post-schedule tasks. -// This is sent by the scheduler to plugins that implement SchedulablePlugin to initiate -// their post-schedule task execution after the plugin has been stopped. - -package net.runelite.client.plugins.microbot.pluginscheduler.event; - -import java.time.ZoneOffset; -import java.time.ZonedDateTime; - -import lombok.Getter; -import net.runelite.client.plugins.Plugin; - -/** - * Event fired when a scheduled plugin should be stopped - */ -/** -* @deprecated Use {@link net.runelite.client.plugins.microbot.pluginscheduler.event.PluginScheduleEntryPostScheduleTaskEvent} instead. -*/ -@Deprecated -public class PluginScheduleEntrySoftStopEvent { - @Getter - private final Plugin plugin; - @Getter - private final ZonedDateTime stopDateTime; - public PluginScheduleEntrySoftStopEvent(Plugin plugin, ZonedDateTime stopDateTime) { - this.plugin = plugin; - this.stopDateTime = stopDateTime; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/model/PluginScheduleEntry.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/model/PluginScheduleEntry.java deleted file mode 100644 index de53fa467fd..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/model/PluginScheduleEntry.java +++ /dev/null @@ -1,3584 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.model; -import java.time.Duration; -import java.time.Instant; -import java.time.LocalTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.DayOfWeek; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.GameState; -import net.runelite.client.config.ConfigDescriptor; -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.pluginscheduler.api.SchedulablePlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionManager; -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.condition.logical.OrCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.enums.UpdateOption; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.DayOfWeekCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.enums.RepeatCycle; -import net.runelite.client.plugins.microbot.pluginscheduler.config.ScheduleEntryConfigManager; -import net.runelite.client.plugins.microbot.pluginscheduler.event.ExecutionResult; -import net.runelite.client.plugins.microbot.pluginscheduler.event.PluginScheduleEntryPostScheduleTaskEvent; -import net.runelite.client.plugins.microbot.pluginscheduler.event.PluginScheduleEntryPreScheduleTaskEvent; -import net.runelite.client.plugins.microbot.pluginscheduler.event.PluginScheduleEntrySoftStopEvent; -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; -@Data -@AllArgsConstructor -@Getter -@Slf4j -/** - * Represents a scheduled plugin entry in the plugin scheduler system. - *

- * This class manages the scheduling, starting, stopping, and condition management - * for a plugin. It handles both start and stop conditions through {@link ConditionManager} - * instances and provides comprehensive state tracking for the plugin's execution. - *

- * PluginScheduleEntry serves as the core model connecting the UI components in the - * scheduler system with the actual plugin execution logic. It maintains information about: - *

    - *
  • When a plugin should start (start conditions)
  • - *
  • When a plugin should stop (stop conditions)
  • - *
  • Current execution state (running, stopped, enabled/disabled)
  • - *
  • Execution statistics (run count, duration, etc.)
  • - *
  • Plugin configuration and watchdog management
  • - *
- */ -public class PluginScheduleEntry implements AutoCloseable { - // Static formatter for time display - public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); - public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); - // Remove the duplicate executor and use the shared one from ConditionManager - - // Store the scheduled futures so they can be cancelled later - private transient ScheduledFuture startConditionWatchdogFuture; - private transient ScheduledFuture stopConditionWatchdogFuture; - private transient Plugin plugin; - private String name; - private boolean enabled; - private boolean allowContinue = true; // Whether to continue running the plugin after a interruption -> stopReasonType = StopReason.Interrupted - private boolean hasStarted = false; // Flag to indicate if the plugin has started - @Setter - private boolean needsStopCondition = false; // Flag to indicate if a time-based stop condition is needed - private transient ScheduleEntryConfigManager scheduleEntryConfigManager; - - // New fields for tracking stop reason - private String lastStopReason; - @Getter - private boolean lastRunSuccessful; - private boolean onLastStopUserConditionsSatisfied = false; // Flag to indicate if the last stop was due to satisfied conditions - private boolean onLastStopPluginConditionsSatisfied = false; // Flag to indicate if the last stop was due to satisfied conditions - private StopReason lastStopReasonType = StopReason.NONE; - private Duration lastRunDuration = Duration.ZERO; // Duration of the last run - private ZonedDateTime lastRunStartTime; // When the plugin started running - private ZonedDateTime lastRunEndTime; // When the plugin finished running - - /** - * Enumeration of reasons why a plugin might stop - */ - public enum StopReason { - NONE("None"), - MANUAL_STOP("Manually Stopped"), - PLUGIN_FINISHED("Plugin Finished"), - ERROR("Error"), - SCHEDULED_STOP("Scheduled Stop"), - INTERRUPTED("Interrupted"), - HARD_STOP("Hard Stop executed"), - PREPOST_SCHEDULE_STOP("Hard Stop executed after post-schedule tasks"), - CLIENT_SHUTDOWN("Client Shutdown"); - - private final String description; - - StopReason(String description) { - this.description = description; - } - - public String getDescription() { - return description; - } - - @Override - public String toString() { - return description; - } - } - - - - private String cleanName; - final private ConditionManager stopConditionManager; - final private ConditionManager startConditionManager; - private transient boolean stopInitiated = false; - - private boolean allowRandomScheduling = true; // Whether this plugin can be randomly scheduled - private int runCount = 0; // Track how many times this plugin has been run - - // Soft failure tracking - private int consecutiveSoftFailures = 0; // Track consecutive soft failures - private int maxSoftFailuresBeforeHardFailure = 3; // Default: disable after 3 consecutive soft failures - private ZonedDateTime lastSoftFailureTime; // When the last soft failure occurred - - // Watchdog configuration - private boolean autoStartWatchdogs = true; // Whether to auto-start watchdogs on creation - private boolean watchdogsEnabled = true; // Whether watchdogs are allowed to run - - // Startup watchdog configuration - private boolean startupWatchdogEnabled = true; // Whether startup watchdog is enabled - private Duration startupWatchdogTimeout = Duration.ofMinutes(2); // Default 2 minutes timeout for plugin to transition from hasStarted to isRunning - private transient volatile ScheduledFuture startupWatchdogFuture; // Future for startup watchdog monitoring - private transient volatile ZonedDateTime startupWatchdogStartTime; // When startup watchdog monitoring began - private transient ScheduledExecutorService startupWatchdogExecutor; // Executor for startup watchdog - - private ZonedDateTime stopInitiatedTime; // When the first stop was attempted - private ZonedDateTime lastStopAttemptTime; // When the last stop attempt was made - private Duration softStopRetryInterval = Duration.ofSeconds(30); // Default 30 seconds between retries - private Duration hardStopTimeout = Duration.ofMinutes(4); // Default 4 Minutes before hard stop - - - private transient Thread stopMonitorThread; - private transient volatile boolean isMonitoringStop = false; - - - private int priority = 0; // Higher numbers = higher priority - private boolean isDefault = false; // Flag to indicate if this is a default plugin - - /** - * Functional interface for handling successful plugin stop events - */ - @FunctionalInterface - public interface StopCompletionCallback { - /** - * Called when a plugin has successfully completed its stop operation - * @param entry The PluginScheduleEntry that has stopped - * @param wasSuccessful Whether the plugin run was successful - */ - void onStopCompleted(PluginScheduleEntry entry, boolean wasSuccessful); - } - - /** - * Callback that will be invoked when this plugin successfully stops - */ - private transient StopCompletionCallback stopCompletionCallback; - - /** - * Sets the callback to be invoked when this plugin successfully stops - * @param callback The callback to invoke - * @return This PluginScheduleEntry for method chaining - */ - public PluginScheduleEntry setStopCompletionCallback(StopCompletionCallback callback) { - this.stopCompletionCallback = callback; - return this; - } - - - /** - * Sets the serialized ConfigDescriptor for this schedule entry - * This is used during deserialization - * - * @param serializedConfigDescriptor The serialized ConfigDescriptor as a JsonObject - */ - public void setSerializedConfigDescriptor(ConfigDescriptor serializedConfigDescriptor) { - // If we already have a scheduleEntryConfigManager, update it with the new config - if (this.scheduleEntryConfigManager != null) { - this.scheduleEntryConfigManager.setConfigScheduleEntryDescriptor(serializedConfigDescriptor); - } - } - - /** - * Gets the serialized ConfigDescriptor for this schedule entry - * - * @return The serialized ConfigDescriptor as a JsonObject, or null if not set - */ - public ConfigDescriptor getConfigScheduleEntryDescriptor() { - // If we have a scheduleEntryConfigManager, get the serialized config from it - if (this.scheduleEntryConfigManager != null) { - return this.scheduleEntryConfigManager.getConfigScheduleEntryDescriptor(); - } - return null; - } - public PluginScheduleEntry(String pluginName, String duration, boolean enabled, boolean allowRandomScheduling) { - this(pluginName, parseDuration(duration), enabled, allowRandomScheduling); - } - private TimeCondition mainTimeStartCondition; - private static Duration parseDuration(String duration) { - // If duration is specified, parse it - if (duration != null && !duration.isEmpty()) { - try { - String[] parts = duration.split(":"); - if (parts.length == 2) { - int hours = Integer.parseInt(parts[0]); - int minutes = Integer.parseInt(parts[1]); - return Duration.ofHours(hours).plusMinutes(minutes); - } - } catch (Exception e) { - // Invalid duration format, no condition added - throw new IllegalArgumentException("Invalid duration format: " + duration); - } - } - return null; - } - - public PluginScheduleEntry(String pluginName, Duration interval, boolean enabled, boolean allowRandomScheduling) { //allowRandomScheduling .>allows soft start - this(pluginName, new IntervalCondition(interval), enabled, allowRandomScheduling); - } - - public PluginScheduleEntry(String pluginName, TimeCondition startingCondition, boolean enabled, boolean allowRandomScheduling) { - this(pluginName, startingCondition, enabled, allowRandomScheduling, true); - } - - public PluginScheduleEntry( String pluginName, - TimeCondition startingCondition, - boolean enabled, - boolean allowRandomScheduling, - boolean autoStartWatchdogs){ - this(pluginName, startingCondition, enabled, allowRandomScheduling, autoStartWatchdogs, true); - } - public PluginScheduleEntry( String pluginName, - TimeCondition startingCondition, - boolean enabled, - boolean allowRandomScheduling, - boolean autoStartWatchdogs, - boolean allowContinue - ) { - this.name = pluginName; - this.enabled = enabled; - this.allowRandomScheduling = allowRandomScheduling; - this.autoStartWatchdogs = autoStartWatchdogs; - this.cleanName = pluginName.replaceAll("|", "") - .replaceAll("<[^>]*>([^<]*)]*>", "$1") - .replaceAll("<[^>]*>", ""); - - this.stopConditionManager = new ConditionManager(); - this.startConditionManager = new ConditionManager(); - - // Check if this is a default/1-second interval plugin - boolean isDefaultByScheduleType = false; - if (startingCondition != null) { - if (startingCondition instanceof IntervalCondition) { - IntervalCondition interval = (IntervalCondition) startingCondition; - if (interval.getInterval().getSeconds() <= 1) { - isDefaultByScheduleType = true; - } - } - this.mainTimeStartCondition = startingCondition; - startConditionManager.setUserLogicalCondition(new OrCondition(startingCondition)); - } - - // If it's a default by schedule type, enforce the default settings - if (isDefaultByScheduleType) { - this.isDefault = true; - this.priority = 0; - } - //registerPluginConditions(); - scheduleConditionWatchdogs(10000, UpdateOption.SYNC); - // Only start watchdogs if auto-start is enabled - if (autoStartWatchdogs) { - //stopConditionManager.resumeWatchdogs(); - //startConditionManager.resumeWatchdogs(); - } - - // Always register events if enabled - if (enabled) { - startConditionManager.registerEvents(); - }else { - startConditionManager.unregisterEventsAndPauseWatchdogs(); - stopConditionManager.unregisterEventsAndPauseWatchdogs(); - } - this.allowContinue = allowContinue; - } - - /** - * Creates a scheduled event with a one-time trigger at a specific time - * - * @param pluginName The plugin name - * @param triggerTime The time when the plugin should trigger once - * @param enabled Whether the schedule is enabled - * @return A new PluginScheduleEntry configured to trigger once at the specified time - */ - public static PluginScheduleEntry createOneTimeSchedule(String pluginName, ZonedDateTime triggerTime, boolean enabled) { - SingleTriggerTimeCondition condition = new SingleTriggerTimeCondition(triggerTime, Duration.ZERO, 1); - PluginScheduleEntry entry = new PluginScheduleEntry( - pluginName, - condition, - enabled, - false); // One-time events are typically not randomized - - return entry; - } - - public void setEnabled(boolean enabled) { - if (this.enabled == enabled) { - return; // No change in enabled state - } - this.enabled = enabled; - if (!enabled) { - stopConditionManager.unregisterEventsAndPauseWatchdogs(); - startConditionManager.unregisterEventsAndPauseWatchdogs(); - runCount = 0; - } else { - //stopConditionManager.registerEvents(); - log.debug("registering start events for plugin '{}'", name); - startConditionManager.registerEvents(); - //log this object id-> memory hashcode - log.debug("PluginScheduleEntry {} - {} - {} - {} - {}", this.hashCode(), this.name, this.cleanName, this.enabled, this.allowRandomScheduling); - //registerPluginConditions(); - this.setLastStopReason(""); - this.setLastRunSuccessful(false); - this.setLastStopReasonType(PluginScheduleEntry.StopReason.NONE); - - // Resume watchdogs if they were previously configured and watchdogs are enabled - if (watchdogsEnabled) { - startConditionManager.resumeWatchdogs(); - stopConditionManager.resumeWatchdogs(); - } - } - } - - /** - * Controls whether watchdogs are allowed to run for this schedule entry. - * This provides a way to temporarily disable watchdogs without losing their configuration. - * - * @param enabled true to enable watchdogs, false to disable them - */ - public void setWatchdogsEnabled(boolean enabled) { - if (this.watchdogsEnabled == enabled) { - return; // No change - } - - this.watchdogsEnabled = enabled; - - if (enabled) { - // Resume watchdogs if the plugin is enabled - if (this.enabled) { - startConditionManager.resumeWatchdogs(); - stopConditionManager.resumeWatchdogs(); - log.debug("Watchdogs resumed for '{}'", name); - } - } else { - // Pause watchdogs regardless of plugin state - startConditionManager.pauseWatchdogs(); - stopConditionManager.pauseWatchdogs(); - log.debug("Watchdogs paused for '{}'", name); - } - } - - /** - * Checks if watchdogs are currently running for this schedule entry - * - * @return true if at least one watchdog is running - */ - public boolean areWatchdogsRunning() { - return startConditionManager.areWatchdogsRunning() || - stopConditionManager.areWatchdogsRunning(); - } - - /** - * Manually start the condition watchdogs for this schedule entry. - * This will only have an effect if watchdogs are enabled and the plugin is enabled. - * - * @param intervalMillis The interval at which to check for condition changes - * @param updateOption How to handle condition changes - * @return true if watchdogs were successfully started - */ - public boolean startConditionWatchdogs(long intervalMillis, UpdateOption updateOption) { - if (!watchdogsEnabled || !enabled) { - return false; - } - - return scheduleConditionWatchdogs(intervalMillis, updateOption); - } - - /** - * Stops all watchdogs associated with this schedule entry - */ - public void stopWatchdogs() { - log.debug("Stopping all watchdogs for '{}'", name); - startConditionManager.pauseWatchdogs(); - stopConditionManager.pauseWatchdogs(); - stopStartupWatchdog(); - } - - /** - * Starts the startup watchdog to monitor plugin transition from hasStarted to isRunning. - * This watchdog will trigger a hard failure if the plugin doesn't transition to running - * state within the configured timeout period. - */ - public void startStartupWatchdog() { - if (!startupWatchdogEnabled) { - return; // Disabled - } - if (startupWatchdogFuture != null) { - if (!startupWatchdogFuture.isDone() && !startupWatchdogFuture.isCancelled()) { - return; // Already running - } - startupWatchdogFuture = null; - } - - startupWatchdogStartTime = ZonedDateTime.now(); - log.debug("Starting startup watchdog for plugin '{}' with timeout of {} minutes", - name, startupWatchdogTimeout.toMinutes()); - - startupWatchdogExecutor = Executors.newSingleThreadScheduledExecutor(r -> { - Thread t = new Thread(r, "StartupWatchdog-" + name); - t.setDaemon(true); - return t; - }); - - // Start periodic monitoring every 2 seconds - startupWatchdogFuture = startupWatchdogExecutor.scheduleAtFixedRate(() -> { - try { - ZonedDateTime now = ZonedDateTime.now(); - Duration elapsed = Duration.between(startupWatchdogStartTime, now); - - // Check if plugin has successfully transitioned to running state - if (isRunning()) { - log.debug("Startup watchdog: Plugin '{}' successfully transitioned to running state after {} seconds", - name, elapsed.getSeconds()); - - // Stop the watchdog since plugin is now running - stopStartupWatchdog(); - return; - } - - // Check if we've exceeded the timeout - if (elapsed.compareTo(startupWatchdogTimeout) >= 0) { - long elapsedMinutes = elapsed.toMinutes(); - String timeoutMessage = String.format( - "Plugin '%s' failed to transition to running state within %d minutes (started but not running)", - getCleanName(), elapsedMinutes); - - log.error("Startup watchdog timeout: {}", timeoutMessage); - - // Stop the plugin with hard failure - stop(ExecutionResult.HARD_FAILURE, StopReason.ERROR, timeoutMessage); - - // Stop the watchdog - stopStartupWatchdog(); - return; - } - - // Continue monitoring - log progress every 30 seconds - if (elapsed.getSeconds() % 30 == 0 && elapsed.getSeconds() > 0) { - log.debug("Startup watchdog: Plugin '{}' still starting... {} seconds elapsed", - name, elapsed.getSeconds()); - } - - } catch (Exception e) { - log.error("Error in startup watchdog for plugin '{}': {}", name, e.getMessage(), e); - stopStartupWatchdog(); - } - }, 0, 2, TimeUnit.SECONDS); // Check every 2 seconds - } - - /** - * Stops the startup watchdog if it's currently running - */ - public void stopStartupWatchdog() { - log.debug("Stopping startup watchdog for plugin '{}'", name); - if (startupWatchdogFuture != null && !startupWatchdogFuture.isDone()) { - startupWatchdogFuture.cancel(false); - } - startupWatchdogFuture = null; - startupWatchdogStartTime = null; - if (startupWatchdogExecutor != null) { - startupWatchdogExecutor.shutdownNow(); - startupWatchdogExecutor = null; - } - } - - /** - * Checks if the startup watchdog is currently active - * - * @return true if startup watchdog is monitoring this plugin - */ - public boolean isStartupWatchdogActive() { - return startupWatchdogFuture != null && !startupWatchdogFuture.isDone(); - } - - /** - * Gets the remaining time until startup watchdog timeout - * - * @return Optional containing remaining time, or empty if watchdog is not active - */ - public Optional getStartupWatchdogTimeRemaining() { - if (!isStartupWatchdogActive() || startupWatchdogStartTime == null) { - return Optional.empty(); - } - - Duration elapsed = Duration.between(startupWatchdogStartTime, ZonedDateTime.now()); - Duration remaining = startupWatchdogTimeout.minus(elapsed); - - return !remaining.isNegative() && !remaining.isZero() ? Optional.of(remaining) : Optional.empty(); - } - - /** - * Sets the startup watchdog timeout duration - * - * @param timeout Duration to wait before timing out plugin startup - */ - public void setStartupWatchdogTimeout(Duration timeout) { - if (timeout != null && timeout.toSeconds() > 0) { - this.startupWatchdogTimeout = timeout; - log.debug("Updated startup watchdog timeout for plugin '{}' to {} minutes", - name, timeout.toMinutes()); - } - } - - /** - * Enables or disables the startup watchdog - * - * @param enabled true to enable startup watchdog, false to disable - */ - public void setStartupWatchdogEnabled(boolean enabled) { - if (this.startupWatchdogEnabled != enabled) { - this.startupWatchdogEnabled = enabled; - log.debug("Startup watchdog {} for plugin '{}'", - enabled ? "enabled" : "disabled", name); - - // If disabling, stop any active watchdog - if (!enabled) { - stopStartupWatchdog(); - } - } - } - - public Plugin getPlugin() { - if (this.plugin == null) { - this.plugin = Microbot.getPluginManager().getPlugins().stream() - .filter(p -> Objects.equals(p.getName(), name)) - .findFirst() - .orElse(null); - - // Initialize scheduleEntryConfigManager when plugin is first retrieved - if (this.plugin instanceof SchedulablePlugin && scheduleEntryConfigManager == null) { - SchedulablePlugin schedulablePlugin = (SchedulablePlugin) this.plugin; - ConfigDescriptor descriptor = schedulablePlugin.getConfigDescriptor(); - if (descriptor != null) { - scheduleEntryConfigManager = new ScheduleEntryConfigManager(descriptor); - } - } - } - return plugin; - } - - /** - * checks if the plugin referenced by this schedule entry is currently available - * in the plugin manager. this is useful for handling hot-loaded plugins from - * the plugin hub that might not be available when the schedule is loaded. - * - * @return true if the plugin is available, not hidden, and implements SchedulablePlugin - */ - public boolean isPluginAvailable() { - if (name == null || name.isEmpty()) { - return false; - } - - // try to find the plugin by name in the current plugin manager - Plugin foundPlugin = Microbot.getPluginManager().getPlugins().stream() - .filter(p -> Objects.equals(p.getName(), name)) - .filter(p -> { - // check if plugin is not hidden - PluginDescriptor descriptor = p.getClass().getAnnotation(PluginDescriptor.class); - return descriptor != null && !descriptor.hidden(); - }) - .filter(p -> p instanceof SchedulablePlugin) // must implement SchedulablePlugin - .findFirst() - .orElse(null); - - return foundPlugin != null; - } - - public boolean start(boolean logConditions) { - if (getPlugin() == null) { - return false; - } - - try { - StringBuilder logBuilder = new StringBuilder(); - logBuilder.append("\nStarting plugin '").append(name).append("':\n"); - - if (!this.isEnabled()) { - logBuilder.append(" - Plugin is disabled, not starting\n"); - log.info(logBuilder.toString()); - return false; - } - - // Log defined conditions when starting - if (logConditions) { - logBuilder.append(" - Starting with conditions\n"); - // These methods do their own logging as they're complex and used elsewhere - logStartConditionsWithDetails(); - logStopConditionsWithDetails(); - } - - // Reset stop conditions before starting, if we are not continuing, and we are not interrupted - if (!this.allowContinue || (lastStopReasonType != StopReason.INTERRUPTED)) { - logBuilder.append(" - Not continuing, resetting stop conditions\n") - .append(" - allowContinue: ").append(allowContinue) - .append("\n - last Stop Reason Type: ").append(lastStopReasonType).append("\n"); - resetStopConditions(); - } else { - logBuilder.append(" - Continuing, not resetting stop conditions\n"); - stopConditionManager.resetPluginConditions(); - - if (!onLastStopUserConditionsSatisfied && areUserDefinedStopConditionsMet()) { - logBuilder.append(" - On last interrupt user stop conditions were not satisfied, now they are, resetting user stop conditions\n"); - stopConditionManager.resetUserConditions(); - } - } - - if (lastStopReasonType != StopReason.NONE) { - logBuilder.append(" - Last stop reason: ").append(lastStopReasonType.getDescription()) - .append("\n - message: ").append(lastStopReason).append("\n"); - } - - this.setLastStopReason(""); - this.setLastRunSuccessful(false); - this.setLastStopReasonType(PluginScheduleEntry.StopReason.NONE); - this.setOnLastStopPluginConditionsSatisfied(false); - this.setOnLastStopUserConditionsSatisfied(false); - - // Set scheduleMode to true in plugin config - if (scheduleEntryConfigManager != null) { - scheduleEntryConfigManager.setScheduleMode(true); - logBuilder.append(" - Set \"scheduleMode\" in config of the plugin\n"); - } - - // Check if plugin implements SchedulablePlugin and use event-driven startup - Plugin plugin = getPlugin(); - if (plugin instanceof SchedulablePlugin) { - logBuilder.append(" - Plugin implements SchedulablePlugin\n"); - } else { - logBuilder.append(" - Plugin does not implement SchedulablePlugin\n"); - } - - // Start the plugin directly - it will handle scheduler mode detection in its startUp() method - Microbot.getClientThread().runOnSeperateThread(() -> { - Plugin pluginToStart = getPlugin(); - if (pluginToStart == null) { - log.error("Plugin '{}' not found -> can't start plugin", name); - return false; - } - Microbot.startPlugin(pluginToStart); - return false; - }); - - stopInitiated = false; - hasStarted = true; - lastRunDuration = Duration.ZERO; // Reset last run duration - lastRunStartTime = ZonedDateTime.now(); // Set the start time of the last run - - // Start startup watchdog to monitor transition to running state - if (startupWatchdogEnabled) { - startStartupWatchdog(); - logBuilder.append(" - Started startup watchdog with timeout of ").append(startupWatchdogTimeout.toMinutes()).append(" minutes\n"); - } - - // Register/unregister appropriate event handlers - logBuilder.append(" - Registering stopping conditions\n"); - stopConditionManager.registerEvents(); - - logBuilder.append(" - Unregistering start conditions\n"); - startConditionManager.unregisterEvents(); - - // Log all collected information at once - log.info(logBuilder.toString()); - - return true; - } catch (Exception e) { - log.error("Error starting plugin '{}': {}", name, e.getMessage(), e); - return false; - } - } - - /** - * Triggers pre-schedule tasks for a SchedulablePlugin. - * This method should be called after the plugin has started and is subscribed to EventBus. - * - * @return true if the trigger was successful, false otherwise - */ - public boolean triggerPreScheduleTasks() { - try { - Plugin plugin = getPlugin(); - if (!(plugin instanceof SchedulablePlugin)) { - log.warn("Plugin '{}' does not implement SchedulablePlugin - cannot trigger pre-schedule tasks", name); - return false; - } - - SchedulablePlugin schedulablePlugin = (SchedulablePlugin) plugin; - if (!isRunning()){ - log.warn("Plugin '{}' is not running - cannot trigger pre-schedule tasks", name); - return false; - } - if (schedulablePlugin.getPrePostScheduleTasks() == null) { - log.debug("Plugin '{}' has no pre/post schedule tasks configured", name); - return false; - } - if (!schedulablePlugin.getPrePostScheduleTasks().canStartPreScheduleTasks()) { - log.warn("Pre-schedule tasks cannot be started for plugin '{}' - already running or not ready", name); - return false; - } - // Check if pre-schedule tasks are already running or completed - if (schedulablePlugin.getPrePostScheduleTasks().isPreScheduleRunning()) { - log.warn("Pre-schedule tasks are already running for plugin '{}' - cannot trigger again", name); - return false; - } - - if (schedulablePlugin.getPrePostScheduleTasks().isPreTaskComplete()) { - log.debug("Pre-schedule tasks already completed for plugin '{}' - skipping trigger", name); - return false; - } - - log.info("Triggering pre-schedule tasks for plugin '{}'", name); - - // Send start event to the running plugin - Microbot.getEventBus().post(new PluginScheduleEntryPreScheduleTaskEvent(plugin, true)); - - return true; - } catch (Exception e) { - log.error("Error triggering pre-schedule tasks for plugin '{}': {}", name, e.getMessage(), e); - return false; - } - } - - /** - * Triggers post-schedule tasks for a plugin that implements SchedulablePlugin. - *

- * This method should be called after the plugin has been stopped. - * - * @param successful Whether the plugin run was successful - * @return true if the trigger was successful, false otherwise - */ - public boolean triggerPostScheduleTasks(StopReason stopReason) { - boolean successful = (stopReason == StopReason.PLUGIN_FINISHED || stopReason == StopReason.SCHEDULED_STOP); - try { - Plugin plugin = getPlugin(); - if (!(plugin instanceof SchedulablePlugin)) { - log.warn("Plugin '{}' does not implement SchedulablePlugin - cannot trigger post-schedule tasks", name); - return false; - } - - SchedulablePlugin schedulablePlugin = (SchedulablePlugin) plugin; - - // Send post-schedule task event to the plugin - ZonedDateTime current_time = ZonedDateTime.now(ZoneId.systemDefault()); - - if (schedulablePlugin.getPrePostScheduleTasks() == null && isRunning()){ - // we must send it, when we dont have a pre/post schedule task, so the plugin can handle it a soft stop without defining a post schedule task - log.info("Plugin '{}' has no pre/post schedule tasks configured, sending soft stop event", name); - Microbot.getEventBus().post(new PluginScheduleEntrySoftStopEvent(plugin, current_time)); - Microbot.getEventBus().post(new PluginScheduleEntryPostScheduleTaskEvent(plugin,current_time)); - return true; - } - if (schedulablePlugin.getPrePostScheduleTasks() == null || !isRunning()) { - log.warn("Plugin '{}' has no pre/post schedule tasks configured", name); - return false; - } - if (!schedulablePlugin.getPrePostScheduleTasks().canStartPostScheduleTasks()) { - log.warn("Post-schedule tasks cannot be started for plugin '{}' - already running or not ready", name); - return false; - } - - // Check if post-schedule tasks are already running - if (schedulablePlugin.getPrePostScheduleTasks().isPostScheduleRunning()) { - log.warn("Post-schedule tasks are already running for plugin '{}' - cannot trigger again", name); - return false; - } - - log.info("Triggering post-schedule tasks for plugin '{}' (successful: {})", name, successful); - - Microbot.getEventBus().post(new PluginScheduleEntrySoftStopEvent(plugin, current_time)); - Microbot.getEventBus().post(new PluginScheduleEntryPostScheduleTaskEvent(plugin,current_time)); - return true; - } catch (Exception e) { - log.error("Error triggering post-schedule tasks for plugin '{}': {}", name, e.getMessage(), e); - return false; - } - } - - /** - * Initiates a graceful (soft) stop of the plugin. - *

- * This method notifies the plugin that it should stop via a {@link PluginScheduleEntryPostScheduleTaskEvent}, - * allowing the plugin to finish critical operations before shutting down. It also: - *

    - *
  • Resets and re-registers start condition monitors
  • - *
  • Unregisters stop condition monitors
  • - *
  • Records timing information about the stop attempt
  • - *
  • Starts a monitoring thread to track the stopping process
  • - *
- *

- * After sending the stop event, the plugin is responsible for handling its own shutdown. - * - * @param successfulRun indicates whether the plugin completed its task successfully - */ - private void softStop(StopReason stopReason) { - if (getPlugin() == null) { - return; - } - - try { - // Reset start conditions - startConditionManager.registerEvents(); - stopConditionManager.unregisterEvents(); - - Microbot.getClientThread().runOnClientThreadOptional(() -> { - ZonedDateTime current_time = ZonedDateTime.now(ZoneId.systemDefault()); - triggerPostScheduleTasks(stopReason); - //Microbot.getEventBus().post(new PluginScheduleEntryPostScheduleTaskEvent(plugin, current_time)); - // Notify the plugin to stop gracefully -> about to removal, switch to the post schedule task event - //Microbot.getEventBus().post(new PluginScheduleEntrySoftStopEvent(plugin, current_time)); - return true; - }); - if(!stopInitiated){ - this.stopInitiated = true; - this.stopInitiatedTime = ZonedDateTime.now(); - // Stop startup watchdog since plugin is being stopped - stopStartupWatchdog(); - } - // If no custom stop reason was set, use the default reason from the enum - if (lastStopReason == null && lastStopReasonType != null) { - lastStopReason = lastStopReasonType.getDescription(); - } - this.lastStopAttemptTime = ZonedDateTime.now(); - this.lastRunDuration = Duration.between(lastRunStartTime, ZonedDateTime.now()); - this.lastRunEndTime = ZonedDateTime.now(); - // Start monitoring for successful stop - startStopMonitoringThread(stopReason); - - if (getPlugin() instanceof SchedulablePlugin) { - log.info("soft stopping for plugin '{}'", name); - } - return; - } catch (Exception e) { - return; - } - } - - /** - * Forces an immediate (hard) stop of the plugin. - *

- * This method is used when a soft stop has failed or timed out and the plugin - * needs to be forcibly terminated. It directly calls Microbot's stopPlugin method - * to immediately terminate the plugin's execution. - *

- * Hard stops should only be used as a last resort when soft stops fail, as they - * don't allow the plugin to perform cleanup operations or save state. - * - * @param successfulRun indicates whether to record this run as successful - */ - private void hardStop(StopReason stopReason) { - if (getPlugin() == null) { - return; - } - boolean successfulRun = (stopReason == StopReason.PLUGIN_FINISHED || stopReason == StopReason.SCHEDULED_STOP); - try { - Microbot.getClientThread().runOnSeperateThread(() -> { - log.info("Hard stopping plugin '{}' - successfulRun {}", name, successfulRun); - Plugin stopPlugin = Microbot.getPlugin(plugin.getClass().getName()); - Microbot.stopPlugin(stopPlugin); - return false; - }); - if(!stopInitiated){ - stopInitiated = true; - stopInitiatedTime = ZonedDateTime.now(); - // Stop startup watchdog since plugin is being stopped - stopStartupWatchdog(); - } - lastStopAttemptTime = ZonedDateTime.now(); - // Set these fields to match what softStop does - lastRunDuration = Duration.between(lastRunStartTime, ZonedDateTime.now()); - lastRunEndTime = ZonedDateTime.now(); - hasStarted = false; - // Also set a descriptive stop reason if one isn't already set - if (lastStopReason == null) { - lastStopReason = lastStopReasonType != null && lastStopReasonType == StopReason.HARD_STOP - ? lastStopReasonType.getDescription() - : "Plugin was forcibly stopped after not responding to soft stop"; - } - // Start monitoring for successful stop - startStopMonitoringThread(stopReason); - - return; - } catch (Exception e) { - return; - } - } - - /** - * Starts a monitoring thread that tracks the stopping process of a plugin. - *

- * This method creates a daemon thread that periodically checks if a plugin - * that is in the process of stopping has completed its shutdown. When the plugin - * successfully stops, this method updates the next scheduled run time and clears - * all stopping-related state flags. - *

- * The monitoring thread will only be started if one is not already running - * (controlled by the isMonitoringStop flag). It checks the plugin's running state - * every 500ms until the plugin stops or monitoring is canceled. - *

- * The thread is created as a daemon thread to prevent it from blocking JVM shutdown. - */ - private void startStopMonitoringThread(StopReason stopReason) { - // Don't start a new thread if one is already running - if (isMonitoringStop) { - return; - } - - isMonitoringStop = true; - - stopMonitorThread = new Thread(() -> { - StringBuilder logMsg = new StringBuilder(); - logMsg.append("\n\tMonitoring thread started for stopping the plugin '").append(getCleanName()).append("' "); - log.info(logMsg.toString()); - logMsg = new StringBuilder(); - boolean successfulRun = (stopReason == StopReason.PLUGIN_FINISHED || stopReason == StopReason.SCHEDULED_STOP); - try { - // Keep checking until the stop completes or is abandoned - while (stopInitiated && isMonitoringStop) { - // Check if plugin has stopped running - if (!isRunning()) { - - logMsg.append("\nPlugin '").append(getCleanName()).append("' has successfully stopped") - .append(" - updating state - successfulRun ").append(successfulRun); - - // Set scheduleMode back to false when the plugin stops - if (scheduleEntryConfigManager != null) { - scheduleEntryConfigManager.setScheduleMode(false); - logMsg.append("\n unset \"scheduleMode\" - flag in the config. of the plugin '").append(getCleanName()).append("'"); - } - - - - break; - } - else { - // Plugin is still running, log the status - if (stopInitiatedTime != null && Duration.between(stopInitiatedTime, ZonedDateTime.now()).getSeconds()% 60==0) { - logMsg = new StringBuilder(); - logMsg.append("\nPlugin '").append(getCleanName()).append("' is still running"); - logMsg.append("\n- stop initiated at: ").append(stopInitiatedTime.format(DATE_TIME_FORMATTER)) - .append("\n- current time: ").append(ZonedDateTime.now().format(DATE_TIME_FORMATTER)); - logMsg.append("\n- elapsed time: ").append(Duration.between(stopInitiatedTime, ZonedDateTime.now()).toSeconds()) - .append(" sec - successfulRun ").append(successfulRun); - log.debug(logMsg.toString()); - } - stop(successfulRun); // Call the stop method to handle any additional logic - } - - // Check every 600ms to be responsive but not wasteful - Thread.sleep(600); - } - } catch (InterruptedException e) { - // Thread was interrupted, just exit - log.warn("\n\tStop monitoring thread for '" + name + "' was interrupted"); - } finally { - // Update lastRunTime and start conditions for next run based on stop reason - if (stopReason == StopReason.PLUGIN_FINISHED || stopReason == StopReason.SCHEDULED_STOP) { - // Success cases - reset start conditions - resetStartConditions(); - } else if (stopReason == StopReason.ERROR || stopReason == StopReason.HARD_STOP) { - // Hard failures - disable the plugin - setEnabled(false); - } else { - // For other cases (interruptions, manual stops), keep enabled but don't reset conditions - log.debug("Plugin '{}' stopped with reason: {}, keeping enabled", name, stopReason.getDescription()); - } - log.debug(logMsg.toString()); - //logStopConditionsWithDetails(); - // Reset stop state - isMonitoringStop = false; // Reset the monitoring flag - stopInitiated = false; - hasStarted = false; - stopInitiatedTime = null; - lastStopAttemptTime = null; - // Invoke the stop completion callback if one is registered - if (stopCompletionCallback != null) { - try { - boolean wasSuccessful = (stopReason == StopReason.PLUGIN_FINISHED || stopReason == StopReason.SCHEDULED_STOP); - stopCompletionCallback.onStopCompleted(PluginScheduleEntry.this, wasSuccessful); - log.debug("Stop completion callback executed for plugin '{}'", name); - } catch (Exception e) { - log.error("Error executing stop completion callback for plugin '{}'", name, e); - } - } - log.debug("Stop monitoring thread exited for plugin '" + name + "'"); - - } - }); - - stopMonitorThread.setName("StopMonitor-" + name); - stopMonitorThread.setDaemon(true); // Use daemon thread to not prevent JVM exit - stopMonitorThread.start(); - - } - public void cancelStop(){ - stopMonitoringThread(); // Stop the monitoring thread if it's running - isMonitoringStop = false; // Reset the monitoring flag - stopInitiated = false; - stopInitiatedTime = null; - lastStopAttemptTime = null; - } - - /** - * Stops the monitoring thread if it's running - */ - private void stopMonitoringThread() { - if (isMonitoringStop && stopMonitorThread != null) { - isMonitoringStop = false; - stopMonitorThread.interrupt(); - stopMonitorThread = null; - } - } - - /** - * Checks if this plugin schedule has any defined stop conditions - * - * @return true if at least one stop condition is defined - */ - public boolean hasAnyStopConditions() { - return stopConditionManager != null && - !stopConditionManager.getConditions().isEmpty(); - } - - /** - * Checks if this plugin has any one-time stop conditions that can only trigger once - * - * @return true if at least one single-trigger condition exists in the stop conditions - */ - public boolean hasAnyOneTimeStopConditions() { - return stopConditionManager != null && - stopConditionManager.hasAnyOneTimeConditions(); - } - - /** - * Checks if any stop conditions have already triggered and cannot trigger again - * - * @return true if at least one stop condition has triggered and cannot trigger again - */ - public boolean hasTriggeredOneTimeStopConditions() { - return stopConditionManager != null && - stopConditionManager.hasTriggeredOneTimeConditions(); - } - - /** - * Determines if the stop conditions can trigger again in the future - * Considers the nested logical structure and one-time conditions - * - * @return true if the stop condition structure can trigger again - */ - public boolean canStopTriggerAgain() { - return stopConditionManager != null && - stopConditionManager.canTriggerAgain(); - } - - /** - * Gets the next time when any stop condition is expected to trigger - * - * @return Optional containing the next stop trigger time, or empty if none exists - */ - public Optional getNextStopTriggerTime() { - if (stopConditionManager == null) { - return Optional.empty(); - } - return stopConditionManager.getCurrentTriggerTime(); - } - - /** - * Gets a human-readable string representing when the next stop condition will trigger - * - * @return String with the time until the next stop trigger, or a message if none exists - */ - public String getNextStopTriggerTimeString() { - if (stopConditionManager == null) { - return "No stop conditions defined"; - } - return stopConditionManager.getCurrentTriggerTimeString(); - } - - /** - * Checks if the stop conditions are fulfillable based on their structure and state - * A condition is considered unfulfillable if it contains one-time conditions that - * have all already triggered in an OR structure, or if any have triggered in an AND structure - * - * @return true if the stop conditions can still be fulfilled - */ - public boolean hasFullfillableStopConditions() { - if (!hasAnyStopConditions()) { - return false; - } - - // If we have any one-time conditions that can't trigger again - // and the structure is such that it can't satisfy anymore, then it's not fulfillable - if (hasAnyOneTimeStopConditions() && !canStopTriggerAgain()) { - return false; - } - - return true; - } - - /** - * Gets the remaining duration until the next stop condition trigger - * - * @return Optional containing the duration until next stop trigger, or empty if none available - */ - public Optional getDurationUntilStopTrigger() { - if (stopConditionManager == null) { - return Optional.empty(); - } - return stopConditionManager.getDurationUntilNextTrigger(); - } - - - - - /** - * Determines whether this plugin is currently running. - *

- * This method checks if the plugin is enabled in the RuneLite plugin system. - * It uses the Microbot API to query if the plugin associated with this schedule - * entry is currently in an active/running state. - * - * @return true if the plugin is currently running, false otherwise - */ - public boolean isRunning() { - Plugin plugin = getPlugin(); - if (plugin != null) { - return Microbot.isPluginEnabled(plugin.getClass()) && hasStarted; - } - return false; - } - - public boolean isStopped() { - Plugin plugin = getPlugin(); - if (plugin != null) { - return !Microbot.isPluginEnabled(plugin.getClass()) && !stopInitiated; - } - return false; - } - public boolean isStopping() { - return stopInitiated; - } - - /** - * Round time to nearest minute (remove seconds and milliseconds) - */ - private ZonedDateTime roundToMinutes(ZonedDateTime time) { - return time.withSecond(0).withNano(0); - } - private void logStartCondtions() { - List conditionList = startConditionManager.getConditions(); - logConditionInfo(conditionList,"Defined Start Conditions", true); - } - private void logStartConditionsWithDetails() { - List conditionList = startConditionManager.getConditions(); - logConditionInfo(conditionList,"Defined Start Conditions", true); - } - - /** - * Checks if this plugin schedule has any defined start conditions - * - * @return true if at least one start condition is defined - */ - public boolean hasAnyStartConditions() { - return startConditionManager != null && - !startConditionManager.getConditions().isEmpty(); - } - - /** - * Checks if this plugin has any one-time start conditions that can only trigger once - * - * @return true if at least one single-trigger condition exists in the start conditions - */ - public boolean hasAnyOneTimeStartConditions() { - return startConditionManager != null && - startConditionManager.hasAnyOneTimeConditions(); - } - - /** - * Checks if any start conditions have already triggered and cannot trigger again - * - * @return true if at least one start condition has triggered and cannot trigger again - */ - public boolean hasTriggeredOneTimeStartConditions() { - return startConditionManager != null && - startConditionManager.hasTriggeredOneTimeConditions(); - } - - /** - * Determines if the start conditions can trigger again in the future - * Considers the nested logical structure and one-time conditions - * - * @return true if the start condition structure can trigger again - */ - public boolean canStartTriggerAgain() { - return startConditionManager != null && - startConditionManager.canTriggerAgain(); - } - - /** - * Gets the next time when any start condition is expected to trigger - * - * @return Optional containing the next start trigger time, or empty if none exists - */ - public Optional getCurrentStartTriggerTime() { - if (startConditionManager == null) { - return Optional.empty(); - } - return startConditionManager.getCurrentTriggerTime(); - } - - /** - * Gets a human-readable string representing when the next start condition will trigger - * - * @return String with the time until the next start trigger, or a message if none exists - */ - public String getCurrentStartTriggerTimeString() { - if (startConditionManager == null) { - return "No start conditions defined"; - } - return startConditionManager.getCurrentTriggerTimeString(); - } - - /** - * Checks if the start conditions are fulfillable based on their structure and state - * A condition is considered unfulfillable if it contains one-time conditions that - * have all already triggered in an OR structure, or if any have triggered in an AND structure - * - * @return true if the start conditions can still be fulfilled - */ - public boolean hasFullfillableStartConditions() { - if (!hasAnyStartConditions()) { - return false; - } - - // If we have any one-time conditions that can't trigger again - // and the structure is such that it can't satisfy anymore, then it's not fulfillable - if (hasAnyOneTimeStartConditions() && !canStartTriggerAgain()) { - return false; - } - - return true; - } - - /** - * Gets the remaining duration until the next start condition trigger - * - * @return Optional containing the duration until next start trigger, or empty if none available - */ - public Optional getDurationUntilStartTrigger() { - if (startConditionManager == null) { - return Optional.empty(); - } - return startConditionManager.getDurationUntilNextTrigger(); - } - /** - * Gets a detailed description of the stop conditions status - * - * @return A string with detailed information about stop conditions - */ - public String getDetailedStopConditionsStatus() { - if (!hasAnyStopConditions()) { - return "No stop conditions defined"; - } - - StringBuilder sb = new StringBuilder("Stop conditions: "); - - // Add logic type - sb.append(stopConditionManager.requiresAll() ? "ALL must be met" : "ANY can be met"); - - // Add fulfillability status - if (!hasFullfillableStopConditions()) { - sb.append(" (UNFULFILLABLE)"); - } - - // Add condition count - int total = getTotalStopConditionCount(); - int satisfied = getSatisfiedStopConditionCount(); - sb.append(String.format(" - %d/%d conditions met", satisfied, total)); - - // Add next trigger time if available - Optional nextTrigger = getNextStopTriggerTime(); - if (nextTrigger.isPresent()) { - sb.append(" - Next trigger: ").append(getNextStopTriggerTimeString()); - } - - return sb.toString(); - } - /** - * Gets a detailed description of the start conditions status - * - * @return A string with detailed information about start conditions - */ - public String getDetailedStartConditionsStatus() { - if (!hasAnyStartConditions()) { - return "No start conditions defined"; - } - - StringBuilder sb = new StringBuilder("Start conditions: "); - - // Add logic type - sb.append(startConditionManager.requiresAll() ? "ALL must be met" : "ANY can be met"); - - // Add fulfillability status - if (!hasFullfillableStartConditions()) { - sb.append(" (UNFULFILLABLE)"); - } - - // Add condition count and satisfaction status - int totalStartConditions = startConditionManager.getConditions().size(); - long satisfiedStartConditions = startConditionManager.getConditions().stream() - .filter(Condition::isSatisfied) - .count(); - sb.append(String.format(" - %d/%d conditions met", satisfiedStartConditions, totalStartConditions)); - - // Add next trigger time if available - Optional nextTrigger = getCurrentStartTriggerTime(); - if (nextTrigger.isPresent()) { - sb.append(" - Next trigger: ").append(getCurrentStartTriggerTimeString()); - } - - return sb.toString(); - } - - /** - * Determines if the plugin should be started immediately based on its current - * start condition status - * - * @return true if the plugin should be started immediately - */ - public boolean shouldStartImmediately() { - // If no start conditions, don't start automatically - if (!hasAnyStartConditions()) { - return false; - } - - // If start conditions are met, start the plugin - if (areUserStartConditionsMet() && arePluginStartConditionsMet()) { - if ( startConditionManager.getConditions().isEmpty()){ - log.info("Plugin '{}' has no start conditions defined, starting immediately", name); - return false; - } - return true; - } - - return false; - } - public boolean canBeStarted() { - // If no start conditions, don't start automatically - if (isRunning()) { - return false; - } - if(!isEnabled()){ - return false; - } - - - // If start conditions are met, start the plugin - if (areUserStartConditionsMet() && arePluginStartConditionsMet()) { - return true; - } - - return false; - - } - - /** - * Logs the defined start conditions with their current states - */ - private void logDefinedStartConditionWithStates() { - logStartConditionsWithDetails(); - - // If the conditions are unfulfillable, log a warning - if (!hasFullfillableStartConditions()) { - log.warn("Plugin {} has unfulfillable start conditions - may not start properly", name); - } - - // Log progress percentage - double progress = startConditionManager.getProgressTowardNextTrigger(); - log.info("Plugin {} start condition progress: {:.2f}%", name, progress); - } - - /** - * Updates the isDueToRun method to use the diagnostic helper for logging - */ - public boolean isDueToRun() { - // Check if we're already running - if (isRunning()) { - return false; - } - - // For plugins with start conditions, check if those conditions are met - if (!hasAnyStartConditions()) { - //log.info("No start conditions defined for plugin '{}'", name); - return false; - } - - - - // Log at appropriate levels - if (Microbot.isDebug()) { - // Build comprehensive log info using our diagnostic helper - String diagnosticInfo = diagnoseStartConditions(); - // In debug mode, log the full detailed diagnostics - log.debug("\n[isDueToRun] - \n"+diagnosticInfo); - } - - - // Check if start conditions are met - return startConditionManager.areAllConditionsMet(); - } - - /** - * Updates the primary time condition for this plugin schedule entry. - * This method replaces the original time condition that was added when the entry was created, - * but preserves any additional conditions that might have been added later. - * - * @param newTimeCondition The new time condition to use - * @return true if a time condition was found and replaced, false otherwise - */ - public boolean updatePrimaryTimeCondition(TimeCondition newTimeCondition) { - if (startConditionManager == null || newTimeCondition == null) { - return false; - } - startConditionManager.pauseWatchdogs(); - // First, find the existing time condition. We'll assume the first time condition - // we find is the primary one that was added at creation - TimeCondition existingTimeCondition = this.mainTimeStartCondition; - - // If we found a time condition, replace it - if (existingTimeCondition != null) { - Optional currentTrigDateTime = existingTimeCondition.getCurrentTriggerTime(); - Optional newTrigDateTime = newTimeCondition.getCurrentTriggerTime(); - log.debug("Replacing time condition {} with {}", - existingTimeCondition.getDescription(), - newTimeCondition.getDescription()); - - - boolean isDefaultByScheduleType = this.isDefault(); - - - // Check if new condition is a one-second interval (default) - boolean willBeDefaultByScheduleType = false; - if (newTimeCondition instanceof IntervalCondition) { - IntervalCondition intervalCondition = (IntervalCondition) newTimeCondition; - if (intervalCondition.getInterval().getSeconds() <= 1) { - willBeDefaultByScheduleType = true; - } - } - - // Remove the existing condition and add the new one - if (startConditionManager.removeCondition(existingTimeCondition)) { - if (!startConditionManager.containsCondition(newTimeCondition)) { - startConditionManager.addUserCondition(newTimeCondition); - } - - // Update default status if needed - if (willBeDefaultByScheduleType) { - //this.setDefault(true); - //this.setPriority(0); - } else if (isDefaultByScheduleType && !willBeDefaultByScheduleType) { - // Only change from default if it was set automatically by condition type - //this.setDefault(false); - } - - this.mainTimeStartCondition = newTimeCondition; - } - if (currentTrigDateTime.isPresent() && newTrigDateTime.isPresent()) { - // Check if the new trigger time is different from the current one - if (!currentTrigDateTime.get().equals(newTrigDateTime.get())) { - log.debug("\n\tUpdated main start time for Plugin'{}'\nfrom {}\nto {}", - name, - currentTrigDateTime.get().format(DATE_TIME_FORMATTER), - newTrigDateTime.get().format(DATE_TIME_FORMATTER)); - } else { - log.debug("\n\tStart next time for Pugin '{}' remains unchanged", name); - } - } - } else { - // No existing time condition found, just add the new one - log.info("No existing time condition found, adding new condition: {}", - newTimeCondition.getDescription()); - // Check if the condition already exists before adding it - if (startConditionManager.containsCondition(newTimeCondition)) { - log.info("Condition {} already exists in the manager, not adding a duplicate", - newTimeCondition.getDescription()); - // Still need to update start conditions in case the existing one needs resetting - }else{ - startConditionManager.addUserCondition(newTimeCondition); - } - this.mainTimeStartCondition = newTimeCondition; - //updateStartConditions();// we have new condition -> new start time ? - } - startConditionManager.resumeWatchdogs(); - return true; - } - - /** - * Update the lastRunTime to now and reset start conditions - */ - private void resetStartConditions() { - if (startConditionManager == null) { - return; - } - - StringBuilder logMsg = new StringBuilder("\n"); - Optional nextTriggerTimeBeforeReset = getCurrentStartTriggerTime(); - - logMsg.append("Updating start conditions for plugin '").append(getCleanName()).append("'"); - logMsg.append("\n -last stop reason: ").append(lastStopReasonType.getDescription()); - logMsg.append("\n -last stop reason message:\n\t").append(lastStopReason); - logMsg.append("\n -allowContinue: ").append(allowContinue); - logMsg.append("\n -last run duration: ").append(lastRunDuration.toMillis()).append(" ms"); - if (this.lastStopReasonType != StopReason.INTERRUPTED || !allowContinue) { - - logMsg.append("\n -Completed successfully, resetting all start conditions"); - startConditionManager.reset(); - // Increment the run count since we completed a full run - incrementRunCount(); - } else { - logMsg.append("\n -Only resetting plugin '").append(getCleanName()).append("' start conditions"); - startConditionManager.resetPluginConditions(); - } - - Optional triggerTimeAfterReset = getCurrentStartTriggerTime(); - - // Update the nextRunTime for legacy compatibility if possible - if (triggerTimeAfterReset.isPresent()) { - ZonedDateTime nextRunTime = triggerTimeAfterReset.get(); - logMsg.append("\n - Updated run time for Plugin '").append(getCleanName()).append("'") - .append("\n Before: ").append(nextTriggerTimeBeforeReset.map(t -> t.format(DATE_TIME_FORMATTER)).orElse("N/A")) - .append("\n After: ").append(nextRunTime.format(DATE_TIME_FORMATTER)); - } else if (hasTriggeredOneTimeStartConditions() && !canStartTriggerAgain()) { - logMsg.append("\n - One-time conditions triggered, not scheduling next run"); - } - - // Output the consolidated log message - log.info(logMsg.toString()); - } - - /** - * Reset stop conditions - */ - private void resetStopConditions() { - if (!stopInitiated){ - log.info("resetting stop conditions on start up of plugin '{}'", name); - if (stopConditionManager != null) { - stopConditionManager.reset(); - // Log that stop conditions were reset - log.debug("Reset stop conditions for plugin '{}'", name); - } - }else{ - - } - } - /** - * Reset stop conditions - */ - public void hardResetConditions() { - - if (stopConditionManager != null) { - stopConditionManager.hardResetUserConditions(); - // Log that stop conditions were reset - log.debug("Hard Reset stop conditions for plugin '{}'", name); - } - if (startConditionManager != null) { - startConditionManager.hardResetUserConditions(); - // Log that stop conditions were reset - log.debug("Hard Reset start conditions for plugin '{}'", name); - } - - } - - - - /** - * Get a formatted display of the scheduling interval - */ - public String getIntervalDisplay() { - if (!hasAnyStartConditions()) { - return "No schedule defined"; - } - - List timeConditions = startConditionManager.getTimeConditions(); - if (timeConditions.isEmpty()) { - return "Non-time conditions only"; - } - - // Check for common condition types - if (timeConditions.size() == 1) { - TimeCondition condition = timeConditions.get(0); - - return getTimeDisplayFromTimeCondition(condition); - } - - // If we have multiple time conditions, find the one that will trigger first - if (timeConditions.size() > 1) { - TimeCondition earliestTriggerCondition = findEarliestTriggerTimeCondition(timeConditions); - if (earliestTriggerCondition != null) { - return getTimeDisplayFromTimeCondition(earliestTriggerCondition) + " (Next to trigger)"; - } - } - - // If we have multiple time conditions or other complex scenarios but couldn't determine earliest - return "Complex time schedule"; - } - - /** - * Finds the time condition that will trigger first among a list of time conditions - * - * @param timeConditions List of time conditions to check - * @return The time condition that will trigger first, or null if none is found - */ - private TimeCondition findEarliestTriggerTimeCondition(List timeConditions) { - ZonedDateTime earliestTriggerTime = null; - TimeCondition earliestCondition = null; - - for (TimeCondition condition : timeConditions) { - Optional triggerTime = condition.getCurrentTriggerTime(); - if (triggerTime.isPresent()) { - ZonedDateTime nextTrigger = triggerTime.get(); - - // If this is the first valid trigger time we've found, or it's earlier than our current earliest - if (earliestTriggerTime == null || nextTrigger.isBefore(earliestTriggerTime)) { - earliestTriggerTime = nextTrigger; - earliestCondition = condition; - } - } - } - - return earliestCondition; - } - private String getTimeDisplayFromTimeCondition(TimeCondition condition) { - if (condition instanceof SingleTriggerTimeCondition) { - Optional triggerTime = ((SingleTriggerTimeCondition) condition).getNextTriggerTimeWithPause(); - if (!triggerTime.isPresent()) { - return "No trigger time available"; - } - return "Once at " + triggerTime.get().format(DATE_TIME_FORMATTER); - } - else if (condition instanceof IntervalCondition) { - return formatIntervalCondition((IntervalCondition) condition); - } - else if (condition instanceof TimeWindowCondition) { - return formatTimeWindowCondition((TimeWindowCondition) condition); - } - else if (condition instanceof DayOfWeekCondition) { - return formatDayOfWeekCondition((DayOfWeekCondition) condition); - } - return "Unknown time condition type: " + condition.getClass().getSimpleName(); - } - - /** - * Formats an interval condition into a user-friendly string - */ - private String formatIntervalCondition(IntervalCondition condition) { - Duration avgInterval = condition.getInterval(); - Duration minInterval = condition.getMinInterval(); - Duration maxInterval = condition.getMaxInterval(); - boolean isRandomized = condition.isRandomize(); - - if (!isRandomized) { - return formatTimeRange(avgInterval, null, false); - } else { - return "Randomized " + formatTimeRange(minInterval, maxInterval, true); - } - } - - /** - * Formats a time window condition into a user-friendly string - */ - private String formatTimeWindowCondition(TimeWindowCondition condition) { - LocalTime startTime = condition.getStartTime(); - LocalTime endTime = condition.getEndTime(); - String timesStr = String.format("%s-%s", - startTime.format(DateTimeFormatter.ofPattern("HH:mm")), - endTime.format(DateTimeFormatter.ofPattern("HH:mm"))); - - // Check repeat cycle - String cycleStr = ""; - boolean useRandomization = false; - - try { - useRandomization = condition.isUseRandomization(); - RepeatCycle repeatCycle = condition.getRepeatCycle(); - int interval = condition.getRepeatIntervalUnit(); - - switch (repeatCycle) { - case DAYS: - cycleStr = (interval == 1) ? "daily" : "every " + interval + " days"; - break; - case WEEKS: - cycleStr = (interval == 1) ? "weekly" : "every " + interval + " weeks"; - break; - case HOURS: - cycleStr = (interval == 1) ? "hourly" : "every " + interval + " hours"; - break; - case MINUTES: - cycleStr = (interval == 1) ? "every minute" : "every " + interval + " minutes"; - break; - case ONE_TIME: - cycleStr = "once"; - break; - default: - cycleStr = "daily"; - } - } catch (Exception e) { - // Fallback if we can't access some property - cycleStr = "daily"; - } - - return useRandomization - ? String.format("Randomized %s %s", timesStr, cycleStr) - : String.format("%s %s", timesStr, cycleStr); - } - - /** - * Formats a day of week condition into a user-friendly string - */ - private String formatDayOfWeekCondition(DayOfWeekCondition condition) { - Set activeDays = condition.getActiveDays(); - - // Format day names - StringBuilder daysStr = new StringBuilder(); - - if (activeDays.size() == 7) { - daysStr.append("Every day"); - } else if (activeDays.size() == 5 && activeDays.contains(DayOfWeek.MONDAY) && - activeDays.contains(DayOfWeek.TUESDAY) && activeDays.contains(DayOfWeek.WEDNESDAY) && - activeDays.contains(DayOfWeek.THURSDAY) && activeDays.contains(DayOfWeek.FRIDAY)) { - daysStr.append("Weekdays"); - } else if (activeDays.size() == 2 && activeDays.contains(DayOfWeek.SATURDAY) && - activeDays.contains(DayOfWeek.SUNDAY)) { - daysStr.append("Weekends"); - } else { - List dayNames = new ArrayList<>(); - for (DayOfWeek day : activeDays) { - // Convert to short day name (Mon, Tue, etc.) - String dayName = day.toString().substring(0, 3); - dayNames.add(dayName.charAt(0) + dayName.substring(1).toLowerCase()); - } - // Sort days in week order (Monday first) - Collections.sort(dayNames); - daysStr.append(String.join("/", dayNames)); - } - - // Check if it has an interval condition - if (condition.hasIntervalCondition()) { - Optional intervalOpt = condition.getIntervalCondition(); - if (intervalOpt.isPresent()) { - IntervalCondition interval = intervalOpt.get(); - - // Add interval info - if (interval.isRandomize()) { - Duration minInterval = interval.getMinInterval(); - Duration maxInterval = interval.getMaxInterval(); - daysStr.append(", random ").append(formatTimeRange(minInterval, maxInterval, true)); - } else { - Duration avgInterval = interval.getInterval(); - daysStr.append(", ").append(formatTimeRange(avgInterval, null, false)); - } - } - } - - // Add max repeats information if applicable - long maxPerDay = condition.getMaxRepeatsPerDay(); - long maxPerWeek = condition.getMaxRepeatsPerWeek(); - - if (maxPerDay > 0 || maxPerWeek > 0) { - daysStr.append(" ("); - boolean needsComma = false; - - if (maxPerDay > 0) { - daysStr.append("max ").append(maxPerDay).append("/day"); - needsComma = true; - } - - if (maxPerWeek > 0) { - if (needsComma) { - daysStr.append(", "); - } - daysStr.append("max ").append(maxPerWeek).append("/week"); - } - - daysStr.append(")"); - } - - return daysStr.toString(); - } - - /** - * Helper to format time durations in a user-friendly string - */ - private String formatTimeRange(Duration duration, Duration maxDuration, boolean isRange) { - if (duration == null) { - return "unknown interval"; - } - - long hours = duration.toHours(); - long minutes = duration.toMinutes() % 60; - - if (!isRange) { - if (hours > 0) { - return String.format("every %d hour%s%s", - hours, - hours > 1 ? "s" : "", - minutes > 0 ? " " + minutes + " min" : ""); - } else { - return String.format("every %d minute%s", - minutes, - minutes > 1 ? "s" : ""); - } - } else { - // Format a range - "every X to Y hours/minutes" - if (maxDuration == null) { - return formatTimeRange(duration, null, false); - } - - long maxHours = maxDuration.toHours(); - long maxMinutes = maxDuration.toMinutes() % 60; - - if (hours > 0) { - if (maxHours > 0) { - // Both have hours component - String minStr = String.format("%d hour%s%s", - hours, - hours > 1 ? "s" : "", - minutes > 0 ? " " + minutes + "m" : ""); - - String maxStr = String.format("%d hour%s%s", - maxHours, - maxHours > 1 ? "s" : "", - maxMinutes > 0 ? " " + maxMinutes + "m" : ""); - - return String.format("every %s to %s", minStr, maxStr); - } else { - // Min has hours but max only has minutes - return String.format("every %d hour%s%s to %d minutes", - hours, - hours > 1 ? "s" : "", - minutes > 0 ? " " + minutes + "m" : "", - maxMinutes); - } - } else { - if (maxHours > 0) { - // Min has only minutes but max has hours - return String.format("every %d minutes to %d hour%s%s", - minutes, - maxHours, - maxHours > 1 ? "s" : "", - maxMinutes > 0 ? " " + maxMinutes + "m" : ""); - } else { - // Both only have minutes - return String.format("every %d to %d minute%s", - minutes, - maxMinutes, - maxMinutes > 1 ? "s" : ""); - } - } - } - } - - - /** - * Gets the time remaining until the next plugin - * - * @return Duration until next plugin or null if no plugins scheduled - */ - public Optional getTimeUntilNextRun() { - if (!enabled) { - return Optional.empty(); - } - // Get the next trigger time for this plugin - Optional nextTriggerTime = this.getCurrentStartTriggerTime(); - if (!nextTriggerTime.isPresent()) { - // If no trigger time is available, return empty - return Optional.empty(); - } - - // Calculate time until trigger - return Optional.of(Duration.between(ZonedDateTime.now(ZoneId.systemDefault()), nextTriggerTime.get())); - } - /** - * Get a formatted display of when this plugin will run next - */ - public String getNextRunDisplay() { - return getNextRunDisplay(System.currentTimeMillis()); - } - - /** - * Get a formatted display of when this plugin will run next, including - * condition information. - * - * @param currentTimeMillis Current system time in milliseconds - * @return Human-readable description of next run time or condition status - */ - public String getNextRunDisplay(long currentTimeMillis) { - if (!enabled) { - return "Disabled"; - } - - // If plugin is running, show progress or status information - if (isRunning()) { - String prefixLabel = "Running"; - if(stopConditionManager.isPaused()){ - prefixLabel = "Paused"; - } - - if (!stopConditionManager.getConditions().isEmpty()) { - double progressPct = getStopConditionProgress(); - if (progressPct > 0 && progressPct < 100) { - return String.format("%s (%.1f%% complete)", prefixLabel,progressPct); - } - return String.format("%s with conditions", prefixLabel); - } - return prefixLabel; - } - - // Check for start conditions - if (hasAnyStartConditions()) { - // Check if we can determine the next trigger time - Optional nextTrigger = getCurrentStartTriggerTime(); - if (nextTrigger.isPresent()) { - ZonedDateTime triggerTime = nextTrigger.get(); - ZonedDateTime currentTime = ZonedDateTime.ofInstant( - Instant.ofEpochMilli(currentTimeMillis), - ZoneId.systemDefault()); - - // If it's due to run now - if (!currentTime.isBefore(triggerTime)) { - return "Due to run"; - } - - // Calculate time until next run - Duration timeUntil = Duration.between(currentTime, triggerTime); - long hours = timeUntil.toHours(); - long minutes = timeUntil.toMinutes() % 60; - long seconds = timeUntil.getSeconds() % 60; - - if (hours > 0) { - return String.format("In %dh %dm", hours, minutes); - } else if (minutes > 0) { - return String.format("In %dm %ds", minutes, seconds); - } else { - return String.format("In %ds", seconds); - } - } else if (shouldStartImmediately()) { - return "Due to run"; - } else if (hasTriggeredOneTimeStartConditions() && !canStartTriggerAgain()) { - return "Completed"; - } - - return "Waiting for conditions"; - } - - - - return "Schedule not set"; - } - - /** - * Adds a user-defined start condition to this plugin schedule entry. - * Start conditions determine when the plugin should be executed. - * - * @param condition The condition to add to the start conditions list - */ - public void addStartCondition(Condition condition) { - startConditionManager.addUserCondition(condition); - } - - /** - * Adds a user-defined stop condition to this plugin schedule entry. - * Stop conditions determine when the plugin should terminate. - * - * @param condition The condition to add to the stop conditions list - */ - public void addStopCondition(Condition condition) { - stopConditionManager.addUserCondition(condition); - } - - /** - * Returns all stop conditions configured for this plugin schedule entry. - * - * @return List of currently active stop conditions - */ - public List getStopConditions() { - return stopConditionManager.getConditions(); - } - - /** - * Checks whether any stop conditions are defined for this plugin. - * - * @return true if at least one stop condition exists, false otherwise - */ - public boolean hasStopConditions() { - return stopConditionManager.hasConditions(); - } - - /** - * Checks whether any start conditions are defined for this plugin. - * - * @return true if at least one start condition exists, false otherwise - */ - public boolean hasStartConditions() { - return startConditionManager.hasConditions(); - } - - /** - * Returns all start conditions configured for this plugin schedule entry. - * - * @return List of currently active start conditions - */ - public List getStartConditions() { - return startConditionManager.getConditions(); - } - - /** - * Determines if the plugin can be stopped based on its current state and conditions. - *

- * A plugin can be stopped if: - *

    - *
  • It has finished its task
  • - *
  • It is running but has been disabled
  • - *
  • Plugin-defined stop conditions are met
  • - *
- * - * @return true if the plugin can be stopped, false otherwise - */ - public boolean allowedToBeStop() { - - if (isRunning()) { - if (!isEnabled()){ - return true; //enabled was disabled -> stop the plugin gracefully -> soft stop should be trigged when possible - } - } - // Check if conditions are met and we should stop when conditions are met - if (arePluginStopConditionsMet() ) { - return true; - } - - return false; - } - public boolean shouldBeStopped() { - if (isRunning()) { - if (!isEnabled()){ - return true; //enabled was disabled -> stop the plugin gracefully -> soft stop should be trigged when possible - } - } - // Check if conditions are met and we should stop when conditions are met - if (arePluginStopConditionsMet() && (areUserDefinedStopConditionsMet())) { - if (stopConditionManager.getUserConditions().isEmpty()) { - //* -> do not stop a plugin if there are no user defined stop conditions - //* -> in that case the plugin must report finished to be stop or the user must manually stop it - return false; // we have plugin stop conditions -> we can stop the plugin -> stop condition of the plugin are look conditions, so if no condition are defined, we are allow to stop the plugin - } - return true; - } - - return false; - } - - /** - * Checks if plugin-defined stop conditions are met. - * If no plugin conditions are defined, returns true to allow stopping. - * - * @return true if plugin-defined stop conditions are met or none are defined - */ - private boolean arePluginStopConditionsMet() { - if (stopConditionManager.getPluginConditions().isEmpty()) { - return true; // we have plugin stop conditions -> we can stop the plugin -> stop condition of the plugin are look conditions, so if no condition are defined, we are allow to stop the plugin - } - return stopConditionManager.arePluginConditionsMet(); - } - - /** - * Checks if user-defined stop conditions are met. - * These are conditions added through the UI rather than by the plugin itself. - - * @return true if user-defined stop conditions are met, false if none exist or they're not met - */ - private boolean areUserDefinedStopConditionsMet() { - if (stopConditionManager.getUserConditions().isEmpty()) { - return true; - } - return stopConditionManager.areUserConditionsMet(); - } - - /** - * Checks if user-defined start conditions are met. - * These are conditions added through the UI rather than by the plugin itself. - * - * @return true if user-defined start conditions are met, false if none exist or they're not met - */ - private boolean areUserStartConditionsMet() { - if (startConditionManager.getUserConditions().isEmpty()) { - return true; - } - return startConditionManager.areUserConditionsMet(); - } - /** - * Checks if plugin-defined start conditions are met. - * If no plugin conditions are defined, returns true to allow starting. - * - * @return true if plugin-defined start conditions are met or none are defined - */ - private boolean arePluginStartConditionsMet() { - if (startConditionManager.getPluginConditions().isEmpty()) { - return true; // we have plugin start conditions -> we can start the plugin -> start condition of the plugin are look conditions, so if no condition are defined, we are allow to start the plugin - } - return startConditionManager.arePluginConditionsMet(); - } - /** - * Gets a description of the stop conditions for this plugin. - * - * @return A string describing the stop conditions - */ - public String getConditionsDescription() { - return stopConditionManager.getDescription(); - } - - /** - * Stops the plugin with ExecutionResult and custom message for the stop reason. - *

- * This method handles the graceful shutdown of a plugin by first attempting a soft - * stop, with a custom message indicating why the plugin was stopped. The ExecutionResult - * determines whether the plugin should be disabled (HARD_FAILURE) or remain enabled (SUCCESS/SOFT_FAILURE). - * - * @param result the execution result indicating success state - * @param reason the enum reason why the plugin is being stopped - * @param reasonMessage a custom message explaining why the plugin was stopped - * @return true if stop was initiated, false otherwise - */ - public boolean stop(ExecutionResult result, StopReason reason, String reasonMessage) { - // Set the custom stop reason message - if (!stopInitiated){ - this.lastStopReason = reasonMessage; - } - - // Call the ExecutionResult-based stop method - return stop(result, reason); - } - - /** - * @deprecated Use {@link #stop(ExecutionResult, StopReason, String)} instead for granular result reporting - */ - @Deprecated - public boolean stop(boolean successfulRun, StopReason reason, String reasonMessage) { - ExecutionResult result = successfulRun ? ExecutionResult.SUCCESS : ExecutionResult.HARD_FAILURE; - return stop(result, reason, reasonMessage); - } - /** - * Initiates the stopping process for a plugin with ExecutionResult-based monitoring. - *

- * This method handles the graceful shutdown of a plugin by first attempting a soft - * stop, which allows the plugin to finish any critical operations. The ExecutionResult - * determines whether the plugin should be disabled (HARD_FAILURE) or remain enabled (SUCCESS/SOFT_FAILURE). - *

- * For SOFT_FAILURE, the plugin remains enabled but the failure is tracked. After - * multiple consecutive soft failures, the plugin may be disabled. - * - * @param result the execution result indicating success state - * @param reason the enum reason why the plugin is being stopped - * @return true if stop was initiated, false otherwise - */ - public boolean stop(ExecutionResult result, StopReason reason) { - // Handle soft failure tracking - if (result == ExecutionResult.SOFT_FAILURE) { - recordSoftFailure(reason.getDescription()); - // If soft failures exceed threshold, convert to hard failure and disable - if (shouldConvertToHardFailure()) { - log.warn("Plugin '{}' exceeded soft failure threshold ({}), converting to hard failure and disabling", - getCleanName(), getMaxSoftFailuresBeforeHardFailure()); - result = ExecutionResult.HARD_FAILURE; - } - } else if (result == ExecutionResult.SUCCESS) { - recordSuccess(); - } - - // Convert ExecutionResult to boolean for existing logic - boolean successfulRun = result.isSuccess(); - - // Call the existing stop method with boolean - boolean stopInitiated = stop(successfulRun, reason); - if (this.getStopConditions() != null ){ - // Check if any LockConditions are still locked - boolean hasLockedConditions = this.getStopConditions().stream() - .filter(condition -> condition instanceof LockCondition) - .map(condition -> (LockCondition) condition) - .anyMatch(LockCondition::isLocked); - if(hasLockedConditions){ - //Should be done by the plugin itself, on shout down.. - //-- it also should only be relevant on a hard stop-> because otherwise the plugin should not be able to be stoped at the first hand** - //-- safeguard only, if the plugin is not running any more - if (reason != StopReason.HARD_STOP ){ - log.warn("Plugin '{}' has locked conditions but stop reason is '{}'. This may indicate the plugin did not handle shutdown properly.", - this.getCleanName(), reason); - } - log.debug("Unlocking any remaining lock conditions for the plugin '{}'", this.getCleanName()); - this.getStopConditions().stream() - .filter(condition -> condition instanceof LockCondition) - .map(condition -> (LockCondition) condition) - .forEach(lockCondition -> { - if (lockCondition.isLocked()) { - log.debug("Unlocking condition: {}", lockCondition.getReason()); - lockCondition.unlock(); - } - }); - - } - } - // Handle disabling for hard failures AFTER the stop is initiated - if (stopInitiated && result == ExecutionResult.HARD_FAILURE) { - log.warn("Plugin '{}' hard failure - will be disabled after stopping", getCleanName()); - // Schedule the disable operation to run after the plugin stops - // This will be handled in the monitoring thread's finally block - // by checking if lastRunSuccessful is false - } - - return stopInitiated; - } - - /** - * @deprecated Use {@link #stop(ExecutionResult, StopReason)} instead for granular result reporting - */ - @Deprecated - public boolean stop(boolean successfulRun, StopReason reason) { - ZonedDateTime now = ZonedDateTime.now(); - // Initial stop attempt - if (allowedToBeStop() || reason == StopReason.HARD_STOP || reason == StopReason.PREPOST_SCHEDULE_STOP || reason == StopReason.PLUGIN_FINISHED ){ - if(!stopInitiated){ - if (stopInitiatedTime == null) { - stopInitiatedTime = now; - } - if (lastStopAttemptTime == null) { - lastStopAttemptTime = now; - } - this.setLastRunSuccessful(successfulRun); - this.setLastStopReasonType(reason); - this.onLastStopPluginConditionsSatisfied = arePluginStopConditionsMet(); - this.onLastStopUserConditionsSatisfied = areUserDefinedStopConditionsMet(); - } - StringBuilder logMsg = new StringBuilder(); - logMsg.append("\n\tStopping the plugin \"").append(getCleanName()+"\""); - String blockingStartMsg = startConditionManager.getBlockingExplanation(); - String blockingStopMsg = stopConditionManager.getBlockingExplanation(); - if (reason != null) { - logMsg.append("\n\t---current stop reason:").append("\n\t\t"+reason.toString()); - if (this.lastStopReason != null && !this.lastStopReason.isEmpty()) { - logMsg.append("\n\t---last stop reason:\n********\n").append(this.lastStopReason+ "\n********"); - } - } - logMsg.append("\n\t---is running: ").append(isRunning()); - logMsg.append("\n\t---plugin stop conditions satisfied: ").append(arePluginStopConditionsMet()); - logMsg.append("\n\t---user stop conditions satisfied: ").append(areUserDefinedStopConditionsMet()); - log.info(logMsg.toString()); - logStopConditionsWithDetails(); - if (!stopInitiated && reason != StopReason.HARD_STOP && reason != StopReason.PREPOST_SCHEDULE_STOP) { - this.softStop(reason); // This will start the monitoring thread - }else if (reason == StopReason.HARD_STOP || reason == StopReason.PREPOST_SCHEDULE_STOP) { - // If we are already stopping and the reason is hard stop, just log it - this.hardStop(reason); // frist try soft stop, then hard stop if needed - } - }else{ - StringBuilder logMsg = new StringBuilder(); - logMsg.append("\n\tPlugin ").append(name).append(" is not allowed to stop. "); - String blockingStartMsg = startConditionManager.getBlockingExplanation(); - String blockingStopMsg = stopConditionManager.getBlockingExplanation(); - if (blockingStopMsg != null) { - logMsg.append("\n\t -Blocking reason: ").append(blockingStopMsg); - } - if (reason != null) { - logMsg.append("\n\t -Current stop reason: ").append(reason.toString()).append(" -- ").append(this.lastStopReason); - } - logMsg.append("\n\t -is running: ").append(isRunning()); - logMsg.append("\n\t -plugin stop conditions: ").append(arePluginStopConditionsMet()); - logMsg.append("\n\t -user stop conditions: ").append(areUserDefinedStopConditionsMet()); - log.info(logMsg.toString()); - } - log.info("\n\tPlugin {} stop initiated: {}", name, stopInitiated); - return this.stopInitiated; - } - private void stop(boolean successfulRun) { - ZonedDateTime now = ZonedDateTime.now(); - // Plugin didn't stop after previous attempts - if (isRunning()) { - Duration timeSinceFirstAttempt = Duration.between(this.stopInitiatedTime, now); - Duration timeSinceLastAttempt = Duration.between(this.lastStopAttemptTime, now); - // Force hard stop if we've waited too long - if ( (hardStopTimeout.compareTo(Duration.ZERO) > 0 && timeSinceFirstAttempt.compareTo(hardStopTimeout) > 0) - && (getPlugin() instanceof SchedulablePlugin) - && ((SchedulablePlugin) getPlugin()).allowHardStop()) { - log.warn("Plugin {} failed to respond to soft stop after {} seconds - forcing hard stop", - name, timeSinceFirstAttempt.toSeconds()); - - // Stop current monitoring and start new one for hard stop - stopMonitoringThread(); - this.setLastStopReasonType(StopReason.HARD_STOP); - this.hardStop(StopReason.HARD_STOP); - }else if(getLastStopReasonType() == StopReason.HARD_STOP){ // Stop current monitoring and start new one for hard stop - log.warn("Plugin {} user requested hard stop after {} seconds - forcing hard stop", - name, timeSinceFirstAttempt.toSeconds()); - stopMonitoringThread(); - this.setLastStopReasonType(StopReason.HARD_STOP); - this.hardStop(StopReason.HARD_STOP); - }else if (getLastStopReasonType() == StopReason.PREPOST_SCHEDULE_STOP){ - stopMonitoringThread(); - this.setLastStopReasonType(StopReason.PREPOST_SCHEDULE_STOP); - this.hardStop(StopReason.PREPOST_SCHEDULE_STOP); - } - // Retry soft stop at configured intervals - else if (timeSinceLastAttempt.compareTo(softStopRetryInterval) > 0) { - log.info("Plugin {} still running after soft stop - retrying (attempt time: {} seconds)", - name, timeSinceFirstAttempt.toSeconds()); - lastStopAttemptTime = now; - this.setLastStopReasonType(getLastStopReasonType()); - this.softStop(getLastStopReasonType()); - }else if (hardStopTimeout.compareTo(Duration.ZERO) > 0 && timeSinceFirstAttempt.compareTo(hardStopTimeout.multipliedBy(3)) > 0) { - log.error("Forcibly shutting down the client due to unresponsive plugin: {}", name); - // Schedule client shutdown on the client thread to ensure it happens safely - Microbot.getClientThread().invoke(() -> { - try { - // Log that we're shutting down - log.warn("Initiating emergency client shutdown due to plugin: {} cant be stopped", name); - // Give a short delay for logging to complete - Thread.sleep(1000); - // Forcibly exit the JVM with a non-zero status code to indicate abnormal termination - System.exit(1); - } catch (Exception e) { - log.error("Failed to shut down client", e); - // Ultimate fallback - Runtime.getRuntime().halt(1); - } - return true; - }); - } - } - } - - /** - * Checks if the plugin should be stopped based on its conditions and performs a stop if needed. - *

- * This method evaluates both plugin-defined and user-defined stop conditions to determine - * if the plugin should be stopped. If conditions indicate the plugin should stop, it initiates - * the stop process. - *

- * It also handles resetting stop state if conditions no longer require the plugin to stop. - * - * @param successfulRun whether to mark this run as successful when stopping - * @return true if stop process was initiated or is in progress, false otherwise - */ - public boolean checkConditionsAndStop(boolean successfulRun) { - - if (shouldBeStopped()) { - if (!this.stopInitiated){ - this.stopInitiated = this.stop(successfulRun,StopReason.SCHEDULED_STOP); - } - // Monitor thread will handle the successful stop case - } - // Reset stop tracking if conditions no longer require stopping - else if (!isRunning() && stopInitiated) { - log.info("Plugin {} conditions no longer require stopping - resetting stop state", name); - this.stopInitiated = false; - this.stopInitiatedTime = null; - this.lastStopAttemptTime = null; - stopMonitoringThread(); - } - return this.stopInitiated; - - } - - /** - * Logs all defined conditions when plugin starts - */ - private void logStopConditions() { - List conditionList = stopConditionManager.getConditions(); - logConditionInfo(conditionList,"Defined Stop Conditions", true); - } - - /** - * Logs which conditions are met and which aren't when plugin stops - */ - private void logStopConditionsWithDetails() { - List conditionList = stopConditionManager.getConditions(); - logConditionInfo(conditionList,"Defined Stop Conditions", true); - } - - - - - /** - * Creates a consolidated log of all condition-related information - * @param logINFOHeader The header to use for the log message - * @param includeDetails Whether to include full details of conditions - */ - public void logConditionInfo(List conditionList, String logINFOHeader, boolean includeDetails) { - - StringBuilder sb = new StringBuilder(); - - sb.append("\n\tPlugin '").append(cleanName).append("' [").append(logINFOHeader).append("]: "); - - if (conditionList.isEmpty()) { - sb.append("\n\t\tNo stop conditions defined"); - log.info(sb.toString()); - return; - } - - // Basic condition count and logic - sb.append(" \n\t\t"+conditionList.size()+" condition(s) using ") - .append(stopConditionManager.requiresAll() ? "AND" : "OR").append(" logic\n\t\t"); - - if (!includeDetails) { - log.info(sb.toString()); - return; - } - - // Detailed condition listing with status - - int metCount = 0; - - for (int i = 0; i < conditionList.size(); i++) { - Condition condition = conditionList.get(i); - boolean isSatisfied = condition.isSatisfied(); - if (isSatisfied) metCount++; - - // Use the new getStatusInfo method for detailed status - sb.append(" ").append(i + 1).append(". ") - .append(condition.getStatusInfo(0, includeDetails).replace("\n", "\n\t\t ")); - - sb.append("\n\t\t"); - } - - if (includeDetails) { - sb.append("Summary: ").append(metCount).append("/").append(conditionList.size()) - .append(" conditions met"); - } - - log.info(sb.toString()); - } - /** - * Registers conditions from the plugin in an efficient manner. - * This method uses the new updatePluginCondition approach to intelligently - * merge conditions while preserving state and reducing unnecessary reinitializations. - * - * @param updateMode Controls how conditions are merged (default: ADD_ONLY) - */ - private void registerPluginConditions(UpdateOption updateOption) { - if (this.plugin == null) { - this.plugin = getPlugin(); - } - - log.debug("Registering plugin conditions for plugin '{}' with update mode: {}", name, updateOption); - - // Register start conditions - boolean startConditionsUpdated = registerPluginStartingConditions(updateOption); - - // Register stop conditions - boolean stopConditionsUpdated = registerPluginStoppingConditions(updateOption); - - if (startConditionsUpdated || stopConditionsUpdated) { - log.debug("Successfully updated plugin conditions for '{}'", name); - // Optimize structure if changes were made - if (updateOption != UpdateOption.REMOVE_ONLY) { - optimizeConditionStructures(); - } - } else { - log.debug("No changes needed to plugin conditions for '{}'", name); - } - } - - /** - * Default version of registerPluginConditions that uses ADD_ONLY mode - */ - private void registerPluginConditions() { - registerPluginConditions(UpdateOption.SYNC); - } - - /** - * Registers or updates starting conditions from the plugin. - * Uses the updatePluginCondition method to efficiently merge conditions. - * - * @return true if conditions were updated, false if no changes were needed - */ - private boolean registerPluginStartingConditions(UpdateOption updateOption) { - if (this.plugin == null) { - this.plugin = getPlugin(); - } - - log.debug("Registering start conditions for plugin '{}'", name); - this.startConditionManager.pauseWatchdogs(); - this.startConditionManager.setPluginCondition(new OrCondition()); - if (!(this.plugin instanceof SchedulablePlugin)) { - log.debug("Plugin '{}' is not a SchedulablePlugin, skipping start condition registration", name); - return false; - } - - SchedulablePlugin provider = (SchedulablePlugin) plugin; - - // Get conditions from the provider - if (provider.getStartCondition() == null) { - log.warn("Plugin '{}' implements ConditionProvider but provided no start conditions", plugin.getName()); - return false; - } - - List pluginConditions = provider.getStartCondition().getConditions(); - if (pluginConditions == null || pluginConditions.isEmpty()) { - log.debug("Plugin '{}' provided no explicit start conditions", plugin.getName()); - return false; - } - - // Get or create plugin's logical structure - LogicalCondition pluginLogic = provider.getStartCondition(); - - if (pluginLogic == null) { - log.warn("Plugin '{}' returned null start condition", name); - return false; - } - // Use the new update method with the specified option - boolean updated = getStartConditionManager().updatePluginCondition(pluginLogic, updateOption); - - // Log with a consolidated method if changes were made - if (updated) { - log.debug("Updated start conditions for plugin '{}'", name); - logStartConditionsWithDetails(); - - // Validate the condition structure - validateStartConditions(); - } - this.startConditionManager.resumeWatchdogs(); - - return updated; - } - - /** - * Registers or updates stopping conditions from the plugin. - * Uses the updatePluginCondition method to efficiently merge conditions. - * - * @return true if conditions were updated, false if no changes were needed - */ - private boolean registerPluginStoppingConditions(UpdateOption updateOption) { - if (this.plugin == null) { - this.plugin = getPlugin(); - } - this.stopConditionManager.pauseWatchdogs(); - this.stopConditionManager.setPluginCondition(new OrCondition()); - log.debug("Registering stopping conditions for plugin '{}'", name); - - if (!(this.plugin instanceof SchedulablePlugin)) { - log.debug("Plugin '{}' is not a SchedulablePlugin, skipping stop condition registration", name); - return false; - } - - SchedulablePlugin provider = (SchedulablePlugin) plugin; - - // Get conditions from the provider - if (provider.getStopCondition() == null) { - log.debug("Plugin '{}' provided no explicit stop conditions", plugin.getName()); - return false; - } - List pluginConditions = provider.getStopCondition().getConditions(); - if (pluginConditions == null || pluginConditions.isEmpty()) { - log.debug("Plugin '{}' provided no explicit stop conditions", plugin.getName()); - return false; - } - - // Get plugin's logical structure - LogicalCondition pluginLogic = provider.getStopCondition(); - - if (pluginLogic == null) { - log.warn("Plugin '{}' returned null stop condition", name); - return false; - } - - // Use the new update method with the specified option - boolean updated = getStopConditionManager().updatePluginCondition(pluginLogic, updateOption); - - // Log with the consolidated method if changes were made - if (updated) { - log.debug("Updated stop conditions for plugin '{}'", name); - logStopConditionsWithDetails(); - - // Validate the condition structure - validateStopConditions(); - } - this.stopConditionManager.resumeWatchdogs(); - - return updated; - } - - /** - * Creates and schedules watchdogs to monitor for condition changes from the plugin. - * This allows plugins to dynamically update their conditions at runtime, - * and have those changes automatically detected and integrated. - * - * Both start and stop condition watchdogs are scheduled using the shared thread pool - * from ConditionManager to avoid creating redundant resources. - * - * @param checkIntervalMillis How often to check for condition changes in milliseconds - * @param updateMode Controls how conditions are merged during updates - * @return true if at least one watchdog was successfully scheduled - */ - public boolean scheduleConditionWatchdogs(long checkIntervalMillis, UpdateOption updateOption) { - if(this.plugin == null) { - this.plugin = getPlugin(); - } - - if (!watchdogsEnabled) { - log.debug("Watchdogs are disabled for '{}', not scheduling", name); - return false; - } - - log.debug("\nScheduling condition watchdogs for plugin \n\t:'{}' with interval {}ms using update mode: {}", - name, checkIntervalMillis, updateOption); - - if (!(this.plugin instanceof SchedulablePlugin)) { - log.debug("Cannot schedule condition watchdogs for non-SchedulablePlugin"); - return false; - } - - // Cancel any existing watchdog tasks first - //cancelConditionWatchdogs(); - - SchedulablePlugin schedulablePlugin = (SchedulablePlugin) this.plugin; - boolean anyScheduled = false; - - try { - // Create suppliers that get the current plugin conditions - Supplier startConditionSupplier = - () -> schedulablePlugin.getStartCondition(); - - Supplier stopConditionSupplier = - () -> schedulablePlugin.getStopCondition(); - - // Schedule the start condition watchdog - startConditionWatchdogFuture = startConditionManager.scheduleConditionWatchdog( - startConditionSupplier, - checkIntervalMillis, - updateOption - ); - - // Schedule the stop condition watchdog - stopConditionWatchdogFuture = stopConditionManager.scheduleConditionWatchdog( - stopConditionSupplier, - checkIntervalMillis, - updateOption - ); - - anyScheduled = true; - log.debug("Scheduled condition watchdogs for plugin '{}' with interval {} ms using update mode: {}", - name, checkIntervalMillis, updateOption); - } catch (Exception e) { - log.error("Failed to schedule condition watchdogs for '{}'", name, e); - } - - return anyScheduled; - } - - /** - * Schedules condition watchdogs with the default ADD_ONLY update mode. - * - * @param checkIntervalMillis How often to check for condition changes in milliseconds - * @return true if at least one watchdog was successfully scheduled - */ - public boolean scheduleConditionWatchdogs(long checkIntervalMillis) { - return scheduleConditionWatchdogs(checkIntervalMillis, UpdateOption.SYNC); - } - -/** - * Validates the start conditions structure and logs any issues found. - * This helps identify potential problems with condition hierarchies. - */ - private void validateStartConditions() { - LogicalCondition startLogical = getStartConditionManager().getFullLogicalCondition(); - if (startLogical != null) { - List issues = startLogical.validateStructure(); - if (!issues.isEmpty()) { - log.warn("Validation issues found in start conditions for '{}':", name); - for (String issue : issues) { - log.warn(" - {}", issue); - } - } - } - } - /** - * Validates the stop conditions structure and logs any issues found. - * This helps identify potential problems with condition hierarchies. - */ - private void validateStopConditions() { - LogicalCondition stopLogical = getStopConditionManager().getFullLogicalCondition(); - if (stopLogical != null) { - List issues = stopLogical.validateStructure(); - if (!issues.isEmpty()) { - log.warn("Validation issues found in stop conditions for '{}':", name); - for (String issue : issues) { - log.warn(" - {}", issue); - } - } - } - } - /** - * Optimizes both start and stop condition structures by flattening unnecessary nesting - * and removing empty logical conditions. - */ - private void optimizeConditionStructures() { - // Optimize start conditions - LogicalCondition startLogical = getStartConditionManager().getFullLogicalCondition(); - if (startLogical != null) { - boolean optimized = startLogical.optimizeStructure(); - if (optimized) { - log.debug("Optimized start condition structure for '{}'", name); - } - } - - // Optimize stop conditions - LogicalCondition stopLogical = getStopConditionManager().getFullLogicalCondition(); - if (stopLogical != null) { - boolean optimized = stopLogical.optimizeStructure(); - if (optimized) { - log.debug("Optimized stop condition structure for '{}'", name); - } - } - } - - - /** - * Checks if any condition watchdogs are currently active for this plugin. - * - * @return true if at least one watchdog is active - */ - public boolean hasActiveWatchdogs() { - return (startConditionManager != null && startConditionManager.areWatchdogsRunning()) || - (stopConditionManager != null && stopConditionManager.areWatchdogsRunning()); - } - - /** - * Properly clean up resources when this object is closed or disposed. - * This is more reliable than using finalize() which is deprecated. - */ - @Override - public void close() { - // Clean up watchdogs and other resources - //cancelConditionWatchdogs(); - - // Stop any monitoring threads - stopMonitoringThread(); - stopStartupWatchdog(); - // Ensure both condition managers are closed properly - if (startConditionManager != null) { - startConditionManager.close(); - } - - if (stopConditionManager != null) { - stopConditionManager.close(); - } - - log.debug("Resources cleaned up for plugin schedule entry: '{}'", name); - } - - /** - * Calculates overall progress percentage across all conditions. - * This respects the logical structure of conditions. - * Returns 0 if progress cannot be determined. - */ - public double getStopConditionProgress() { - // If there are no conditions, no progress to report - if (stopConditionManager == null || stopConditionManager.getConditions().isEmpty()) { - return 0; - } - - // If using logical root condition, respect its logical structure - LogicalCondition rootLogical = stopConditionManager.getFullLogicalCondition(); - if (rootLogical != null) { - return rootLogical.getProgressPercentage(); - } - - // Fallback for direct condition list: calculate based on AND/OR logic - boolean requireAll = stopConditionManager.requiresAll(); - List conditions = stopConditionManager.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 total number of conditions being tracked. - */ - public int getTotalStopConditionCount() { - if (stopConditionManager == null) { - return 0; - } - - LogicalCondition rootLogical = stopConditionManager.getFullLogicalCondition(); - if (rootLogical != null) { - return rootLogical.getTotalConditionCount(); - } - - return stopConditionManager.getConditions().stream() - .mapToInt(Condition::getTotalConditionCount) - .sum(); - } - - /** - * Gets the number of conditions that are currently met. - */ - public int getSatisfiedStopConditionCount() { - if (stopConditionManager == null) { - return 0; - } - - LogicalCondition rootLogical = stopConditionManager.getFullLogicalCondition(); - if (rootLogical != null) { - return rootLogical.getMetConditionCount(); - } - - return stopConditionManager.getConditions().stream() - .mapToInt(Condition::getMetConditionCount) - .sum(); - } - public LogicalCondition getLogicalStopCondition() { - return stopConditionManager.getFullLogicalCondition(); - } - - - // Add getter/setter for the new fields - public boolean isAllowRandomScheduling() { - return allowRandomScheduling; - } - - public void setAllowRandomScheduling(boolean allowRandomScheduling) { - this.allowRandomScheduling = allowRandomScheduling; - } - - public int getRunCount() { - return runCount; - } - - private void incrementRunCount() { - this.runCount++; - } - - // Setter methods for the configurable timeouts - public void setSoftStopRetryInterval(Duration interval) { - if (interval == null || interval.isNegative() || interval.isZero()) { - return; // Invalid interval, do not set - } - if(interval.compareTo(Duration.ofSeconds(30)) < 0) { - interval = Duration.ofSeconds(30); // Ensure minimum interval of 1 second - } - this.softStopRetryInterval = interval; - } - - public void setHardStopTimeout(Duration timeout) { - this.hardStopTimeout = timeout; - } - - - - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != getClass()) return false; - - PluginScheduleEntry that = (PluginScheduleEntry) o; - - // Two entries are equal if: - // 1. They have the same name AND - // 2. They have the same start conditions and stop conditions - // OR they are the same object reference - - if (!Objects.equals(name, that.name)) return false; - - // If they're the same name, we need to distinguish by conditions - if (startConditionManager != null && that.startConditionManager != null) { - if (!startConditionManager.getConditions().equals(that.startConditionManager.getConditions())) { - return false; - } - } else if (startConditionManager != null || that.startConditionManager != null) { - return false; - } - - if (stopConditionManager != null && that.stopConditionManager != null) { - return stopConditionManager.getConditions().equals(that.stopConditionManager.getConditions()); - } else { - return stopConditionManager == null && that.stopConditionManager == null; - } - } - - @Override - public int hashCode() { - int result = name != null ? name.hashCode() : 0; - result = 31 * result + (startConditionManager != null ? startConditionManager.getConditions().hashCode() : 0); - result = 31 * result + (stopConditionManager != null ? stopConditionManager.getConditions().hashCode() : 0); - return result; - } - - public int getPriority() { - return priority; - } - - public void setPriority(int priority) { - this.priority = priority; - } - - public boolean isDefault() { - return isDefault; - } - - public void setDefault(boolean isDefault) { - this.isDefault = isDefault; - } - /** - * Generic helper method to build condition diagnostics for both start and stop conditions - * - * @param isStartCondition Whether to diagnose start conditions (true) or stop conditions (false) - * @return A detailed diagnostic string - */ - private String buildConditionDiagnostics(boolean isStartCondition) { - StringBuilder sb = new StringBuilder(); - String conditionType = isStartCondition ? "Start" : "Stop"; - ConditionManager conditionManager = isStartCondition ? startConditionManager : stopConditionManager; - List conditions = isStartCondition ? getStartConditions() : getStopConditions(); - - // Header with plugin name - sb.append("[").append(cleanName).append("] ").append(conditionType).append(" condition diagnostics:\n"); - - // Check if running (only relevant for start conditions) - if (isStartCondition && isRunning()) { - sb.append("- Plugin is already running (will not start again until stopped)\n"); - return sb.toString(); - } - - // Check for conditions - if (conditions.isEmpty()) { - sb.append("- No ").append(conditionType.toLowerCase()).append(" conditions defined\n"); - return sb.toString(); - } - - // Condition logic type - sb.append("- Logic: ") - .append(conditionManager.requiresAll() ? "ALL conditions must be met" : "ANY condition can be met") - .append("\n"); - - // Condition description - sb.append("- Conditions: ") - .append(conditionManager.getDescription()) - .append("\n"); - - // Check if they can be fulfilled - boolean canBeFulfilled = isStartCondition ? - hasFullfillableStartConditions() : - hasFullfillableStopConditions(); - - if (!canBeFulfilled) { - sb.append("- Conditions cannot be fulfilled (e.g., one-time conditions already triggered)\n"); - } - - // Progress - double progress = isStartCondition ? - conditionManager.getProgressTowardNextTrigger() : - getStopConditionProgress(); - sb.append("- Progress: ") - .append(String.format("%.1f%%", progress)) - .append("\n"); - - // Next trigger time - Optional nextTrigger = isStartCondition ? - getCurrentStartTriggerTime() : - getNextStopTriggerTime(); - - sb.append("- Next trigger: "); - if (nextTrigger.isPresent()) { - ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); - ZonedDateTime triggerTime = nextTrigger.get(); - - sb.append(triggerTime).append("\n"); - sb.append("- Current time: ").append(now).append("\n"); - - if (triggerTime.isBefore(now)) { - sb.append("- Trigger time is in the past but conditions not met - may need reset\n"); - } else { - Duration timeUntil = Duration.between(now, triggerTime); - sb.append("- Time until trigger: ").append(formatDuration(timeUntil)).append("\n"); - } - } else { - sb.append("No future trigger time determined\n"); - } - - // Overall condition status - boolean areConditionsMet = isStartCondition ? - startConditionManager.areAllConditionsMet() : - arePluginStopConditionsMet() && areUserDefinedStopConditionsMet(); - - sb.append("- Status: ") - .append(areConditionsMet ? - "CONDITIONS MET - Plugin is " + (isStartCondition ? "due to run" : "due to stop") : - "CONDITIONS NOT MET - Plugin " + (isStartCondition ? "will not run" : "will continue running")) - .append("\n"); - - // Individual condition status - sb.append("- Individual conditions:\n"); - for (int i = 0; i < conditions.size(); i++) { - Condition condition = conditions.get(i); - sb.append(" ").append(i+1).append(". ") - .append(condition.getDescription()) - .append(": ") - .append(condition.isSatisfied() ? "SATISFIED" : "NOT SATISFIED"); - - // Add progress if available - double condProgress = condition.getProgressPercentage(); - if (condProgress > 0 && condProgress < 100) { - sb.append(String.format(" (%.1f%%)", condProgress)); - } - - // For time conditions, show next trigger time - if (condition instanceof TimeCondition) { - Optional condTrigger = condition.getCurrentTriggerTime(); - if (condTrigger.isPresent()) { - sb.append(" (next trigger: ").append(condTrigger.get()).append(")"); - } - } - - sb.append("\n"); - } - - return sb.toString(); - } - - /** - * Performs a diagnostic check on start conditions and returns detailed information - * about why a plugin might not be due to run - * - * @return A string containing diagnostic information - */ - public String diagnoseStartConditions() { - return buildConditionDiagnostics(true); - } - - /** - * Performs a diagnostic check on stop conditions and returns detailed information - * about why a plugin might or might not be due to stop - * - * @return A string containing diagnostic information - */ - public String diagnoseStopConditions() { - return buildConditionDiagnostics(false); - } - - /** - * Formats a duration in a human-readable way - */ - private String formatDuration(Duration duration) { - long seconds = duration.getSeconds(); - if (seconds < 60) { - return seconds + " seconds"; - } else if (seconds < 3600) { - return String.format("%dm %ds", seconds / 60, seconds % 60); - } else if (seconds < 86400) { - return String.format("%dh %dm %ds", seconds / 3600, (seconds % 3600) / 60, seconds % 60); - } else { - return String.format("%dd %dh %dm", seconds / 86400, (seconds % 86400) / 3600, (seconds % 3600) / 60); - } - } - - /** - * Checks whether this schedule entry contains only time-based conditions. - * This is useful to determine if the plugin schedule is purely time-based - * or if it has other types of conditions (e.g., resource, skill, etc.). - * - * @return true if the schedule only contains TimeCondition instances, false otherwise - */ - public boolean hasOnlyTimeConditions() { - // Check if start conditions contain only time conditions - if (startConditionManager != null && !startConditionManager.hasOnlyTimeConditions()) { - return false; - } - - // Check if stop conditions contain only time conditions - if (stopConditionManager != null && !stopConditionManager.hasOnlyTimeConditions()) { - return false; - } - - // Both condition managers contain only time conditions (or are empty) - return true; - } - - /** - * Returns all non-time-based conditions from both start and stop conditions. - * This can help identify which non-time conditions are present in the schedule. - * - * @return A list of all non-TimeCondition instances in this schedule entry - */ - public List getNonTimeConditions() { - List nonTimeConditions = new ArrayList<>(); - - // Add non-time conditions from start conditions - if (startConditionManager != null) { - nonTimeConditions.addAll(startConditionManager.getNonTimeConditions()); - } - - // Add non-time conditions from stop conditions - if (stopConditionManager != null) { - nonTimeConditions.addAll(stopConditionManager.getNonTimeConditions()); - } - - return nonTimeConditions; - } - - /** - * Checks if this plugin would be due to run based only on its time conditions, - * ignoring any non-time conditions that may be present in the schedule. - * This is useful to determine if a plugin is being blocked from running by - * time conditions or by other types of conditions. - * - * @return true if the plugin would be scheduled to run based solely on time conditions - */ - public boolean wouldRunBasedOnTimeConditionsOnly() { - // Check if we're already running - if (isRunning()) { - return false; - } - - // If no start conditions defined, plugin can't run automatically - if (!hasAnyStartConditions()) { - return false; - } - - // Check if time conditions alone would be satisfied - return startConditionManager.wouldBeTimeOnlySatisfied(); - } - - /** - * Provides detailed diagnostic information about why a plugin is or isn't - * running based on its time conditions only. - * - * @return A diagnostic string explaining the time condition status - */ - public String diagnoseTimeConditionScheduling() { - StringBuilder sb = new StringBuilder(); - sb.append("Time condition scheduling diagnosis for '").append(cleanName).append("':\n"); - - // First check if plugin is already running - if (isRunning()) { - sb.append("Plugin is already running - will not be scheduled again until stopped.\n"); - return sb.toString(); - } - - // Check if there are any start conditions - if (!hasAnyStartConditions()) { - sb.append("No start conditions defined - plugin can't be automatically scheduled.\n"); - return sb.toString(); - } - - // Get time-only condition status - boolean wouldRunOnTimeOnly = startConditionManager.wouldBeTimeOnlySatisfied(); - boolean allConditionsMet = startConditionManager.areAllConditionsMet(); - - sb.append("Time conditions only: ").append(wouldRunOnTimeOnly ? "WOULD RUN" : "WOULD NOT RUN").append("\n"); - sb.append("All conditions: ").append(allConditionsMet ? "SATISFIED" : "NOT SATISFIED").append("\n"); - - // If time conditions would run but all conditions wouldn't, non-time conditions are blocking - if (wouldRunOnTimeOnly && !allConditionsMet) { - sb.append("Plugin is being blocked by non-time conditions.\n"); - - // List the non-time conditions that are not satisfied - List nonTimeConditions = startConditionManager.getNonTimeConditions(); - sb.append("Non-time conditions blocking execution:\n"); - - for (Condition condition : nonTimeConditions) { - if (!condition.isSatisfied()) { - sb.append(" - ").append(condition.getDescription()) - .append(" (").append(condition.getType()).append(")\n"); - } - } - } - // If time conditions would not run, show time condition status - else if (!wouldRunOnTimeOnly) { - sb.append("Plugin is waiting for time conditions to be met.\n"); - - // Show next trigger time if available - Optional nextTrigger = startConditionManager.getCurrentTriggerTime(); - if (nextTrigger.isPresent()) { - ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); - Duration until = Duration.between(now, nextTrigger.get()); - - sb.append("Next time trigger at: ").append(nextTrigger.get()) - .append(" (").append(formatDuration(until)).append(" from now)\n"); - } else { - sb.append("No future time trigger determined.\n"); - } - } - - // Add detailed time condition diagnosis from condition manager - sb.append("\n").append(startConditionManager.diagnoseTimeConditionsSatisfaction()); - - return sb.toString(); - } - - /** - * Creates a modified version of this schedule entry that contains only time conditions. - * This is useful for evaluating how the plugin would be scheduled if only time - * conditions were considered. - * - * @return A new PluginScheduleEntry with the same configuration but only time conditions - */ - public PluginScheduleEntry createTimeOnlySchedule() { - // Create a new schedule entry with the same basic properties - PluginScheduleEntry timeOnlyEntry = new PluginScheduleEntry( - name, - mainTimeStartCondition != null ? mainTimeStartCondition : null, - enabled, - allowRandomScheduling - ); - - // Create time-only condition managers - if (startConditionManager != null) { - ConditionManager timeOnlyStartManager = startConditionManager.createTimeOnlyConditionManager(); - timeOnlyEntry.startConditionManager.setUserLogicalCondition( - timeOnlyStartManager.getUserLogicalCondition()); - timeOnlyEntry.startConditionManager.setPluginCondition( - timeOnlyStartManager.getPluginCondition()); - } - - if (stopConditionManager != null) { - ConditionManager timeOnlyStopManager = stopConditionManager.createTimeOnlyConditionManager(); - timeOnlyEntry.stopConditionManager.setUserLogicalCondition( - timeOnlyStopManager.getUserLogicalCondition()); - timeOnlyEntry.stopConditionManager.setPluginCondition( - timeOnlyStopManager.getPluginCondition()); - } - - return timeOnlyEntry; - } - - /** - * Flag to track whether this plugin entry is currently paused - */ - private boolean paused = false; - - /** - * Pauses all time conditions in both stop and start condition managers. - * When paused, time conditions cannot be satisfied and their trigger times - * will be shifted when resumed. - * - * @return true if successfully paused, false if already paused - */ - public boolean pause() { - if (paused) { - return false; // Already paused - } - - // Pause both condition managers - if (stopConditionManager != null) { - stopConditionManager.pause(); - } - - if (startConditionManager != null) { - startConditionManager.pause(); - } - - paused = true; - log.debug("Paused time conditions for plugin: {}", name); - return true; - } - - /** - * resumes all time conditions in both stop and start condition managers. - * When resumed, time conditions will resume with their trigger times shifted - * by the duration of the pause. - * - * @return true if successfully resumed, false if not currently paused - */ - public boolean resume() { - if (!paused) { - return false; // Not paused - } - // resume both condition managers - if (stopConditionManager != null) { - stopConditionManager.resume(); - } - - if (startConditionManager != null) { - startConditionManager.resume(); - } - paused = false; - return true; - } - - /** - * Checks if this plugin entry is currently paused. - * - * @return true if paused, false otherwise - */ - public boolean isPaused() { - return paused; - } - - /** - * Gets the estimated time until start conditions will be satisfied. - * This method uses the new estimation system to provide more accurate - * predictions for when the plugin can start running. - * - * @return Optional containing the estimated duration until start conditions are satisfied - */ - public Optional getEstimatedStartTimeWhenIsSatisfied() { - if (!enabled) { - return Optional.empty(); - } - - if (startConditionManager == null) { - // No start conditions means plugin can start immediately - return Optional.of(Duration.ZERO); - } - - return startConditionManager.getEstimatedDurationUntilSatisfied(); - } - - /** - * Gets the estimated time until start conditions will be satisfied, considering only user-defined conditions. - * This method focuses only on user-configurable start conditions. - * - * @return Optional containing the estimated duration until user start conditions are satisfied - */ - public Optional getEstimatedStartTimeWhenIsSatisfiedUserBased() { - if (!enabled) { - return Optional.empty(); - } - - if (startConditionManager == null) { - return Optional.of(Duration.ZERO); - } - - return startConditionManager.getEstimatedDurationUntilUserConditionsSatisfied(); - } - - /** - * Gets the estimated time until stop conditions will be satisfied. - * This method uses only user-defined stop conditions to predict when the plugin - * should stop based on user configuration. - * - * @return Optional containing the estimated duration until stop conditions are satisfied - */ - public Optional getEstimatedStopTimeWhenIsSatisfied() { - if (stopConditionManager == null) { - // No stop conditions means plugin will run indefinitely - return Optional.empty(); - } - - return stopConditionManager.getEstimatedDurationUntilUserConditionsSatisfied(); - } - - /** - * Gets a formatted string representation of the estimated start time. - * - * @return A human-readable string describing when the plugin is estimated to start - */ - public String getEstimatedStartTimeDisplay() { - Optional estimate = getEstimatedStartTimeWhenIsSatisfied(); - if (estimate.isPresent()) { - return formatEstimatedDuration(estimate.get(), "start"); - } - return "Cannot estimate start time"; - } - - /** - * Gets a formatted string representation of the estimated stop time. - * - * @return A human-readable string describing when the plugin is estimated to stop - */ - public String getEstimatedStopTimeDisplay() { - Optional estimate = getEstimatedStopTimeWhenIsSatisfied(); - if (estimate.isPresent()) { - return formatEstimatedDuration(estimate.get(), "stop"); - } - return "No stop conditions or cannot estimate"; - } - - /** - * Helper method to format estimated durations into human-readable strings. - * - * @param duration The duration to format - * @param action The action description ("start" or "stop") - * @return A formatted string representation - */ - private String formatEstimatedDuration(Duration duration, String action) { - long seconds = duration.getSeconds(); - - if (seconds <= 0) { - return "Ready to " + action + " now"; - } else if (seconds < 60) { - return String.format("Estimated to %s in ~%d seconds", action, seconds); - } else if (seconds < 3600) { - return String.format("Estimated to %s in ~%d minutes", action, seconds / 60); - } else if (seconds < 86400) { - return String.format("Estimated to %s in ~%d hours", action, seconds / 3600); - } else { - long days = seconds / 86400; - return String.format("Estimated to %s in ~%d days", action, days); - } - } - - // ==================== Soft Failure Tracking Methods ==================== - - /** - * Records a soft failure for this plugin schedule entry. - * Soft failures are tracked consecutively and can lead to hard failure (disabling) - * if the maximum threshold is reached. - * - * @param reason The reason for the soft failure - */ - public void recordSoftFailure(String reason) { - consecutiveSoftFailures++; - lastSoftFailureTime = ZonedDateTime.now(); - - log.warn("Plugin '{}' soft failure #{}: {}", - getCleanName(), consecutiveSoftFailures, reason); - - if (consecutiveSoftFailures >= maxSoftFailuresBeforeHardFailure) { - log.error("Plugin '{}' reached maximum soft failures ({}). Converting to hard failure and disabling entry.", - getCleanName(), maxSoftFailuresBeforeHardFailure); - - // Convert to hard failure by disabling the entry - setEnabled(false); - setLastStopReason("Disabled due to " + maxSoftFailuresBeforeHardFailure + " consecutive soft failures. Last failure: " + reason); - setLastStopReasonType(StopReason.ERROR); - } - } - - /** - * Records a successful execution, which resets the consecutive soft failure counter. - */ - public void recordSuccess() { - if (consecutiveSoftFailures > 0) { - log.info("Plugin '{}' successfully executed after {} soft failures. Resetting failure counter.", - getCleanName(), consecutiveSoftFailures); - } - consecutiveSoftFailures = 0; - lastSoftFailureTime = null; - } - - /** - * @return The number of consecutive soft failures for this plugin - */ - public int getConsecutiveSoftFailures() { - return consecutiveSoftFailures; - } - - /** - * @return The maximum number of soft failures before converting to hard failure - */ - public int getMaxSoftFailuresBeforeHardFailure() { - return maxSoftFailuresBeforeHardFailure; - } - - /** - * Sets the maximum number of soft failures before converting to hard failure. - * - * @param maxFailures The maximum number of consecutive soft failures allowed - */ - public void setMaxSoftFailuresBeforeHardFailure(int maxFailures) { - this.maxSoftFailuresBeforeHardFailure = Math.max(1, maxFailures); - } - - /** - * Checks if the consecutive soft failures have exceeded the threshold - * and should be converted to a hard failure. - * - * @return true if soft failures should be converted to hard failure - */ - public boolean shouldConvertToHardFailure() { - return consecutiveSoftFailures >= maxSoftFailuresBeforeHardFailure; - } - - /** - * @return The time of the last soft failure, or null if no soft failures have occurred - */ - public ZonedDateTime getLastSoftFailureTime() { - return lastSoftFailureTime; - } - - /** - * @return true if this plugin is currently in a soft failure state - */ - public boolean isInSoftFailureState() { - return consecutiveSoftFailures > 0; - } - - /** - * @return true if this plugin is approaching the hard failure threshold - */ - public boolean isApproachingHardFailure() { - return consecutiveSoftFailures >= (maxSoftFailuresBeforeHardFailure - 1); - } - - /** - * Resets the soft failure counter manually. This should be used sparingly, - * typically only when the underlying issue has been resolved manually. - */ - public void resetSoftFailureCounter() { - int previousFailures = consecutiveSoftFailures; - consecutiveSoftFailures = 0; - lastSoftFailureTime = null; - - if (previousFailures > 0) { - log.info("Manually reset soft failure counter for plugin '{}' (was at {} failures)", - getCleanName(), previousFailures); - } - } - - - /** - * 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 getPrePostTasks() { - if (this.getPlugin() == null || Microbot.getClient() == null || Microbot.getClient().getGameState() != GameState.LOGGED_IN) { - return null; - } - Plugin plugin = this.getPlugin(); - if (plugin instanceof SchedulablePlugin && isRunning() - ) { - SchedulablePlugin schedulablePlugin = - (SchedulablePlugin) plugin; - return schedulablePlugin.getPrePostScheduleTasks(); - } - - return null; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/ExcludeTransientAndNonSerializableFieldsStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/ExcludeTransientAndNonSerializableFieldsStrategy.java deleted file mode 100644 index cfa0f479cf0..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/ExcludeTransientAndNonSerializableFieldsStrategy.java +++ /dev/null @@ -1,47 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization; - -import com.google.gson.ExclusionStrategy; -import com.google.gson.FieldAttributes; - -import java.util.function.Consumer; - -/** - * Excludes fields that shouldn't be serialized: - * - Transient fields - * - Functional interfaces (Consumer, Supplier, etc.) - * - Thread objects - * - Any other non-serializable types we identify - */ -public class ExcludeTransientAndNonSerializableFieldsStrategy implements ExclusionStrategy { - @Override - public boolean shouldSkipField(FieldAttributes field) { - // Skip transient fields - if (field.hasModifier(java.lang.reflect.Modifier.TRANSIENT)) { - return true; - } - - // Get the field type - Class fieldType = field.getDeclaredClass(); - - // Skip functional interfaces and other non-serializable types - return fieldType != null && ( - java.util.function.Consumer.class.isAssignableFrom(fieldType) || - java.util.function.Supplier.class.isAssignableFrom(fieldType) || - java.util.function.Function.class.isAssignableFrom(fieldType) || - java.util.function.Predicate.class.isAssignableFrom(fieldType) || - java.lang.Thread.class.isAssignableFrom(fieldType) || - java.util.concurrent.ScheduledFuture.class.isAssignableFrom(fieldType) || - java.awt.Component.class.isAssignableFrom(fieldType) - ); - } - - @Override - public boolean shouldSkipClass(Class clazz) { - // Skip functional interfaces at class level too - return Consumer.class.isAssignableFrom(clazz) || - java.util.function.Supplier.class.isAssignableFrom(clazz) || - java.lang.Thread.class.isAssignableFrom(clazz) || - java.util.concurrent.ScheduledFuture.class.isAssignableFrom(clazz) || - java.awt.Component.class.isAssignableFrom(clazz); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/ScheduledSerializer.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/ScheduledSerializer.java deleted file mode 100644 index 71741758ecc..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/ScheduledSerializer.java +++ /dev/null @@ -1,205 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization; - -import com.google.gson.*; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.config.*; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionManager; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization.DayOfWeekConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization.DurationAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization.IntervalConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization.LocalDateAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization.LocalTimeAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization.SingleTriggerTimeConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization.TimeConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.serialization.TimeWindowConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.VarbitCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.serialization.VarbitConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.ConditionTypeAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.ConditionManagerAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.ZonedDateTimeAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config.*; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.DayOfWeekCondition; -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.serialization.LogicalConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.serialization.NotConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.PluginScheduleEntryAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.BankItemCountCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.GatheredResourceCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.InventoryItemCountCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.LootItemCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ProcessItemCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.ResourceCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization.BankItemCountConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization.GatheredResourceConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization.InventoryItemCountConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization.LootItemConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization.ProcessItemConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.serialization.ResourceConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.SkillLevelCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.SkillXpCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.serialization.SkillLevelConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.serialization.SkillXpConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.npc.NpcKillCountCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.npc.serialization.NpcKillCountConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.AreaCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.RegionCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.PositionCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.serialization.AreaConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.serialization.RegionConditionAdapter; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.serialization.PositionConditionAdapter; - -import java.io.IOException; -import java.lang.reflect.Type; -import java.time.LocalDate; -import java.time.LocalTime; -import java.time.ZonedDateTime; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; - -/** - * Handles serialization and deserialization of ScheduledPlugin objects. - * This centralizes all JSON conversion logic. - */ -@Slf4j -public class ScheduledSerializer { - - /** - * Creates a properly configured Gson instance with all necessary type adapters - */ - private static Gson createGson() { - GsonBuilder builder = new GsonBuilder() - .setExclusionStrategies(new ExcludeTransientAndNonSerializableFieldsStrategy()) - .setPrettyPrinting(); - - // Register all the type adapters - builder.registerTypeAdapter(LocalDate.class, new LocalDateAdapter()); - builder.registerTypeAdapter(LocalTime.class, new LocalTimeAdapter()); - builder.registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeAdapter()); - builder.registerTypeAdapter(Duration.class, new DurationAdapter()); - builder.registerTypeAdapter(Condition.class, new ConditionTypeAdapter()); - - // Register our custom PluginScheduleEntry adapter - builder.registerTypeAdapter(PluginScheduleEntry.class, new PluginScheduleEntryAdapter()); - - // Register config descriptor adapters - builder.registerTypeAdapter(ConfigDescriptor.class, new ConfigDescriptorAdapter()); - builder.registerTypeAdapter(ConfigGroup.class, new ConfigGroupAdapter()); - builder.registerTypeAdapter(ConfigSection.class, new ConfigSectionAdapter()); - builder.registerTypeAdapter(ConfigSectionDescriptor.class, new ConfigSectionDescriptorAdapter()); - builder.registerTypeAdapter(ConfigItem.class, new ConfigItemAdapter()); - builder.registerTypeAdapter(ConfigItemDescriptor.class, new ConfigItemDescriptorAdapter()); - builder.registerTypeAdapter(ConfigInformation.class, new ConfigInformationAdapter()); - builder.registerTypeAdapter(Range.class, new RangeAdapter()); - builder.registerTypeAdapter(Alpha.class, new AlphaAdapter()); - builder.registerTypeAdapter(Units.class, new UnitsAdapter()); - - // Time condition adapters - builder.registerTypeAdapter(TimeCondition.class, new TimeConditionAdapter()); - builder.registerTypeAdapter(IntervalCondition.class, new IntervalConditionAdapter()); - builder.registerTypeAdapter(SingleTriggerTimeCondition.class, new SingleTriggerTimeConditionAdapter()); - builder.registerTypeAdapter(TimeWindowCondition.class, new TimeWindowConditionAdapter()); - builder.registerTypeAdapter(DayOfWeekCondition.class, new DayOfWeekConditionAdapter()); - - // Logical condition adapters - builder.registerTypeAdapter(LogicalCondition.class, new LogicalConditionAdapter()); - builder.registerTypeAdapter(NotCondition.class, new NotConditionAdapter()); - - // Resource condition adapters - builder.registerTypeAdapter(ResourceCondition.class, new ResourceConditionAdapter()); - builder.registerTypeAdapter(BankItemCountCondition.class, new BankItemCountConditionAdapter()); - builder.registerTypeAdapter(InventoryItemCountCondition.class, new InventoryItemCountConditionAdapter()); - builder.registerTypeAdapter(LootItemCondition.class, new LootItemConditionAdapter()); - builder.registerTypeAdapter(GatheredResourceCondition.class, new GatheredResourceConditionAdapter()); - builder.registerTypeAdapter(ProcessItemCondition.class, new ProcessItemConditionAdapter()); - - // Skill condition adapters - builder.registerTypeAdapter(SkillLevelCondition.class, new SkillLevelConditionAdapter()); - builder.registerTypeAdapter(SkillXpCondition.class, new SkillXpConditionAdapter()); - - // NPC condition adapters - builder.registerTypeAdapter(NpcKillCountCondition.class, new NpcKillCountConditionAdapter()); - - // Location condition adapters - builder.registerTypeAdapter(AreaCondition.class, new AreaConditionAdapter()); - builder.registerTypeAdapter(RegionCondition.class, new RegionConditionAdapter()); - builder.registerTypeAdapter(PositionCondition.class, new PositionConditionAdapter()); - - // Varbit condition adapter - builder.registerTypeAdapter(VarbitCondition.class, new VarbitConditionAdapter()); - - // ConditionManager adapter - builder.registerTypeAdapter(ConditionManager.class, new ConditionManagerAdapter()); - builder.registerTypeAdapter(Pattern.class, new TypeAdapter() { - @Override - public void write(JsonWriter out, Pattern value) throws IOException { - if (value == null) { - out.nullValue(); - } else { - out.value(value.pattern()); - } - } - - @Override - public Pattern read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - String pattern = in.nextString(); - return Pattern.compile(pattern); - } - }); - return builder.create(); - } - - /** - * Serialize a list of ScheduledPlugin objects to JSON - */ - public static String toJson(List plugins, String version) { - try { - return createGson().toJson(plugins); - } catch (Exception e) { - log.error("Error serializing scheduled plugins", e); - return "[]"; - } - } - - /** - * Deserialize a JSON string to a list of ScheduledPlugin objects - */ - public static List fromJson(String json,String version) { - if (json == null || json.isEmpty()) { - return new ArrayList<>(); - } - - try { - // Check if the JSON contains the old class name - if (json.contains("\"ScheduledPlugin\"")) { - json = json.replace("\"ScheduledPlugin\"", "\"PluginScheduleEntry\""); - } - - Gson gson = createGson(); - Type listType = new TypeToken>(){}.getType(); - - // Let Gson and our adapter handle everything - return gson.fromJson(json, listType); - } catch (Exception e) { - log.error("Error deserializing scheduled plugins", e); - return new ArrayList<>(); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ConditionManagerAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ConditionManagerAdapter.java deleted file mode 100644 index 4289afbc1d2..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ConditionManagerAdapter.java +++ /dev/null @@ -1,105 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionManager; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LogicalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; - -import java.lang.reflect.Type; -import java.time.ZoneId; -import java.util.ArrayList; -import java.util.List; - -/** - * Handles serialization and deserialization of ConditionManager - * with improved timezone handling for time-based conditions - */ -@Slf4j -public class ConditionManagerAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ConditionManager src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Add type information to identify whether this is a start or stop condition manager - // This will be detected and used during deserialization - result.addProperty("requireAll", src.requiresAll()); - - // Serialize plugin-defined logical condition (if present) - LogicalCondition pluginCondition = src.getPluginCondition(); - if (pluginCondition != null && !pluginCondition.getConditions().isEmpty()) { - result.add("pluginLogicalCondition", context.serialize(pluginCondition)); - } - // Serialize user-defined logical condition - LogicalCondition userCondition = src.getUserLogicalCondition(); - if (userCondition != null) { - result.add("userLogicalCondition", context.serialize(userCondition)); - } - return result; - } - - @Override - public ConditionManager deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - ConditionManager manager = new ConditionManager(); - - if (!json.isJsonObject()) { - return manager; // Return empty manager for non-object elements - } - - JsonObject jsonObject = json.getAsJsonObject(); - - // Set requireAll based on serialized value - if (jsonObject.has("requireAll")) { - boolean requireAll = jsonObject.get("requireAll").getAsBoolean(); - if (requireAll) { - manager.setRequireAll(); - } else { - manager.setRequireAny(); - } - } - - // Handle pluginLogicalCondition if present - if (jsonObject.has("pluginLogicalCondition")) { - JsonObject pluginLogicalObj = jsonObject.getAsJsonObject("pluginLogicalCondition"); - - // Only process if there are actual conditions - if (pluginLogicalObj.has("conditions") && - pluginLogicalObj.getAsJsonArray("conditions").size() > 0) { - LogicalCondition logicalCondition = context.deserialize( - pluginLogicalObj, LogicalCondition.class); - if (logicalCondition != null) { - manager.setPluginCondition(logicalCondition); - } - } - } - - // Handle userLogicalCondition properly - if (jsonObject.has("userLogicalCondition")) { - JsonObject userLogicalObj = jsonObject.getAsJsonObject("userLogicalCondition"); - - // Handle case where userLogicalCondition might have an empty conditions array - if (userLogicalObj.has("conditions")) { - - JsonArray conditionsArray = userLogicalObj.getAsJsonArray("conditions"); - if (conditionsArray.size() > 0) { - // Only process if there are actual conditions - LogicalCondition logicalCondition = context.deserialize( - userLogicalObj, LogicalCondition.class); - if (logicalCondition != null) { - manager.setUserLogicalCondition(logicalCondition); - - } - } - } - } - - - - return manager; - } - - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ConditionTypeAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ConditionTypeAdapter.java deleted file mode 100644 index a83069d9a22..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ConditionTypeAdapter.java +++ /dev/null @@ -1,303 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter; -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Skill; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.coords.WorldArea; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.location.*; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.*; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.npc.*; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.resource.*; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.skill.*; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.*; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.enums.RepeatCycle; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.varbit.VarbitCondition; - -import java.lang.reflect.Type; -import java.time.DayOfWeek; -import java.time.Duration; -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.LocalTime; -import java.time.LocalDate; -import java.util.EnumSet; -import java.util.Set; - -@Slf4j -public class ConditionTypeAdapter implements JsonSerializer, JsonDeserializer { - private static final String TYPE_FIELD = "type"; - private static final String DATA_FIELD = "data"; - - @Override - public JsonElement serialize(Condition src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - String className = src.getClass().getName(); - result.add(TYPE_FIELD, new JsonPrimitive(className)); - - JsonObject data = new JsonObject(); - - if (src instanceof LogicalCondition) { - data = (JsonObject) context.serialize(src, LogicalCondition.class); - } - else if (src instanceof NotCondition) { - data = (JsonObject) context.serialize(src, NotCondition.class); - } - else if (src instanceof IntervalCondition) { - data = (JsonObject) context.serialize(src, IntervalCondition.class); - } - else if (src instanceof DayOfWeekCondition) { - - data = (JsonObject) context.serialize(src, DayOfWeekCondition.class); - } - else if (src instanceof TimeWindowCondition) { - // Defer to the specialized adapter for TimeWindowCondition - - data = (JsonObject) context.serialize(src, TimeWindowCondition.class); - } - else if (src instanceof VarbitCondition) { - // Use the specialized adapter for VarbitCondition - data = (JsonObject) context.serialize(src, VarbitCondition.class); - } - else if (src instanceof SkillLevelCondition) { - // Use the specialized adapter for SkillLevelCondition - data = (JsonObject) context.serialize(src, SkillLevelCondition.class); - } - else if (src instanceof SkillXpCondition) { - // Use the specialized adapter for SkillXpCondition - data = (JsonObject) context.serialize(src, SkillXpCondition.class); - } - else if (src instanceof LootItemCondition) { - LootItemCondition item = (LootItemCondition) src; - data.addProperty("itemName", item.getItemName()); - data.addProperty("targetAmountMin", item.getTargetAmountMin()); - data.addProperty("targetAmountMax", item.getTargetAmountMax()); - data.addProperty("currentTargetAmount", item.getCurrentTargetAmount()); - data.addProperty("currentTrackedCount", item.getCurrentTrackedCount()); - data.addProperty("includeNoneOwner", item.isIncludeNoneOwner()); - data.addProperty("includeNoted", item.isIncludeNoted()); - } - else if (src instanceof InventoryItemCountCondition) { - InventoryItemCountCondition item = (InventoryItemCountCondition) src; - data.addProperty("itemName", item.getItemName()); - data.addProperty("targetCountMin", item.getTargetCountMin()); - data.addProperty("targetCountMax", item.getTargetCountMax()); - data.addProperty("includeNoted", item.isIncludeNoted()); - data.addProperty("currentTargetCount", item.getCurrentTargetCount()); - data.addProperty("currentItemCount", item.getCurrentItemCount()); - } - else if (src instanceof BankItemCountCondition) { - BankItemCountCondition item = (BankItemCountCondition) src; - data.addProperty("itemName", item.getItemName()); - data.addProperty("targetCountMin", item.getTargetCountMin()); - data.addProperty("targetCountMax", item.getTargetCountMax()); - data.addProperty("currentTargetCount", item.getCurrentTargetCount()); - data.addProperty("currentItemCount", item.getCurrentItemCount()); - } - else if (src instanceof PositionCondition) { - // Use the specialized adapter for PositionCondition - data = (JsonObject) context.serialize(src, PositionCondition.class); - } - else if (src instanceof AreaCondition) { - // Use the specialized adapter for AreaCondition - data = (JsonObject) context.serialize(src, AreaCondition.class); - } - else if (src instanceof RegionCondition) { - // Use the specialized adapter for RegionCondition - data = (JsonObject) context.serialize(src, RegionCondition.class); - } - else if (src instanceof NpcKillCountCondition) { - // Use the specialized adapter for NpcKillCountCondition - data = (JsonObject) context.serialize(src, NpcKillCountCondition.class); - } - else if (src instanceof SingleTriggerTimeCondition) { - assert(1==0); // This should never be serialized here, sperate adapter for this - } - else if (src instanceof LockCondition) { - LockCondition lock = (LockCondition) src; - data.addProperty("reason", lock.getReason()); - data.addProperty("withBreakHandlerLock", lock.isWithBreakHandlerLock()); - } - else { - log.warn("Unknown condition type: {}", src.getClass().getName()); - } - - result.add(DATA_FIELD, data); - return result; - } - - @Override - public Condition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - if (!json.isJsonObject()) { - return null; // Return null for non-object elements - } - - JsonObject jsonObject = json.getAsJsonObject(); - - // Check if the type field exists - if (!jsonObject.has(TYPE_FIELD)) { - return null; - } - String typeStr = jsonObject.get(TYPE_FIELD).getAsString(); - JsonObject data = jsonObject.getAsJsonObject(DATA_FIELD); - - try { - Class clazz = Class.forName(typeStr); - - // Handle specific condition types based on their class - if (AndCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, LogicalCondition.class); - } - else if (OrCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, LogicalCondition.class); - } - else if (NotCondition.class.isAssignableFrom(clazz)) { - - return context.deserialize(data, NotCondition.class); - } - else if (IntervalCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, IntervalCondition.class); - } - else if (DayOfWeekCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, DayOfWeekCondition.class); - } - else if (TimeWindowCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, TimeWindowCondition.class); - } - else if (VarbitCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, VarbitCondition.class); - } - else if (SkillLevelCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, SkillLevelCondition.class); - } - else if (SkillXpCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, SkillXpCondition.class); - } - else if (LootItemCondition.class.isAssignableFrom(clazz)) { - return deserializeLootItemCondition(data); - } - else if (InventoryItemCountCondition.class.isAssignableFrom(clazz)) { - return deserializeInventoryItemCountCondition(data); - } - else if (BankItemCountCondition.class.isAssignableFrom(clazz)) { - return deserializeBankItemCountCondition(data); - } - else if (PositionCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, PositionCondition.class); - } - else if (AreaCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, AreaCondition.class); - } - else if (RegionCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, RegionCondition.class); - } - else if (NpcKillCountCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, NpcKillCountCondition.class); - } - else if (SingleTriggerTimeCondition.class.isAssignableFrom(clazz)) { - return context.deserialize(data, SingleTriggerTimeCondition.class); - } - else if (LockCondition.class.isAssignableFrom(clazz)) { - if (data.has("data")){ - data = data.getAsJsonObject("data"); - } - boolean isWithBreakHandlerLock = data.has("withBreakHandlerLock") ? data.get("withBreakHandlerLock").getAsBoolean(): false; - return new LockCondition(data.get("reason").getAsString(),isWithBreakHandlerLock); - } - - throw new JsonParseException("Unknown condition type: " + typeStr); - } catch (ClassNotFoundException e) { - throw new JsonParseException("Unknown element type: " + typeStr, e); - } - } - - // Helper methods for each condition type - private AndCondition deserializeAndCondition(JsonObject data, JsonDeserializationContext context) { - AndCondition and = new AndCondition(); - JsonArray conditions = data.getAsJsonArray("conditions"); - for (JsonElement element : conditions) { - Condition condition = context.deserialize(element, Condition.class); - if (condition != null) { - and.addCondition(condition); - } - } - return and; - } - - private OrCondition deserializeOrCondition(JsonObject data, JsonDeserializationContext context) { - OrCondition or = new OrCondition(); - JsonArray conditions = data.getAsJsonArray("conditions"); - for (JsonElement element : conditions) { - Condition condition = context.deserialize(element, Condition.class); - if (condition != null) { - or.addCondition(condition); - } - } - return or; - } - - private NotCondition deserializeNotCondition(JsonObject data, JsonDeserializationContext context) { - Condition inner = context.deserialize(data.get("condition"), Condition.class); - return new NotCondition(inner); - } - - private LootItemCondition deserializeLootItemCondition(JsonObject data) { - if (data.has("data")){ - data = data.getAsJsonObject("data"); - } - String itemName = data.get("itemName").getAsString(); - int targetAmountMin = data.get("targetAmountMin").getAsInt(); - int targetAmountMax = data.get("targetAmountMax").getAsInt(); - boolean includeNoted = false; - if (data.has("includeNoted")){ - includeNoted= data.get("includeNoted").getAsBoolean(); - } - boolean includeNoneOwner = false; - if (data.has("includeNoneOwner")) { - includeNoneOwner = data.get("includeNoneOwner").getAsBoolean(); - } - - return LootItemCondition.builder() - .itemName(itemName) - .targetAmountMin(targetAmountMin) - .targetAmountMax(targetAmountMax) - .includeNoneOwner(includeNoneOwner) - .includeNoted(includeNoted) - .build(); - } - - private InventoryItemCountCondition deserializeInventoryItemCountCondition(JsonObject data) { - if (data.has("data")){ - data = data.getAsJsonObject("data"); - } - String itemName = data.get("itemName").getAsString(); - int targetCountMin = data.get("targetCountMin").getAsInt(); - int targetCountMax = data.get("targetCountMax").getAsInt(); - boolean includeNoted = data.get("includeNoted").getAsBoolean(); - - return InventoryItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .includeNoted(includeNoted) - .build(); - } - - private BankItemCountCondition deserializeBankItemCountCondition(JsonObject data) { - if (data.has("data")){ - data = data.getAsJsonObject("data"); - } - String itemName = data.get("itemName").getAsString(); - - int targetCountMin = data.get("targetCountMin").getAsInt(); - int targetCountMax = data.get("targetCountMax").getAsInt(); - - return BankItemCountCondition.builder() - .itemName(itemName) - .targetCountMin(targetCountMin) - .targetCountMax(targetCountMax) - .build(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/PluginScheduleEntryAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/PluginScheduleEntryAdapter.java deleted file mode 100644 index c72c88e6cde..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/PluginScheduleEntryAdapter.java +++ /dev/null @@ -1,275 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.config.ConfigDescriptor; - -import net.runelite.client.plugins.microbot.pluginscheduler.condition.Condition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ConditionManager; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; - -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; - - -import java.lang.reflect.Type; -import java.time.Duration; -import java.time.ZonedDateTime; - - - -/** - * Custom adapter for PluginScheduleEntry that correctly handles mainTimeStartCondition - * and serializes/deserializes ConfigDescriptor - */ -@Slf4j -public class PluginScheduleEntryAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(PluginScheduleEntry src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Serialize all fields - result.addProperty("name", src.getName()); - result.addProperty("enabled", src.isEnabled()); - result.addProperty("cleanName", src.getCleanName()); - result.addProperty("needsStopCondition", src.isNeedsStopCondition()); - result.addProperty("hardResetOnLoad", false); - // Serialize time fields - if (src.getLastRunStartTime() != null) { - result.addProperty("lastRunStartTime", src.getLastRunStartTime().toInstant().toEpochMilli()); - } - if (src.getLastRunEndTime() != null) { - result.addProperty("lastRunEndTime", src.getLastRunEndTime().toInstant().toEpochMilli()); - } - - // Serialize last run duration - if (src.getLastRunDuration() != null) { - result.add("lastRunDuration", context.serialize(src.getLastRunDuration())); - } - - // Serialize stop reason info - if (src.getLastStopReason() != null) { - result.addProperty("lastStopReason", src.getLastStopReason()); - } - if (src.getLastStopReasonType() != null) { - result.addProperty("lastStopReasonType", src.getLastStopReasonType().name()); - } - - // Serialize condition managers - if (src.getStopConditionManager() != null) { - result.add("stopConditionManager", context.serialize(src.getStopConditionManager())); - } - if (src.getStartConditionManager() != null) { - result.add("startConditionManager", context.serialize(src.getStartConditionManager())); - } - - // Serialize the main time condition - if (src.getMainTimeStartCondition() != null) { - result.add("mainTimeStartCondition", context.serialize(src.getMainTimeStartCondition())); - } - - - // Serialize other properties - - result.addProperty("allowRandomScheduling", src.isAllowRandomScheduling()); - result.addProperty("allowContinue", src.isAllowContinue()); - result.addProperty("runCount", src.getRunCount()); - result.addProperty("onLastStopUserConditionsSatisfied", src.isOnLastStopUserConditionsSatisfied()); - result.addProperty("onLastStopPluginConditionsSatisfied", src.isOnLastStopPluginConditionsSatisfied()); - - // Serialize durations - if (src.getSoftStopRetryInterval() != null) { - result.add("softStopRetryInterval", context.serialize(src.getSoftStopRetryInterval())); - } - if (src.getHardStopTimeout() != null) { - result.add("hardStopTimeout", context.serialize(src.getHardStopTimeout())); - } - - // Serialize priority and default flag - result.addProperty("priority", src.getPriority()); - result.addProperty("isDefault", src.isDefault()); - ConfigDescriptor configDescriptor = src.getConfigScheduleEntryDescriptor() != null ? src.getConfigScheduleEntryDescriptor(): null; - if (configDescriptor != null) { - result.add("configDescriptor", context.serialize(configDescriptor)); - }else { - result.add("configDescriptor", new JsonObject()); - } - - return result; - } - - @Override - public PluginScheduleEntry deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - // Get basic properties - String name = jsonObject.get("name").getAsString(); - boolean enabled = jsonObject.has("enabled") ? jsonObject.get("enabled").getAsBoolean() : false; - // Handle mainTimeStartCondition - TimeCondition mainTimeCondition = null; - if (jsonObject.has("mainTimeStartCondition")) { - try { - mainTimeCondition = context.deserialize( - jsonObject.get("mainTimeStartCondition"), TimeCondition.class); - } catch (Exception e) { - log.error("Failed to parse mainTimeStartCondition", e); - } - } - // Create a basic plugin entry first - PluginScheduleEntry entry = new PluginScheduleEntry(name, (TimeCondition)mainTimeCondition, enabled, false); - // Deserialize cleanName if available - if (jsonObject.has("cleanName")) { - entry.setCleanName(jsonObject.get("cleanName").getAsString()); - } - - // Deserialize time fields - handle both old and new time fields for backward compatibility - if (jsonObject.has("lastRunTime")) { - try { - long timestamp = jsonObject.get("lastRunTime").getAsLong(); - ZonedDateTime prvLastRunTime = ZonedDateTime.ofInstant( - java.time.Instant.ofEpochMilli(timestamp), - java.time.ZoneId.systemDefault()); - - // For backward compatibility, set both start and end time to the old lastRunTime - entry.setLastRunStartTime(prvLastRunTime); - entry.setLastRunEndTime(prvLastRunTime); - } catch (Exception e) { - log.error("Failed to parse lastRunTime", e); - } - } - - // Deserialize new time fields - if (jsonObject.has("lastRunStartTime")) { - try { - long timestamp = jsonObject.get("lastRunStartTime").getAsLong(); - ZonedDateTime lastRunStartTime = ZonedDateTime.ofInstant( - java.time.Instant.ofEpochMilli(timestamp), - java.time.ZoneId.systemDefault()); - entry.setLastRunStartTime(lastRunStartTime); - } catch (Exception e) { - log.error("Failed to parse lastRunStartTime", e); - } - } - - if (jsonObject.has("lastRunEndTime")) { - try { - long timestamp = jsonObject.get("lastRunEndTime").getAsLong(); - ZonedDateTime lastRunEndTime = ZonedDateTime.ofInstant( - java.time.Instant.ofEpochMilli(timestamp), - java.time.ZoneId.systemDefault()); - entry.setLastRunEndTime(lastRunEndTime); - } catch (Exception e) { - log.error("Failed to parse lastRunEndTime", e); - } - } - - // Deserialize last run duration - if (jsonObject.has("lastRunDuration")) { - try { - Duration lastRunDuration = context.deserialize( - jsonObject.get("lastRunDuration"), Duration.class); - entry.setLastRunDuration(lastRunDuration); - } catch (Exception e) { - log.error("Failed to parse lastRunDuration", e); - } - } - - // Deserialize condition managers - if (jsonObject.has("stopConditionManager")) { - ConditionManager stopManager = context.deserialize( - jsonObject.get("stopConditionManager"), ConditionManager.class); - if (!entry.getStopConditionManager().getUserConditions().isEmpty()) { - throw new Error("StopConditionManager should be empty"); - } else { - for (Condition condition : stopManager.getUserConditions()) { - if(entry.getStopConditionManager().containsCondition(condition)){ - throw new Error("Condition already exists in startConditionManager"); - } - entry.addStopCondition(condition); - } - } - } - if (jsonObject.has("startConditionManager")) { - ConditionManager startManager = context.deserialize( - jsonObject.get("startConditionManager"), ConditionManager.class); - - for (Condition condition : startManager.getUserConditions()) { - if(!entry.getStartConditionManager().containsCondition(condition)){ - entry.addStartCondition(condition); - } - } - } - - - if (jsonObject.has("allowRandomScheduling")) { - entry.setAllowRandomScheduling(jsonObject.get("allowRandomScheduling").getAsBoolean()); - } - - if (jsonObject.has("allowContinue")) { - entry.setAllowContinue(jsonObject.get("allowContinue").getAsBoolean()); - } - - if (jsonObject.has("runCount")) { - int runCount = jsonObject.get("runCount").getAsInt(); - entry.setRunCount(runCount); - } - - // Deserialize stop reason info - if (jsonObject.has("lastStopReason")) { - entry.setLastStopReason(jsonObject.get("lastStopReason").getAsString()); - } - - if (jsonObject.has("lastStopReasonType")) { - try { - String stopReasonType = jsonObject.get("lastStopReasonType").getAsString(); - entry.setLastStopReasonType(PluginScheduleEntry.StopReason.valueOf(stopReasonType)); - } catch (Exception e) { - log.error("Failed to parse lastStopReasonType", e); - } - } - if (jsonObject.has("onLastStopUserConditionsSatisfied")) { - entry.setOnLastStopUserConditionsSatisfied(jsonObject.get("onLastStopUserConditionsSatisfied").getAsBoolean()); - } - if (jsonObject.has("onLastStopPluginConditionsSatisfied")) { - entry.setOnLastStopPluginConditionsSatisfied(jsonObject.get("onLastStopPluginConditionsSatisfied").getAsBoolean()); - } - - // Deserialize durations - if (jsonObject.has("softStopRetryInterval")) { - Duration softStopRetryInterval = context.deserialize( - jsonObject.get("softStopRetryInterval"), Duration.class); - entry.setSoftStopRetryInterval(softStopRetryInterval); - } - - if (jsonObject.has("hardStopTimeout")) { - Duration hardStopTimeout = context.deserialize( - jsonObject.get("hardStopTimeout"), Duration.class); - entry.setHardStopTimeout(hardStopTimeout); - } - - // Deserialize priority and default flag - if (jsonObject.has("priority")) { - entry.setPriority(jsonObject.get("priority").getAsInt()); - } - - if (jsonObject.has("isDefault")) { - entry.setDefault(jsonObject.get("isDefault").getAsBoolean()); - } - if (jsonObject.has("needsStopCondition")) { - entry.setNeedsStopCondition(jsonObject.get("needsStopCondition").getAsBoolean()); - }else{ - entry.setNeedsStopCondition(false); - } - //entry.registerPluginConditions(); - - if (jsonObject.has("hardResetOnLoad")) { - boolean hardResetFlag = jsonObject.get("hardResetOnLoad").getAsBoolean(); - if (hardResetFlag) { - entry.hardResetConditions(); - } - } - - return entry; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ZonedDateTimeAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ZonedDateTimeAdapter.java deleted file mode 100644 index 5aa3c906e40..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/ZonedDateTimeAdapter.java +++ /dev/null @@ -1,36 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; -import java.io.IOException; -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZonedDateTime; -/** - * Gson TypeAdapter for ZonedDateTime serialization/deserialization - */ -public class ZonedDateTimeAdapter extends TypeAdapter { - @Override - public void write(JsonWriter out, ZonedDateTime value) throws IOException { - if (value == null) { - out.nullValue(); - } else { - out.value(value.toInstant().toEpochMilli()); - } - } - - @Override - public ZonedDateTime read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - long timestamp = in.nextLong(); - return ZonedDateTime.ofInstant( - Instant.ofEpochMilli(timestamp), - ZoneId.systemDefault() - ); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/AlphaAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/AlphaAdapter.java deleted file mode 100644 index 1044946d459..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/AlphaAdapter.java +++ /dev/null @@ -1,30 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config; - -import com.google.gson.*; -import net.runelite.client.config.Alpha; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; - -/** - * Adapter for serializing and deserializing Alpha annotations - */ -public class AlphaAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(Alpha src, Type typeOfSrc, JsonSerializationContext context) { - // Alpha annotation has no properties, so we just return an empty object - return new JsonObject(); - } - - @Override - public Alpha deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - // Create a proxy implementation of Alpha annotation - return new Alpha() { - @Override - public Class annotationType() { - return Alpha.class; - } - }; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigDescriptorAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigDescriptorAdapter.java deleted file mode 100644 index e2d91738354..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigDescriptorAdapter.java +++ /dev/null @@ -1,87 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.config.*; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; - -/** - * Adapter for serializing and deserializing ConfigDescriptor objects - * This allows us to store and restore complete configuration structures - * without needing the actual plugin classes at deserialization time - */ -@Slf4j -public class ConfigDescriptorAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ConfigDescriptor src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Serialize group - if (src.getGroup() != null) { - result.add("group", context.serialize(src.getGroup(), ConfigGroup.class)); - } - - // Serialize sections - if (src.getSections() != null && !src.getSections().isEmpty()) { - result.add("sections", context.serialize(src.getSections(), Collection.class)); - } - - // Serialize items - if (src.getItems() != null && !src.getItems().isEmpty()) { - result.add("items", context.serialize(src.getItems(), Collection.class)); - } - - // Serialize information if present - if (src.getInformation() != null) { - result.add("information", context.serialize(src.getInformation(), ConfigInformation.class)); - } - - return result; - } - - @Override - public ConfigDescriptor deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - // Deserialize group - ConfigGroup group = null; - if (jsonObject.has("group")) { - group = context.deserialize(jsonObject.get("group"), ConfigGroup.class); - } - - // Deserialize sections - Collection sections = Collections.emptyList(); - if (jsonObject.has("sections")) { - Type sectionListType = new TypeToken>() {}.getType(); - sections = context.deserialize(jsonObject.get("sections"), sectionListType); - } - - // Deserialize items - Collection items = Collections.emptyList(); - if (jsonObject.has("items")) { - Type itemsListType = new TypeToken>() {}.getType(); - items = context.deserialize(jsonObject.get("items"), itemsListType); - } - - // Deserialize information - ConfigInformation information = null; - if (jsonObject.has("information")) { - information = context.deserialize(jsonObject.get("information"), ConfigInformation.class); - } - - return new ConfigDescriptor(group, sections, items, information); - } - - private static class TypeToken { - private TypeToken() {} - - public Type getType() { - return getClass().getGenericSuperclass(); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigGroupAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigGroupAdapter.java deleted file mode 100644 index ff01a26f9fa..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigGroupAdapter.java +++ /dev/null @@ -1,41 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config; - -import com.google.gson.*; -import net.runelite.client.config.ConfigGroup; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; - -/** - * Adapter for serializing and deserializing ConfigGroup annotations - * Since annotations are immutable, we need to create a proxy implementation - */ -public class ConfigGroupAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ConfigGroup src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - result.addProperty("value", src.value()); - return result; - } - - @Override - public ConfigGroup deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - final String value = jsonObject.get("value").getAsString(); - // Create a proxy implementation of ConfigGroup annotation - return new ConfigGroup() { - @Override - public Class annotationType() { - return ConfigGroup.class; - } - - @Override - public String value() { - return value; - } - - }; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigInformationAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigInformationAdapter.java deleted file mode 100644 index c76bc1730f3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigInformationAdapter.java +++ /dev/null @@ -1,40 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config; - -import com.google.gson.*; -import net.runelite.client.config.ConfigInformation; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; - -/** - * Adapter for serializing and deserializing ConfigInformation annotations - */ -public class ConfigInformationAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ConfigInformation src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - result.addProperty("value", src.value()); - return result; - } - - @Override - public ConfigInformation deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - final String value = jsonObject.has("value") ? jsonObject.get("value").getAsString() : ""; - - // Create a proxy implementation of ConfigInformation annotation - return new ConfigInformation() { - @Override - public Class annotationType() { - return ConfigInformation.class; - } - - @Override - public String value() { - return value; - } - }; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigItemAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigItemAdapter.java deleted file mode 100644 index 0c7807366ce..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigItemAdapter.java +++ /dev/null @@ -1,90 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config; - -import com.google.gson.*; -import net.runelite.client.config.ConfigItem; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; - -/** - * Adapter for serializing and deserializing ConfigItem annotations - * Since annotations are immutable, we need to create a proxy implementation - */ -public class ConfigItemAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ConfigItem src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - result.addProperty("keyName", src.keyName()); - result.addProperty("name", src.name()); - result.addProperty("description", src.description()); - result.addProperty("section", src.section()); - result.addProperty("position", src.position()); - result.addProperty("hidden", src.hidden()); - result.addProperty("secret", src.secret()); - result.addProperty("warning", src.warning()); - return result; - } - - @Override - public ConfigItem deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - final String keyName = jsonObject.has("keyName") ? jsonObject.get("keyName").getAsString() : ""; - final String name = jsonObject.has("name") ? jsonObject.get("name").getAsString() : ""; - final String description = jsonObject.has("description") ? jsonObject.get("description").getAsString() : ""; - final String section = jsonObject.has("section") ? jsonObject.get("section").getAsString() : ""; - final int position = jsonObject.has("position") ? jsonObject.get("position").getAsInt() : 0; - final boolean hidden = jsonObject.has("hidden") && jsonObject.get("hidden").getAsBoolean(); - final boolean secret = jsonObject.has("secret") && jsonObject.get("secret").getAsBoolean(); - final String warning = jsonObject.has("warning") ? jsonObject.get("warning").getAsString() : ""; - - // Create a proxy implementation of ConfigItem annotation - return new ConfigItem() { - @Override - public Class annotationType() { - return ConfigItem.class; - } - - @Override - public String keyName() { - return keyName; - } - - @Override - public String name() { - return name; - } - - @Override - public String description() { - return description; - } - - @Override - public String section() { - return section; - } - - @Override - public int position() { - return position; - } - - @Override - public boolean hidden() { - return hidden; - } - - @Override - public boolean secret() { - return secret; - } - - @Override - public String warning() { - return warning; - } - }; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigItemDescriptorAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigItemDescriptorAdapter.java deleted file mode 100644 index 787bf00b1c8..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigItemDescriptorAdapter.java +++ /dev/null @@ -1,107 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.config.*; - -import java.lang.reflect.Type; - -/** - * Adapter for serializing and deserializing ConfigItemDescriptor objects - */ -@Slf4j -public class ConfigItemDescriptorAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ConfigItemDescriptor src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Serialize the ConfigItem annotation - if (src.getItem() != null) { - result.add("item", context.serialize(src.getItem(), ConfigItem.class)); - } - - // Serialize the type - if (src.getType() != null) { - result.addProperty("type", src.getType().getTypeName()); - } - - // Serialize Range annotation if present - if (src.getRange() != null) { - result.add("range", context.serialize(src.getRange(), Range.class)); - } - - // Serialize Alpha annotation if present - if (src.getAlpha() != null) { - result.add("alpha", context.serialize(src.getAlpha(), Alpha.class)); - } - - // Serialize Units annotation if present - if (src.getUnits() != null) { - result.add("units", context.serialize(src.getUnits(), Units.class)); - } - - return result; - } - - @Override - public ConfigItemDescriptor deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - // Deserialize the ConfigItem annotation - ConfigItem item = null; - if (jsonObject.has("item")) { - item = context.deserialize(jsonObject.get("item"), ConfigItem.class); - } - - // Deserialize the type - Type itemType = null; - if (jsonObject.has("type")) { - String typeName = jsonObject.get("type").getAsString(); - try { - // Try to load the class if available - itemType = Class.forName(typeName); - } catch (ClassNotFoundException e) { - // If class isn't available, store the type name as a placeholder - log.debug("Class not found for type: {}", typeName); - // For primitive types, handle them specially - if (typeName.equals("boolean") || typeName.equals("java.lang.Boolean")) { - itemType = boolean.class; - } else if (typeName.equals("int") || typeName.equals("java.lang.Integer")) { - itemType = int.class; - } else if (typeName.equals("double") || typeName.equals("java.lang.Double")) { - itemType = double.class; - } else if (typeName.equals("long") || typeName.equals("java.lang.Long")) { - itemType = long.class; - } else if (typeName.equals("float") || typeName.equals("java.lang.Float")) { - itemType = float.class; - } else if (typeName.equals("java.lang.String")) { - itemType = String.class; - } else { - // Use Object as fallback - itemType = Object.class; - } - } - } - - // Deserialize Range annotation if present - Range range = null; - if (jsonObject.has("range")) { - range = context.deserialize(jsonObject.get("range"), Range.class); - } - - // Deserialize Alpha annotation if present - Alpha alpha = null; - if (jsonObject.has("alpha")) { - alpha = context.deserialize(jsonObject.get("alpha"), Alpha.class); - } - - // Deserialize Units annotation if present - Units units = null; - if (jsonObject.has("units")) { - units = context.deserialize(jsonObject.get("units"), Units.class); - } - - return new ConfigItemDescriptor(item, itemType, range, alpha, units); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigSectionAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigSectionAdapter.java deleted file mode 100644 index 1354d6dd32b..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigSectionAdapter.java +++ /dev/null @@ -1,62 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config; - -import com.google.gson.*; -import net.runelite.client.config.ConfigSection; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; - -/** - * Adapter for serializing and deserializing ConfigSection annotations - * Since annotations are immutable, we need to create a proxy implementation - */ -public class ConfigSectionAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ConfigSection src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - result.addProperty("name", src.name()); - result.addProperty("description", src.description()); - result.addProperty("position", src.position()); - result.addProperty("closedByDefault", src.closedByDefault()); - return result; - } - - @Override - public ConfigSection deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - final String name = jsonObject.has("name") ? jsonObject.get("name").getAsString() : ""; - final String description = jsonObject.has("description") ? jsonObject.get("description").getAsString() : ""; - final int position = jsonObject.has("position") ? jsonObject.get("position").getAsInt() : 0; - final boolean closedByDefault = jsonObject.has("closedByDefault") && jsonObject.get("closedByDefault").getAsBoolean(); - - // Create a proxy implementation of ConfigSection annotation - return new ConfigSection() { - @Override - public Class annotationType() { - return ConfigSection.class; - } - - @Override - public String name() { - return name; - } - - @Override - public String description() { - return description; - } - - @Override - public int position() { - return position; - } - - @Override - public boolean closedByDefault() { - return closedByDefault; - } - }; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigSectionDescriptorAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigSectionDescriptorAdapter.java deleted file mode 100644 index b22c4262b90..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/ConfigSectionDescriptorAdapter.java +++ /dev/null @@ -1,51 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config; - -import com.google.gson.*; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.config.ConfigSection; -import net.runelite.client.config.ConfigSectionDescriptor; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; - -/** - * Adapter for serializing and deserializing ConfigSectionDescriptor objects - */ -@Slf4j -public class ConfigSectionDescriptorAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(ConfigSectionDescriptor src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - - // Serialize the key - result.addProperty("key", src.getKey()); - - // Serialize the section annotation - if (src.getSection() != null) { - result.add("section", context.serialize(src.getSection(), ConfigSection.class)); - } - - return result; - } - - @Override - public ConfigSectionDescriptor deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - // Deserialize the key - String key = null; - if (jsonObject.has("key")) { - key = jsonObject.get("key").getAsString(); - } - - // Deserialize the section annotation - ConfigSection section = null; - if (jsonObject.has("section")) { - section = context.deserialize(jsonObject.get("section"), ConfigSection.class); - } - - // Create a ConfigSectionDescriptor instance - return new ConfigSectionDescriptor(key, section); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/RangeAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/RangeAdapter.java deleted file mode 100644 index cb2c3111c33..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/RangeAdapter.java +++ /dev/null @@ -1,47 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config; - -import com.google.gson.*; -import net.runelite.client.config.Range; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; - -/** - * Adapter for serializing and deserializing Range annotations - */ -public class RangeAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(Range src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - result.addProperty("min", src.min()); - result.addProperty("max", src.max()); - return result; - } - - @Override - public Range deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - final int min = jsonObject.has("min") ? jsonObject.get("min").getAsInt() : 0; - final int max = jsonObject.has("max") ? jsonObject.get("max").getAsInt() : Integer.MAX_VALUE; - - // Create a proxy implementation of Range annotation - return new Range() { - @Override - public Class annotationType() { - return Range.class; - } - - @Override - public int min() { - return min; - } - - @Override - public int max() { - return max; - } - }; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/UnitsAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/UnitsAdapter.java deleted file mode 100644 index 8980a56734b..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/serialization/adapter/config/UnitsAdapter.java +++ /dev/null @@ -1,40 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.serialization.adapter.config; - -import com.google.gson.*; -import net.runelite.client.config.Units; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; - -/** - * Adapter for serializing and deserializing Units annotations - */ -public class UnitsAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(Units src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject result = new JsonObject(); - result.addProperty("value", src.value()); - return result; - } - - @Override - public Units deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonObject = json.getAsJsonObject(); - - final String value = jsonObject.has("value") ? jsonObject.get("value").getAsString() : ""; - - // Create a proxy implementation of Units annotation - return new Units() { - @Override - public Class annotationType() { - return Units.class; - } - - @Override - public String value() { - return value; - } - }; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/AbstractPrePostScheduleTasks.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/AbstractPrePostScheduleTasks.java deleted file mode 100644 index 7b09e9b0530..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/AbstractPrePostScheduleTasks.java +++ /dev/null @@ -1,1102 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks; - -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerPlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.api.SchedulablePlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.event.ExecutionResult; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LockCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.PrePostScheduleRequirements; -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.tasks.state.TaskExecutionState; -import net.runelite.client.plugins.microbot.util.events.PluginPauseEvent; -import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; - -import java.awt.event.KeyEvent; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.config.ConfigDescriptor; -import net.runelite.client.input.KeyListener; -import net.runelite.client.input.KeyManager; - -/** - * Abstract base class for managing pre and post schedule tasks for plugins operating under scheduler control. - *

- * This class provides a common infrastructure for handling: - *

    - *
  • Executor service management for both pre and post tasks
  • - *
  • CompletableFuture lifecycle management with timeout support
  • - *
  • Thread-safe shutdown procedures
  • - *
  • Common error handling patterns
  • - *
  • AutoCloseable implementation for resource cleanup
  • - *
  • Emergency cancel hotkey (Ctrl+C) for aborting all tasks
  • - *
- *

- * Concrete implementations must provide: - *

    - *
  • {@link #executePreScheduleTask(LockCondition)} - Plugin-specific preparation logic
  • - *
  • {@link #executePostScheduleTask(LockCondition)} - Plugin-specific cleanup logic
  • - *
  • {@link #isScheduleMode()} - Detection of scheduler mode
  • - *
- * - * @see SchedulablePlugin - * @since 1.0.0 - */ -@Slf4j -public abstract class AbstractPrePostScheduleTasks implements AutoCloseable, KeyListener { - - // TODO: Consider adding configuration for default timeout values - // TODO: Add metrics collection for task execution times and success rates - // TODO: Implement retry mechanism for failed tasks with exponential backoff - // TODO: Add support for task priority levels (critical vs optional tasks) - // TODO: Consider adding a mechanism to pause/resume tasks based on external conditions - // TODO: add custom tasks as callbacks to allow plugins to define their own pre/post tasks in addtion to the pre/post schedule requirements - - protected final SchedulablePlugin plugin; - private ScheduledExecutorService postExecutorService; - private ScheduledExecutorService preExecutorService; - private CompletableFuture preScheduledFuture; - private CompletableFuture postScheduledFuture; - - // Emergency cancel hotkey support (injected via plugin) - - private final KeyManager keyManager; - - // Centralized state tracking - @Getter - private final TaskExecutionState executionState = new TaskExecutionState(); - - private final LockCondition prePostScheduleTaskLock = new LockCondition("Pre/Post Schedule Task Lock", false, true); - - /** - * Constructor for AbstractPrePostScheduleTasks. - * Initializes the task manager with the provided plugin instance. - * - * @param plugin The SchedulablePlugin instance to manage - */ - protected AbstractPrePostScheduleTasks(SchedulablePlugin plugin, KeyManager keyManager) { - this.plugin = plugin; - this.keyManager = keyManager; - initializeCancel(); - log.info("Initialized pre/post schedule task manager for plugin: {}", plugin.getClass().getSimpleName()); - } - - /** - * Initializes the emergency cancel hotkey functionality. - * This method should be called after the plugin's KeyManager is available. - * - * @param keyManager The KeyManager instance from the plugin - */ - public final void initializeCancel() { - - // Register emergency cancel hotkey if KeyManager is available - try { - if (keyManager != null) { - keyManager.registerKeyListener(this); - log.info("Registered emergency cancel hotkey (Ctrl+C) for plugin: {}", plugin.getClass().getSimpleName()); - }else{ - log.warn("KeyManager is not available, cannot register emergency cancel hotkey for plugin: {}", plugin.getClass().getSimpleName()); - } - } catch (Exception e) { - log.warn("Failed to register emergency cancel hotkey for plugin {}: {}", - plugin.getClass().getSimpleName(), e.getMessage()); - } - } - private boolean canStartAnyTask(){ - // Check if the plugin is running in schedule mode - if (plugin == null) { - log.warn("Plugin instance is null, cannot determine schedule mode"); - return false; // Cannot determine schedule mode without plugin instance - } - - if (getRequirements() == null || !getRequirements().isInitialized()) { - log.warn("Requirements are not initialized, cannot execute pre-schedule tasks"); - return false; // Cannot run pre-schedule tasks if requirements are not met - } - - - if (postScheduledFuture != null && !postScheduledFuture.isDone()) { - log.warn("Post-schedule task is still running, cannot execute pre-schedule tasks yet"); - return false; // Cannot run pre-schedule tasks while post-schedule is still running - } - if (preScheduledFuture != null && !preScheduledFuture.isDone()) { - log.warn("Pre-schedule task already running, skipping duplicate execution"); - return false; // Pre-schedule task already running, skip - } - return true; - } - public boolean canStartPreScheduleTasks() { - // Check state before execution - if (!executionState.canExecutePreTasks()) { - log.warn("Pre-schedule tasks cannot be executed - already started and not completed. Use reset() to allow re-execution."); - return false; - } - // Check if the plugin is running in schedule mode - return canStartAnyTask(); - - } - public boolean canStartPostScheduleTasks() { - // Check state before execution - if (!executionState.canExecutePostTasks()) { - log.warn("Post-schedule tasks cannot be executed - already started and not completed. Use reset() to allow re-execution.\n -executionState: {}",executionState); - return false; - } - // Check if the plugin is running in schedule mode - return canStartAnyTask(); - } - /** - * Executes pre-schedule preparation tasks on a separate thread. - * This method runs preparation tasks asynchronously and calls the provided callback when complete. - * - * @param callback The callback to execute when preparation is finished - * @param timeout The timeout value (0 or negative means no timeout) - * @param timeUnit The time unit for the timeout - */ - public final void executePreScheduleTasks(Runnable callback, LockCondition lockCondition, int timeout, TimeUnit timeUnit) { - if (!canStartPreScheduleTasks()) { - log.warn("Cannot execute pre-schedule tasks - conditions not met"); - return; // Cannot run pre-schedule tasks if conditions are not met - } - - // Update state to indicate pre-schedule tasks are starting - executionState.update( TaskExecutionState.ExecutionPhase.PRE_SCHEDULE,TaskExecutionState.ExecutionState.STARTING); - - // Initialize executor service for pre-actions - if (preExecutorService == null || preExecutorService.isShutdown()) { - preExecutorService = Executors.newScheduledThreadPool(2, r -> { - Thread t = new Thread(r, getClass().getSimpleName() + "-PreSchedule"); - t.setDaemon(true); - return t; - }); - } - lockPrePostTask(); - preScheduledFuture = CompletableFuture.supplyAsync(() -> { - try { - log.info("\n --> Starting pre-schedule preparation on separate thread for plugin: \n\t\t{}", - plugin.getClass().getSimpleName()); - - // Execute preparation actions - boolean success = executePreScheduleTask(lockCondition); - - if (success) { - log.info("\n\tPre-schedule preparation completed successfully - executing callback"); - executionState.update( TaskExecutionState.ExecutionPhase.PRE_SCHEDULE,TaskExecutionState.ExecutionState.COMPLETED); - if (callback != null) { - callback.run(); - } - } else { - log.warn("\n\tPre-schedule preparation failed - stopping plugin"); - executionState.update( TaskExecutionState.ExecutionPhase.PRE_SCHEDULE,TaskExecutionState.ExecutionState.FAILED); - } - - return success; - } catch (Exception e) { - log.error("Error during pre-schedule preparation: {}", e.getMessage(), e); - executionState.update( TaskExecutionState.ExecutionPhase.PRE_SCHEDULE,TaskExecutionState.ExecutionState.ERROR); - // Unlock is handled in handlePreTaskCompletion via whenComplete - throw new RuntimeException("Pre-schedule preparation failed", e); - } - }, preExecutorService); - - // Handle timeout and completion - if (timeout > 0) { - preScheduledFuture.orTimeout(timeout, timeUnit) - .whenComplete((result, throwable) -> { - handlePreTaskCompletion(result, throwable); - }); - } else { - preScheduledFuture.whenComplete((result, throwable) -> { - handlePreTaskCompletion(result, throwable); - }); - } - } - private void lockPrePostTask(){ - PluginPauseEvent.setPaused(true); - prePostScheduleTaskLock.lock(); - } - private void unlock() { - PluginPauseEvent.setPaused(false); - prePostScheduleTaskLock.unlock(); - } - - /** - * Convenience method for executing pre-schedule tasks with default timeout. - * - * @param callback The callback to execute when preparation is finished - * @param lockCondition The lock condition to prevent running the pre-schedule tasks while the plugin is in a critical operation - */ - public final void executePreScheduleTasks(Runnable callback, LockCondition lockCondition) { - executePreScheduleTasks(callback,lockCondition, 0, TimeUnit.SECONDS); - } - /** - * Convenience method for executing pre-schedule tasks with default timeout. - * - * @param callback The callback to execute when preparation is finished - */ - public final void executePreScheduleTasks(Runnable callback) { - executePreScheduleTasks(callback,null, 0, TimeUnit.SECONDS); - } - - - public final boolean isPreScheduleRunning() { - return preScheduledFuture != null && !preScheduledFuture.isDone(); - } - /** - * Executes post-schedule cleanup tasks when running under scheduler control. - * This includes graceful shutdown procedures and resource cleanup. - * - * @param lockCondition The lock condition to prevent interruption during critical operations - * @param timeout The timeout value (0 or negative means no timeout) - * @param timeUnit The time unit for the timeout - */ - public final void executePostScheduleTasks(Runnable callback, LockCondition lockCondition, int timeout, TimeUnit timeUnit) { - - if (!canStartPostScheduleTasks()) { - log.warn("Cannot execute post-schedule tasks - conditions not met"); - return; // Cannot run post-schedule tasks if conditions are not met - } - executionState.update( TaskExecutionState.ExecutionPhase.POST_SCHEDULE,TaskExecutionState.ExecutionState.STARTING); - - initializePostExecutorService(); - lockPrePostTask(); - postScheduledFuture = CompletableFuture.supplyAsync(() -> { - try { - if (lockCondition != null && lockCondition.isLocked()) { - log.info("Post-schedule: waiting for current operation to complete"); - // Wait for lock to be released with reasonable timeout - int waitAttempts = 0; - while (lockCondition.isLocked() && waitAttempts < 60) { // Wait up to 60 seconds - try { - Thread.sleep(1000); - waitAttempts++; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.warn("Interrupted while waiting for lock release"); - break; - } - } - } - - log.info("\n\tStarting post-schedule tasks for plugin: \n\t\t{}", plugin.getClass().getSimpleName()); - - // Execute cleanup actions - boolean success = executePostScheduleTask(lockCondition); - - if (success) { - log.info("Post-schedule cleanup completed successfully"); - if (callback != null) { - callback.run(); - } - } else { - log.warn("Post-schedule cleanup failed - stopping plugin"); - } - - return success; - - } catch (Exception ex) { - log.error("Error during post-schedule cleanup: {}", ex.getMessage(), ex); - throw new RuntimeException("Post-schedule cleanup failed", ex); - } - }, postExecutorService); - - // Handle timeout and completion - if (timeout > 0) { - postScheduledFuture.orTimeout(timeout, timeUnit) - .whenComplete((result, throwable) -> { - handlePostTaskCompletion(result, throwable); - postScheduledFuture = null; - }); - } else { - postScheduledFuture.whenComplete((result, throwable) -> { - handlePostTaskCompletion(result, throwable); - postScheduledFuture = null; - }); - } - } - - /** - * Convenience method for executing post-schedule tasks with default timeout. - * - * @param callback The callback to execute when cleanup is finished - * @param lockCondition The lock condition to prevent interruption during critical operations - */ - public final void executePostScheduleTasks(Runnable callback, LockCondition lockCondition) { - executePostScheduleTasks(callback, lockCondition, 0, TimeUnit.SECONDS); - } - /** - * Convenience method for executing post-schedule tasks with default timeout. - * @param callback The callback to execute when cleanup is finished - * @return - */ - public final void executePostScheduleTasks(Runnable callback) { - executePostScheduleTasks(callback, null, 0, TimeUnit.SECONDS); - } - /** - * Convenience method for executing post-schedule tasks with default timeout. - * @param callback The callback to execute when cleanup is finished - * @return - */ - public final void executePostScheduleTasks(LockCondition lockCondition) { - executePostScheduleTasks( () ->{} , lockCondition, 0, TimeUnit.SECONDS); - } - - /** - * Final implementation of pre-schedule task execution that enforces proper threading. - * This method cannot be overridden - it ensures all pre-schedule tasks run through the proper - * executor service infrastructure. Child classes provide their custom logic through - * {@link #executeCustomPreScheduleTask(LockCondition)}. - * - * @param lockCondition The lock condition to prevent interruption during critical operations - * @return true if preparation was successful, false otherwise - */ - protected final boolean executePreScheduleTask(LockCondition lockCondition) { - try { - executionState.update( TaskExecutionState.ExecutionPhase.PRE_SCHEDULE,TaskExecutionState.ExecutionState.FULFILLING_REQUIREMENTS); - log.debug("Executing standard pre-schedule requirements fulfillment"); - - // Always fulfill the standard requirements first - boolean standardRequirementsFulfilled = fulfillPreScheduleRequirements(); - - if (!standardRequirementsFulfilled) { - log.warn("Standard pre-schedule requirements fulfillment failed, but continuing with custom tasks"); - } - - // Execute any custom pre-schedule logic from the child class - executionState.update( TaskExecutionState.ExecutionPhase.PRE_SCHEDULE,TaskExecutionState.ExecutionState.CUSTOM_TASKS); - log.debug("Executing custom pre-schedule tasks"); - boolean customTasksSuccessful = executeCustomPreScheduleTask(preScheduledFuture,lockCondition); - - // Clear state when finished - if (standardRequirementsFulfilled && customTasksSuccessful) { - executionState.update( TaskExecutionState.ExecutionPhase.PRE_SCHEDULE,TaskExecutionState.ExecutionState.COMPLETED); - } else { - executionState.update( TaskExecutionState.ExecutionPhase.PRE_SCHEDULE,TaskExecutionState.ExecutionState.FAILED); - } - - // Return true only if both standard and custom tasks succeeded - // (or if we want to be more lenient, we could return true if custom tasks succeeded) - return standardRequirementsFulfilled && customTasksSuccessful; - - } catch (Exception e) { - executionState.update( TaskExecutionState.ExecutionPhase.PRE_SCHEDULE,TaskExecutionState.ExecutionState.ERROR); - log.error("Error during pre-schedule task execution: {}", e.getMessage(), e); - return false; - } finally { - - } - } - - /** - * Final implementation of post-schedule task execution that enforces proper threading. - * This method cannot be overridden - it ensures all post-schedule tasks run through the proper - * executor service infrastructure. Child classes provide their custom logic through - * {@link #executeCustomPostScheduleTask(LockCondition)}. - * - * @param lockCondition The lock condition to prevent interruption during critical operations - * @return true if cleanup was successful, false otherwise - */ - protected final boolean executePostScheduleTask(LockCondition lockCondition) { - try { - executionState.update( TaskExecutionState.ExecutionPhase.POST_SCHEDULE,TaskExecutionState.ExecutionState.CUSTOM_TASKS); - log.debug("Executing custom post-schedule tasks"); - // Execute any custom post-schedule logic from the child class first - // This allows plugins to handle their specific cleanup (like stopping scripts) - - boolean customTasksSuccessful = executeCustomPostScheduleTask(postScheduledFuture, lockCondition); - - if (!customTasksSuccessful) { - log.warn("Custom post-schedule tasks failed, but continuing with standard cleanup"); - return false; - } - - // Always fulfill the standard requirements after custom tasks - executionState.update( TaskExecutionState.ExecutionPhase.POST_SCHEDULE,TaskExecutionState.ExecutionState.FULFILLING_REQUIREMENTS); - log.debug("Executing standard post-schedule requirements fulfillment"); - boolean standardRequirementsFulfilled = fulfillPostScheduleRequirements(); - log.info("Standard post-schedule requirements fulfilled: {}", standardRequirementsFulfilled); - - // Update completion state - if (customTasksSuccessful && standardRequirementsFulfilled) { - executionState.update( TaskExecutionState.ExecutionPhase.POST_SCHEDULE,TaskExecutionState.ExecutionState.COMPLETED); - // Clear state after a brief delay to show completion - scheduleStateClear(2000); - } else { - executionState.update( TaskExecutionState.ExecutionPhase.POST_SCHEDULE,TaskExecutionState.ExecutionState.FAILED); - } - - // Return true if both succeeded (cleanup is more lenient than setup) - return customTasksSuccessful && standardRequirementsFulfilled; - - } catch (Exception e) { - executionState.update( TaskExecutionState.ExecutionPhase.POST_SCHEDULE,TaskExecutionState.ExecutionState.ERROR); - log.error("Error during post-schedule task execution: {}", e.getMessage(), e); - return false; - } finally { - } - } - public final boolean isPostScheduleRunning() { - return postScheduledFuture != null && !postScheduledFuture.isDone(); - } - - /** - * Abstract method that concrete implementations must provide for custom pre-schedule logic. - * This method is called AFTER the standard requirement fulfillment logic and should contain - * any plugin-specific preparation tasks that are not covered by the standard requirements. - * - * IMPORTANT: This method is always called within the proper executor service threading context. - * Do not call this method directly - use {@link #executePreScheduleTasks} instead. - * - * @param lockCondition The lock condition to prevent interruption during critical operations - * @return true if custom preparation was successful, false otherwise - */ - protected abstract boolean executeCustomPreScheduleTask(CompletableFuture preScheduledFuture,LockCondition lockCondition); - - /** - * Abstract method that concrete implementations must provide for custom post-schedule logic. - * This method is called BEFORE the standard requirement fulfillment logic and should contain - * any plugin-specific cleanup tasks (like stopping scripts, leaving minigames, etc.). - * - * IMPORTANT: This method is always called within the proper executor service threading context. - * Do not call this method directly - use {@link #executePostScheduleTasks} instead. - * - * @param lockCondition The lock condition to prevent interruption during critical operations - * @return true if custom cleanup was successful, false otherwise - */ - protected abstract boolean executeCustomPostScheduleTask(CompletableFuture postScheduledFuture, LockCondition lockCondition); - - /** - * Abstract method to determine if the plugin is running in schedule mode. - * Concrete implementations should check their specific configuration to determine this. - * - * @return true if the plugin is running under scheduler control, false otherwise - */ - protected String getConfigGroupName(){ - /** - * Returns the configuration group name for this plugin. - * This is used by the scheduler to manage configuration state. - * - * @return The configuration group name - */ - ConfigDescriptor pluginConfigDescriptor = this.plugin.getConfigDescriptor(); - if (pluginConfigDescriptor == null) { - log.warn("\"{}\" plugin config descriptor is null", this.plugin.getClass().getSimpleName()); - return ""; // Default group name if descriptor is not available - } - String configGroupName = pluginConfigDescriptor.getGroup().value(); - if (configGroupName == null || configGroupName.isEmpty()) { - log.warn("\"{}\" plugin config group name is null or empty"); - return ""; // Default group name if descriptor is not available - } - log.info("\"{}\" plugin config group name: {}", this.plugin.getClass().getSimpleName(),configGroupName); - return configGroupName; - } - - public boolean isScheduleMode() { - // Check if the plugin is running in schedule mode - if (plugin == null) { - log.warn("Plugin instance is null, cannot determine schedule mode"); - return false; // Cannot determine schedule mode without plugin instance - } - - // Check if the plugin is running in schedule mode - return isScheduleMode(this.plugin, getConfigGroupName()); - - } - /** - * Checks if the plugin is running in schedule mode by checking the GOTR configuration. - * - * @return true if scheduleMode flag is set, false otherwise - */ - public static boolean isScheduleMode( SchedulablePlugin plugin, String configGroupName) { - - SchedulerPlugin schedulablePlugin = (SchedulerPlugin) Microbot.getPlugin(SchedulerPlugin.class.getName()); - Boolean scheduleModeConfig = false; - Boolean scheduleModeDetect = false; - try { - scheduleModeConfig = Microbot.getConfigManager().getConfiguration( - configGroupName, "scheduleMode", Boolean.class); - } catch (Exception e) { - log.error("Failed to check schedule mode: {}", e.getMessage()); - return false; - } - if (schedulablePlugin == null) { - log.warn("SchedulerPlugin is not running, cannot can not be in schedule mode"); - scheduleModeDetect = false; // SchedulerPlugin is not running, cannot determine schedule mode, so we dont run in schedule mode - }else{ - PluginScheduleEntry currentPlugin = schedulablePlugin.getCurrentPlugin(); - if (currentPlugin == null) { - log.warn("\nNo current plugin is running by the Scheduler Plugin, so it also can not be the plugin is start in scheduler mode"); - scheduleModeDetect = false; // No current plugin is running, so it can not be in schedule mode - }else{ - if (currentPlugin.isRunning() && currentPlugin.getPlugin() != null && !currentPlugin.getPlugin().equals(plugin)) { - log.warn("\n\tCurrent plugin {} is running, but it is not the same as the pluginScheduleEntry {}, so it can not be in schedule mode", - currentPlugin.getPlugin().getClass().getSimpleName(), - plugin.getClass().getSimpleName()); - scheduleModeDetect = false; // Current plugin is running, but it's not the same as the pluginSchedule - - }else{ - scheduleModeDetect = true; - } - } - } - - if (configGroupName.isEmpty()) { - log.warn("Config group name is empty, cannot determine schedule mode"); - }else if(scheduleModeConfig){ - Microbot.getConfigManager().setConfiguration(configGroupName, "scheduleMode", scheduleModeConfig); - scheduleModeDetect = true; // If scheduleMode config is set, we are in schedule mode - } - log.debug("\nPlugin {}, with config group name {}, \nis running in schedule mode (plugin detect): {}\n\t\tSchedule mode config: {}", - plugin.getClass().getSimpleName(), configGroupName, scheduleModeDetect, scheduleModeConfig); - return scheduleModeDetect; - - } - private void setScheduleMode(boolean scheduleMode) { - try { - String configGroupName = getConfigGroupName(); - if (configGroupName == null || configGroupName.isEmpty()) { - log.warn("\"{}\" plugin config group name is null or empty", this.plugin.getClass().getSimpleName()); - return; // Cannot set schedule mode without config group - } - Microbot.getConfigManager().setConfiguration(configGroupName, "scheduleMode", scheduleMode); - } catch (Exception e) { - log.error("Failed to set schedule mode: {}", e.getMessage()); - } - } - protected void enableScheduleMode() { - setScheduleMode(true); - } - protected void disableScheduleMode() { - setScheduleMode(false); - } - - - /** - * Abstract method that concrete implementations must provide to supply their requirements. - * This method should return the PrePostScheduleRequirements instance that defines - * what the plugin needs for optimal operation. - * - * @return The PrePostScheduleRequirements instance for this plugin - */ - protected abstract PrePostScheduleRequirements getPrePostScheduleRequirements(); - - /** - * Public accessor for the pre/post schedule requirements. - * This allows external components (like UI panels) to access requirement information. - * - * @return The PrePostScheduleRequirements instance, or null if not implemented - */ - public final PrePostScheduleRequirements getRequirements() { - return getPrePostScheduleRequirements(); - } - - /** - * Adds a custom requirement to this plugin's pre/post schedule requirements. - * Custom requirements are marked as CUSTOM type and are fulfilled after all standard requirements. - * - * @param requirement The requirement to add - * @param TaskContext The context in which this requirement should be fulfilled - * @return true if the requirement was successfully added, false otherwise - */ - public boolean addCustomRequirement(Requirement requirement, - TaskContext taskContext) { - PrePostScheduleRequirements requirements = getPrePostScheduleRequirements(); - if (requirements == null) { - log.warn("Cannot add custom requirement: No pre/post schedule requirements defined for this plugin"); - return false; - } - - // Mark this requirement as a custom requirement by creating a wrapper - // We'll modify the requirement to ensure it's recognized as custom - boolean success = requirements.addCustomRequirement(requirement, taskContext); - - if (success) { - log.info("Successfully added custom requirement: {} for context: {}", - requirement.getDescription(), taskContext); - } else { - log.warn("Failed to add custom requirement: {} for context: {}", - requirement.getDescription(), taskContext); - } - - return success; - } - - /** - * Default implementation for fulfilling pre-schedule requirements. - * This method attempts to fulfill all pre-schedule requirements including: - * - Location requirements (travel to pre-schedule location) - * - Spellbook requirements (switch to required spellbook) - * - Equipment and inventory setup (using the static methods from PrePostScheduleRequirements) - * - * Child classes can override this method to provide custom behavior while still - * leveraging the default requirement fulfillment logic. - * - * @return true if all requirements were successfully fulfilled, false otherwise - */ - protected boolean fulfillPreScheduleRequirements() { - try { - PrePostScheduleRequirements requirements = getPrePostScheduleRequirements(); - if (requirements == null) { - log.info("No pre-schedule requirements defined"); - return true; - } - - executionState.update( TaskExecutionState.ExecutionPhase.PRE_SCHEDULE,TaskExecutionState.ExecutionState.FULFILLING_REQUIREMENTS); - log.info("\n\tFulfilling pre-schedule requirements for {}", requirements.getActivityType()); - - // Use the unified fulfillment method that handles all requirement types including conditional requirements - boolean fulfilled = requirements.fulfillPreScheduleRequirements(preScheduledFuture, true, executionState); // Pass executor service - - if (!fulfilled) { - log.error("Failed to fulfill pre-schedule requirements"); - return false; - } - - log.info("Successfully fulfilled pre-schedule requirements"); - return true; - - } catch (Exception e) { - log.error("Error fulfilling pre-schedule requirements: {}", e.getMessage(), e); - return false; - } - } - - /** - * Default implementation for fulfilling post-schedule requirements. - * This method attempts to fulfill all post-schedule requirements including: - * - Location requirements (travel to post-schedule location) - * - Spellbook restoration (switch back to original spellbook) - * - Banking and cleanup operations - * - * Child classes can override this method to provide custom behavior while still - * leveraging the default requirement fulfillment logic. - * - * @return true if all requirements were successfully fulfilled, false otherwise - */ - protected boolean fulfillPostScheduleRequirements() { - try { - PrePostScheduleRequirements requirements = getPrePostScheduleRequirements(); - if (requirements == null) { - log.info("No post-schedule requirements defined"); - return true; - } - executionState.update( TaskExecutionState.ExecutionPhase.POST_SCHEDULE,TaskExecutionState.ExecutionState.FULFILLING_REQUIREMENTS); - log.info("Fulfilling post-schedule requirements for {}", requirements.getActivityType()); - - // Use the unified fulfillment method that handles all requirement types including conditional requirements - boolean fulfilled = requirements.fulfillPostScheduleRequirements(postScheduledFuture, true,executionState ); // Pass executor service - - if (!fulfilled) { - log.error("Failed to fulfill all post-schedule requirements"); - return false; - } - - log.info("Successfully fulfilled post-schedule requirements"); - return true; - - } catch (Exception e) { - log.error("Error fulfilling post-schedule requirements: {}", e.getMessage(), e); - return false; - } finally { - // Clear requirements state - PrePostScheduleRequirements requirements = getPrePostScheduleRequirements(); - if (requirements != null) { - try { - clearRequirementState(); - } catch (Exception e) { - log.warn("Failed to clear fulfillment state: {}", e.getMessage()); - } - } - } - } - - /** - * Handles the completion of pre-schedule tasks. - * - * @param result The result of the task execution - * @param throwable Any exception that occurred during execution - */ - private void handlePreTaskCompletion(Boolean result, Throwable throwable) { - unlock(); - if (throwable != null) { - if (throwable instanceof TimeoutException) { - log.warn("Pre-schedule task timed out for plugin: {}", plugin.getClass().getSimpleName()); - plugin.reportPreScheduleTaskFinished("Pre-schedule task timed out", ExecutionResult.SOFT_FAILURE); - } else { - log.error("Pre-schedule task failed for plugin: {} - {}", - plugin.getClass().getSimpleName(), throwable.getMessage()); - plugin.reportPreScheduleTaskFinished("Pre-schedule task failed: " + throwable.getMessage(), ExecutionResult.HARD_FAILURE); - } - } else if (result != null && result) { - log.info("Pre-schedule task completed successfully for plugin: {}", plugin.getClass().getSimpleName()); - executionState.update( TaskExecutionState.ExecutionPhase.MAIN_EXECUTION,TaskExecutionState.ExecutionState.STARTING); - plugin.reportPreScheduleTaskFinished("Pre-schedule preparation completed successfully", ExecutionResult.SUCCESS); - }else{ - executionState.update( TaskExecutionState.ExecutionPhase.MAIN_EXECUTION,TaskExecutionState.ExecutionState.ERROR); - plugin.reportPreScheduleTaskFinished("\n\tPre-schedule preparation was not successful", ExecutionResult.SOFT_FAILURE); - } - - } - - /** - * Handles the completion of post-schedule tasks. - * - * @param result The result of the task execution - * @param throwable Any exception that occurred during execution - */ - private void handlePostTaskCompletion(Boolean result, Throwable throwable) { - unlock(); - if (throwable != null) { - if (throwable instanceof TimeoutException) { - log.warn("Post-schedule task timed out for plugin: {}", plugin.getClass().getSimpleName()); - plugin.reportPostScheduleTaskFinished("Post-schedule task timed out", ExecutionResult.SOFT_FAILURE); - } else { - log.error("Post-schedule task failed for plugin: {} - {}", - plugin.getClass().getSimpleName(), throwable.getMessage()); - plugin.reportPostScheduleTaskFinished("Post-schedule task failed: " + throwable.getMessage(), ExecutionResult.HARD_FAILURE); - } - } else if (result != null && result) { - log.debug("Post-schedule task completed successfully for plugin: {}", plugin.getClass().getSimpleName()); - executionState.update( TaskExecutionState.ExecutionPhase.POST_SCHEDULE,TaskExecutionState.ExecutionState.COMPLETED); - plugin.reportPostScheduleTaskFinished("Post-schedule task completed successfully", ExecutionResult.SUCCESS); - }else{ - executionState.update( TaskExecutionState.ExecutionPhase.POST_SCHEDULE,TaskExecutionState.ExecutionState.ERROR); - plugin.reportPostScheduleTaskFinished("\n\tPost-schedule task was not successful", ExecutionResult.SOFT_FAILURE); - } - } - - /** - * Initializes the post executor service if not already initialized. - */ - private void initializePostExecutorService() { - if (postExecutorService == null || postExecutorService.isShutdown()) { - postExecutorService = Executors.newScheduledThreadPool(2, r -> { - Thread t = new Thread(r, getClass().getSimpleName() + "-PostSchedule"); - t.setDaemon(true); - return t; - }); - } - } - - /** - * Safely shuts down an executor service with proper timeout handling. - * - * @param executorService The executor service to shutdown - * @param taskType The type of task (for logging purposes) - */ - private void shutdownExecutorService(ScheduledExecutorService executorService, String taskType) { - if (executorService != null && !executorService.isShutdown()) { - try { - executorService.shutdown(); - - if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { - log.warn("Executor service for {} tasks did not terminate gracefully, forcing shutdown", taskType); - executorService.shutdownNow(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.warn("Interrupted while shutting down {} executor service", taskType); - executorService.shutdownNow(); - } - } - } - - /** - * Returns whether any tasks are currently running. - * - * @return true if any pre or post tasks are currently executing - */ - public final boolean isRunning() { - return (preScheduledFuture != null && !preScheduledFuture.isDone()) || - (postScheduledFuture != null && !postScheduledFuture.isDone()); - } - - /** - * Cancels any running tasks and shuts down all executor services. - * This method implements AutoCloseable for proper resource management. - */ - @Override - public void close() { - // Unregister emergency cancel hotkey - try { - if (keyManager != null) { - keyManager.unregisterKeyListener(this); - log.debug("Unregistered emergency cancel hotkey for plugin: {}", plugin.getClass().getSimpleName()); - } - } catch (Exception e) { - log.warn("Failed to unregister emergency cancel hotkey for plugin {}: {}", - plugin.getClass().getSimpleName(), e.getMessage()); - } - unlock(); - // Cancel any running futures - if (preScheduledFuture != null && !preScheduledFuture.isDone()) { - preScheduledFuture.cancel(true); - preScheduledFuture = null; - } - - if (postScheduledFuture != null && !postScheduledFuture.isDone()) { - postScheduledFuture.cancel(true); - postScheduledFuture = null; - } - executionState.reset(); - - // Shutdown executor services - shutdownExecutorService(preExecutorService, "pre-schedule"); - shutdownExecutorService(postExecutorService, "post-schedule"); - - preExecutorService = null; - postExecutorService = null; - disableScheduleMode(); // Reset schedule mode - log.info("Closed {} pre-post Schedule task task manager", getClass().getSimpleName()); - } - public void cancelPreScheduleTasks() { - if (preScheduledFuture != null && !preScheduledFuture.isDone()) { - preScheduledFuture.cancel(true); - preScheduledFuture = null; - log.info("Cancelled pre-schedule tasks for plugin: {}", plugin.getClass().getSimpleName()); - } else { - log.warn("No pre-schedule tasks to cancel for plugin: {}", plugin.getClass().getSimpleName()); - } - } - - public void cancelPostScheduleTasks() { - if (postScheduledFuture != null && !postScheduledFuture.isDone()) { - postScheduledFuture.cancel(true); - postScheduledFuture = null; - log.info("Cancelled post-schedule tasks for plugin: {}", plugin.getClass().getSimpleName()); - } else { - log.warn("No post-schedule tasks to cancel for plugin: {}", plugin.getClass().getSimpleName()); - } - } - - /** - * Shutdown alias for compatibility. Calls {@link #close()}. - */ - public final void shutdown() { - close(); - } - /** - * Clears the current task state - */ - protected void clearTaskState() { - executionState.clear(); - } - protected void clearRequirementState() { - executionState.clearRequirementState(); - } - - /** - * Gets the current execution status for overlay display - * @return A formatted string describing the current state, or null if not executing - */ - public String getCurrentExecutionStatus() { - return executionState.getDisplayStatus(); - } - - /** - * Checks if any pre/post schedule task is currently executing - */ - public boolean isExecuting() { - return executionState.isExecuting(); - } - - /** - * Resets the task execution state and cancels any running tasks. - * This allows pre/post schedule tasks to be executed again. - * - * WARNING: This will forcibly cancel any currently running tasks. - */ - public synchronized void reset() { - log.info("Resetting pre/post schedule tasks for plugin: {}", plugin.getClass().getSimpleName()); - unlock(); - // Cancel and cleanup running futures - if (preScheduledFuture != null && !preScheduledFuture.isDone()) { - log.info("Cancelling running pre-schedule task"); - preScheduledFuture.cancel(true); - preScheduledFuture = null; - } - - if (postScheduledFuture != null && !postScheduledFuture.isDone()) { - log.info("Cancelling running post-schedule task"); - postScheduledFuture.cancel(true); - postScheduledFuture = null; - } - - // Shutdown executor services - shutdownExecutorService(preExecutorService, "Pre-schedule"); - shutdownExecutorService(postExecutorService, "Post-schedule"); - - preExecutorService = null; - postExecutorService = null; - - // Reset execution state - executionState.reset(); - if ( getRequirements()!= null) { - clearRequirementState(); - getRequirements().reset(); - } - log.info("Reset completed - pre/post schedule tasks can now be executed again"); - } - - // Convenience methods for checking task states - - /** - * Checks if pre-schedule tasks are completed - */ - public boolean isPreTaskComplete() { - return executionState.isPreTaskComplete(); - } - - public boolean isHasPreTaskStarted() { - return executionState.isPreTaskRunning(); - } - /** - * Checks if main task is running - */ - public boolean isHasMainTaskStarted() { - return executionState.isMainTaskRunning(); - } - - /** - * Checks if main task is completed - */ - public boolean isMainTaskComplete() { - return executionState.isMainTaskComplete(); - } - - /** - * Checks if post-schedule tasks are completed - */ - public boolean isPostTaskComplete() { - return executionState.isPostTaskComplete(); - } - - /** - * Checks if pre-schedule tasks are currently running - */ - public boolean isPreTaskRunning() { - return executionState.isPreTaskRunning(); - } - - /** - * Checks if post-schedule tasks are currently running - */ - public boolean isPostTaskRunning() { - return executionState.isPostTaskRunning(); - } - - /** - * Gets a detailed status string for debugging - */ - public String getDetailedExecutionStatus() { - return executionState.getDetailedStatus(); - } - - /** - * Schedules clearing of the task state after a delay - * @param delayMs Delay in milliseconds before clearing the state - */ - private void scheduleStateClear(int delayMs) { - new Thread(() -> { - try { - Thread.sleep(delayMs); - clearTaskState(); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - }).start(); - } - - /** - * Emergency cancellation method for aborting all tasks and operations. - * This method will: - * 1. Cancel all pre and post schedule futures - * 2. Shutdown all executor services - * 3. Clear Rs2Walker target - * 4. Reset task execution state - * - * This is used by the Ctrl+C hotkey for emergency stops. - */ - public final void emergencyCancel() { - try { - log.warn("\n=== EMERGENCY CANCELLATION TRIGGERED ==="); - log.warn("Plugin: {}", plugin.getClass().getSimpleName()); - - // Cancel all futures immediately - if (preScheduledFuture != null && !preScheduledFuture.isDone()) { - log.info(" â€Ē Cancelling pre-schedule future"); - preScheduledFuture.cancel(true); - preScheduledFuture = null; - } - - if (postScheduledFuture != null && !postScheduledFuture.isDone()) { - log.info(" â€Ē Cancelling post-schedule future"); - postScheduledFuture.cancel(true); - postScheduledFuture = null; - } - - // Shutdown executor services immediately - if (preExecutorService != null && !preExecutorService.isShutdown()) { - log.info(" â€Ē Shutting down pre-schedule executor service"); - preExecutorService.shutdownNow(); - preExecutorService = null; - } - - if (postExecutorService != null && !postExecutorService.isShutdown()) { - log.info(" â€Ē Shutting down post-schedule executor service"); - postExecutorService.shutdownNow(); - postExecutorService = null; - } - - // Clear Rs2Walker target to stop any walking operations - try { - log.info(" â€Ē Clearing Rs2Walker target"); - Rs2Walker.setTarget(null); - } catch (Exception e) { - log.warn("Failed to clear Rs2Walker target: {}", e.getMessage()); - } - - // Reset task execution state - log.info(" â€Ē Resetting task execution state"); - executionState.reset(); - unlock(); - log.warn("=== EMERGENCY CANCELLATION COMPLETED ===\n"); - - } catch (Exception e) { - log.error("Error during emergency cancellation: {}", e.getMessage(), e); - } - } - - // ==================== KeyListener Interface Methods ==================== - - @Override - public void keyTyped(KeyEvent e) { - // Not needed for hotkey detection - } - - @Override - public void keyPressed(KeyEvent e) { - // Check for Ctrl+C hotkey combination - if (e.isControlDown() && e.getKeyCode() == KeyEvent.VK_C) { - log.info("Emergency cancel hotkey (Ctrl+C) detected for plugin: {}", plugin.getClass().getSimpleName()); - - // Only trigger if we have running tasks - if (isRunning()) { - emergencyCancel(); - } else { - log.info("No tasks currently running - emergency cancel not needed"); - } - } - } - - @Override - public void keyReleased(KeyEvent e) { - // Not needed for hotkey detection - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/README.md b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/README.md deleted file mode 100644 index 392edb7bd1f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/README.md +++ /dev/null @@ -1,263 +0,0 @@ -# Pre and Post Schedule Tasks Infrastructure - -## Overview - -The Pre and Post Schedule Tasks infrastructure provides a standardized way for Microbot plugins to handle preparation and cleanup when operating under scheduler control. This system ensures consistent resource management, proper plugin lifecycle handling, and graceful startup/shutdown procedures. - -## Architecture - -### Abstract Base Class: `AbstractPrePostScheduleTasks` - -The `AbstractPrePostScheduleTasks` class provides: - -- **Executor Service Management**: Automatic creation and lifecycle management of thread pools for pre and post tasks -- **CompletableFuture Handling**: Asynchronous task execution with timeout support and proper error handling -- **Resource Cleanup**: AutoCloseable implementation ensures proper shutdown of all resources -- **Common Error Patterns**: Standardized logging and error handling across all implementations -- **Thread Safety**: Safe concurrent execution and cancellation of tasks - -### Key Features - -1. **Asynchronous Execution**: Tasks run on separate threads to avoid blocking the main plugin thread -2. **Timeout Support**: Configurable timeouts with graceful handling of timeout scenarios -3. **Lock Integration**: Support for LockCondition to prevent interruption during critical operations -4. **Callback Support**: Execute callbacks when pre-tasks complete successfully -5. **Automatic Cleanup**: Resources are automatically cleaned up on plugin shutdown - -## Implementation Guide - -### Step 1: Create Your Task Implementation - -Extend `AbstractPrePostScheduleTasks` and implement the three required abstract methods: - -```java -public class YourPluginPrePostScheduleTasks extends AbstractPrePostScheduleTasks { - - public YourPluginPrePostScheduleTasks(SchedulablePlugin plugin) { - super(plugin); - // Initialize plugin-specific requirements or dependencies - } - - @Override - protected boolean executePreScheduleTask(LockCondition lockCondition) { - // Add your plugin's preparation logic here - // Return true if successful, false otherwise - } - - @Override - protected boolean executePostScheduleTask(LockCondition lockCondition) { - // Add your plugin's cleanup logic here - // Return true if successful, false otherwise - } - - @Override - protected boolean isScheduleMode() { - // Check your plugin's configuration to determine if running under scheduler - Boolean scheduleMode = Microbot.getConfigManager().getConfiguration( - "YourPluginConfig", "scheduleMode", Boolean.class); - return scheduleMode != null && scheduleMode; - } -} -``` - -### Step 2: Integrate with Your Plugin - -In your plugin class that implements `SchedulablePlugin`: - -```java -@PluginDescriptor(name = "Your Plugin") -public class YourPlugin extends Plugin implements SchedulablePlugin { - - private YourPluginPrePostScheduleTasks prePostTasks; - private LockCondition lockCondition; - - @Override - protected void startUp() { - // Initialize the task manager - prePostTasks = new YourPluginPrePostScheduleTasks(this); - - // Execute pre-schedule tasks with callback - prePostTasks.executePreScheduleTasks(() -> { - // This callback runs when preparation is complete - yourScript.run(config); - }); - } - - @Override - protected void shutDown() { - // Clean up resources - if (prePostTasks != null) { - prePostTasks.close(); - } - } - - @Subscribe - public void onPluginScheduleEntryPostScheduleTaskEvent(PluginScheduleEntryPostScheduleTaskEvent event) { - if (event.getPlugin() == this && prePostTasks != null) { - if (lockCondition != null && lockCondition.isLocked()) { - return; // respect critical section - } - // Execute post-schedule cleanup - prePostTasks.executePostScheduleTasks(lockCondition); - } - } - - @Override - public LogicalCondition getStopCondition() { - if (lockCondition == null) { - lockCondition = new LockCondition("Plugin locked during critical operation", false,true); //ensure unlock on shutdown of the plugin ! - } - AndCondition condition = new AndCondition(); - condition.addCondition(lockCondition); - return condition; - } -} -``` - -### Step 3: Common Patterns - -#### Banking and Equipment Management - -```java -private boolean prepareOptimalSetup() { - if (!Rs2Bank.openBank()) { - return false; - } - - // Deposit current items - Rs2Bank.depositAll(); - Rs2Bank.depositEquipment(); - - // Withdraw required items - Rs2Bank.withdrawOne(ItemID.BRONZE_PICKAXE); - Rs2Bank.withdrawX(ItemID.SALMON, 10); - - Rs2Bank.closeBank(); - return true; -} - -private boolean bankAllItems() { - if (!Rs2Bank.openBank()) { - return false; - } - - Rs2Bank.depositAll(); - Rs2Bank.depositEquipment(); - Rs2Bank.closeBank(); - return true; -} -``` - -#### Walking to Locations - -```java -private boolean walkToLocation() { - if (Rs2Bank.isNearBank(BankLocation.GRAND_EXCHANGE, 6)) { - return true; - } - - boolean walkResult = Rs2Walker.walkWithBankedTransports( - BankLocation.GRAND_EXCHANGE.getWorldPoint(), - true, // Use bank items for transportation - false // Don't force banking route - ); - - return sleepUntil(() -> Rs2Bank.isNearBank(BankLocation.GRAND_EXCHANGE, 6), 30000); -} -``` - -#### Lock Management - -```java -@Override -protected boolean executeCustomPreScheduleTask(CompletableFuture preScheduledFuture, LockCondition lockCondition) { - if (lockCondition != null) { - lockCondition.lock(); // Prevent interruption during setup - } - - try { - // Perform critical setup operations - return performSetup(); - - } finally { - if (lockCondition != null) { - lockCondition.unlock(); - } - } -} -``` - -## Method Reference - -### AbstractPrePostScheduleTasks Methods - -#### Public Methods - -- `executePreScheduleTasks(Runnable callback)` - Execute pre-tasks with callback -- `executePreScheduleTasks(Runnable callback, int timeout, TimeUnit timeUnit)` - Execute pre-tasks with timeout -- `executePostScheduleTasks(LockCondition lockCondition)` - Execute post-tasks -- `executePostScheduleTasks(LockCondition lockCondition, int timeout, TimeUnit timeUnit)` - Execute post-tasks with timeout -- `isRunning()` - Check if any tasks are currently executing -- `close()` - Clean up all resources and cancel running tasks -- `shutdown()` - Alias for close() - -#### Abstract Methods (Must Implement) - -- `executeCustomPreScheduleTask(CompletableFuture preScheduledFuture, LockCondition lockCondition)` - Plugin-specific preparation logic -- `executeCustomPostScheduleTask(CompletableFuture postScheduledFuture, LockCondition lockCondition)` - Plugin-specific cleanup logic -- `getPrePostScheduleRequirements()` - Return the requirements instance for this plugin - -## Error Handling - -The infrastructure provides comprehensive error handling through centralized try-catch blocks in the base class: - -1. **Centralized Exception Handling**: All exceptions from custom task methods are caught and logged by the parent class -2. **Timeout Handling**: Tasks that exceed their timeout are automatically cancelled -3. **Proper Error Reporting**: Failures are reported to the scheduler with appropriate ExecutionResult values -4. **Resource Cleanup**: Resources are always cleaned up, even when errors occur -5. **No Redundant Error Handling**: Custom task methods should NOT include try-catch blocks - let errors bubble up to the centralized handlers - -## TODO Items for Future Enhancement - -The following TODO items are included in the abstract class for future improvements: - -- **Configuration for default timeout values**: Allow global configuration of timeout defaults -- **Metrics collection**: Track task execution times and success rates for monitoring -- **Retry mechanism**: Implement exponential backoff for failed tasks -- **Task priority levels**: Support for critical vs optional task classification - -## Integration with Requirements System - -The task infrastructure works seamlessly with the existing requirements system located in: -`runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/` - -You can use requirement classes like `ItemRequirement`, `RequirementCollection`, etc. within your task implementations for standardized equipment and item management. - -## Examples - -See the following implementations for reference: - -1. **GotrPrePostScheduleTasks**: Complete implementation for Guardians of the Rift plugin -2. **ExamplePrePostScheduleTasks**: Template implementation showing common patterns - -## Best Practices - -1. **Always check isScheduleMode()**: Only perform schedule-specific logic when actually running under scheduler -2. **Use lock conditions**: Prevent interruption during critical operations like minigame participation -3. **Handle failures gracefully**: Return false from task methods to indicate failure - exceptions will be handled by the parent class -4. **Log appropriately**: Use structured logging with appropriate log levels -5. **Avoid redundant error handling**: Do not add try-catch blocks in custom task methods - the parent class provides centralized error handling -6. **Clean up resources**: Always implement proper resource cleanup in your task methods -7. **Test both modes**: Ensure your plugin works both with and without scheduler control - - -<<<<<<< HEAD -Design note: -Further improvemnts: We should improve the requirement fulfillment flow and clarify ItemRequirement semantics. Equipment requirements target a specific EquipmentInventorySlot; inventory requirements should not. To avoid overloading a single type with sentinel values (e.g., null slot), consider: -======= -Design note: We should improve the requirement fulfillment flow and clarify ItemRequirement semantics. Equipment requirements target a specific EquipmentInventorySlot; inventory requirements should not. To avoid overloading a single type with sentinel values (e.g., null slot), consider: ->>>>>>> ff36783985 ((feat,bugfixes,core): cache architecture overhaul and comprehensive pre/post schedule tasks system) -- Making ItemRequirement an abstract base type. -- Introduce EquipmentRequirement (has a non-null EquipmentInventorySlot). -- Introduce InventoryRequirement (no slot; optional quantity/stack rules). -This separation will eliminate magic values, reduce null checks, and make the fulfillment process simpler and safer. \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleRequirements.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleRequirements.java deleted file mode 100644 index 4c735e698b0..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleRequirements.java +++ /dev/null @@ -1,151 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.examples; - -import java.util.Arrays; - -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.api.Skill; -import net.runelite.api.gameval.ItemID; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; - - -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.PrePostScheduleRequirements; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location.LocationRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.SpellbookRequirement; -import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; -import net.runelite.client.plugins.microbot.util.magic.Rs2Spellbook; - -/** - * Example implementation of PrePostScheduleRequirements showing basic usage patterns. - * This serves as a template for creating requirements collections for other plugins. - * - * This example demonstrates: - * - Basic equipment requirements with different priority levels - * - Location requirements for pre and post schedule positioning - * - Optional spellbook requirements - * - How to organize requirements by category and effectiveness - */ -public class ExamplePrePostScheduleRequirements extends PrePostScheduleRequirements { - - public ExamplePrePostScheduleRequirements() { - super("Example", "General", false); - } - - /** - * Initializes the item requirement collection with example items. - * This demonstrates typical patterns for different equipment slots and priorities. - */ - @Override - protected boolean initializeRequirements() { - this.getRegistry().clear(); // Clear previous requirements if any - - - - // Example: Optional teleport spellbook for faster travel - SpellbookRequirement normalSpellbookRequirement = new SpellbookRequirement( - Rs2Spellbook.MODERN, - TaskContext.PRE_SCHEDULE, // Only need it before script - RequirementPriority.RECOMMENDED, - 6, // Rating 6/10 - moderately useful - "Modern spellbook for teleport spells during travel" - ); - this.register(normalSpellbookRequirement); - - // Set location requirements - // Pre-schedule: Start at Grand Exchange for easy access to supplies - this.register(new LocationRequirement(BankLocation.GRAND_EXCHANGE, true,-1,TaskContext.PRE_SCHEDULE, RequirementPriority.RECOMMENDED)); - // Post-schedule: Return to Grand Exchange for selling/organizing items - this.register(new LocationRequirement(BankLocation.GRAND_EXCHANGE, true,-1,TaskContext.POST_SCHEDULE, RequirementPriority.RECOMMENDED)); - - TaskContext taskContext = TaskContext.PRE_SCHEDULE; // Default to pre-schedule context - // HEAD - Example progression: best to worst - this.register(new ItemRequirement( - ItemID.GRACEFUL_HOOD, - EquipmentInventorySlot.HEAD, RequirementPriority.RECOMMENDED, 8, "Graceful hood for weight reduction",taskContext - )); - this.register(new ItemRequirement( - ItemID.VIKING_HELMET, - EquipmentInventorySlot.HEAD, RequirementPriority.RECOMMENDED, 5, "Basic head protection",taskContext - )); - - // CAPE - Example with skill requirements - this.register(new ItemRequirement( - ItemID.GRACEFUL_CAPE, - EquipmentInventorySlot.CAPE, RequirementPriority.RECOMMENDED, 8, "Graceful cape for weight reduction", taskContext - )); - this.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.SKILLCAPE_AGILITY, ItemID.SKILLCAPE_AGILITY_TRIMMED), - 1, - EquipmentInventorySlot.CAPE, - -2, - RequirementPriority.RECOMMENDED, - 10, - "Agility cape (99 Agility required)", - taskContext, - Skill.AGILITY, - 99, - null, - null - )); - - - this.register(new ItemRequirement( - ItemID.AMULET_OF_POWER, - EquipmentInventorySlot.AMULET, RequirementPriority.RECOMMENDED, 4, "Basic amulet of power", taskContext - )); - - // BODY - Example weight reduction focus - this.register(new ItemRequirement( - ItemID.GRACEFUL_TOP, - EquipmentInventorySlot.BODY, RequirementPriority.RECOMMENDED, 8, "Graceful top for weight reduction",taskContext - )); - this.register(new ItemRequirement( - ItemID.HARDLEATHER_BODY, - EquipmentInventorySlot.BODY, RequirementPriority.RECOMMENDED, 3, "Basic leather body",taskContext - )); - - // LEGS - Continue weight reduction theme - this.register(new ItemRequirement( - ItemID.GRACEFUL_LEGS, - EquipmentInventorySlot.LEGS, RequirementPriority.RECOMMENDED, 8, "Graceful legs for weight reduction",taskContext - )); - this.register(new ItemRequirement( - ItemID.LEATHER_CHAPS, - EquipmentInventorySlot.LEGS, RequirementPriority.RECOMMENDED, 3, "Basic leather chaps",taskContext - )); - - // BOOTS - Complete the graceful set - this.register(new ItemRequirement( - ItemID.GRACEFUL_BOOTS, - EquipmentInventorySlot.BOOTS, RequirementPriority.RECOMMENDED, 8, "Graceful boots for weight reduction",taskContext - )); - this.register(new ItemRequirement( - ItemID.LEATHER_BOOTS, - EquipmentInventorySlot.BOOTS, RequirementPriority.RECOMMENDED, 3, "Basic leather boots",taskContext - )); - - // GLOVES - Graceful gloves to complete the set - this.register(new ItemRequirement( - ItemID.GRACEFUL_GLOVES, - EquipmentInventorySlot.GLOVES, RequirementPriority.RECOMMENDED, 8, "Graceful gloves for weight reduction",taskContext - )); - this.register(new ItemRequirement( - ItemID.LEATHER_GLOVES, - EquipmentInventorySlot.GLOVES, RequirementPriority.RECOMMENDED, 3, "Basic leather gloves",taskContext - )); - - // INVENTORY ITEMS - Example tools and supplies - this.register(new ItemRequirement( - ItemID.COINS, 1, -1, - RequirementPriority.RECOMMENDED, 9, "Coins for purchases and teleports",taskContext - )); - - this.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.LOBSTER, ItemID.SWORDFISH, ItemID.TUNA), 1, null,-1, - RequirementPriority.RECOMMENDED, 5, "Food for healing if needed",taskContext - )); - return true; // Initialization successful - // EITHER ITEMS - Items that can be equipped or kept in inventory - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleTasks.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleTasks.java deleted file mode 100644 index e56b64c7829..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/examples/ExamplePrePostScheduleTasks.java +++ /dev/null @@ -1,206 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.examples; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.api.SchedulablePlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LockCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.AbstractPrePostScheduleTasks; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.PrePostScheduleRequirements; -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.util.equipment.Rs2Equipment; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; - -import java.util.concurrent.CompletableFuture; - -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -/** - * Example implementation showing how to extend {@link AbstractPrePostScheduleTasks} - * for a generic plugin with basic banking and equipment management. - *

- * This serves as a template for creating pre/post schedule tasks for other plugins. - * Simply copy this class and customize the preparation and cleanup logic for your specific plugin needs. - * - * TODO: Customize the following methods for your plugin: - * - {@link #executePreScheduleTask(LockCondition)} - Add your plugin's preparation logic - * - {@link #executePostScheduleTask(LockCondition)} - Add your plugin's cleanup logic - * - {@link #isScheduleMode()} - Check your plugin's configuration for schedule mode - * - Add any plugin-specific helper methods as needed - */ -@Slf4j -public class ExamplePrePostScheduleTasks extends AbstractPrePostScheduleTasks { - - private ExamplePrePostScheduleRequirements exampleRequirements; - - /** - * Constructor for ExamplePrePostScheduleTasks. - * - * @param plugin The SchedulablePlugin instance to manage - */ - public ExamplePrePostScheduleTasks(SchedulablePlugin plugin) { - super(plugin,null); - this.exampleRequirements = new ExamplePrePostScheduleRequirements(); - } - - /** - * Provides the example requirements for the default implementation to use. - * - * @return The ExamplePrePostScheduleRequirements instance - */ - @Override - protected PrePostScheduleRequirements getPrePostScheduleRequirements() { - return exampleRequirements; - } - - /** - * Executes pre-schedule preparation for the example plugin. - * Customize this method with your plugin's specific preparation logic. - * - * @param lockCondition The lock condition to prevent interruption during critical operations - * @return true if preparation was successful, false otherwise - */ - @Override - protected boolean executeCustomPreScheduleTask(CompletableFuture preScheduledFuture, LockCondition lockCondition) { - if (lockCondition != null) { - lockCondition.lock(); - } - - try { - log.info("Starting example plugin pre-schedule preparation..."); - - // Example: Walk to Grand Exchange bank - if (!walkToBank()) { - log.error("Failed to reach bank location"); - return false; - } - - // Example: Prepare basic equipment and inventory - if (!prepareBasicSetup()) { - log.error("Failed to prepare basic setup"); - return false; - } - - log.info("Example plugin pre-schedule preparation completed successfully"); - return true; - - } finally { - if (lockCondition != null) { - lockCondition.unlock(); - } - } - } - - /** - * Executes post-schedule cleanup for the example plugin. - * Customize this method with your plugin's specific cleanup logic. - * - * @param lockCondition The lock condition to prevent interruption during critical operations - * @return true if cleanup was successful, false otherwise - */ - @Override - protected boolean executeCustomPostScheduleTask(CompletableFuture postScheduledFuturem, LockCondition lockCondition) { - log.info("Starting example plugin post-schedule cleanup..."); - - // Example: Bank all items for safe shutdown - if (!bankAllItems()) { - log.warn("Warning: Failed to bank all items during post-schedule cleanup"); - } - - log.info("Example plugin post-schedule cleanup completed - stopping plugin"); - Microbot.getClientThread().invokeLater(() -> { - Microbot.stopPlugin((net.runelite.client.plugins.Plugin) plugin); - return true; - }); - - return true; - } - - - - /** - * Example helper method: Walks to a bank location. - * Customize this for your plugin's specific bank location needs. - * - * @return true if successfully reached the bank, false otherwise - */ - private boolean walkToBank() { - // Example: Walk to Grand Exchange bank - if (Rs2Bank.isNearBank(BankLocation.GRAND_EXCHANGE, 6)) { - return true; - } - - log.info("Walking to bank..."); - - boolean walkResult = Rs2Walker.walkWithBankedTransports( - BankLocation.GRAND_EXCHANGE.getWorldPoint(), - false // Don't force banking route if direct is faster - ); - - if (!walkResult) { - log.warn("Failed to initiate walking to bank, trying fallback method"); - Rs2Walker.walkTo(BankLocation.GRAND_EXCHANGE.getWorldPoint(), 4); - } - - return sleepUntil(() -> Rs2Bank.isNearBank(BankLocation.GRAND_EXCHANGE, 6), 30000); - } - - /** - * Example helper method: Prepares basic equipment and inventory setup. - * Customize this for your plugin's specific equipment and item needs. - * - * @return true if setup was successful, false otherwise - */ - private boolean prepareBasicSetup() { - if (!Rs2Bank.openBank()) { - log.error("Failed to open bank"); - return false; - } - - // Deposit all current items - Rs2Bank.depositAll(); - sleepUntil(() -> Rs2Inventory.isEmpty(), 5000); - Rs2Bank.depositEquipment(); - sleepUntil(() -> !Rs2Equipment.isWearing(), 5000); - - // TODO: Add your plugin's specific equipment and item withdrawal logic here - // Example: - // Rs2Bank.withdrawOne(ItemID.BRONZE_PICKAXE); - // Rs2Bank.withdrawX(ItemID.SALMON, 10); - - Rs2Bank.closeBank(); - log.info("Successfully prepared basic setup"); - return true; - } - - /** - * Example helper method: Banks all equipment and inventory items for safe shutdown. - * This is a generic implementation that most plugins can use as-is. - * - * @return true if banking was successful, false otherwise - */ - private boolean bankAllItems() { - // Walk to bank if not already there - if (!Rs2Bank.isNearBank(6)) { - if (!walkToBank()) { - return false; - } - } - - if (!Rs2Bank.openBank()) { - return false; - } - - // Deposit all inventory items - Rs2Bank.depositAll(); - sleepUntil(() -> Rs2Inventory.isEmpty(), 5000); - - // Deposit all equipment - Rs2Bank.depositEquipment(); - - Rs2Bank.closeBank(); - log.info("Successfully banked all items"); - return true; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/overlay/PrePostScheduleTasksOverlayComponents.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/overlay/PrePostScheduleTasksOverlayComponents.java deleted file mode 100644 index 29ee42f37fa..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/overlay/PrePostScheduleTasksOverlayComponents.java +++ /dev/null @@ -1,480 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.overlay; - -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.AbstractPrePostScheduleTasks; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.PrePostScheduleRequirements; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location.LocationRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.collection.LootRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.SpellbookRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.state.TaskExecutionState; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import net.runelite.client.ui.overlay.components.LineComponent; -import net.runelite.client.ui.overlay.components.TitleComponent; - -import lombok.extern.slf4j.Slf4j; - -import java.awt.Color; -import java.util.ArrayList; -import java.util.List; - -/** - * Factory class for creating overlay components that display the current state of - * PrePostScheduleRequirements and AbstractPrePostScheduleTasks. - * - * This provides convenience methods to generate overlay components showing: - * - Current task execution phase and status - * - Active requirements being processed - * - Progress indicators for different requirement types - * - * The components are designed to be concise and non-cluttering for in-game display. - */ -@Slf4j -public class PrePostScheduleTasksOverlayComponents { - - // Color scheme for different states - private static final Color TITLE_COLOR = Color.CYAN; - private static final Color ACTIVE_COLOR = Color.YELLOW; - private static final Color SUCCESS_COLOR = Color.GREEN; - private static final Color ERROR_COLOR = Color.RED; - private static final Color INFO_COLOR = Color.WHITE; - private static final Color DISABLED_COLOR = Color.GRAY; - - /** - * Creates a title component for the requirement overlay. - * - * @param pluginName The name of the plugin - * @param tasks The task manager instance - * @return A TitleComponent for the overlay - */ - public static TitleComponent createTitleComponent(String pluginName, AbstractPrePostScheduleTasks tasks) { - String title = pluginName + " Tasks"; - Color titleColor = TITLE_COLOR; - - if (tasks != null && tasks.isExecuting()) { - title += " (ACTIVE)"; - titleColor = ACTIVE_COLOR; - } - - return TitleComponent.builder() - .text(title) - .color(titleColor) - .build(); - } - - /** - * Creates line components showing the current execution status. - * - * @param tasks The task manager instance - * @param requirements The requirements instance - * @return List of LineComponents showing current status - */ - public static List createExecutionStatusComponents(AbstractPrePostScheduleTasks tasks, PrePostScheduleRequirements requirements) { - List components = new ArrayList<>(); - - if (tasks == null) { - return components; - } - - // Get state from either tasks or requirements (they should be synchronized) - TaskExecutionState state = tasks.getExecutionState(); - if (requirements != null && state.isExecuting()) { - // If requirements are actively being fulfilled, use that state instead - state = tasks.getExecutionState(); - } - - String displayStatus = state.getDisplayStatus(); - if (displayStatus != null) { - Color statusColor = getStatusColor(state); - components.add(LineComponent.builder() - .left("Status:") - .right(displayStatus) - .leftColor(INFO_COLOR) - .rightColor(statusColor) - .build()); - - // Show current requirement name if available - String currentRequirementName = state.getCurrentRequirementName(); - if (currentRequirementName != null && !currentRequirementName.isEmpty() && state.isFulfillingRequirements()) { - // Truncate long requirement names for overlay display - String displayName = currentRequirementName.length() > 25 ? - currentRequirementName.substring(0, 22) + "..." : currentRequirementName; - - components.add(LineComponent.builder() - .left(" Processing:") - .right(displayName) - .leftColor(INFO_COLOR) - .rightColor(ACTIVE_COLOR) - .build()); - } - - // Show step progress if available - if (state.isFulfillingRequirements() && state.getTotalRequirementsInStep() > 0) { - String progress = String.format("Item %d/%d", - state.getCurrentRequirementIndex(), state.getTotalRequirementsInStep()); - components.add(LineComponent.builder() - .left(" Progress:") - .right(progress) - .leftColor(INFO_COLOR) - .rightColor(state.getCurrentRequirementIndex() == state.getTotalRequirementsInStep() ? SUCCESS_COLOR : ACTIVE_COLOR) - .build()); - } - - // Show overall progress for requirement fulfillment - if (state.isFulfillingRequirements() && state.getTotalSteps() > 0) { - int overallProgress = state.getProgressPercentage(); - components.add(LineComponent.builder() - .left(" Overall:") - .right(overallProgress + "% (" + state.getCurrentStepNumber() + "/" + state.getTotalSteps() + ")") - .leftColor(INFO_COLOR) - .rightColor(overallProgress == 100 ? SUCCESS_COLOR : ACTIVE_COLOR) - .build()); - } - } - - return components; - } - - /** - * Gets the appropriate color for the current execution state - */ - private static Color getStatusColor(TaskExecutionState state) { - if (state.isInErrorState()) { - return ERROR_COLOR; - } - - switch (state.getCurrentState()) { - case COMPLETED: - return SUCCESS_COLOR; - case FAILED: - case ERROR: - return ERROR_COLOR; - case FULFILLING_REQUIREMENTS: - case CUSTOM_TASKS: - return ACTIVE_COLOR; - case STARTING: - default: - return INFO_COLOR; - } - } - - /** - * Creates line components showing current location requirement status. - * - * @param requirements The requirements instance - * @param context The schedule context (PRE_SCHEDULE or POST_SCHEDULE) - * @return List of LineComponents showing location status - */ - public static List createLocationStatusComponents(PrePostScheduleRequirements requirements, TaskContext context) { - List components = new ArrayList<>(); - - if (requirements == null) { - return components; - } - - List locationReqs = requirements.getRegistry().getRequirements(LocationRequirement.class, context); - if (locationReqs.isEmpty()) { - return components; - } - - LocationRequirement locationReq = locationReqs.get(0); // Take first one - WorldPoint targetLocation = locationReq.getBestAvailableLocation().getWorldPoint(); - WorldPoint currentLocation = Rs2Player.getWorldLocation(); - - // Calculate distance - int distance = currentLocation != null && targetLocation != null ? - currentLocation.distanceTo(targetLocation) : -1; - - String contextLabel = context == TaskContext.PRE_SCHEDULE ? "Pre-Loc" : "Post-Loc"; - - components.add(LineComponent.builder() - .left(contextLabel + ":") - .right(locationReq.getName()) - .leftColor(INFO_COLOR) - .rightColor(distance <= 10 ? SUCCESS_COLOR : ACTIVE_COLOR) - .build()); - - if (distance >= 0) { - components.add(LineComponent.builder() - .left("Distance:") - .right(distance + " tiles") - .leftColor(INFO_COLOR) - .rightColor(distance <= 10 ? SUCCESS_COLOR : (distance <= 50 ? ACTIVE_COLOR : ERROR_COLOR)) - .build()); - } - - return components; - } - - /** - * Creates line components showing current spellbook requirement status. - * - * @param requirements The requirements instance - * @param context The schedule context - * @return List of LineComponents showing spellbook status - */ - public static List createSpellbookStatusComponents(PrePostScheduleRequirements requirements, TaskContext context) { - List components = new ArrayList<>(); - - if (requirements == null) { - return components; - } - - List spellbookReqs = requirements.getRegistry().getRequirements(SpellbookRequirement.class, context); - if (spellbookReqs.isEmpty()) { - return components; - } - - SpellbookRequirement spellbookReq = spellbookReqs.get(0); // Take first one - String contextLabel = context == TaskContext.PRE_SCHEDULE ? "Pre-Spell" : "Post-Spell"; - - components.add(LineComponent.builder() - .left(contextLabel + ":") - .right(spellbookReq.getRequiredSpellbook().name()) - .leftColor(INFO_COLOR) - .rightColor(ACTIVE_COLOR) - .build()); - - return components; - } - - /** - * Creates line components showing current loot requirement status. - * - * @param requirements The requirements instance - * @param context The schedule context - * @return List of LineComponents showing loot status - */ - public static List createLootStatusComponents(PrePostScheduleRequirements requirements, TaskContext context) { - List components = new ArrayList<>(); - - if (requirements == null) { - return components; - } - - List lootReqs = requirements.getRegistry().getRequirements(LootRequirement.class, context); - if (lootReqs.isEmpty()) { - return components; - } - - String contextLabel = context == TaskContext.PRE_SCHEDULE ? "Pre-Loot" : "Post-Loot"; - - for (LootRequirement lootReq : lootReqs) { - // Calculate total amount from the loot requirements map - int totalAmount = lootReq.getAmounts().values().stream() - .mapToInt(Integer::intValue) - .sum(); - - components.add(LineComponent.builder() - .left(contextLabel + ":") - .right(lootReq.getName() + " (" + totalAmount + ")") - .leftColor(INFO_COLOR) - .rightColor(ACTIVE_COLOR) - .build()); - } - - return components; - } - - /** - * Creates line components showing current item requirement status. - * Only shows the most critical items to avoid clutter. - * - * @param requirements The requirements instance - * @param context The schedule context - * @return List of LineComponents showing item status - */ - public static List createItemStatusComponents(PrePostScheduleRequirements requirements, TaskContext context) { - List components = new ArrayList<>(); - - if (requirements == null) { - return components; - } - - List itemReqs = requirements.getRegistry().getRequirements(ItemRequirement.class, context); - if (itemReqs.isEmpty()) { - return components; - } - - String contextLabel = context == TaskContext.PRE_SCHEDULE ? "Pre-Items" : "Post-Items"; - - // Only show first few items to avoid clutter - int maxItems = 3; - int count = 0; - - for (ItemRequirement itemReq : itemReqs) { - if (count >= maxItems) { - break; - } - - // For now, use a simplified approach to get item name - String itemName = "Item"; - try { - itemName = itemReq.toString(); // Fallback to toString if getName() is not accessible - if (itemName.length() > 30) { - itemName = itemName.substring(0, 27) + "..."; - } - } catch (Exception e) { - itemName = "Unknown Item"; - } - - components.add(LineComponent.builder() - .left(contextLabel + ":") - .right(itemName + " (" + itemReq.getAmount() + ")") - .leftColor(INFO_COLOR) - .rightColor(ACTIVE_COLOR) - .build()); - - count++; - } - - if (itemReqs.size() > maxItems) { - components.add(LineComponent.builder() - .left("") - .right("+" + (itemReqs.size() - maxItems) + " more items") - .leftColor(INFO_COLOR) - .rightColor(DISABLED_COLOR) - .build()); - } - - return components; - } - - /** - * Creates line components showing the current requirement being processed. - * Only shows information about the specific requirement currently being fulfilled. - * - * @param requirements The requirements instance - * @return List of LineComponents showing current requirement details - */ - public static List createCurrentRequirementComponents(AbstractPrePostScheduleTasks tasks, PrePostScheduleRequirements requirements) { - List components = new ArrayList<>(); - - if (requirements == null || tasks == null) { - return components; - } - TaskExecutionState state = tasks.getExecutionState(); - if (state == null || !state.isFulfillingRequirements()) { - return components; - } - - - - // Show the current step being processed - if (state.getCurrentStep() != null) { - components.add(LineComponent.builder() - .left("Current Step:") - .right(state.getCurrentStep().getDisplayName()) - .leftColor(INFO_COLOR) - .rightColor(ACTIVE_COLOR) - .build()); - } - - // Show current details if available - String details = state.getCurrentDetails(); - if (details != null && !details.isEmpty()) { - // Shorten details if too long - if (details.length() > 30) { - details = details.substring(0, 27) + "..."; - } - - components.add(LineComponent.builder() - .left("Details:") - .right(details) - .leftColor(INFO_COLOR) - .rightColor(INFO_COLOR) - .build()); - } - - return components; - } - - /** - * Creates a complete set of overlay components for the current requirement status. - * This is the main method that should be called from plugin overlays. - * - * @param pluginName The name of the plugin - * @param tasks The task manager instance - * @param requirements The requirements instance - * @return List of all overlay components - */ - public static List createAllComponents(String pluginName, AbstractPrePostScheduleTasks tasks, PrePostScheduleRequirements requirements) { - List components = new ArrayList<>(); - - // Add title - components.add(createTitleComponent(pluginName, tasks)); - - // Add execution status - components.addAll(createExecutionStatusComponents(tasks, requirements)); - TaskExecutionState state = tasks.getExecutionState(); - if (requirements != null && tasks != null && (tasks.isExecuting() || state.isFulfillingRequirements())) { - // Show only the current requirement being processed - components.addAll(createCurrentRequirementComponents(tasks,requirements)); - } - - return components; - } - - /** - * Creates concise summary components for main overlay display. - * Shows only essential information to avoid clutter. - * - * @param pluginName The name of the plugin - * @param tasks The task manager instance - * @param requirements The requirements instance - * @return List of overlay components for concise display - */ - public static List createConciseComponents(String pluginName, AbstractPrePostScheduleTasks tasks, PrePostScheduleRequirements requirements) { - List components = new ArrayList<>(); - - // Only show title and execution status for concise view - if (tasks != null && tasks.isExecuting()) { - TaskExecutionState state = tasks.getExecutionState(); - - // Concise title with status - String titleText = pluginName + " Tasks"; - Color titleColor = ACTIVE_COLOR; - - if (state.isInErrorState()) { - titleColor = ERROR_COLOR; - titleText += " (ERROR)"; - } else if (state.isExecuting()) { - titleColor = ACTIVE_COLOR; - titleText += " (ACTIVE)"; - } - - components.add(TitleComponent.builder() - .text(titleText) - .color(titleColor) - .build()); - - // Show only current phase and progress - String phase = state.getCurrentPhase() != null ? state.getCurrentPhase().toString() : "UNKNOWN"; - int progress = state.getProgressPercentage(); - String progressText = progress > 0 ? progress + "%" : "Working..."; - - components.add(LineComponent.builder() - .left(phase + ":") - .right(progressText) - .leftColor(INFO_COLOR) - .rightColor(ACTIVE_COLOR) - .build()); - } else { - // Show status when not executing - components.add(TitleComponent.builder() - .text(pluginName + " Tasks") - .color(INFO_COLOR) - .build()); - - components.add(LineComponent.builder() - .left("Status:") - .right("Ready") - .leftColor(INFO_COLOR) - .rightColor(SUCCESS_COLOR) - .build()); - } - - return components; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/PrePostScheduleRequirements.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/PrePostScheduleRequirements.java deleted file mode 100644 index 31a69ed5889..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/PrePostScheduleRequirements.java +++ /dev/null @@ -1,1513 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements; -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.api.GameState; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.inventorysetups.InventorySetup; -import net.runelite.client.plugins.microbot.inventorysetups.MInventorySetupsPlugin; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.cache.Rs2CacheManager; -import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; -import net.runelite.client.plugins.microbot.util.inventory.Rs2FuzzyItem; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; -import net.runelite.client.plugins.microbot.util.magic.Rs2Spellbook; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; -import net.runelite.api.Constants; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import org.slf4j.event.Level; - -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.OrRequirementMode; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.registry.RequirementRegistry; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.InventorySetupPlanner; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location.LocationRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.collection.LootRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.ShopRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.SpellbookRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.InventorySetupRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.ConditionalRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.OrderedRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.LogicalRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.OrRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.util.RequirementSolver; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.state.FulfillmentStep; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.state.TaskExecutionState; - -import java.util.stream.Collectors; - -/** - * Enhanced collection that manages ItemRequirement objects with support for inventory and equipment requirements. - * - * This class now uses a centralized RequirementRegistry for improved consistency, uniqueness enforcement, - * and simplified requirement management while maintaining backward compatibility. - */ -@Slf4j -public abstract class PrePostScheduleRequirements { - - - @Getter - private final String collectionName; - @Getter - private final String activityType; - private boolean initialized = false; - private InventorySetupPlanner currentPreScheduleLayoutPlan = null; - private InventorySetupPlanner currentPostScheduleLayoutPlan = null; - // Centralized requirement management - - private final RequirementRegistry registry = new RequirementRegistry(); - - /** - * Mode for handling OR requirements during planning. - * Default is ANY_COMBINATION for backward compatibility. - */ - @Getter - @Setter - private OrRequirementMode orRequirementMode = OrRequirementMode.ANY_COMBINATION; - - /** - * Tracks the original spellbook before switching for pre-schedule requirements. - * This is used to restore the original spellbook during post-schedule fulfillment. - */ - private volatile Rs2Spellbook originalSpellbook; - - - - @Getter - private final boolean isWildernessCollection; - - public PrePostScheduleRequirements() { - this("", "", false); - } - - /** - * Creates a new collection with name and activity type for better organization. - * - * @param collectionName A descriptive name for this collection - * @param activityType The type of activity these requirements are for (e.g., "GOTR", "Mining", "Combat") - */ - public PrePostScheduleRequirements(String collectionName, String activityType, boolean isWildernessCollection) { - this.collectionName = collectionName; - this.activityType = activityType; - this.isWildernessCollection = isWildernessCollection; // Set wilderness flag - this.currentPreScheduleLayoutPlan = null; - this.currentPostScheduleLayoutPlan = null; - initialize(); // try to Initialize requirements collection - - } - public boolean isInitialized(){ - if (!initialized) { - initialized = initialize(); // Initialize if not already done - if (initialized) log.info("\nPrePostScheduleRequirements <{}> initialized:\n{}",collectionName, this.getDetailedDisplay()); - return initialized; - } - return initialized; // Return current initialization state - } - public boolean initialize() { - if (initialized) { - log.warn("Requirements collection already initialized: " + collectionName); - return false; // Already initialized - } - if (!Microbot.isLoggedIn() || !Rs2CacheManager.isCacheDataValid()){ - log.error("Cannot initialize requirements collection: " + collectionName + " - not logged in or cache data invalid"); - return false; // Cannot initialize if not logged in or cache is invalid - } - try { - boolean success = initializeRequirements(); - if (success) { - initialized = true; - this.currentPreScheduleLayoutPlan = null; - this.currentPostScheduleLayoutPlan = null; - log.info("Successfully initialized requirements collection: " + collectionName); - } else { - log.error("Failed to initialize requirements collection: " + collectionName); - } - return success; - } catch (Exception e) { - log.error("Error initializing requirements collection: " + collectionName, e); - return false; - } - } - /** - * Initializes the requirements for this collection. - * This method should be called after the plugin is started to ensure all requirements are set up. - * - * @return true if initialization was successful, false otherwise - */ - protected abstract boolean initializeRequirements(); - public void reset() { - initialized = false; - clearOriginalSpellbook(); - this.getRegistry().clear(); // Clear the registry to remove all requirements - } - - /** - * Gets access to the internal requirement registry. - * This is useful for overlay components that need to access requirements by type and context. - * - * @return The requirement registry - */ - public RequirementRegistry getRegistry() { - return registry; - } - - /** - * Adds a custom requirement to this requirements collection. - * Custom requirements are marked with CUSTOM type and are fulfilled after all standard requirements. - * - * @param requirement The requirement to add - * @param TaskContext The context in which this requirement should be fulfilled - * @return true if the requirement was successfully added, false otherwise - */ - public boolean addCustomRequirement(Requirement requirement, TaskContext taskContext) { - if (requirement == null) { - log.warn("Cannot add null custom requirement"); - return false; - } - - if (taskContext == null) { - log.warn("Cannot add custom requirement without schedule context"); - return false; - } - - try { - // Update the requirement's schedule context if needed - if (requirement.getTaskContext() != taskContext && - requirement.getTaskContext() != TaskContext.BOTH) { - requirement.setTaskContext(taskContext); - } - - // Register in the registry as an external requirement - boolean registered = registry.registerExternal(requirement); - - if (registered) { - log.info("Successfully registered custom requirement: {} for context: {}", - requirement.getDescription(), taskContext); - return true; - } else { - log.warn("Failed to register custom requirement in registry: {}", - requirement.getDescription()); - return false; - } - - } catch (Exception e) { - log.error("Error adding custom requirement: {}", e.getMessage(), e); - return false; - } - } - - /** - * Merges another collection into this one. - */ - public void merge(PrePostScheduleRequirements other) { - // Merge all requirements through the registry - for (Requirement requirement : other.registry.getAllRequirements()) { - registry.register(requirement); - } - } - - - - - /** - * Switches back to the original spellbook that was active before pre-schedule requirements. - * This method should be called after completing activities that required a specific spellbook. - * - * @return true if switch was successful or no switch was needed, false if switch failed - */ - public boolean switchBackToOriginalSpellbook() { - if (originalSpellbook == null) { - log.info("No original spellbook saved - no switch needed"); - return true; // No original spellbook saved, so no switch needed - } - - Rs2Spellbook currentSpellbook = Rs2Spellbook.getCurrentSpellbook(); - if (currentSpellbook == originalSpellbook) { - log.info("Already on original spellbook: " + originalSpellbook); - return true; // Already on the original spellbook - } - - log.info("Switching back to original spellbook: " + originalSpellbook + " from current: " + currentSpellbook); - boolean success = SpellbookRequirement.switchBackToSpellbook(originalSpellbook); - - if (success) { - // Clear the saved spellbook after successful restoration - originalSpellbook = null; - log.info("Successfully restored original spellbook"); - } else { - log.error("Failed to restore original spellbook: " + originalSpellbook); - } - - return success; - } - - /** - * Gets the original spellbook that was saved before pre-schedule requirements. - * - * @return The original spellbook, or null if none was saved - */ - public Rs2Spellbook getOriginalSpellbook() { - return originalSpellbook; - } - - /** - * Checks if an original spellbook is currently saved. - * - * @return true if an original spellbook is saved, false otherwise - */ - public boolean hasOriginalSpellbook() { - return originalSpellbook != null; - } - - /** - * Clears the saved original spellbook. This can be useful for cleanup - * or when you want to start fresh without restoring the previous spellbook. - */ - public void clearOriginalSpellbook() { - if (originalSpellbook != null) { - log.debug("Clearing saved original spellbook: " + originalSpellbook); - originalSpellbook = null; - } - } - - - /** - * Registers a requirement in the central registry. - * The registry automatically handles categorization, uniqueness, and consistency. - * - * @param requirement The requirement to register - */ - public void register(Requirement requirement) { - if (requirement == null) { - return; - } - if(registry.contains(requirement)) { - log.debug("Requirement already registered: " + requirement.getName(), Level.WARN); - return; // Avoid duplicate registration - } - registry.register(requirement); - } - - /** - * Fulfills all requirements for the specified schedule context. - * This is a convenience method that calls all the specific fulfillment methods. - * - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param scheduledFuture The CompletableFuture to monitor for cancellation - * @param saveCurrentSpellbook Whether to save the current spellbook for restoration - * @return true if all requirements were fulfilled successfully, false otherwise - */ - - private boolean fulfillAllRequirements(CompletableFuture scheduledFuture, TaskContext context, - boolean saveCurrentSpellbook, - TaskExecutionState executionState) - { - boolean success = true; - ScheduledExecutorService cancellationWatchdogService = null; - ScheduledFuture cancellationWatchdog = null; - - try { - // Fulfill requirements in logical order -> we should always fulfill loot requirements first, then shop, then item, then spellbook, and finally location requirements - // when adding new requirements, make sure to follow this order or think about the order in which they should be fulfilled - // we can also think about changing the order for pre and post schedule requirements, but for now we will keep it the same - // Initialize state tracking - TaskExecutionState.ExecutionPhase phase = context == TaskContext.PRE_SCHEDULE ? - TaskExecutionState.ExecutionPhase.PRE_SCHEDULE : TaskExecutionState.ExecutionPhase.POST_SCHEDULE; - if (context == null) { - log.error("Schedule Context is null!"); - executionState.markError("Context cannot be null"); - return false; - } - if (Microbot.getClient().isClientThread()) { - log.error("\n\tPlease run fulfillAllRequirements() on a non-client thread."); - executionState.markError("Cannot run on client thread"); - return false; - } - - - executionState.update(phase,TaskExecutionState.ExecutionState.FULFILLING_REQUIREMENTS ); - - // Start cancellation watchdog if we have a scheduledFuture to monitor - if (scheduledFuture != null) { - cancellationWatchdogService = Executors.newSingleThreadScheduledExecutor(r -> { - Thread thread = new Thread(r, "RequirementFulfillment - CancellationWatchdog - context:" + context); - thread.setDaemon(true); - return thread; - }); - - cancellationWatchdog = startCancellationWatchdog(cancellationWatchdogService, scheduledFuture, context); - log.debug("Started cancellation watchdog for requirement fulfillment: {}", context); - } - StringBuilder logMessage = new StringBuilder(); - logMessage.append("\n" + "=".repeat(80)); - logMessage.append(String.format("\nFULFILLING REQUIREMENTS FOR CONTEXT: {}", context)); - logMessage.append(String.format("\nCollection: %s| Activity: %s | Wilderness: %s\n", collectionName, activityType, isWildernessCollection)); - logMessage.append("=".repeat(80)); - - // Display complete registry information - logMessage.append("\n=== COMPLETE REQUIREMENT REGISTRY ==="); - logMessage.append(registry.getDetailedCacheStringForContext(context)); - //logMessage.append(this.registry.getDetailedCacheStringForContext(context)); - log.info(logMessage.toString()); - // Step 0: Conditional and Ordered Requirements (execute first as they may contain prerequisites) - List conditionalReqs = this.registry.getRequirements(ConditionalRequirement.class, context); - List orderedReqs = this.registry.getRequirements(OrderedRequirement.class, context); - - StringBuilder conditionalReqInfo = new StringBuilder(); - conditionalReqInfo.append("\n=== STEP 0: CONDITIONAL REQUIREMENTS ===\n"); - if (conditionalReqs.isEmpty()) { - conditionalReqInfo.append(String.format("No conditional requirements for context: %s\n", context)); - } else { - conditionalReqInfo.append(String.format("Found %d conditional requirement(s):\n", conditionalReqs.size())); - for (int i = 0; i < conditionalReqs.size(); i++) { - conditionalReqInfo.append(String.format("\n--- Conditional Requirement %d ---\n", i + 1)); - conditionalReqInfo.append(conditionalReqs.get(i).displayString()).append("\n"); - } - } - - conditionalReqInfo.append("\n=== STEP 1: ORDERED REQUIREMENTS ===\n"); - if (orderedReqs.isEmpty()) { - conditionalReqInfo.append(String.format("No ordered requirements for context: %s\n", context)); - } else { - conditionalReqInfo.append(String.format("Found %d ordered requirement(s):\n", orderedReqs.size())); - for (int i = 0; i < orderedReqs.size(); i++) { - conditionalReqInfo.append(String.format("\n--- Ordered Requirement %d ---\n", i + 1)); - conditionalReqInfo.append(orderedReqs.get(i).displayString()).append("\n"); - } - } - - - - // Only fulfill 'mixed' conditional requirements (not just item requirements alone) - List mixedConditionalReqs = this.registry.getMixedConditionalRequirements(context); - if (!mixedConditionalReqs.isEmpty() || !orderedReqs.isEmpty()) { - success &= RequirementSolver.fulfillConditionalRequirements(scheduledFuture,executionState, mixedConditionalReqs, orderedReqs, context); - } - - // Check for cancellation after conditional requirements - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Requirements fulfillment cancelled after conditional requirements"); - return false; - } - - if (!success) { - executionState.markFailed("Failed to fulfill conditional requirements"); - return false; - } - - // Step 2: Loot Requirements - List lootReqs = this.registry.getRequirements(LootRequirement.class, context); - StringBuilder lootReqInfo = new StringBuilder(); - lootReqInfo.append("\n=== STEP 2: LOOT REQUIREMENTS ===\n"); - if (lootReqs.isEmpty()) { - lootReqInfo.append(String.format("\tNo loot requirements for context: %s\n", context)); - } else { - lootReqInfo.append(String.format("\tFound %d loot requirement(s):\n", lootReqs.size())); - for (int i = 0; i < lootReqs.size(); i++) { - lootReqInfo.append(String.format("\n\t\t--- Loot Requirement %d ---\n\t\t\t", i + 1)); - lootReqInfo.append(lootReqs.get(i).displayString()).append("\n"); - } - } - log.info(lootReqInfo.toString()); - success &= fulfillPrePostLootRequirements(scheduledFuture, executionState,context); - - // Check for cancellation after loot requirements - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Requirements fulfillment cancelled after loot requirements"); - return false; - } - - if (!success) { - executionState.markFailed("Failed to fulfill loot requirements"); - return false; - } - - // Step 3: Shop Requirements - List shopReqs = this.registry.getRequirements(ShopRequirement.class, context); - StringBuilder shopReqInfo = new StringBuilder(); - shopReqInfo.append("\n=== STEP 3: SHOP REQUIREMENTS ===\n"); - if (shopReqs.isEmpty()) { - shopReqInfo.append(String.format("\tNo shop requirements for context: %s\n", context)); - } else { - shopReqInfo.append(String.format("\tFound %d shop requirement(s):\n", shopReqs.size())); - for (int i = 0; i < shopReqs.size(); i++) { - shopReqInfo.append(String.format("\n--- Shop Requirement %d ---\n", i + 1)); - shopReqInfo.append(shopReqs.get(i).displayString()).append("\n"); - } - } - - log.info(shopReqInfo.toString()); - success &= fulfillPrePostShopRequirements(scheduledFuture, executionState,context); - - // Check for cancellation after shop requirements - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("\n\tRequirements fulfillment cancelled after shop requirements"); - return false; - } - - if (!success) { - executionState.markFailed("Failed to fulfill shop requirements"); - return false; - } - - // Step 4: Item Requirements - StringBuilder itemReqInfo = new StringBuilder(); - RequirementRegistry.RequirementBreakdown itemReqBreakdown = this.registry.getItemRequirementBreakdown( context); - itemReqInfo.append("\n=== STEP 4: ITEM REQUIREMENTS ===\n"); - if (itemReqBreakdown.isEmpty()) { - itemReqInfo.append(String.format("\tNo item requirements for context: %s\n", context)); - } else { - itemReqInfo.append("\t"+itemReqBreakdown.getDetailedBreakdownString()); - } - - log.info(itemReqInfo.toString()); - executionState.updateFulfillmentStep(FulfillmentStep.ITEMS, "Preparing inventory and equipment"); - success &= fulfillPrePostItemRequirements(scheduledFuture,executionState,context); - - // Check for cancellation after item requirements - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Requirements fulfillment cancelled after item requirements"); - return false; - } - - if (!success) { - executionState.markFailed("Failed to fulfill item requirements"); - return false; - } - - // Step 5: Spellbook Requirements - List spellbookReqs = this.registry.getRequirements(SpellbookRequirement.class, context); - StringBuilder spellbookReqInfo = new StringBuilder(); - spellbookReqInfo.append("\n=== STEP 5: SPELLBOOK REQUIREMENTS ===\n"); - if (spellbookReqs.isEmpty()) { - spellbookReqInfo.append(String.format("No spellbook requirements for context: %s\n", context)); - } else { - spellbookReqInfo.append(String.format("Found %d spellbook requirement(s):\n", spellbookReqs.size())); - for (int i = 0; i < spellbookReqs.size(); i++) { - spellbookReqInfo.append(String.format("\n--- Spellbook Requirement %d ---\n", i + 1)); - spellbookReqInfo.append(spellbookReqs.get(i).displayString()).append("\n"); - } - } - - log.info(spellbookReqInfo.toString()); - executionState.updateFulfillmentStep(FulfillmentStep.SPELLBOOK, "Switching spellbook"); - success &= fulfillPrePostSpellbookRequirements(scheduledFuture, executionState,context, saveCurrentSpellbook); - - // Check for cancellation after spellbook requirements - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Requirements fulfillment cancelled after spellbook requirements"); - return false; - } - - if (!success) { - executionState.markFailed("Failed to fulfill spellbook requirements"); - return false; - } - - // Step 6: Location Requirements (always fulfill location requirements last) - List locationReqs = this.registry.getRequirements(LocationRequirement.class, context); - StringBuilder locationReqInfo = new StringBuilder(); - locationReqInfo.append("\n=== STEP 6: LOCATION REQUIREMENTS ===\n"); - if (locationReqs.isEmpty()) { - locationReqInfo.append(String.format("No location requirements for context: %s\n", context)); - } else { - locationReqInfo.append(String.format("Found %d location requirement(s):\n", locationReqs.size())); - for (int i = 0; i < locationReqs.size(); i++) { - locationReqInfo.append(String.format("\n--- Location Requirement %d ---\n", i + 1)); - locationReqInfo.append(locationReqs.get(i).displayString()).append("\n"); - } - } - - log.info(locationReqInfo.toString()); - - success &= fulfillPrePostLocationRequirements(scheduledFuture,executionState, context); - - // Check for cancellation after location requirements - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Requirements fulfillment cancelled after location requirements"); - return false; - } - - // Step 7: Handle External Requirements (added by plugins or external systems) - if (success) { - List externalRequirements = registry.getExternalRequirements(context); - - StringBuilder externalReqInfo = new StringBuilder(); - externalReqInfo.append("\n=== STEP 7: EXTERNAL REQUIREMENTS ===\n"); - if (externalRequirements.isEmpty()) { - externalReqInfo.append(String.format("No external requirements for context: %s\n", context)); - } else { - externalReqInfo.append(String.format("Found %d external requirement(s):\n", externalRequirements.size())); - for (int i = 0; i < externalRequirements.size(); i++) { - externalReqInfo.append(String.format("\n--- External Requirement %d ---\n", i + 1)); - externalReqInfo.append(externalRequirements.get(i).displayString()).append("\n"); - } - } - log.info(externalReqInfo.toString()); - - if (!externalRequirements.isEmpty()) { - updateFulfillmentStep( executionState,FulfillmentStep.EXTERNAL_REQUIREMENTS, "Fulfilling external requirements", externalRequirements.size()); - - for (Requirement externalReq : externalRequirements) { - try { - log.info("\nFulfilling external requirement: \n\t{}", externalReq.getDescription()); - boolean externalSuccess = externalReq.fulfillRequirement(scheduledFuture); - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Executor service is shutdown, skipping external requirement fulfillment: {}", externalReq.getDescription()); - return false; // Skip if executor service is shutdown - } - if (!externalSuccess) { - log.error("Failed to fulfill external requirement: {}", externalReq.getDescription()); - success = false; - break; - } else { - log.info("Successfully fulfilled external requirement: {}", externalReq.getDescription()); - } - } catch (Exception e) { - log.error("Error fulfilling external requirement: {}", externalReq.getDescription(), e); - success = false; - break; - } - } - - if (success) { - log.info("All external requirements fulfilled successfully"); - } else { - log.error("Failed to fulfill external requirements"); - } - } else { - log.info("External requirements step completed - no external requirements to fulfill"); - } - } - - if (success) { - executionState.update(phase,TaskExecutionState.ExecutionState.COMPLETED ); - log.info("\n" + "=".repeat(80) + "\nALL REQUIREMENTS FULFILLED SUCCESSFULLY FOR CONTEXT: {}\n" + "=".repeat(80), context); - } else { - executionState.markFailed("Failed to fulfill location requirements"); - log.error("\n" + "=".repeat(80) + "\nFAILED TO FULFILL REQUIREMENTS FOR CONTEXT: {}\n" + "=".repeat(80), context); - } - - return success; - } finally { - // Always clean up the watchdog - if (cancellationWatchdog != null && !cancellationWatchdog.isDone()) { - cancellationWatchdog.cancel(true); - } - - // Shutdown the executor service - if (cancellationWatchdogService != null) { - cancellationWatchdogService.shutdown(); - try { - if (!cancellationWatchdogService.awaitTermination(2, TimeUnit.SECONDS)) { - cancellationWatchdogService.shutdownNow(); - if (!cancellationWatchdogService.awaitTermination(1, TimeUnit.SECONDS)) { - log.warn("Cancellation watchdog executor service did not terminate cleanly for context: {}", context); - } - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - cancellationWatchdogService.shutdownNow(); - } - } - } - } - - /** - * Starts a cancellation watchdog that monitors for task cancellation and stops any ongoing walking operations. - * This prevents walking operations from continuing when the overall requirement fulfillment has been cancelled. - * - * @param executorService The executor service to run the watchdog on - * @param scheduledFuture The future to monitor for cancellation - * @param context The schedule context for logging purposes - * @return The scheduled future for the watchdog task - */ - private ScheduledFuture startCancellationWatchdog( ScheduledExecutorService executorService, - CompletableFuture scheduledFuture, - TaskContext context) { - return executorService.scheduleAtFixedRate(() -> { - try { - // Check for cancellation - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.info("Requirement fulfillment cancellation watchdog triggered for context: {}", context); - - // Stop any ongoing walking by clearing the walker target - Rs2Walker.setTarget(null); - - // Cancel this watchdog by throwing an exception - throw new RuntimeException("Requirement fulfillment cancelled - stopping walking operations"); - } - } catch (Exception e) { - log.debug("Cancellation watchdog stopping for context {}: {}", context, e.getMessage()); - throw e; // Re-throw to stop the scheduled task - } - }, 2, 2, TimeUnit.SECONDS); // Check every 2 seconds (more frequent than location watchdog) - } - - /** - * Convenience method to fulfill all pre-schedule requirements. - * - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @param saveCurrentSpellbook Whether to save current spellbook for restoration - * @return true if all pre-schedule requirements were fulfilled successfully, false otherwise - */ - public boolean fulfillPreScheduleRequirements(CompletableFuture scheduledFuture, boolean saveCurrentSpellbook,TaskExecutionState executionState) { - return fulfillAllRequirements(scheduledFuture,TaskContext.PRE_SCHEDULE, saveCurrentSpellbook,executionState); - } - - - - /** - * Convenience method to fulfill all post-schedule requirements. - * - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @param saveCurrentSpellbook Whether to save current spellbook for restoration - * @return true if all post-schedule requirements were fulfilled successfully, false otherwise - */ - public boolean fulfillPostScheduleRequirements(CompletableFuture scheduledFuture, boolean saveCurrentSpellbook, TaskExecutionState executionState) { - return fulfillAllRequirements(scheduledFuture, TaskContext.POST_SCHEDULE, saveCurrentSpellbook, executionState); - } - - - // === UNIFIED REQUIREMENT FULFILLMENT FUNCTIONS === - - /** - * Fulfills all item requirements (equipment, inventory, either) for the specified schedule context. - * - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if all item requirements were fulfilled successfully, false otherwise - */ - public boolean fulfillPrePostItemRequirements(CompletableFuture scheduledFuture, TaskExecutionState executionState, TaskContext context) { - // Check for InventorySetupRequirements first - they take precedence over progressive item management - List inventorySetupReqs = registry.getRequirements(InventorySetupRequirement.class, context); - - if (!inventorySetupReqs.isEmpty()) { - log.info("Found {} inventory setup requirement(s) for context: {} - using inventory setup approach instead of progressive item management", - inventorySetupReqs.size(), context); - - // Initialize step tracking for inventory setup - updateFulfillmentStep(executionState, FulfillmentStep.ITEMS, "Loading inventory setup(s)", inventorySetupReqs.size()); - - boolean success = true; - for (int i = 0; i < inventorySetupReqs.size(); i++) { - InventorySetupRequirement inventorySetupReq = inventorySetupReqs.get(i); - - // Update current requirement tracking - updateCurrentRequirement(executionState, inventorySetupReq, i + 1); - - try { - log.info("Fulfilling inventory setup requirement {}/{}: {}", - i + 1, inventorySetupReqs.size(), inventorySetupReq.getName()); - - boolean fulfilled = inventorySetupReq.fulfillRequirement(scheduledFuture); - if (!fulfilled && inventorySetupReq.isMandatory()) { - log.error("Failed to fulfill mandatory inventory setup requirement: {}", inventorySetupReq.getName()); - success = false; - break; - } else if (!fulfilled) { - log.warn("Failed to fulfill optional inventory setup requirement: {}", inventorySetupReq.getName()); - } else { - log.info("Successfully fulfilled inventory setup requirement: {}", inventorySetupReq.getName()); - } - } catch (Exception e) { - log.error("Error fulfilling inventory setup requirement {}: {}", inventorySetupReq.getName(), e.getMessage()); - if (inventorySetupReq.isMandatory()) { - success = false; - break; - } - } - } - - log.debug("Inventory setup requirements fulfillment completed. Success: {}", success); - return success; - } - - // No inventory setup requirements found - use progressive item management approach - log.debug("No inventory setup requirements found - using progressive item management approach"); - - // Get count of logical requirements for this context using the unified API - int logicalReqsCount = registry.getItemCount(context); - - if (logicalReqsCount == 0) { - log.debug("No item requirements to fulfill for context: {}", context); - return true; // No requirements to fulfill - } - - // Initialize step tracking - updateFulfillmentStep(executionState, FulfillmentStep.ITEMS, "Processing item requirements", logicalReqsCount); - - boolean success = fulfillOptimalInventoryAndEquipmentLayout(scheduledFuture, context); - - log.debug("Item requirements fulfillment completed. Success: {}", success); - return success; - } - - /** - * Fulfills shop requirements for the specified schedule context. - * Uses the unified RequirementSolver to handle both standard and external requirements. - * - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param scheduledFuture The CompletableFuture to monitor for cancellation - * @return true if all shop requirements were fulfilled successfully, false otherwise - */ - public boolean fulfillPrePostShopRequirements(CompletableFuture scheduledFuture,TaskExecutionState executionState, TaskContext context ) { - LinkedHashSet shopLogical = registry.getShopRequirements(context); - - if (shopLogical.isEmpty()) { - log.debug("No shop requirements to fulfill for context: {}", context); - return true; // No requirements to fulfill - } - - // Initialize step tracking - - updateFulfillmentStep(executionState, FulfillmentStep.SHOP, "Processing shop requirements", shopLogical.size()); - log.info("Processing shop requirements for context: {} number of req.", context, shopLogical.size()); - // Use the utility class for fulfillment - return LogicalRequirement.fulfillLogicalRequirements( - scheduledFuture, - new ArrayList<>(shopLogical), - "shop" - ); - } - - - /** - * Fulfills loot requirements for the specified schedule context. - * Uses the unified RequirementSolver to handle both standard and external requirements. - * - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param scheduledFuture The CompletableFuture to monitor for cancellation - * @return true if all loot requirements were fulfilled successfully, false otherwise - */ - public boolean fulfillPrePostLootRequirements(CompletableFuture scheduledFuture, TaskExecutionState executionState, TaskContext context ) { - // Get requirements count for step tracking - LinkedHashSet lootLogical = registry.getLootLogicalRequirements(context); - - if (lootLogical.isEmpty()) { - log.debug("No loot requirements to fulfill for context: {}", context); - return true; // No requirements to fulfill - } - - // Initialize step tracking - List contextReqs = LogicalRequirement.filterByContext(new ArrayList<>(lootLogical), context); - updateFulfillmentStep(executionState,FulfillmentStep.LOOT, "Collecting loot items", contextReqs.size()); - - // Use the utility class for fulfillment - return LogicalRequirement.fulfillLogicalRequirements(scheduledFuture,contextReqs, "loot"); - - - } - - - - /** - * Fulfills location requirements for the specified schedule context. - * Uses the unified filtering system to automatically handle pre/post schedule requirements. - * - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if all location requirements were fulfilled successfully, false otherwise - */ - public boolean fulfillPrePostLocationRequirements(CompletableFuture scheduledFuture,TaskExecutionState executionState, TaskContext context) { - // Get requirements count for step tracking - List locationReqs = this.registry.getRequirements(LocationRequirement.class, context); - - if (locationReqs.isEmpty()) { - log.debug("No location requirements to fulfill for context: {}", context); - return true; // No requirements to fulfill - } - if (locationReqs.size() > 1) { - Microbot.log("Multiple location requirements found for context " + context + ". Only one should be set at a time.", Level.ERROR); - return false; // Only one location requirement should be set per context - } - - // Initialize step tracking - LocationRequirement locationReq = locationReqs.get(0); - updateFulfillmentStep(executionState,FulfillmentStep.LOCATION, "Moving to " + locationReq.getName(), locationReqs.size()); - - boolean success = true; - - for (int i = 0; i < locationReqs.size(); i++) { - LocationRequirement requirement = locationReqs.get(i); - - // Update current requirement tracking - updateCurrentRequirement(executionState,requirement, i + 1); - - try { - log.debug("Processing location requirement {}/{}: {}", i + 1, locationReqs.size(), requirement.getName()); - boolean fulfilled = requirement.fulfillRequirement(scheduledFuture); - if (!fulfilled && requirement.isMandatory()) { - Microbot.log("Failed to fulfill mandatory location requirement: " + requirement.getName()); - success = false; - } else if (!fulfilled) { - Microbot.log("Failed to fulfill optional location requirement: " + requirement.getName()); - } - } catch (Exception e) { - Microbot.log("Error fulfilling location requirement " + requirement.getName() + ": " + e.getMessage()); - if (requirement.isMandatory()) { - success = false; - } - } - } - - log.info("Location requirements fulfillment completed. Success: {}", success); - return success; - } - - - - /** - * Fulfills spellbook requirements for the specified schedule context. - * Uses the unified filtering system to automatically handle pre/post schedule requirements. - * - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param saveCurrentSpellbook Whether to save the current spellbook before switching (for pre-schedule) - * @return true if all spellbook requirements were fulfilled successfully, false otherwise - */ - public boolean fulfillPrePostSpellbookRequirements(CompletableFuture scheduledFuture,TaskExecutionState executionState, TaskContext context, boolean saveCurrentSpellbook) { - List spellbookReqs = this.registry.getRequirements(SpellbookRequirement.class, context); - - if (spellbookReqs.isEmpty()) { - log.debug("No spellbook requirements to fulfill for context: {}", context); - return true; // No requirements to fulfill - } - if(spellbookReqs.size() > 1) { - Microbot.log("Multiple spellbook requirements found for context " + context + ". Only one should be set at a time.", Level.ERROR); - return false; // Only one spellbook requirement should be set per context - } - - SpellbookRequirement spellbookReq = spellbookReqs.get(0); - - // Initialize step tracking - updateFulfillmentStep(executionState,FulfillmentStep.SPELLBOOK, "Switching to " + spellbookReq.getRequiredSpellbook().name(), spellbookReqs.size()); - - boolean success = true; - - // Save original spellbook if this is for pre-schedule and we should save it - if (context == TaskContext.PRE_SCHEDULE && saveCurrentSpellbook) { - originalSpellbook = Rs2Spellbook.getCurrentSpellbook(); - log.debug("Saved original spellbook: " + originalSpellbook + " before switching for pre-schedule requirements"); - } - - for (int i = 0; i < spellbookReqs.size(); i++) { - SpellbookRequirement requirement = spellbookReqs.get(i); - - // Update current requirement tracking - updateCurrentRequirement(executionState,requirement, i + 1); - - try { - log.debug("Processing spellbook requirement {}/{}: {}", i + 1, spellbookReqs.size(), requirement.getName()); - boolean fulfilled = requirement.fulfillRequirement(scheduledFuture); - if (!fulfilled && requirement.isMandatory()) { - Microbot.log("Failed to fulfill mandatory spellbook requirement: " + requirement.getName()); - success = false; - } else if (!fulfilled) { - Microbot.log("Failed to fulfill optional spellbook requirement: " + requirement.getName()); - } - } catch (Exception e) { - Microbot.log("Error fulfilling spellbook requirement " + requirement.getName() + ": " + e.getMessage()); - if (requirement.isMandatory()) { - success = false; - } - } - } - - // Special handling for post-schedule: if no post-schedule spellbook requirement is defined - // but we have a saved original spellbook, automatically restore it - if (context == TaskContext.POST_SCHEDULE && spellbookReqs.isEmpty() && originalSpellbook != null) { - log.debug("No post-schedule spellbook requirement defined, automatically restoring original spellbook"); - boolean restored = switchBackToOriginalSpellbook(); - if (!restored) { - Microbot.log("Failed to automatically restore original spellbook during post-schedule fulfillment", Level.WARN); - // Don't mark as failure since this is automatic restoration, not an explicit requirement - } - } - - log.debug("Spellbook requirements fulfillment completed. Success: {}", success); - return success; - } - - /** - * Fulfills conditional and ordered requirements for the specified schedule context. - * These requirements are processed first as they may contain prerequisites for other requirements. - * - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param scheduledFuture The CompletableFuture to monitor for cancellation - * @return true if all conditional requirements were fulfilled successfully, false otherwise - */ - public boolean fulfillConditionalRequirements(CompletableFuture scheduledFuture, TaskExecutionState executionState, TaskContext context ) { - // Get requirements count for step tracking - List conditionalReqs = this.registry.getRequirements(ConditionalRequirement.class, context); - List orderedReqs = this.registry.getRequirements(OrderedRequirement.class, context); - - // Initialize step tracking - int totalReqs = conditionalReqs.size() + orderedReqs.size(); - updateFulfillmentStep(executionState,FulfillmentStep.CONDITIONAL, - "Processing " + totalReqs + " conditional/ordered requirement(s)", totalReqs); - - // Use the utility class for fulfillment - return RequirementSolver.fulfillConditionalRequirements(scheduledFuture,executionState,conditionalReqs, orderedReqs, context); - - } - - - - /** - * Updates the fulfillment state for a specific step with requirement counting. - * This is the preferred method for tracking step-level progress. - * - * @param step The fulfillment step being processed - * @param details Descriptive text about what's happening in this step - * @param totalRequirements Total number of requirements in this step - */ - protected void updateFulfillmentStep(TaskExecutionState executionState, FulfillmentStep step, String details, int totalRequirements) { - executionState.updateFulfillmentStep(step, details, totalRequirements); - } - - /** - * Updates the current requirement being processed within a step. - * This provides granular tracking of individual requirement progress. - * - * @param requirement The specific requirement being processed - * @param requirementIndex The 1-based index of this requirement in the current step - */ - protected void updateCurrentRequirement(TaskExecutionState executionState, Requirement requirement, int requirementIndex) { - if (requirement != null) { - executionState.updateCurrentRequirement(requirement, requirement.getName(), requirementIndex); - } - } - /** - * Generates the inventory setup name for pre-schedule requirements. - * The format is: [OS]_{collectionName}_PRE_SCHEDULE - * This name is used to identify the corresponding InventorySetup in the plugin. - * - * @return The inventory setup name for pre-schedule requirements - */ - public String getPreInventorySetupName() { - return "[OS]_" + collectionName + "_" + TaskContext.PRE_SCHEDULE.name(); - - } - /** - * Retrieves the InventorySetup object for pre-schedule requirements. - * Searches the MInventorySetupsPlugin for a setup matching the pre-schedule name. - * - * @return The InventorySetup for pre-schedule, or null if not found - */ - public InventorySetup getPreInventorySetup() { - String setupName = getPreInventorySetupName(); - return getInventorySetup(setupName); - } - private InventorySetup getInventorySetup(String setupName) { - InventorySetup inventorySetup = MInventorySetupsPlugin.getInventorySetups().stream() - .filter(Objects::nonNull) - .filter(x -> x.getName().equalsIgnoreCase(setupName)) - .findFirst() - .orElse(null); - return inventorySetup; - - } - - /** - * Generates the inventory setup name for post-schedule requirements. - * The format is: [OS]_{collectionName}_POST_SCHEDULE - * This name is used to identify the corresponding InventorySetup in the plugin. - * - * @return The inventory setup name for post-schedule requirements - */ - public String getPostInventorySetupName() { - return "[OS]_" + collectionName + "_" + TaskContext.POST_SCHEDULE.name(); - } - /** - * Retrieves the InventorySetup object for post-schedule requirements. - * Searches the MInventorySetupsPlugin for a setup matching the post-schedule name. - * - * @return The InventorySetup for post-schedule, or null if not found - */ - public InventorySetup getPostInventorySetup() { - String setupName = getPostInventorySetupName(); - - return getInventorySetup(setupName); - } - - - /** - * Comprehensive inventory and equipment layout planning and fulfillment system. - * This method analyzes all requirements and creates optimal item placement maps, - * considering slot constraints, priority levels, and availability. - * - * @param context The schedule context - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if all mandatory requirements can be fulfilled - */ - private boolean fulfillOptimalInventoryAndEquipmentLayout(CompletableFuture scheduledFuture, TaskContext context) { - try { - StringBuilder sb = new StringBuilder(); - sb.append("\n" + "=".repeat(60)); - sb.append("\n\tOPTIMAL INVENTORY AND EQUIPMENT LAYOUT PLANNING"); - sb.append("\n\tContext: "+ context +"| Collection: "+collectionName).append("\n"); - sb.append("=".repeat(60)); - log.info(sb.toString()); - - - // Step 1: Analyze all requirements and create constraint maps - log.info("\n--- Step 1: Analyzing Requirements and Creating Layout Plan ---"); - InventorySetupPlanner layoutPlan = null; - String inventorySetupName = null; - if (context == TaskContext.PRE_SCHEDULE) { - log.info("Using pre-schedule inventory setup name: {}", getPreInventorySetupName()); - inventorySetupName = getPreInventorySetupName(); - if (currentPreScheduleLayoutPlan == null) { - log.info("No existing layout plan found, analyzing requirements to create new plan"); - this.currentPreScheduleLayoutPlan = analyzeRequirementsAndCreateLayoutPlan(scheduledFuture,context); - } else { - log.info("Existing layout plan found, re-analyzing requirements to update plan"); - } - layoutPlan = this.currentPreScheduleLayoutPlan; - }else if( context == TaskContext.POST_SCHEDULE) { - log.info("Using post-schedule inventory setup name: {}", getPostInventorySetupName()); - inventorySetupName = getPostInventorySetupName(); - if (currentPostScheduleLayoutPlan == null) { - log.info("No existing layout plan found, analyzing requirements to create new plan"); - this.currentPostScheduleLayoutPlan = analyzeRequirementsAndCreateLayoutPlan(scheduledFuture,context); - } else { - log.info("Existing layout plan found, re-analyzing requirements to update plan"); - } - layoutPlan = this.currentPostScheduleLayoutPlan; - } else { - log.error("Invalid context for inventory and equipment layout planning: {}", context); - return false; - } - - if (layoutPlan == null) { - log.error("Failed to create inventory layout plan"); - return false; - } - - // Display detailed plan information - log.info("\n--- Generated Layout Plan ---"); - log.info("\n"+layoutPlan.getDetailedPlanString()); - - // Step 2: Check if the plan is feasible (all mandatory items can be fulfilled) - log.info("\n--- Step 2: Feasibility Check ---"); - boolean feasible = layoutPlan.isFeasible(); - log.info("\n---Plan Feasibility: {}", feasible ? "FEASIBLE" : "NOT FEASIBLE"); - - if (!feasible) { - - log.error("Layout plan is not feasible - missing mandatory items or insufficient space"); - - // Log detailed failure reasons - if (!layoutPlan.getMissingMandatoryItems().isEmpty()) { - log.error("Missing mandatory items:"); - for (ItemRequirement missing : layoutPlan.getMissingMandatoryItems()) { - log.error("\t- {}", missing.getName()); - } - } - - if (!layoutPlan.getMissingMandatoryEquipment().isEmpty()) { - log.error("Missing mandatory equipment slots:"); - for (Map.Entry> entry : layoutPlan.getMissingMandatoryEquipment().entrySet()) { - EquipmentInventorySlot slot = entry.getKey(); - List missingItems = entry.getValue(); - String itemNames = missingItems.stream() - .map(ItemRequirement::getName) - .collect(Collectors.joining(", ")); - log.error("\t- {}: {}", slot.name(), itemNames); - } - } - - return false; - } - - // Display slot utilization summary - log.debug("\n--- Slot Utilization Summary ---"); - log.debug("\n"+layoutPlan.getOccupiedSlotsSummary()); - - // Step 2.5: Convert plan to InventorySetup and add to plugin BEFORE execution - log.debug("\n--- Step 2.5: Creating InventorySetup from Plan ---"); - InventorySetup createdSetup = layoutPlan.addToInventorySetupsPlugin(inventorySetupName); - - if (createdSetup == null) { - log.error("Failed to create InventorySetup from plan"); - return false; - } - - log.debug("Successfully created InventorySetup: {}", createdSetup.getName()); - - // Step 3: Execute using Rs2InventorySetup approach - log.debug("\n--- Step 3: Executing Plan Using Rs2InventorySetup ---"); - boolean success = layoutPlan.executeUsingRs2InventorySetup(scheduledFuture, createdSetup.getName()); - - if (success) { - log.debug("\n" + "=".repeat(60)); - log.debug("SUCCESSFULLY EXECUTED OPTIMAL INVENTORY AND EQUIPMENT LAYOUT"); - log.debug("Used Rs2InventorySetup approach with setup: {}", createdSetup.getName()); - log.debug("=".repeat(60)); - } else { - log.error("\n" + "=".repeat(60)); - log.error("FAILED TO EXECUTE INVENTORY AND EQUIPMENT LAYOUT PLAN"); - log.error("Rs2InventorySetup approach failed for setup: {}", createdSetup.getName()); - log.error("=".repeat(60)); - } - if (Rs2Bank.isOpen()) { - Rs2Bank.closeBank(); - log.info("Closed bank after inventory and equipment fulfillment"); - } - return success; - - } catch (Exception e) { - log.error("\nError in comprehensive inventory and equipment fulfillment: {}", e.getMessage(), e); - return false; - } - } - - /** - * Analyzes all requirements and creates an optimal inventory and equipment layout plan. - * Uses the enhanced InventorySetupPlanner with requirement registry support. - * - * @param scheduledFuture The scheduled future for cancellation checking - * @param context The schedule context (PRE_SCHEDULE or POST_SCHEDULE) - * @return The created inventory setup plan, or null if planning failed - */ - private InventorySetupPlanner analyzeRequirementsAndCreateLayoutPlan(CompletableFuture scheduledFuture, TaskContext context) { - // Ensure bank is open for all operations - if (Rs2Bank.bankItems().size() == 0 && !Rs2Bank.isOpen()) { - log.info("\n\tBank Cach is null and bank not open, attempting to open bank for item management, update bank data"); - if (!Rs2Bank.walkToBankAndUseBank() && !Rs2Player.isInteracting() && !Rs2Player.isMoving()) { - log.error("\n\tFailed to open bank for comprehensive item management"); - } - boolean openBank= sleepUntil(() -> Rs2Bank.isOpen(), 5000); - if (!openBank) { - log.error("\n\tFailed to open bank within timeout for context: {}", context); - return null; - } - } - // get all inventory Item (Rs2Item) and all equipment items currently in inventory or equipped which are not in the inenvtory or equipment requirements -// List currentInventoryItems = Rs2Inventory.get(); - // List currentEquipmentItems = Rs2Equipment.getAllItems(); - List itemNotInItemReqsInventory = new ArrayList<>(); - List itemNotInItemReqsEquipment = new ArrayList<>(); - Map> registeredEquipmentMap = registry.getEquipmentSlotItems(context); - Map> registeredInventoryMap = registry.getInventorySlotItems(context); - LinkedHashSet registeredAnyInventoryMap = registry.getAnyInventorySlotItems(context); - List registeredConditionalInventory = registry.getConditionalItemRequirements(context); - //iterate over inventoy, retrive unnecessary inventory items - for (int iiSlot = 0; iiSlot < 28; iiSlot++) { - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Executor service is shutdown, skipping inventory scan at slot: {}", iiSlot); - return null; // Skip if executor service is shutdown - } - Rs2ItemModel invItem = Rs2Inventory.getItemInSlot(iiSlot); - if (invItem != null) { - boolean foundInReqs = false; - // Check against registered inventory requirements - if (registeredInventoryMap.containsKey(iiSlot)) { - for (ItemRequirement req : registeredInventoryMap.get(iiSlot)) { - if(invItem.getId() == req.getId()){ - foundInReqs = true; - break; - } - //if (invItem.getName().toLowerCase().contains(req.getName().toLowerCase())) { - // foundInReqs = true; - // break; - // } - } - } - // Check against registered any-inventory requirements - if (!foundInReqs && !registeredAnyInventoryMap.isEmpty()) { - for (ItemRequirement req : registeredAnyInventoryMap) { - if(invItem.getId() == req.getId()){ - foundInReqs = true; - break; - } - } - } - if (!foundInReqs && !registeredConditionalInventory.isEmpty()) { - for (ConditionalRequirement condReq : registeredConditionalInventory) { - for (ItemRequirement req : condReq.getActiveItemRequirements()) { - if(invItem.getId() == req.getId()){ - foundInReqs = true; - break; - } - } - if(foundInReqs) break; - } - } - if (!foundInReqs) { - itemNotInItemReqsInventory.add(invItem); - } - } - } - //iterate over equipment slots, retrive unnecessary equipment items - for (EquipmentInventorySlot slot : EquipmentInventorySlot.values()) { - Rs2ItemModel equipItem = Rs2Equipment.get(slot); - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Executor service is shutdown, skipping equipment scan at slot: {}", slot); - return null; // Skip if executor service is shutdown - } - if (equipItem != null) { - boolean foundInReqs = false; - // Check against registered equipment requirements - if (registeredEquipmentMap.containsKey(slot)) { - for (ItemRequirement req : registeredEquipmentMap.get(slot)) { - if (equipItem.getName().toLowerCase().contains(req.getName().toLowerCase())) { - foundInReqs = true; - break; - } - } - } - if (!foundInReqs && !registeredConditionalInventory.isEmpty()) { - for (ConditionalRequirement condReq : registeredConditionalInventory) { - for (ItemRequirement req : condReq.getActiveItemRequirements()) { - if(equipItem.getId() == req.getId()){ - foundInReqs = true; - break; - } - } - if(foundInReqs) break; - } - } - if (!foundInReqs) { - itemNotInItemReqsEquipment.add(equipItem); - } - } - } - // we need do bank items which are not in the item requirements, move to bank and deposit - if (!itemNotInItemReqsInventory.isEmpty() || !itemNotInItemReqsEquipment.isEmpty()) { - log.info("\n\tFound {} inventory items and {} equipment items not in item requirements, depositing them to bank", itemNotInItemReqsInventory.size(), itemNotInItemReqsEquipment.size()); - Rs2Bank.walkToBankAndUseBank(); - boolean bankOpened = sleepUntil(() -> Rs2Bank.isOpen(), 5000); - if (!bankOpened) { - log.error("\n\tFailed to open bank within timeout for context: {}", context); - return null; - } - // Deposit inventory items - for (Rs2ItemModel item : itemNotInItemReqsInventory) { - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Executor service is shutdown, skipping item deposit: {}", item.getName()); - return null; // Skip if executor service is shutdown - } - Rs2Bank.depositAll(item.getId()); - boolean deposited = sleepUntil(()->!Rs2Inventory.hasItem(item.getName()), Constants.GAME_TICK_LENGTH*2); - if (deposited) { - log.info("\n\tDeposited inventory item: {} x{}", item.getName(), item.getQuantity()); - } else { - log.warn("\n\tFailed to deposit inventory item: {} x{}", item.getName(), item.getQuantity()); - } - } - // deposit all unnecessary equipment items at once using bulk deposit - if (!itemNotInItemReqsEquipment.isEmpty()) { - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Executor service is shutdown, skipping equipment deposit"); - return null; // Skip if executor service is shutdown - } - - // collect all equipment slots to deposit - EquipmentInventorySlot[] slotsToDeposit = itemNotInItemReqsEquipment.stream() - .mapToInt(Rs2ItemModel::getSlot) - .filter(slot -> slot >= 0 && slot < EquipmentInventorySlot.values().length) - .mapToObj(slot -> EquipmentInventorySlot.values()[slot]) - .toArray(EquipmentInventorySlot[]::new); - - log.info("Depositing {} unnecessary equipment items in bulk", slotsToDeposit.length); - boolean deposited = Rs2Bank.depositEquippedItems(slotsToDeposit); - - if (deposited) { - StringBuilder sb = new StringBuilder(); - sb.append("Successfully deposited equipment items:\\n"); - for (Rs2ItemModel item : itemNotInItemReqsEquipment) { - sb.append(String.format("\\t✓ %s x%d\\n", item.getName(), item.getQuantity())); - } - log.info(sb.toString()); - } else { - log.warn("Failed to deposit some or all equipment items"); - // fallback to individual deposits if bulk fails - for (Rs2ItemModel item : itemNotInItemReqsEquipment) { - EquipmentInventorySlot itemSlot = EquipmentInventorySlot.values()[item.getSlot()]; - Rs2Bank.depositEquippedItem(itemSlot); - boolean individualDeposited = sleepUntil(()->!Rs2Equipment.isWearing(item.getId()), Constants.GAME_TICK_LENGTH*2); - if (individualDeposited) { - log.info("\\tDeposited individual equipment item: {} x{}", item.getName(), item.getQuantity()); - } else { - log.warn("\\tFailed to deposit individual equipment item: {} x{}", item.getName(), item.getQuantity()); - } - } - } - } - } - - // Create enhanced planner with registry and OR requirement mode - InventorySetupPlanner plan = new InventorySetupPlanner(registry, context, orRequirementMode); - - // Create the plan from requirements - boolean planningSuccessful = plan.createPlanFromRequirements(); - - if (!planningSuccessful) { - log.error("Failed to create inventory setup plan for context: {}", context); - return null; - } - - log.info("Successfully created inventory setup plan for context: {} with OR mode: {}", context, orRequirementMode); - return plan; - } - - /** - * Adds a dummy equipment requirement to block a specific equipment slot. - * Dummy items are used to reserve slots without specifying actual items. - * - * @param equipmentSlot The equipment slot to block - * @param TaskContext When this requirement applies - * @param description Description for the dummy requirement - */ - protected void addDummyEquipmentRequirement(EquipmentInventorySlot equipmentSlot, - TaskContext taskContext, - String description) { - ItemRequirement dummy = ItemRequirement.createDummyEquipmentRequirement( - equipmentSlot, taskContext, description); - registry.register(dummy); - log.debug("Added dummy equipment requirement for slot {}: {}", equipmentSlot, description); - } - - /** - * Adds a dummy inventory requirement to block a specific inventory slot. - * Dummy items are used to reserve slots without specifying actual items. - * - * @param inventorySlot The inventory slot to block (0-27) - * @param TaskContext When this requirement applies - * @param description Description for the dummy requirement - */ - protected void addDummyInventoryRequirement(int inventorySlot, - TaskContext taskContext, - String description) { - ItemRequirement dummy = ItemRequirement.createDummyInventoryRequirement( - inventorySlot, taskContext, description); - registry.register(dummy); - log.debug("Added dummy inventory requirement for slot {}: {}", inventorySlot, description); - } - public String getDetailedDisplay(){ - StringBuilder sb = new StringBuilder(); - sb.append("=== Pre/Post Schedule Requirements Summary ===\n"); - sb.append("Collection: ").append(collectionName).append("\n"); - sb.append("Current Spellbook: ").append(Rs2Spellbook.getCurrentSpellbook()).append("\n"); - if (originalSpellbook != null) { - sb.append("Original Spellbook: ").append(originalSpellbook).append("\n"); - } - sb.append("Total Pre\\Post Requirements Registered: ").append(getRegistry().getAllRequirements().size()).append("\n"); - - // Pre-Schedule Requirements - sb.append(" Pre Requirements Registered: ").append(this.registry.getRequirements(TaskContext.PRE_SCHEDULE).size()).append("\n"); - sb.append(" - Spellbook Requirements: ").append(this.registry.getRequirements(SpellbookRequirement.class,TaskContext.PRE_SCHEDULE).size()).append("\n"); - sb.append(" - Location Requirements: ").append(this.registry.getRequirements(LocationRequirement.class, TaskContext.PRE_SCHEDULE).size()).append("\n"); - sb.append(" - Loot Requirements: ").append(this.registry.getRequirements(LootRequirement.class, TaskContext.PRE_SCHEDULE).size()).append("\n"); - - // Equipment Requirements breakdown - RequirementRegistry.RequirementBreakdown preEquipBreakdown = registry.getItemRequirementBreakdown(TaskContext.PRE_SCHEDULE); - sb.append(" - Equipment Requirements: ").append(preEquipBreakdown.getTotalEquipmentCount()).append("\n"); - sb.append(" └─ Mandatory: ").append(preEquipBreakdown.getEquipmentCount(RequirementPriority.MANDATORY)).append(", "); - sb.append("Recommended: ").append(preEquipBreakdown.getEquipmentCount(RequirementPriority.RECOMMENDED)).append("\n "); - - // Equipment slot details for Pre - Map> preEquipSlots = preEquipBreakdown.getEquipmentSlotBreakdown(); - if (!preEquipSlots.isEmpty()) { - sb.append(" └─ Equipment Slots Detail:\n"); - for (Map.Entry> entry : preEquipSlots.entrySet()) { - EquipmentInventorySlot slot = entry.getKey(); - Map counts = entry.getValue(); - sb.append(" ").append(slot.name()).append(": M=").append(counts.getOrDefault(RequirementPriority.MANDATORY, 0)) - .append(", R=").append(counts.getOrDefault(RequirementPriority.RECOMMENDED, 0)).append("\n"); - } - } - - // Inventory Requirements breakdown - sb.append(" - Inventory Requirements: ").append(preEquipBreakdown.getTotalInventoryCount()).append("\n"); - sb.append(" └─ Mandatory: ").append(preEquipBreakdown.getInventoryCount(RequirementPriority.MANDATORY)).append("\n"); - sb.append(" └─ Recommended: ").append(preEquipBreakdown.getInventoryCount(RequirementPriority.RECOMMENDED)).append(""); - - // Inventory slot details for Pre - Map> preInventorySlots = preEquipBreakdown.getInventorySlotBreakdown(); - if (!preInventorySlots.isEmpty()) { - sb.append(" └─ Inventory Slots Detail:\n"); - for (Map.Entry> entry : preInventorySlots.entrySet()) { - Integer slot = entry.getKey(); - Map counts = entry.getValue(); - sb.append(" Slot ").append(slot).append(": M=").append(counts.getOrDefault(RequirementPriority.MANDATORY, 0)) - .append(", R=").append(counts.getOrDefault(RequirementPriority.RECOMMENDED, 0)) - .append(", O=").append(counts.getOrDefault(RequirementPriority.RECOMMENDED, 0)).append("\n"); - } - } - - sb.append("\n - Shop Requirements: ").append(this.registry.getRequirements(ShopRequirement.class, TaskContext.PRE_SCHEDULE).size()).append("\n"); - sb.append(" - all external requirements: ").append(registry.getExternalRequirements(TaskContext.PRE_SCHEDULE).size()).append("\n"); - - // Post-Schedule Requirements - sb.append(" Post Requirements Registered: ").append(this.registry.getRequirements(TaskContext.POST_SCHEDULE).size()).append("\n"); - sb.append(" - Spellbook Requirements: ").append(this.registry.getRequirements(SpellbookRequirement.class, TaskContext.POST_SCHEDULE).size()).append("\n"); - sb.append(" - Location Requirements: ").append(this.registry.getRequirements(LocationRequirement.class, TaskContext.POST_SCHEDULE).size()).append("\n"); - sb.append(" - Loot Requirements: ").append(this.registry.getRequirements(LootRequirement.class, TaskContext.POST_SCHEDULE).size()).append("\n"); - - // Equipment Requirements breakdown for Post - RequirementRegistry.RequirementBreakdown postEquipBreakdown = registry.getItemRequirementBreakdown(TaskContext.POST_SCHEDULE); - sb.append(" - Equipment Requirements: ").append(postEquipBreakdown.getTotalEquipmentCount()).append("\n"); - sb.append(" └─ Mandatory: ").append(postEquipBreakdown.getEquipmentCount(RequirementPriority.MANDATORY)).append(", "); - sb.append("Recommended: ").append(postEquipBreakdown.getEquipmentCount(RequirementPriority.RECOMMENDED)); - - // Equipment slot details for Post - Map> postEquipSlots = postEquipBreakdown.getEquipmentSlotBreakdown(); - if (!postEquipSlots.isEmpty()) { - sb.append(" └─ Equipment Slots Detail:\n"); - for (Map.Entry> entry : postEquipSlots.entrySet()) { - EquipmentInventorySlot slot = entry.getKey(); - Map counts = entry.getValue(); - sb.append(" ").append(slot.name()).append(": M=").append(counts.getOrDefault(RequirementPriority.MANDATORY, 0)) - .append(", R=").append(counts.getOrDefault(RequirementPriority.RECOMMENDED, 0)).append("\n"); - } - } - - // Extra statistics before Inventory Requirements breakdown for Post - Map> postInventorySlots = postEquipBreakdown.getInventorySlotBreakdown(); - sb.append("\n - Inventory Slot Statistics (specific slots only):\n"); - sb.append(" └─ Total Specific Slots Used: ").append(postInventorySlots.size()).append("\n"); - if (!postInventorySlots.isEmpty()) { - sb.append(" └─ Slot Range: ").append(postInventorySlots.keySet().stream().min(Integer::compareTo).orElse(0)) - .append(" to ").append(postInventorySlots.keySet().stream().max(Integer::compareTo).orElse(0)).append("\n"); - } - - // Inventory Requirements breakdown for Post - sb.append(" - Inventory Requirements: ").append(postEquipBreakdown.getTotalInventoryCount()).append("\n"); - sb.append(" └─ Mandatory: ").append(postEquipBreakdown.getInventoryCount(RequirementPriority.MANDATORY)).append(", "); - sb.append("Recommended: ").append(postEquipBreakdown.getInventoryCount(RequirementPriority.RECOMMENDED)); - - - // Inventory slot details for Post - if (!postInventorySlots.isEmpty()) { - sb.append(" └─ Inventory Slots Detail:\n"); - for (Map.Entry> entry : postInventorySlots.entrySet()) { - Integer slot = entry.getKey(); - Map counts = entry.getValue(); - sb.append(" Slot ").append(slot).append(": M=").append(counts.getOrDefault(RequirementPriority.MANDATORY, 0)) - .append(", R=").append(counts.getOrDefault(RequirementPriority.RECOMMENDED, 0)) - .append(", O=").append(counts.getOrDefault(RequirementPriority.RECOMMENDED, 0)).append("\n"); - } - } - - sb.append("\n - Shop Requirements: ").append(this.registry.getRequirements(ShopRequirement.class, TaskContext.POST_SCHEDULE).size()).append("\n"); - sb.append(" - all external requirements: ").append(registry.getExternalRequirements(TaskContext.POST_SCHEDULE).size()).append("\n"); - sb.append("=============================================\n"); - return sb.toString(); - } - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/data/ItemRequirementCollection.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/data/ItemRequirementCollection.java deleted file mode 100644 index dd920dffcba..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/data/ItemRequirementCollection.java +++ /dev/null @@ -1,1444 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.data; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.api.Skill; -import net.runelite.api.gameval.ItemID; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.PrePostScheduleRequirements; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.OrRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.ConditionalRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.OrderedRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.RunePouchRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.util.ConditionalRequirementBuilder; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location.LocationRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.SpellbookRequirement; -import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; -import net.runelite.client.plugins.microbot.util.magic.Rs2Spellbook; -import net.runelite.client.plugins.microbot.util.magic.Runes; -import net.runelite.client.plugins.microbot.util.misc.Rs2Food; - -/** - * Static collection of requirement registration methods for common OSRS equipment sets, - * outfits, and progression-based tool collections. - * - * This class provides standardized requirement collections that can be registered - * with PrePostScheduleRequirements instances to ensure consistency across plugins. - */ -public class ItemRequirementCollection { - - /** - * Registers basic mining equipment requirements for the plugin scheduler. - * This includes various pickaxes with their respective requirements. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerPickAxes(PrePostScheduleRequirements requirements, RequirementPriority priority, TaskContext taskContext) { - // because all are in the same slot, these are or requirements, any of these is important - requirements.register(new ItemRequirement( - ItemID.CRYSTAL_PICKAXE, 1, - EquipmentInventorySlot.WEAPON, -1,//-1 allows to be in inventory - priority, 10, "Crystal pickaxe (best for mining fragments)", - taskContext, Skill.MINING, 71, Skill.ATTACK, 70 // Mining level 71 required to equip - )); - requirements.register(new ItemRequirement( - ItemID.DRAGON_PICKAXE, 1, - EquipmentInventorySlot.WEAPON,-1, - - priority, 8, "Dragon pickaxe (excellent for mining fragments)", - taskContext, Skill.MINING, 61, Skill.ATTACK, 60 // Mining level 61 required to equip - )); - requirements.register(new ItemRequirement( - ItemID.RUNE_PICKAXE, 1, - EquipmentInventorySlot.WEAPON,-1, priority, 6, "Rune pickaxe (good for mining fragments)", - taskContext, Skill.MINING, 41, Skill.ATTACK, 40 // Mining level 41 required to equip - )); - requirements.register(new ItemRequirement( - ItemID.ADAMANT_PICKAXE, 1, - EquipmentInventorySlot.WEAPON,-1, priority, 4, "Adamant pickaxe (adequate for mining fragments)", - taskContext, Skill.MINING, 31, Skill.ATTACK, 30 // Mining level 31 required to equip - )); - requirements.register(new ItemRequirement( - ItemID.MITHRIL_PICKAXE, 1, - EquipmentInventorySlot.WEAPON,-1, priority, 4, "Mithril pickaxe (adequate for mining fragments)", - taskContext, Skill.MINING, 21, Skill.ATTACK, 20 // Mining level 21 required to equip - )); - requirements.register(new ItemRequirement( - ItemID.STEEL_PICKAXE, 1, - EquipmentInventorySlot.WEAPON,-1, priority, 2, "Steel pickaxe (for mining fragments)", - taskContext, Skill.MINING, 6, Skill.ATTACK, 10 // Mining level 6 required to equip - )); - requirements.register(new ItemRequirement( - ItemID.IRON_PICKAXE, 1, - EquipmentInventorySlot.WEAPON,-1, priority, 2, "Iron pickaxe (for mining fragments)", - taskContext, Skill.MINING, 0, Skill.ATTACK, 0// Mining level 1 required to equip - )); - requirements.register(new ItemRequirement( - ItemID.BRONZE_PICKAXE, 1, - EquipmentInventorySlot.WEAPON,-1, priority, 1, "Bronze pickaxe (for mining fragments, if no better option available)", - taskContext - // No skill requirement for bronze pickaxe - anyone can use it - )); - } - - /** - * Registers progression-based woodcutting axes for the plugin scheduler. - * This includes all axes from bronze to crystal with their respective requirements. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerWoodcuttingAxes(PrePostScheduleRequirements requirements, RequirementPriority priority, TaskContext taskContext, int inventorySlot) { - requirements.register(new ItemRequirement( - ItemID._3A_AXE, 1, - EquipmentInventorySlot.WEAPON, inventorySlot, priority, 10, "3rd age axe (best woodcutting axe available)", - taskContext, Skill.WOODCUTTING, 65, Skill.ATTACK, 65 // 3rd age axe requirements - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.CRYSTAL_AXE, ItemID.CRYSTAL_AXE_INACTIVE), 1, - EquipmentInventorySlot.WEAPON, inventorySlot, priority, 9, "Crystal axe (excellent for woodcutting)", - taskContext, Skill.WOODCUTTING, 71, Skill.ATTACK, 70 // Crystal axe requirements - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.INFERNAL_AXE, ItemID.INFERNAL_AXE_EMPTY), 1, - EquipmentInventorySlot.WEAPON, inventorySlot, priority, 8, "Infernal axe (burns logs automatically)", - taskContext, Skill.WOODCUTTING, 61, Skill.ATTACK, 60 // Infernal axe requirements - )); - requirements.register(new ItemRequirement( - ItemID.DRAGON_AXE, 1, - EquipmentInventorySlot.WEAPON, inventorySlot, - priority, 8, "Dragon axe (excellent for woodcutting)", - taskContext, Skill.WOODCUTTING, 61, Skill.ATTACK, 60 // Dragon axe requirements - )); - requirements.register(new ItemRequirement( - ItemID.RUNE_AXE, 1, - EquipmentInventorySlot.WEAPON, inventorySlot, priority, 6, "Rune axe (good for woodcutting)", - taskContext, Skill.WOODCUTTING, 41, Skill.ATTACK, 40 // Rune axe requirements - )); - requirements.register(new ItemRequirement( - ItemID.ADAMANT_AXE, 1, - EquipmentInventorySlot.WEAPON, inventorySlot, priority, 4, "Adamant axe (adequate for woodcutting)", - taskContext, Skill.WOODCUTTING, 31, Skill.ATTACK, 30 // Adamant axe requirements - )); - requirements.register(new ItemRequirement( - ItemID.MITHRIL_AXE, 1, - EquipmentInventorySlot.WEAPON, inventorySlot, priority, 4, "Mithril axe (adequate for woodcutting)", - taskContext, Skill.WOODCUTTING, 21, Skill.ATTACK, 20 // Mithril axe requirements - )); - requirements.register(new ItemRequirement( - ItemID.BLACK_AXE, 1, - EquipmentInventorySlot.WEAPON, inventorySlot, priority, 3, "Black axe (for woodcutting)", - taskContext, Skill.WOODCUTTING, 6, Skill.ATTACK, 10 // Black axe requirements - )); - requirements.register(new ItemRequirement( - ItemID.STEEL_AXE, 1, - EquipmentInventorySlot.WEAPON, inventorySlot, priority, 2, "Steel axe (for woodcutting)", - taskContext, Skill.WOODCUTTING, 6, Skill.ATTACK, 5 // Steel axe requirements - )); - requirements.register(new ItemRequirement( - ItemID.IRON_AXE, 1, - EquipmentInventorySlot.WEAPON, inventorySlot, priority, 2, "Iron axe (for woodcutting)", - taskContext, Skill.WOODCUTTING, 1, Skill.ATTACK, 1 // Iron axe requirements - )); - requirements.register(new ItemRequirement( - ItemID.BRONZE_AXE, 1, - EquipmentInventorySlot.WEAPON, inventorySlot, priority, 1, "Bronze axe (basic woodcutting axe)", - taskContext - // No skill requirement for bronze axe - anyone can use it - )); - } - - /** - * Registers the complete Graceful outfit for the plugin scheduler. - * Includes all regular graceful pieces plus all Zeah house variants. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the graceful outfit (MANDATORY, RECOMMENDED, or OPTIONAL) - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerGracefulOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, TaskContext taskContext,int rating) { - registerGracefulOutfit(requirements, priority, taskContext, rating, false, false, false, false, false, false); - } - - /** - * Registers the complete Graceful outfit for the plugin scheduler with convenience flags. - * Includes all regular graceful pieces plus all Zeah house variants. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the graceful outfit (MANDATORY, RECOMMENDED, or OPTIONAL) - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param skipHead Skip head slot if true - * @param skipCape Skip cape slot if true - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - * @param skipGloves Skip gloves slot if true - * @param skipBoots Skip boots slot if true - */ - public static void registerGracefulOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, TaskContext taskContext, int rating, - boolean skipHead, boolean skipCape, boolean skipBody, - boolean skipLegs, boolean skipGloves, boolean skipBoots) { - - // Combined Graceful outfit (all variants in one requirement) - if (!skipHead) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.GRACEFUL_HOOD, ItemID.ZEAH_GRACEFUL_HOOD_ARCEUUS, ItemID.ZEAH_GRACEFUL_HOOD_PISCARILIUS, - ItemID.ZEAH_GRACEFUL_HOOD_LOVAKENGJ, ItemID.ZEAH_GRACEFUL_HOOD_SHAYZIEN, - ItemID.ZEAH_GRACEFUL_HOOD_HOSIDIUS, ItemID.ZEAH_GRACEFUL_HOOD_KOUREND), 1, - EquipmentInventorySlot.HEAD, -2, - priority, rating, "Graceful hood (weight reduction and run energy restoration)", - taskContext - )); - } - if (!skipCape) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.GRACEFUL_CAPE, ItemID.ZEAH_GRACEFUL_CAPE_ARCEUUS, ItemID.ZEAH_GRACEFUL_CAPE_PISCARILIUS, - ItemID.ZEAH_GRACEFUL_CAPE_LOVAKENGJ, ItemID.ZEAH_GRACEFUL_CAPE_SHAYZIEN, - ItemID.ZEAH_GRACEFUL_CAPE_HOSIDIUS, ItemID.ZEAH_GRACEFUL_CAPE_KOUREND),1, - EquipmentInventorySlot.CAPE, -2, - priority, rating, "Graceful cape (weight reduction and run energy restoration)", - taskContext - )); - } - if (!skipBody) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.GRACEFUL_TOP, ItemID.ZEAH_GRACEFUL_TOP_ARCEUUS, ItemID.ZEAH_GRACEFUL_TOP_PISCARILIUS, - ItemID.ZEAH_GRACEFUL_TOP_LOVAKENGJ, ItemID.ZEAH_GRACEFUL_TOP_SHAYZIEN, - ItemID.ZEAH_GRACEFUL_TOP_HOSIDIUS, ItemID.ZEAH_GRACEFUL_TOP_KOUREND),1, - EquipmentInventorySlot.BODY,-2, - priority, rating, "Graceful top (weight reduction and run energy restoration)", - taskContext - )); - } - if (!skipLegs) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.GRACEFUL_LEGS, ItemID.ZEAH_GRACEFUL_LEGS_ARCEUUS, ItemID.ZEAH_GRACEFUL_LEGS_PISCARILIUS, - ItemID.ZEAH_GRACEFUL_LEGS_LOVAKENGJ, ItemID.ZEAH_GRACEFUL_LEGS_SHAYZIEN, - ItemID.ZEAH_GRACEFUL_LEGS_HOSIDIUS, ItemID.ZEAH_GRACEFUL_LEGS_KOUREND),1, - EquipmentInventorySlot.LEGS,-2, - priority, rating, "Graceful legs (weight reduction and run energy restoration)", - taskContext - )); - } - if (!skipGloves) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.GRACEFUL_GLOVES, ItemID.ZEAH_GRACEFUL_GLOVES_ARCEUUS, ItemID.ZEAH_GRACEFUL_GLOVES_PISCARILIUS, - ItemID.ZEAH_GRACEFUL_GLOVES_LOVAKENGJ, ItemID.ZEAH_GRACEFUL_GLOVES_SHAYZIEN, - ItemID.ZEAH_GRACEFUL_GLOVES_HOSIDIUS, ItemID.ZEAH_GRACEFUL_GLOVES_KOUREND), 1, - EquipmentInventorySlot.GLOVES,-2, - priority, rating, "Graceful gloves (weight reduction and run energy restoration)", - taskContext - )); - } - if (!skipBoots) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.GRACEFUL_BOOTS, ItemID.ZEAH_GRACEFUL_BOOTS_ARCEUUS, ItemID.ZEAH_GRACEFUL_BOOTS_PISCARILIUS, - ItemID.ZEAH_GRACEFUL_BOOTS_LOVAKENGJ, ItemID.ZEAH_GRACEFUL_BOOTS_SHAYZIEN, - ItemID.ZEAH_GRACEFUL_BOOTS_HOSIDIUS, ItemID.ZEAH_GRACEFUL_BOOTS_KOUREND), 1, - EquipmentInventorySlot.BOOTS, -2, - priority, rating, "Graceful boots (weight reduction and run energy restoration)", - taskContext - )); - } - } - - /** - * Registers the complete Runecrafting Outfit (Robes of the Eye) for the plugin scheduler. - * This is the specialized outfit for runecrafting activities. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the runecrafting outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerRunecraftingOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, TaskContext taskContext) { - registerRunecraftingOutfit(requirements, priority, taskContext, false, false, false); - } - - /** - * Registers the complete Runecrafting Outfit (Robes of the Eye) for the plugin scheduler with convenience flags. - * This is the specialized outfit for runecrafting activities. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the runecrafting outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param skipHead Skip head slot if true - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - */ - public static void registerRunecraftingOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, TaskContext taskContext, - boolean skipHead, boolean skipBody, boolean skipLegs) { - // Original (default) color variants - highest priority - if (!skipHead) { - requirements.register(new ItemRequirement( - ItemID.HAT_OF_THE_EYE, 1, - EquipmentInventorySlot.HEAD, - priority, 10, "Hat of the Eye (optimal for runecrafting)", - taskContext - )); - } - if (!skipBody) { - requirements.register(new ItemRequirement( - ItemID.ROBE_TOP_OF_THE_EYE, 1, - EquipmentInventorySlot.BODY, - priority, 10, "Robe top of the Eye (optimal for runecrafting)", - taskContext - )); - } - if (!skipLegs) { - requirements.register(new ItemRequirement( - ItemID.ROBE_BOTTOM_OF_THE_EYE, 1, - EquipmentInventorySlot.LEGS, - priority, 10, "Robe bottoms of the Eye (optimal for runecrafting)", - taskContext - )); - } - - // Colored variants (red, green, blue) - slightly lower priority - int coloredVariantRating = Math.max(1, priority == RequirementPriority.MANDATORY ? 8 : priority == RequirementPriority.RECOMMENDED ? 6 : 4); - - if (!skipHead) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.HAT_OF_THE_EYE_RED, ItemID.HAT_OF_THE_EYE_GREEN, ItemID.HAT_OF_THE_EYE_BLUE), - EquipmentInventorySlot.HEAD, - priority, coloredVariantRating, "Hat of the Eye (colored variants)", - taskContext - )); - } - if (!skipBody) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.ROBE_TOP_OF_THE_EYE_RED, ItemID.ROBE_TOP_OF_THE_EYE_GREEN, ItemID.ROBE_TOP_OF_THE_EYE_BLUE), - EquipmentInventorySlot.BODY, - priority, coloredVariantRating, "Robe top of the Eye (colored variants)", - taskContext - )); - } - if (!skipLegs) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.ROBE_BOTTOM_OF_THE_EYE_RED, ItemID.ROBE_BOTTOM_OF_THE_EYE_GREEN, ItemID.ROBE_BOTTOM_OF_THE_EYE_BLUE), - EquipmentInventorySlot.LEGS, - priority, coloredVariantRating, "Robe bottoms of the Eye (colored variants)", - taskContext - )); - } - } - - /** - * Registers the complete Lumberjack Outfit for the plugin scheduler. - * This is the specialized outfit for woodcutting activities. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the lumberjack outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerLumberjackOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext) { - registerLumberjackOutfit(requirements, priority, rating, taskContext, false, false, false, false); - } - - /** - * Registers the complete Lumberjack Outfit for the plugin scheduler with convenience flags. - * This is the specialized outfit for woodcutting activities. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the lumberjack outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param skipHead Skip head slot if true - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - * @param skipBoots Skip boots slot if true - */ - public static void registerLumberjackOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext, - boolean skipHead, boolean skipBody, boolean skipLegs, boolean skipBoots) { - // Combined Lumberjack outfit (all variants in one requirement) - if (!skipHead) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.FORESTRY_LUMBERJACK_HAT, ItemID.RAMBLE_LUMBERJACK_HAT), - EquipmentInventorySlot.HEAD, - priority, rating, "Lumberjack hat - optimal for woodcutting XP", - taskContext - )); - } - if (!skipBody) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.FORESTRY_LUMBERJACK_TOP, ItemID.RAMBLE_LUMBERJACK_TOP), - EquipmentInventorySlot.BODY, - priority, rating, "Lumberjack top - optimal for woodcutting XP", - taskContext - )); - } - if (!skipLegs) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.FORESTRY_LUMBERJACK_LEGS, ItemID.RAMBLE_LUMBERJACK_LEGS), - EquipmentInventorySlot.LEGS, - priority, rating, "Lumberjack legs - optimal for woodcutting XP", - taskContext - )); - } - if (!skipBoots) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.FORESTRY_LUMBERJACK_BOOTS, ItemID.RAMBLE_LUMBERJACK_BOOTS), - EquipmentInventorySlot.BOOTS, - priority, rating, "Lumberjack boots - optimal for woodcutting XP", - taskContext - )); - } - } - - /** - * Registers the complete Angler Outfit for the plugin scheduler. - * This is the specialized outfit for fishing activities including all variants. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the angler outfit - */ - public static void registerAnglerOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext) { - registerAnglerOutfit(requirements, priority, rating, taskContext, false, false, false, false); - } - - /** - * Registers the complete Angler Outfit for the plugin scheduler with convenience flags. - * This is the specialized outfit for fishing activities including all variants. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the angler outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param skipHead Skip head slot if true - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - * @param skipBoots Skip boots slot if true - */ - public static void registerAnglerOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext, - boolean skipHead, boolean skipBody, boolean skipLegs, boolean skipBoots) { - // Spirit Angler outfit (enhanced version) - highest priority - int spiritRating = rating; - - if (!skipHead) { - requirements.register(new ItemRequirement( - ItemID.SPIRIT_ANGLER_HAT, 1, - EquipmentInventorySlot.HEAD, - priority, spiritRating, "Spirit angler hat - enhanced fishing XP bonus",taskContext - )); - } - if (!skipBody) { - requirements.register(new ItemRequirement( - ItemID.SPIRIT_ANGLER_TOP, 1, - EquipmentInventorySlot.BODY, - priority, spiritRating, "Spirit angler top - enhanced fishing XP bonus",taskContext - )); - } - if (!skipLegs) { - requirements.register(new ItemRequirement( - ItemID.SPIRIT_ANGLER_LEGS, 1, - EquipmentInventorySlot.LEGS, - priority, spiritRating, "Spirit angler legs - enhanced fishing XP bonus",taskContext - )); - } - if (!skipBoots) { - requirements.register(new ItemRequirement( - ItemID.SPIRIT_ANGLER_BOOTS, 1, - EquipmentInventorySlot.BOOTS, - priority, spiritRating, "Spirit angler boots - enhanced fishing XP bonus",taskContext - )); - } - - // Regular Angler outfit (Trawler reward) - int regularRating = Math.max(1,spiritRating-1); - - if (!skipHead) { - requirements.register(new ItemRequirement( - ItemID.TRAWLER_REWARD_HAT, 1, - EquipmentInventorySlot.HEAD, - priority, regularRating, "Angler hat - provides fishing XP bonus",taskContext - )); - } - if (!skipBody) { - requirements.register(new ItemRequirement( - ItemID.TRAWLER_REWARD_TOP, 1, - EquipmentInventorySlot.BODY, - priority, regularRating, "Angler top - provides fishing XP bonus",taskContext - )); - } - if (!skipLegs) { - requirements.register(new ItemRequirement( - ItemID.TRAWLER_REWARD_LEGS, 1, - EquipmentInventorySlot.LEGS, - priority, regularRating, "Angler legs - provides fishing XP bonus",taskContext - )); - } - if (!skipBoots) { - requirements.register(new ItemRequirement( - ItemID.TRAWLER_REWARD_BOOTS, 1, - EquipmentInventorySlot.BOOTS, - priority, regularRating, "Angler boots - provides fishing XP bonus",taskContext - )); - } - } - - /** - * Registers high-healing food items for combat and survival activities. - * Uses the Rs2Food enum to get the most effective food options. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for food items - * @param quantity The quantity of food to require - */ - public static void registerHighHealingFood(PrePostScheduleRequirements requirements, RequirementPriority priority, TaskContext taskContext,int quantity) { - // Register food in order of healing effectiveness (highest heal values first) - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.Dark_Crab.getId()), quantity, - null, -1, priority, 10, "Dark crab - heals " + Rs2Food.Dark_Crab.getHeal() + " HP (best healing food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.ROCKTAIL.getId()), quantity, - null, -1, priority, 9, "Rocktail - heals " + Rs2Food.ROCKTAIL.getHeal() + " HP (excellent healing)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.MANTA.getId()), quantity, - null, -1, priority, 8, "Manta ray - heals " + Rs2Food.MANTA.getHeal() + " HP (very good healing)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.SEA_TURTLE.getId()), quantity, - null, -1, priority, 7, "Sea turtle - heals " + Rs2Food.SEA_TURTLE.getHeal() + " HP (good healing)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.SHARK.getId()), quantity, - null, -1, priority, 6, "Shark - heals " + Rs2Food.SHARK.getHeal() + " HP (standard high-level food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.KARAMBWAN.getId()), quantity, - null, -1, priority, 5, "Cooked karambwan - heals " + Rs2Food.KARAMBWAN.getHeal() + " HP (can combo with other food)",taskContext - )); - } - - /** - * Registers mid-tier healing food items for general use. - * Includes commonly available and cost-effective food options. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for food items - * @param quantity The quantity of food to require - */ - public static void registerMidTierFood(PrePostScheduleRequirements requirements, RequirementPriority priority,TaskContext taskContext ,int quantity) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.MONKFISH.getId()), quantity, - null, -1, priority, 8, "Monkfish - heals " + Rs2Food.MONKFISH.getHeal() + " HP (good mid-tier food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.SWORDFISH.getId()), quantity, - null, -1, priority, 6, "Swordfish - heals " + Rs2Food.SWORDFISH.getHeal() + " HP (decent mid-tier food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.BASS.getId()), quantity, - null, -1, priority, 5, "Bass - heals " + Rs2Food.BASS.getHeal() + " HP (alternative mid-tier food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.LOBSTER.getId()), quantity, - null, -1, priority, 4, "Lobster - heals " + Rs2Food.LOBSTER.getHeal() + " HP (common mid-tier food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.TUNA.getId()), quantity, - null, -1, priority, 3, "Tuna - heals " + Rs2Food.TUNA.getHeal() + " HP (affordable mid-tier food)",taskContext - )); - } - - /** - * Registers fast food items that can be eaten in 1 tick. - * Useful for combo eating or quick healing during combat. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for food items - * @param quantity The quantity of food to require - */ - public static void registerFastFood(PrePostScheduleRequirements requirements, RequirementPriority priority, TaskContext taskContext,int quantity) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.KARAMBWAN.getId()), quantity, - null, -1, priority, 9, "Cooked karambwan - heals " + Rs2Food.KARAMBWAN.getHeal() + " HP (1-tick food, good for combos)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.CHOCOLATE_CAKE.getId()), quantity, - null, -1, priority, 6, "Chocolate cake - heals " + Rs2Food.CHOCOLATE_CAKE.getHeal() + " HP (1-tick food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.CAKE.getId()), quantity, - null, -1, priority, 5, "Cake - heals " + Rs2Food.CAKE.getHeal() + " HP (1-tick food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.PLAIN_PIZZA.getId()), quantity, - null, -1, priority, 4, "Plain pizza - heals " + Rs2Food.PLAIN_PIZZA.getHeal() + " HP (1-tick food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.MEAT_PIZZA.getId()), quantity, - null, -1, priority, 4, "Meat pizza - heals " + Rs2Food.MEAT_PIZZA.getHeal() + " HP (1-tick food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.ANCHOVY_PIZZA.getId()), quantity, - null, -1, priority, 4, "Anchovy pizza - heals " + Rs2Food.ANCHOVY_PIZZA.getHeal() + " HP (1-tick food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.PINEAPPLE_PIZZA.getId()), quantity, - null, -1, priority, 5, "Pineapple pizza - heals " + Rs2Food.PINEAPPLE_PIZZA.getHeal() + " HP (1-tick food)",taskContext - )); - } - - /** - * Registers basic/emergency food items for low-level activities. - * Includes cheap and easily obtainable food options. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for food items - * @param quantity The quantity of food to require - */ - public static void registerBasicFood(PrePostScheduleRequirements requirements, RequirementPriority priority, TaskContext taskContext,int quantity) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.SALMON.getId()), quantity, - null, -1, priority, 5, "Salmon - heals " + Rs2Food.SALMON.getHeal() + " HP (basic food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.TROUT.getId()), quantity, - null, -1, priority, 4, "Trout - heals " + Rs2Food.TROUT.getHeal() + " HP (basic food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.PIKE.getId()), quantity, - null, -1, priority, 4, "Pike - heals " + Rs2Food.PIKE.getHeal() + " HP (basic food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.COD.getId()), quantity, - null, -1, priority, 3, "Cod - heals " + Rs2Food.COD.getHeal() + " HP (basic food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.HERRING.getId()), quantity, - null, -1, priority, 3, "Herring - heals " + Rs2Food.HERRING.getHeal() + " HP (basic food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.SARDINE.getId()), quantity, - null, -1, priority, 2, "Sardine - heals " + Rs2Food.SARDINE.getHeal() + " HP (basic food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.SHRIMPS.getId()), quantity, - null, -1, priority, 2, "Shrimps - heals " + Rs2Food.SHRIMPS.getHeal() + " HP (basic food)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(Rs2Food.BREAD.getId()), quantity, - null, -1, priority, 2, "Bread - heals " + Rs2Food.BREAD.getHeal() + " HP (emergency food)",taskContext - )); - } - - /** - * Registers runes required for NPC Contact spell, which is used for pouch repair. - * This is recommended for efficiency in the Guardians of the Rift minigame. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - */ - public static void registerRunesForNPCContact(PrePostScheduleRequirements requirements, RequirementPriority priority,TaskContext taskContext, int rating) { - // NPC Contact runes for pouch repair (recommended for efficiency) - requirements.register(new ItemRequirement( - ItemID.COSMICRUNE, - 1, -1, priority, rating, "Cosmic runes (for NPC Contact spell)",taskContext - )); - requirements.register(new ItemRequirement( - ItemID.ASTRALRUNE, - 1, -1, priority, rating, "Astral runes (for NPC Contact spell)",taskContext - )); - requirements.register(new ItemRequirement( - ItemID.AIRRUNE, - 1, -1, priority, rating, "Air runes (for NPC Contact spell)",taskContext - )); - } - - /** - * Registers rune pouches for efficient runecrafting. - * This includes all pouch types with their level requirements. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the rune pouches - */ - public static void registerRunePouches(PrePostScheduleRequirements requirements, RequirementPriority priority,TaskContext taskContext) { - // Rune pouches for efficient rune storage - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.RCU_POUCH_COLOSSAL, ItemID.RCU_POUCH_COLOSSAL_DEGRADE),1, - -1, priority, 10, "Colossal pouch (for maximum essence carrying)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.RCU_POUCH_GIANT, ItemID.RCU_POUCH_GIANT_DEGRADE),1, - -1, priority, 8, "Giant pouch (for high essence carrying)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.RCU_POUCH_LARGE, ItemID.RCU_POUCH_LARGE_DEGRADE),1, - -1, priority, 6, "Large pouch (for good essence carrying)",taskContext - )); - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.RCU_POUCH_MEDIUM, ItemID.RCU_POUCH_MEDIUM_DEGRADE),1, - -1, priority, 4, "Medium pouch (for decent essence carrying)",taskContext - )); - requirements.register(new ItemRequirement( - ItemID.RCU_POUCH_SMALL, - 1, -1, priority, 2, "Small pouch (basic essence carrying)",taskContext - )); - } - - /** - * Registers the Varrock diary armour for the plugin scheduler. - * This includes all tiers of Varrock diary armour (Easy, Medium, Hard, Elite). - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the Varrock armour - */ - public static void registerVarrockDiaryArmour(PrePostScheduleRequirements requirements, RequirementPriority priority,TaskContext taskContext) { - // Varrock armour progression (Elite > Hard > Medium > Easy) - requirements.register(new ItemRequirement( - ItemID.VARROCK_ARMOUR_ELITE, 1, - EquipmentInventorySlot.BODY, priority, 10, "Varrock armour 4 (Elite) - best diary armour with all benefits",taskContext - )); - requirements.register(new ItemRequirement( - ItemID.VARROCK_ARMOUR_HARD, 1, - EquipmentInventorySlot.BODY, priority, 8, "Varrock armour 3 (Hard) - good diary armour",taskContext - )); - requirements.register(new ItemRequirement( - ItemID.VARROCK_ARMOUR_MEDIUM, 1, - EquipmentInventorySlot.BODY, priority, 6, "Varrock armour 2 (Medium) - decent diary armour",taskContext - )); - requirements.register(new ItemRequirement( - ItemID.VARROCK_ARMOUR_EASY, 1, - EquipmentInventorySlot.BODY, priority, 4, "Varrock armour 1 (Easy) - basic diary armour",taskContext - )); - } - - /** - * Registers the complete Prospector/Motherlode Mine outfit for the plugin scheduler. - * This includes all variants (regular, gold, and fossil variants). - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the prospector outfit - */ - public static void registerProspectorOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority,int rating,TaskContext taskContext) { - registerProspectorOutfit(requirements, priority,rating,taskContext, false, false, false, false); - } - - /** - * Registers the complete Prospector/Motherlode Mine outfit for the plugin scheduler with convenience flags. - * This includes all variants (regular, gold, and fossil variants). - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the prospector outfit - * @param skipHead Skip head slot if true - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - * @param skipBoots Skip boots slot if true - */ - public static void registerProspectorOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority,int rating,TaskContext taskContext, - boolean skipHead, boolean skipBody, boolean skipLegs, boolean skipBoots) { - - - // Prospector outfit pieces for additional mining benefits - if (!skipHead) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.MOTHERLODE_REWARD_HAT, ItemID.MOTHERLODE_REWARD_HAT_GOLD, ItemID.FOSSIL_MOTHERLODE_REWARD_HAT), - EquipmentInventorySlot.HEAD, priority, rating, "Prospector helmet", taskContext - )); - } - if (!skipBody) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.MOTHERLODE_REWARD_TOP, ItemID.MOTHERLODE_REWARD_TOP_GOLD, ItemID.FOSSIL_MOTHERLODE_REWARD_TOP), - EquipmentInventorySlot.BODY, priority, rating, "Prospector jacket", taskContext - )); - } - if (!skipLegs) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.MOTHERLODE_REWARD_LEGS, ItemID.MOTHERLODE_REWARD_LEGS_GOLD, ItemID.FOSSIL_MOTHERLODE_REWARD_LEGS), - EquipmentInventorySlot.LEGS, priority, rating, "Prospector legs", taskContext - )); - } - if (!skipBoots) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.MOTHERLODE_REWARD_BOOTS, ItemID.MOTHERLODE_REWARD_BOOTS_GOLD, ItemID.FOSSIL_MOTHERLODE_REWARD_BOOTS), - EquipmentInventorySlot.BOOTS, priority, rating, "Prospector boots", taskContext - )); - } - - - } - public static void registerRunecraftingCape(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext) { - // Register runecrafting capes for additional benefits - // Skill capes for additional benefits - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.SKILLCAPE_RUNECRAFTING, ItemID.SKILLCAPE_RUNECRAFTING_TRIMMED),1, - EquipmentInventorySlot.CAPE, -2,priority, rating, - "Runecrafting cape (any variant)",taskContext,null,-1, - Skill.RUNECRAFT,99 )); - - } - public static void registerMiningCape(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext) { - // Register mining capes for additional benefits - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.SKILLCAPE_MINING, ItemID.SKILLCAPE_MINING_TRIMMED),1, - EquipmentInventorySlot.CAPE, -2,priority, rating, - "Mining cape (any variant)",taskContext,null,-1, - Skill.MINING,99 )); - } - - /** - * Registers the complete Pyromancer Outfit for the plugin scheduler. - * This is the specialized outfit for firemaking activities from Wintertodt. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the pyromancer outfit - * @param rating The rating for the pyromancer outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerPyromancerOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext) { - registerPyromancerOutfit(requirements, priority, rating, taskContext, false, false, false, false, false); - } - - /** - * Registers the complete Pyromancer Outfit for the plugin scheduler with convenience flags. - * This is the specialized outfit for firemaking activities from Wintertodt. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the pyromancer outfit - * @param rating The rating for the pyromancer outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param skipHead Skip head slot if true - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - * @param skipBoots Skip boots slot if true - * @param skipGloves Skip gloves slot if true - */ - public static void registerPyromancerOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext, - boolean skipHead, boolean skipBody, boolean skipLegs, boolean skipBoots, boolean skipGloves) { - if (!skipHead) { - requirements.register(new ItemRequirement( - ItemID.PYROMANCER_HOOD, 1, - EquipmentInventorySlot.HEAD, - priority, rating, "Pyromancer hood - provides firemaking XP bonus", - taskContext - )); - } - if (!skipBody) { - requirements.register(new ItemRequirement( - ItemID.PYROMANCER_TOP, 1, - EquipmentInventorySlot.BODY, - priority, rating, "Pyromancer garb - provides firemaking XP bonus", - taskContext - )); - } - if (!skipLegs) { - requirements.register(new ItemRequirement( - ItemID.PYROMANCER_BOTTOM, 1, - EquipmentInventorySlot.LEGS, - priority, rating, "Pyromancer robe - provides firemaking XP bonus", - taskContext - )); - } - if (!skipBoots) { - requirements.register(new ItemRequirement( - ItemID.PYROMANCER_BOOTS, 1, - EquipmentInventorySlot.BOOTS, - priority, rating, "Pyromancer boots - provides firemaking XP bonus", - taskContext - )); - } - if (!skipGloves) { - requirements.register(new ItemRequirement( - ItemID.PYROMANCER_GLOVES, 1, - EquipmentInventorySlot.GLOVES, - priority, rating, "Pyromancer gloves - provides firemaking XP bonus", - taskContext - )); - } - } - - /** - * Registers the complete Farmer's Outfit for the plugin scheduler. - * This is the specialized outfit for farming activities from Tithe Farm. - * Note: These are cosmetic farmer clothing items, not the actual skilling outfit. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the farmer's outfit - * @param rating The rating for the farmer's outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerFarmersOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext) { - registerFarmersOutfit(requirements, priority, rating, taskContext, false, false, false, false); - } - - /** - * Registers the complete Farmer's Outfit for the plugin scheduler with convenience flags. - * This is the specialized outfit for farming activities from Tithe Farm. - * Note: These are cosmetic farmer clothing items, not the actual skilling outfit. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the farmer's outfit - * @param rating The rating for the farmer's outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param skipHead Skip head slot if true - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - * @param skipBoots Skip boots slot if true - */ - public static void registerFarmersOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext, - boolean skipHead, boolean skipBody, boolean skipLegs, boolean skipBoots) { - if (!skipHead) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.TITHE_REWARD_HAT_MALE, ItemID.TITHE_REWARD_HAT_FEMALE), // Farmer's strawhats - EquipmentInventorySlot.HEAD, - priority, rating, "Farmer's strawhat - cosmetic farming item", - taskContext - )); - } - if (!skipBody) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.TITHE_REWARD_TORSO_MALE,ItemID.TITHE_REWARD_TORSO_FEMALE), // Farmer's jacket - EquipmentInventorySlot.BODY, - priority, rating, "Farmer's jacket - cosmetic farming item", - taskContext - )); - } - if (!skipLegs) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.TITHE_REWARD_LEGS_MALE, ItemID.TITHE_REWARD_LEGS_FEMALE), // Farmer's boro trousers - EquipmentInventorySlot.LEGS, - priority, rating, "Farmer's boro trousers - cosmetic farming item", - taskContext - )); - } - if (!skipBoots) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.TITHE_REWARD_FEET_MALE, ItemID.TITHE_REWARD_FEET_FEMALE), // Farmer's boots - EquipmentInventorySlot.BOOTS, - priority, rating, "Farmer's boots - cosmetic farming item", - taskContext - )); - } - } - - /** - * Registers the complete Carpenter's Outfit for the plugin scheduler. - * This is the specialized outfit for construction activities from Mahogany Homes. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the carpenter's outfit - * @param rating The rating for the carpenter's outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerCarpentersOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext) { - registerCarpentersOutfit(requirements, priority, rating, taskContext, false, false, false, false); - } - - /** - * Registers the complete Carpenter's Outfit for the plugin scheduler with convenience flags. - * This is the specialized outfit for construction activities from Mahogany Homes. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the carpenter's outfit - * @param rating The rating for the carpenter's outfit - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param skipHead Skip head slot if true - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - * @param skipBoots Skip boots slot if true - */ - public static void registerCarpentersOutfit(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext, - boolean skipHead, boolean skipBody, boolean skipLegs, boolean skipBoots) { - if (!skipHead) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(24872), // Carpenter's helmet - EquipmentInventorySlot.HEAD, - priority, rating, "Carpenter's helmet - provides construction XP bonus", - taskContext - )); - } - if (!skipBody) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(24874), // Carpenter's shirt - EquipmentInventorySlot.BODY, - priority, rating, "Carpenter's shirt - provides construction XP bonus", - taskContext - )); - } - if (!skipLegs) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(24876), // Carpenter's trousers - EquipmentInventorySlot.LEGS, - priority, rating, "Carpenter's trousers - provides construction XP bonus", - taskContext - )); - } - if (!skipBoots) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(24878), // Carpenter's boots - EquipmentInventorySlot.BOOTS, - priority, rating, "Carpenter's boots - provides construction XP bonus", - taskContext - )); - } - } - - /** - * Registers the complete Zealot's Robes for the plugin scheduler. - * This is the specialized outfit for prayer activities from Shade Catacombs. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the zealot's robes - * @param rating The rating for the zealot's robes - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerZealotsRobes(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext) { - registerZealotsRobes(requirements, priority, rating, taskContext, false, false, false, false); - } - - /** - * Registers the complete Zealot's Robes for the plugin scheduler with convenience flags. - * This is the specialized outfit for prayer activities from Shade Catacombs. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the zealot's robes - * @param rating The rating for the zealot's robes - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param skipHead Skip head slot if true - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - * @param skipBoots Skip boots slot if true - */ - public static void registerZealotsRobes(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext, - boolean skipHead, boolean skipBody, boolean skipLegs, boolean skipBoots) { - if (!skipHead) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(25438), // Zealot's helm - EquipmentInventorySlot.HEAD, - priority, rating, "Zealot's helm - chance to save bones/ensouled heads", - taskContext - )); - } - if (!skipBody) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(25434), // Zealot's robe top - EquipmentInventorySlot.BODY, - priority, rating, "Zealot's robe top - chance to save bones/ensouled heads", - taskContext - )); - } - if (!skipLegs) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(25436), // Zealot's robe bottom - EquipmentInventorySlot.LEGS, - priority, rating, "Zealot's robe bottom - chance to save bones/ensouled heads", - taskContext - )); - } - if (!skipBoots) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(25440), // Zealot's boots - EquipmentInventorySlot.BOOTS, - priority, rating, "Zealot's boots - chance to save bones/ensouled heads", - taskContext - )); - } - } - - /** - * Registers the complete Rogue Equipment for the plugin scheduler. - * This is the specialized outfit for thieving activities from Rogues' Den. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the rogue equipment - * @param rating The rating for the rogue equipment - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerRogueEquipment(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext) { - registerRogueEquipment(requirements, priority, rating, taskContext, false, false, false, false, false); - } - - /** - * Registers the complete Rogue Equipment for the plugin scheduler with convenience flags. - * This is the specialized outfit for thieving activities from Rogues' Den. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the rogue equipment - * @param rating The rating for the rogue equipment - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param skipHead Skip head slot if true - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - * @param skipGloves Skip gloves slot if true - * @param skipBoots Skip boots slot if true - */ - public static void registerRogueEquipment(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext, - boolean skipHead, boolean skipBody, boolean skipLegs, boolean skipGloves, boolean skipBoots) { - if (!skipHead) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(5554), // Rogue mask - EquipmentInventorySlot.HEAD, - priority, rating, "Rogue mask - chance for double loot when pickpocketing", - taskContext - )); - } - if (!skipBody) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.ROGUESDEN_BODY), // Rogue top - EquipmentInventorySlot.BODY, - priority, rating, "Rogue top - chance for double loot when pickpocketing", - taskContext - )); - } - if (!skipLegs) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.ROGUESDEN_LEGS), // Rogue trousers - EquipmentInventorySlot.LEGS, - priority, rating, "Rogue trousers - chance for double loot when pickpocketing", - taskContext - )); - } - if (!skipGloves) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.ROGUESDEN_GLOVES), // Rogue gloves - EquipmentInventorySlot.GLOVES, - priority, rating, "Rogue gloves - chance for double loot when pickpocketing", - taskContext - )); - } - if (!skipBoots) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.ROGUESDEN_BOOTS), // Rogue boots - EquipmentInventorySlot.BOOTS, - priority, rating, "Rogue boots - chance for double loot when pickpocketing", - taskContext - )); - } - } - - /** - * Registers the complete Smith's Uniform for the plugin scheduler. - * This is the specialized outfit for smithing activities from Giants' Foundry. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the smith's uniform - * @param rating The rating for the smith's uniform - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - */ - public static void registerSmithsUniform(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext) { - registerSmithsUniform(requirements, priority, rating, taskContext, false, false, false, false); - } - - /** - * Registers the complete Smith's Uniform for the plugin scheduler with convenience flags. - * This is the specialized outfit for smithing activities from Giants' Foundry. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the smith's uniform - * @param rating The rating for the smith's uniform - * @param TaskContext The schedule context for these requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param skipBody Skip body slot if true - * @param skipLegs Skip legs slot if true - * @param skipGloves Skip gloves slot if true - * @param skipBoots Skip boots slot if true - */ - public static void registerSmithsUniform(PrePostScheduleRequirements requirements, RequirementPriority priority, int rating, TaskContext taskContext, - boolean skipBody, boolean skipLegs, boolean skipGloves, boolean skipBoots) { - if (!skipBody) { - requirements.register(new ItemRequirement( - ItemID.SMITHING_UNIFORM_TORSO, 1, - EquipmentInventorySlot.BODY, - priority, rating, "Smith's uniform torso - speeds up smithing actions", - taskContext - )); - } - if (!skipLegs) { - requirements.register(new ItemRequirement( - ItemID.SMITHING_UNIFORM_LEGS, 1, - EquipmentInventorySlot.LEGS, - priority, rating, "Smith's uniform legs - speeds up smithing actions", - taskContext - )); - } - if (!skipGloves) { - requirements.register(ItemRequirement.createOrRequirement( - Arrays.asList(ItemID.SMITHING_UNIFORM_GLOVES, ItemID.SMITHING_UNIFORM_GLOVES_ICE), - EquipmentInventorySlot.GLOVES, - priority, rating, "Smith's uniform gloves - speeds up smithing actions", - taskContext - )); - } - if (!skipBoots) { - requirements.register(new ItemRequirement( - ItemID.SMITHING_UNIFORM_BOOTS, 1, - EquipmentInventorySlot.BOOTS, - priority, rating, "Smith's uniform boots - speeds up smithing actions", - taskContext - )); - } - } - /** - * Registers mid-tier healing food items using logical OR requirement. - * This demonstrates the new logical requirement system where only one food type is needed. - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for food items - * @param TaskContext The schedule context for these requirements - * @param quantity The quantity of food to require - */ - public static void registerMidTierFoodLogical(PrePostScheduleRequirements requirements, RequirementPriority priority, - TaskContext taskContext, int quantity) { - // Create individual food item requirements - ItemRequirement monkfish = new ItemRequirement( - Rs2Food.MONKFISH.getId(), quantity, - null, -1, priority, 8, - "Monkfish - heals " + Rs2Food.MONKFISH.getHeal() + " HP (good mid-tier food)", taskContext - ); - - ItemRequirement swordfish = new ItemRequirement( - Rs2Food.SWORDFISH.getId(), quantity, - null, -1, priority, 6, - "Swordfish - heals " + Rs2Food.SWORDFISH.getHeal() + " HP (decent mid-tier food)", taskContext - ); - - ItemRequirement bass = new ItemRequirement( - Rs2Food.BASS.getId(), quantity, - null, -1, priority, 5, - "Bass - heals " + Rs2Food.BASS.getHeal() + " HP (alternative mid-tier food)", taskContext - ); - - ItemRequirement lobster = new ItemRequirement( - Rs2Food.LOBSTER.getId(), quantity, - null, -1, priority, 4, - "Lobster - heals " + Rs2Food.LOBSTER.getHeal() + " HP (common mid-tier food)", taskContext - ); - - ItemRequirement tuna =new ItemRequirement( - Rs2Food.TUNA.getId(), quantity, - null, -1, priority, 3, - "Tuna - heals " + Rs2Food.TUNA.getHeal() + " HP (affordable mid-tier food)", taskContext - ); - - // Create an OR requirement combining all food options - // Only one of these food types needs to be available - OrRequirement midTierFoodOptions = new OrRequirement( - priority, 8, "Mid-tier food options", taskContext, - monkfish, swordfish, bass, lobster, tuna - ); - - // Register the logical OR requirement - requirements.register(midTierFoodOptions); - } - - /** - * Demonstrates a more complex logical requirement with both AND and OR logic. - * This shows how you might require a complete combat setup where: - * - You need BOTH a weapon AND armor (AND requirement) - * - For each category, any suitable option will do (OR requirements) - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param priority The priority level for the combat setup - * @param TaskContext The schedule context for these requirements - */ - public static void registerCombatSetupLogical(PrePostScheduleRequirements requirements, RequirementPriority priority, - TaskContext taskContext) { - // TODO: Implement combat setup requirements using ConditionalRequirement - // Example: Create weapon and armor requirements with OR logic for each category - // and AND logic between categories - - } - public static void registerStaminaPotions(PrePostScheduleRequirements requirements,int amount, RequirementPriority priority,int rating, - TaskContext taskContext,boolean preferLowerCharges) { - requirements.register(OrRequirement.fromItemIds( - Arrays.asList(ItemID._1DOSESTAMINA, - ItemID._2DOSESTAMINA, - ItemID._3DOSESTAMINA, - ItemID._4DOSESTAMINA),amount, - null, -1, - priority, rating, "Stamina potions for energy restoration", - taskContext, preferLowerCharges - )); - } - public static void registerRingOfDueling(PrePostScheduleRequirements requirements, RequirementPriority priority,int rating, - TaskContext taskContext,boolean preferLowerCharges) { - requirements.register(OrRequirement.fromItemIds( - Arrays.asList(ItemID.RING_OF_DUELING_8, ItemID.RING_OF_DUELING_7, ItemID.RING_OF_DUELING_6, - ItemID.RING_OF_DUELING_5, ItemID.RING_OF_DUELING_4, ItemID.RING_OF_DUELING_3, - ItemID.RING_OF_DUELING_2, ItemID.RING_OF_DUELING_1),1, - EquipmentInventorySlot.RING, -1, - priority, - rating, "Ring of dueling for teleports",taskContext,preferLowerCharges - - )); - - } - public static void registerAmuletOfGlory(PrePostScheduleRequirements requirements, RequirementPriority priority,int rating, - TaskContext taskContext,boolean preferLowerCharges) { - - // AMULET - Example with charged items - requirements.register( OrRequirement.fromItemIds( - Arrays.asList(ItemID.AMULET_OF_GLORY_6, - ItemID.AMULET_OF_GLORY_5, - ItemID.AMULET_OF_GLORY_4, - ItemID.AMULET_OF_GLORY_3, - ItemID.AMULET_OF_GLORY_2, - ItemID.AMULET_OF_GLORY_1),1, - EquipmentInventorySlot.AMULET, -1, - priority, rating, - "Amulet of glory for teleports", - taskContext, preferLowerCharges - )); - } - /** - * Registers a smart mining equipment conditional requirement that upgrades equipment based on player capabilities. - * This demonstrates the power of conditional requirements over simple AND/OR logic. - * - * Workflow: - * 1. Ensure basic pickaxe if none available - * 2. If player has sufficient GP and mining level, upgrade to better pickaxe - * 3. If player has high mining level, consider dragon pickaxe - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param TaskContext The schedule context for these requirements - */ - public static void registerSmartMiningEquipment(PrePostScheduleRequirements requirements, TaskContext taskContext) { - // Create a conditional requirement for smart mining equipment management - ConditionalRequirement smartMiningEquipment = ConditionalRequirementBuilder.createEquipmentUpgrader( - new int[]{ItemID.BRONZE_PICKAXE, ItemID.IRON_PICKAXE}, // Basic equipment - new int[]{ItemID.RUNE_PICKAXE, ItemID.DRAGON_PICKAXE}, // Upgrade equipment - 100000, // Min GP for upgrade - EquipmentInventorySlot.WEAPON, - "mining pickaxe", - RequirementPriority.RECOMMENDED, - taskContext - ); - - requirements.register(smartMiningEquipment); - } - - /** - * Registers a complete preparation workflow for wilderness activities using OrderedRequirement. - - * This shows how ordered requirements can manage complex preparation sequences. - * - * Order: - * 1. Bank valuable items - * 2. Withdraw wilderness supplies - * 3. Equip appropriate gear - * 4. Set up inventory - * 5. Move to wilderness entry point - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param TaskContext The schedule context for these requirements - */ - public static void registerWildernessPreparation(PrePostScheduleRequirements requirements, TaskContext taskContext) { - OrderedRequirement wildernessPrep = new OrderedRequirement( - RequirementPriority.MANDATORY, 9, "Complete Wilderness Preparation", taskContext - ); - /* - // Step 1: Bank valuable items first - wildernessPrep.addStep( - new ItemRequirement(RequirementType.INVENTORY, Priority.MANDATORY, 8, - "Bank valuable items before wilderness", Arrays.asList(), taskContext), - "Bank valuable items" - ); - - // Step 2: Withdraw wilderness food - wildernessPrep.addStep( - new ItemRequirement(RequirementType.INVENTORY, Priority.MANDATORY, 9, - "Food for wilderness survival", - Arrays.asList(ItemID.SHARK, ItemID.KARAMBWAN, ItemID.MANTA_RAY), taskContext), - "Withdraw food for wilderness" - ); - - // Step 3: Equip budget gear (optional step - can proceed without) - wildernessPrep.addOptionalStep( - new ItemRequirement(RequirementType.EQUIPMENT, Priority.OPTIONAL, 6, - "Budget wilderness combat gear", - Arrays.asList(ItemID.RUNE_SCIMITAR, ItemID.RUNE_FULL_HELM), taskContext), - "Equip budget wilderness gear" - ); */ - - // Step 4: Final location check - wildernessPrep.addStep( - new LocationRequirement(BankLocation.EDGEVILLE, true, -1, taskContext, RequirementPriority.MANDATORY), - "Move to wilderness entry point" - ); - - requirements.register(wildernessPrep); - } - - /** - * Registers a level-based spellbook progression using ConditionalRequirement. - * This demonstrates conditional logic based on player skill levels. - * - * Logic: - * - If Magic < 50: Stay on standard spellbook - * - If Magic 50-64: Consider Ancient spellbook for combat - * - If Magic 65+: Consider Lunar spellbook for utility - * - * @param requirements The PrePostScheduleRequirements instance to register the requirements with - * @param TaskContext The schedule context for these requirements - */ - public static void registerSmartSpellbookProgression(PrePostScheduleRequirements requirements, TaskContext taskContext) { - // Ancient spellbook conditional (for combat) - ConditionalRequirement ancientSpellbook = ConditionalRequirementBuilder.createLevelBasedRequirement( - Skill.MAGIC, 50, - new SpellbookRequirement(Rs2Spellbook.ANCIENT, taskContext, RequirementPriority.RECOMMENDED, 7, - "Ancient spellbook for combat spells"), - "Ancient spellbook for combat (Magic 50+)", - RequirementPriority.RECOMMENDED, - taskContext - ); - - // Lunar spellbook conditional (for utility) - ConditionalRequirement lunarSpellbook = ConditionalRequirementBuilder.createLevelBasedRequirement( - Skill.MAGIC, 65, - new SpellbookRequirement(Rs2Spellbook.LUNAR, taskContext, RequirementPriority.RECOMMENDED, 8, - "Lunar spellbook for utility spells"), - "Lunar spellbook for utility (Magic 65+)", - RequirementPriority.RECOMMENDED, - taskContext - ); - - requirements.register(ancientSpellbook); - requirements.register(lunarSpellbook); - } - - - - /** - * Creates a rune pouch requirement for teleportation magic. - * Requires various teleport runes. - */ - public static RunePouchRequirement createTeleportRunePouch() { - Map requiredRunes = new HashMap<>(); - requiredRunes.put(Runes.LAW, 50); // Law runes for teleports - requiredRunes.put(Runes.WATER, 50); // Water runes for various teleports - requiredRunes.put(Runes.AIR, 50); // Air runes for various teleports - requiredRunes.put(Runes.EARTH, 50); // Earth runes for various teleports - - return new RunePouchRequirement( - requiredRunes, - false, // Strict matching - no combination runes - RequirementPriority.MANDATORY, - 9, // Very high rating for essential teleportation - "Rune pouch for essential teleportation magic", - TaskContext.PRE_SCHEDULE - ); - } - - /** - * Creates a rune pouch requirement for alchemy training. - * Requires Nature runes and Fire runes for High Level Alchemy. - */ - public static void registerAlchemyRunePouch(int runeAmount,PrePostScheduleRequirements requirements, RequirementPriority priority,int rating, - TaskContext taskContext) { - Map requiredRunes = new HashMap<>(); - requiredRunes.put(Runes.NATURE, runeAmount); // 1000 nature runes for alchemy - requiredRunes.put(Runes.FIRE, runeAmount*5); // 5000 fire runes for alchemy - - RunePouchRequirement runePouchRequirement = new RunePouchRequirement( - requiredRunes, - false, // Dont Allow lava runes to substitute for fire runes - priority, - rating, - "Rune pouch for high level alchemy training", - taskContext - ); - requirements.register(runePouchRequirement); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/OrRequirementMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/OrRequirementMode.java deleted file mode 100644 index 8d89c0342cf..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/OrRequirementMode.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums; - -/** - * Defines how OR requirements should be planned and fulfilled. - */ -public enum OrRequirementMode { - /** - * ANY_COMBINATION mode: The total amount can be fulfilled by any combination of items in the OR requirement. - * For example, if 5 food items are needed, we could have 2 lobsters + 3 swordfish. - * This is the default mode and matches the current behavior. - */ - ANY_COMBINATION, - - /** - * SINGLE_TYPE mode: Must fulfill the entire amount with exactly one type of item from the OR requirement. - * For example, if 5 food items are needed, we must have exactly 5 lobsters OR 5 swordfish OR 5 monkfish, - * but not a combination. - */ - SINGLE_TYPE -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementMode.java deleted file mode 100644 index 1490aca1159..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementMode.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums; - -/** - * Defines the source mode for requirement fulfillment. - * This determines which cache (standard or external) to use for getting requirements. - */ -public enum RequirementMode { - /** - * Use only standard requirements from the main cache. - * This is the default for normal pre/post schedule requirement fulfillment. - */ - STANDARD, - - /** - * Use only external requirements from the external cache. - * This is used for externally added requirements that should not mix with standard ones. - */ - EXTERNAL, - - /** - * Use both standard and external requirements combined. - * This is rarely used but available for special cases where both sources are needed. - */ - BOTH -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementPriority.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementPriority.java deleted file mode 100644 index 4419b7c2e4d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementPriority.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums; - -/** - * Represents the priority level of a requirement. - * Used to determine how essential a requirement is for optimal plugin performance. - */ -public enum RequirementPriority { - /** - * Essential requirements that are absolutely required for the plugin to function. - * Plugin should not start or should warn user if these requirements are unavailable. - */ - MANDATORY, - - /** - * Important requirements that significantly improve plugin performance or efficiency. - * Plugin can function without these but with reduced effectiveness. - */ - RECOMMENDED -} - - diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementType.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementType.java deleted file mode 100644 index 1e221b39f62..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/RequirementType.java +++ /dev/null @@ -1,64 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums; - -/** - * Defines where a requirement needs to be located or what state it needs to be in - * for optimal gameplay. - */ -public enum RequirementType { - /** - * Requirement must be equipped in an equipment slot - */ - EQUIPMENT, - - /** - * Requirement must be in inventory but not necessarily equipped - */ - INVENTORY, - - /** - * Requirement can be either equipped or in inventory - */ - EITHER, - - /** - * Requirement is related to player state (skills, quests, etc.) - */ - PLAYER_STATE, - - /** - * Requirement is related to game configuration (spellbook, etc.) - */ - GAME_CONFIG, - - /** - * Requirement is related to player location (must be at specific world point) - */ - LOCATION, - - /** - * Logical OR requirement - at least one child requirement must be fulfilled - */ - OR_LOGICAL, - - /** - * Conditional requirement - executes requirements in sequence based on conditions - * This provides much more powerful workflow control than simple AND/OR logic - */ - CONDITIONAL, - - /** - * Shop requirement - buying or selling items from shops - */ - SHOP, - - /** - * Loot requirement - looting specific items from the ground or activities - */ - LOOT, - - /** - * Custom requirement - externally added requirements that should be fulfilled last - * These are added by plugins through the custom requirement registration system - */ - CUSTOM -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/TaskContext.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/TaskContext.java deleted file mode 100644 index 52f0c901825..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/enums/TaskContext.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums; - -public enum TaskContext { - PRE_SCHEDULE, // Before script execution (start location) -> can also be meant to be used as a pre task in script - POST_SCHEDULE, // After script completion (end location) -> can also be ment to be use a post task in script - BOTH // Both before and after (if same location needed for both pre post schedule) -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/registry/RequirementRegistry.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/registry/RequirementRegistry.java deleted file mode 100644 index 03d0a5fa46a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/registry/RequirementRegistry.java +++ /dev/null @@ -1,2833 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.registry; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location.LocationRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.collection.LootRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.ConditionalRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.ShopRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.SpellbookRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.InventorySetupRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.RunePouchRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.LogicalRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.OrRequirement; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -import org.benf.cfr.reader.util.output.BytecodeDumpConsumer.Item; - -/** - * Enhanced requirement registry that manages all types of requirements with automatic - * uniqueness enforcement, consistency guarantees, and efficient lookup. - * - * This class solves the issues with the previous approach: - * - Eliminates duplication between specific collections and central registry - * - Enforces uniqueness automatically - * - Provides type-safe access while maintaining consistency - * - Simplifies requirement management - */ -@Slf4j -public class RequirementRegistry { - - /** - * Key class for ensuring requirement uniqueness. - * Two requirements are considered the same if they have the same type, - * schedule context, and core identity (defined by the requirement itself). - */ - public static class RequirementKey { - private final Class type; - private final TaskContext taskContext; - private final String identity; // Unique identifier from the requirement - - public RequirementKey(Requirement requirement) { - this.type = requirement.getClass(); - this.taskContext = requirement.hasTaskContext() ? requirement.getTaskContext() : null; - this.identity = requirement.getUniqueIdentifier(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - RequirementKey that = (RequirementKey) o; - return Objects.equals(type, that.type) && - Objects.equals(taskContext, that.taskContext) && - Objects.equals(identity, that.identity); - } - - @Override - public int hashCode() { - return Objects.hash(type, taskContext, identity); - } - - @Override - public String toString() { - return String.format("%s[%s:%s]", type.getSimpleName(), taskContext, identity); - } - } - - // Central storage - this is the single source of truth for standard requirements - private final Map requirements = new ConcurrentHashMap<>(); - - // Separate storage for externally added requirements to prevent mixing - private final Map externalRequirements = new ConcurrentHashMap<>(); - - // Standard requirements cached views for efficient access (rebuilt when requirements change) - private volatile Map>> equipmentItemsCache = new HashMap<>(); - private volatile Map>> inventorySlotRequirementsCache = new HashMap<>(); - private volatile Map> anyInventorySlotRequirementsCache = new HashMap<>(); - private volatile Map> shopRequirementsCache = new HashMap<>(); - private volatile Map> lootRequirementsCache = new HashMap<>(); - private volatile Map> conditionalItemRequirementsCache = new HashMap<>(); - - // External requirements cached views for efficient access (rebuilt when external requirements change) - private volatile Map>> externalEquipmentItemsCache = new HashMap<>(); - private volatile Map>> externalInventorySlotRequirementsCache = new HashMap<>(); - private volatile Map> externalAnyInventorySlotRequirementsCache = new HashMap<>(); - private volatile Map> externalShopRequirementsCache = new HashMap<>(); - private volatile Map> externalLootRequirementsCache = new HashMap<>(); - private volatile Map> externalIConditionalItemRequirementsCache = new HashMap<>(); - - // Single-instance requirements (enforced by registry) - @Getter - private volatile SpellbookRequirement preScheduleSpellbookRequirement = null; - @Getter - private volatile SpellbookRequirement postScheduleSpellbookRequirement = null; - @Getter - private volatile LocationRequirement preScheduleLocationRequirement = null; - @Getter - private volatile LocationRequirement postScheduleLocationRequirement = null; - @Getter - private volatile InventorySetupRequirement preScheduleInventorySetupRequirement = null; - @Getter - private volatile InventorySetupRequirement postScheduleInventorySetupRequirement = null; - - private volatile boolean cacheValid = false; - private volatile boolean externalCacheValid = false; - - /** - * Registers a requirement in the registry. - * Automatically handles uniqueness, categorization, and cache invalidation. - * Includes special validation for dummy items to ensure proper slot assignment. - * - * @param requirement The requirement to register - * @return true if the requirement was added (new), false if it replaced an existing one - */ - public boolean register(Requirement requirement) { - if (requirement == null) { - log.warn("Attempted to register null requirement"); - return false; - } - - // Special validation for dummy items - if (requirement instanceof ItemRequirement) { - ItemRequirement itemReq = (ItemRequirement) requirement; - if (itemReq.isDummyItemRequirement()) { - // Validate dummy item configuration - if (itemReq.getEquipmentSlot() == null && itemReq.getInventorySlot() == null) { - log.error("Dummy item requirement must specify either equipment slot or inventory slot"); - return false; - } - if (itemReq.getEquipmentSlot() != null && itemReq.getInventorySlot() != null && - itemReq.getInventorySlot() >= 0) { - log.error("Dummy item requirement cannot specify both equipment slot and specific inventory slot"); - return false; - } - log.debug("Registering dummy item requirement for slot: {} (equipment: {}, inventory: {})", - itemReq.getDescription(), itemReq.getEquipmentSlot(), itemReq.getInventorySlot()); - } - } - - RequirementKey key = new RequirementKey(requirement); - - // Special handling for single-instance requirements - if (requirement instanceof SpellbookRequirement) { - return registerSpellbookRequirement((SpellbookRequirement) requirement, key); - } else if (requirement instanceof LocationRequirement) { - return registerLocationRequirement((LocationRequirement) requirement, key); - } else if (requirement instanceof InventorySetupRequirement) { - return registerInventorySetupRequirement((InventorySetupRequirement) requirement, key); - } else if (requirement instanceof RunePouchRequirement) { - return registerRunePouchRequirement((RunePouchRequirement) requirement, key); - } - - // For multi-instance requirements, just add to central storage - Requirement previous = requirements.put(key, requirement); - invalidateCache(); - - if (previous != null) { - log.debug("Replaced existing requirement: {} -> {}", previous, requirement); - return false; - } else { - log.debug("Added new requirement: {}", requirement); - return true; - } - } - - /** - * Registers an externally added requirement in the registry. - * These requirements are tracked separately and fulfilled after all standard requirements. - * - * @param requirement The externally added requirement to register - * @return true if the requirement was added (new), false if it replaced an existing one - */ - public boolean registerExternal(Requirement requirement) { - if (requirement == null) { - log.warn("Attempted to register null external requirement"); - return false; - } - - RequirementKey key = new RequirementKey(requirement); - - // Store directly in external requirements map - Requirement previous = externalRequirements.put(key, requirement); - invalidateExternalCache(); - - if (previous != null) { - log.debug("Replaced external requirement: {} -> {}", previous.getDescription(), requirement.getDescription()); - return false; - } else { - log.debug("Registered new external requirement: {}", requirement.getDescription()); - return true; - } - } - - /** - * Gets all externally added requirements for a specific schedule context. - * These requirements should be fulfilled after all standard requirements. - * - * @param context The schedule context to filter by - * @return List of externally added requirements for the given context - */ - public List getExternalRequirements(TaskContext context) { - return externalRequirements.values().stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(Collectors.toList()); - } - - private boolean registerSpellbookRequirement(SpellbookRequirement requirement, RequirementKey key) { - boolean isPreSchedule = requirement.isPreSchedule(); - boolean isPostSchedule = requirement.isPostSchedule(); - - if (isPreSchedule && preScheduleSpellbookRequirement != null) { - log.warn("Replacing existing pre-schedule spellbook requirement: {} -> {}", - preScheduleSpellbookRequirement, requirement); - RequirementKey preScheduleKey = new RequirementKey(preScheduleSpellbookRequirement); - requirements.remove(preScheduleKey); // Remove old requirement to avoid duplicates - } - if (isPostSchedule && postScheduleSpellbookRequirement != null) { - log.warn("Replacing existing post-schedule spellbook requirement: {} -> {}", - postScheduleSpellbookRequirement, requirement); - RequirementKey postScheduleKey = new RequirementKey(postScheduleSpellbookRequirement); - requirements.remove(postScheduleKey); // Remove old requirement to avoid duplicates - } - - Requirement previous = requirements.put(key, requirement); - - if (isPreSchedule) { - preScheduleSpellbookRequirement = requirement; - } - if (isPostSchedule) { - postScheduleSpellbookRequirement = requirement; - } - - invalidateCache(); - return previous == null; - } - - private boolean registerLocationRequirement(LocationRequirement requirement, RequirementKey key) { - boolean isPreSchedule = requirement.isPreSchedule(); - boolean isPostSchedule = requirement.isPostSchedule(); - - if (isPreSchedule && preScheduleLocationRequirement != null) { - log.warn("Replacing existing pre-schedule location requirement: {} -> {}", - preScheduleLocationRequirement, requirement); - RequirementKey preRequirementKey = new RequirementKey(preScheduleLocationRequirement); - requirements.remove(preRequirementKey); // Remove old requirement to avoid duplicates - } - if (isPostSchedule && postScheduleLocationRequirement != null) { - log.warn("Replacing existing post-schedule location requirement: {} -> {}", - postScheduleLocationRequirement, requirement); - RequirementKey postRequirementKey = new RequirementKey(postScheduleLocationRequirement); - requirements.remove(postRequirementKey); // Remove old requirement to avoid duplicates - } - - Requirement previous = requirements.put(key, requirement); - - if (isPreSchedule) { - preScheduleLocationRequirement = requirement; - } - if (isPostSchedule) { - postScheduleLocationRequirement = requirement; - } - - invalidateCache(); - return previous == null; - } - - private boolean registerInventorySetupRequirement(InventorySetupRequirement requirement, RequirementKey key) { - boolean isPreSchedule = requirement.isPreSchedule(); - boolean isPostSchedule = requirement.isPostSchedule(); - - if (isPreSchedule && preScheduleInventorySetupRequirement != null) { - log.warn("Replacing existing pre-schedule inventory setup requirement: {} -> {}", - preScheduleInventorySetupRequirement, requirement); - RequirementKey preRequirementKey = new RequirementKey(preScheduleInventorySetupRequirement); - requirements.remove(preRequirementKey); // Remove old requirement to avoid duplicates - } - if (isPostSchedule && postScheduleInventorySetupRequirement != null) { - log.warn("Replacing existing post-schedule inventory setup requirement: {} -> {}", - postScheduleInventorySetupRequirement, requirement); - RequirementKey postRequirementKey = new RequirementKey(postScheduleInventorySetupRequirement); - requirements.remove(postRequirementKey); // Remove old requirement to avoid duplicates - } - - Requirement previous = requirements.put(key, requirement); - - if (isPreSchedule) { - preScheduleInventorySetupRequirement = requirement; - } - if (isPostSchedule) { - postScheduleInventorySetupRequirement = requirement; - } - - invalidateCache(); - return previous == null; - } - - private boolean registerRunePouchRequirement(RunePouchRequirement requirement, RequirementKey key) { - // Check if any RunePouchRequirement already exists - RunePouchRequirement existingRunePouchRequirement = requirements.values().stream() - .filter(r -> r instanceof RunePouchRequirement) - .map(r -> (RunePouchRequirement) r) - .findFirst() - .orElse(null); - - if (existingRunePouchRequirement != null) { - log.warn("Replacing existing rune pouch requirement: {} -> {}", - existingRunePouchRequirement, requirement); - RequirementKey existingKey = new RequirementKey(existingRunePouchRequirement); - requirements.remove(existingKey); // Remove old requirement to avoid duplicates - } - - Requirement previous = requirements.put(key, requirement); - invalidateCache(); - - log.debug("Registered rune pouch requirement: {}", requirement); - return previous == null; - } - - /** - * Removes a requirement from the registry. - * - * @param requirement The requirement to remove - * @return true if the requirement was removed, false if it wasn't found - */ - public boolean unregister(Requirement requirement) { - if (requirement == null) { - return false; - } - - RequirementKey key = new RequirementKey(requirement); - Requirement removed = requirements.remove(key); - - if (removed != null) { - // Clear single-instance references if applicable - if (removed == preScheduleSpellbookRequirement) { - preScheduleSpellbookRequirement = null; - } - if (removed == postScheduleSpellbookRequirement) { - postScheduleSpellbookRequirement = null; - } - if (removed == preScheduleLocationRequirement) { - preScheduleLocationRequirement = null; - } - if (removed == postScheduleLocationRequirement) { - postScheduleLocationRequirement = null; - } - if (removed == preScheduleInventorySetupRequirement) { - preScheduleInventorySetupRequirement = null; - } - if (removed == postScheduleInventorySetupRequirement) { - postScheduleInventorySetupRequirement = null; - } - - invalidateCache(); - log.debug("Removed requirement: {}", removed); - return true; - } - - return false; - } - - /** - * Gets all requirements of a specific type. - */ - @SuppressWarnings("unchecked") - public List getRequirements(Class clazz) { - return requirements.values().stream() - .filter(clazz::isInstance) - .map(req -> (T) req) - .collect(Collectors.toList()); - } - - /** - * Gets all requirements for a specific schedule context. - */ - public List getRequirements(TaskContext context) { - return requirements.values().stream() - .filter(req -> req.hasTaskContext() && - (req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH)) - .collect(Collectors.toList()); - } - - - /** - * Gets all standard (non-external) requirements of a specific type for a specific schedule context. - * This excludes externally added requirements to prevent double processing. - */ - @SuppressWarnings("unchecked") - public List getRequirements(Class clazz, TaskContext context) { - return requirements.values().stream() - .filter(clazz::isInstance) - .filter(req -> req.hasTaskContext() && - (req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH)) - .map(req -> (T) req) - .collect(Collectors.toList()); - } - - /** - * Gets all requirements in the registry. - */ - public LinkedHashSet getAllRequirements() { - return new LinkedHashSet<>(requirements.values()); - } - - /** - * Checks if a requirement exists in the registry. - */ - public boolean contains(Requirement requirement) { - if (requirement == null) { - return false; - } - return requirements.containsKey(new RequirementKey(requirement)); - } - - /** - * Gets the total number of requirements. - */ - public int size() { - return requirements.size(); - } - - /** - * Clears all requirements from the registry. - */ - public void clear() { - requirements.clear(); - preScheduleSpellbookRequirement = null; - postScheduleSpellbookRequirement = null; - preScheduleLocationRequirement = null; - postScheduleLocationRequirement = null; - preScheduleInventorySetupRequirement = null; - postScheduleInventorySetupRequirement = null; - invalidateCache(); - } - - /** - * Helper class to hold a complete set of caches returned by the unified rebuild method. - */ - private static class CacheSet { - final Map>> equipmentCache; - final Map>> inventorySlotCache; - final Map> anyInventorySlotCache; - final Map> shopCache; - final Map> lootCache; - final Map> conditionalCache; - - CacheSet(Map>> equipmentCache, - Map>> inventorySlotCache, - Map> anyInventorySlotCache, - Map> shopCache, - Map> lootCache, - Map> conditionalCache) { - this.equipmentCache = equipmentCache; - this.inventorySlotCache = inventorySlotCache; - this.anyInventorySlotCache = anyInventorySlotCache; - this.shopCache = shopCache; - this.lootCache = lootCache; - this.conditionalCache = conditionalCache; - } - } - - /** - * Unified cache rebuilding logic used by both standard and external cache rebuild methods. - * This method groups compatible requirements into logical requirements for better organization. - * - * @param requirementCollection The collection of requirements to process - * @param cacheType The type of cache being rebuilt ("standard" or "external") for logging - * @return A CacheSet containing all the rebuilt caches - */ - /** - * Unified cache rebuilding logic used by both standard and external cache rebuild methods. - * This method groups compatible requirements into logical requirements for better organization. - * - * @param requirementCollection The collection of requirements to process - * @param cacheType The type of cache being rebuilt ("standard" or "external") for logging - * @return A CacheSet containing all the rebuilt caches - */ - private CacheSet rebuildCacheUnified(Collection requirementCollection, String cacheType) { - // New caches with logical requirements - - - Map> newConditionalCache = new HashMap<>(); - - // Group requirements by schedule context FIRST, then by type and slot - Map>> newEquipmentCache = new HashMap<>(); - Map>> newInventorySlotCache = new HashMap<>(); - Map> newAnyInventorySlotCacheByContext = new HashMap<>(); - Map> newShopCache = new HashMap<>(); - Map> newLootCache = new HashMap<>(); - /*for (TaskContext context : TaskContext.values()) { - newEquipmentCache.put(context, new HashMap<>()); - newInventorySlotCache.put(context, new HashMap<>()); - newAnyInventorySlotCacheByContext.put(context, new ArrayList<>()); - newAnyInventorySlotCacheByContext.get(context).add(new OrRequirement( - RequirementPriority.MANDATORY, - 0, - "default mandatory any inventory slot requirement", - context, - ItemRequirement.class - )); - newAnyInventorySlotCacheByContext.get(context).add(new OrRequirement( - RequirementPriority.RECOMMENDED, - 0, - "default recommend any inventory slot requirement", - context, - ItemRequirement.class - )); - - newShopCache.put(context, new ArrayList<>()); - newShopCache.get(context).add(new OrRequirement( - RequirementPriority.MANDATORY, - 0, - "default mandatory shop requirement", - context, - ShopRequirement.class - )); - newShopCache.get(context).add(new OrRequirement( - RequirementPriority.RECOMMENDED, - 0, - "default recommended shop requirement", - context, - ShopRequirement.class - )); - newLootCache.put(context, new ArrayList<>()); - newLootCache.get(context).add(new OrRequirement( - RequirementPriority.MANDATORY, - 0, - "default mandatory loot requirement", - context, - LootRequirement.class - )); - newLootCache.get(context).add(new OrRequirement( - RequirementPriority.RECOMMENDED, - 0, - "default recommended loot requirement", - context, - LootRequirement.class - )); - newConditionalCache.put(context, new LinkedHashSet<>()); - - }*/ - // First pass: collect and group requirements by schedule context, then by slot - for (Requirement requirement : requirementCollection) { - if (requirement instanceof LogicalRequirement) { - // Handle existing LogicalRequirement - add it directly to appropriate cache based on its child requirements - LogicalRequirement logical = (LogicalRequirement) requirement; - TaskContext context = logical.getTaskContext(); - - if (logical.containsOnlyItemRequirements()) { - // Check if this is an OR requirement for flexible inventory items - List itemReqs = logical.getAllItemRequirements(); - if (!itemReqs.isEmpty()) { - // Check if all items are flexible inventory items (slot -1) - boolean allFlexible = itemReqs.stream() - .allMatch(item -> item.getRequirementType() == RequirementType.INVENTORY && - item.allowsAnyInventorySlot()); - - if (allFlexible && logical instanceof OrRequirement) { - // This is a flexible inventory OR requirement (like food) - add it directly - newAnyInventorySlotCacheByContext.computeIfAbsent(context, k -> new LinkedHashSet<>()) - .add((OrRequirement) logical); - log.debug("Added flexible inventory OR requirement: {} with {} items", - logical.getDescription(), itemReqs.size()); - } else { - // Mixed types, specific slots, or not an OrRequirement - decompose it - log.debug("Decomposing logical requirement: {}", logical.getDescription()); - decomposeLogicalRequirement(logical, newEquipmentCache, newInventorySlotCache, - newAnyInventorySlotCacheByContext, newShopCache, newLootCache); - } - } - } else { - // Non-item logical requirements - decompose them - log.debug("Decomposing non-item logical requirement: {}", logical.getDescription()); - decomposeLogicalRequirement(logical, newEquipmentCache, newInventorySlotCache, - newAnyInventorySlotCacheByContext, newShopCache, newLootCache); - } - } else if (requirement instanceof ConditionalRequirement) { - // Handle ConditionalRequirement - only cache if it contains only ItemRequirements - ConditionalRequirement conditionalReq = (ConditionalRequirement) requirement; - if (conditionalReq.containsOnlyItemRequirements()) { - newConditionalCache.computeIfAbsent(conditionalReq.getTaskContext(), k -> new LinkedHashSet<>()) - .add(conditionalReq); - // Log the caching of ConditionalRequirement with only ItemRequirements - log.debug("Cached ConditionalRequirement with only ItemRequirements: {}", conditionalReq.getName()); - } else { - log.debug("Skipped ConditionalRequirement with mixed requirement types for now: {}", conditionalReq.getName()); - } - } else if (requirement instanceof ItemRequirement) { - ItemRequirement itemReq = (ItemRequirement) requirement; - TaskContext context = itemReq.getTaskContext(); - int slot = -2; - switch (itemReq.getRequirementType()) { - case EQUIPMENT: - slot = itemReq.getInventorySlot(); - if( slot != -2) { - throw new IllegalArgumentException("Equipment requirement must not specify specific inventory slot"); - } - if (itemReq.getEquipmentSlot() != null) { - newEquipmentCache - .computeIfAbsent(context, k -> new HashMap<>()) - .computeIfAbsent(itemReq.getEquipmentSlot(), k -> new LinkedHashSet<>()) - .add(itemReq); - } - break; - case INVENTORY: - slot = itemReq.hasSpecificInventorySlot() ? itemReq.getInventorySlot() : -1; - if (slot != -1) { - newInventorySlotCache - .computeIfAbsent(context, k -> new HashMap<>()) - .computeIfAbsent(slot, k -> new LinkedHashSet<>()) - .add(itemReq); - } else { - OrRequirement orReq = new OrRequirement(itemReq.getPriority(), itemReq.getRating(), - itemReq.getName(), context, ItemRequirement.class); - orReq.addRequirement(itemReq); - newAnyInventorySlotCacheByContext.computeIfAbsent(context, k -> new LinkedHashSet<>()) - .add(orReq); - - } - break; - case EITHER: - slot = itemReq.hasSpecificInventorySlot() ? itemReq.getInventorySlot() : -1; - EquipmentInventorySlot equipmentSlot = itemReq.getEquipmentSlot(); - if (equipmentSlot== null || slot == -2) { - throw new IllegalArgumentException("Either requirement must specify either equipment slot or specific inventory slot"); - } - if (slot != -1) { - newInventorySlotCache - .computeIfAbsent(context, k -> new HashMap<>()) - .computeIfAbsent(slot, k -> new LinkedHashSet<>()) - .add(itemReq); - } else { - LinkedHashSet contextCache = newAnyInventorySlotCacheByContext.computeIfAbsent(context, k -> new LinkedHashSet<>()); - OrRequirement orReq = new OrRequirement(itemReq.getPriority(), itemReq.getRating(), - itemReq.getName(), context, ItemRequirement.class); - orReq.addRequirement(itemReq); - contextCache.add(orReq); - } - newEquipmentCache - .computeIfAbsent(context, k -> new HashMap<>()) - .computeIfAbsent(itemReq.getEquipmentSlot(), k -> new LinkedHashSet<>()) - .add(itemReq); - break; - case PLAYER_STATE: - case LOCATION: - case GAME_CONFIG: - // These are handled elsewhere - break; - case OR_LOGICAL: - log.warn("ItemRequirement with logical type: {}", itemReq); - break; - case SHOP: - case CONDITIONAL: - case LOOT: - case CUSTOM: - // These types are not expected for ItemRequirement - log.warn("Unexpected requirement type for ItemRequirement: {}", itemReq.getRequirementType()); - break; - } - } else if (requirement instanceof ShopRequirement) { - ShopRequirement shopReq = (ShopRequirement) requirement; - RequirementPriority priority = shopReq.getPriority(); - TaskContext context = shopReq.getTaskContext(); - - if( context == null) { - log.warn("ShopRequirement without a shop context: {}", shopReq); - continue; // Skip invalid shop requirements - } - - // Group by context and priority - LinkedHashSet contextCache = newShopCache.computeIfAbsent(context, k -> new LinkedHashSet<>()); - OrRequirement orReq = new OrRequirement(priority, 0, shopReq.getName(), context, ShopRequirement.class); - orReq.addRequirement(shopReq); - contextCache.add(orReq); - - } else if (requirement instanceof LootRequirement) { - LootRequirement lootReq = (LootRequirement) requirement; - RequirementPriority priority = lootReq.getPriority(); - TaskContext context = lootReq.getTaskContext(); - - if (context == null) { - log.warn("LootRequirement without a loot context: {}", lootReq); - continue; // Skip invalid loot requirements - } - - // Group by context and priority - LinkedHashSet contextCache = newLootCache.computeIfAbsent(context, k -> new LinkedHashSet<>()); - OrRequirement orReq = new OrRequirement(priority, 0, lootReq.getName(), context, LootRequirement.class); - orReq.addRequirement(lootReq); - contextCache.add(orReq); - } - } - - // Debug equipment grouping before creating logical requirements (only for standard cache) - if ("standard".equals(cacheType)) { - log.debug("=== EQUIPMENT GROUPING DEBUG ==="); - for (Map.Entry>> contextEntry : newEquipmentCache.entrySet()) { - TaskContext context = contextEntry.getKey(); - log.debug("Schedule Context: {}", context); - for (Map.Entry> slotEntry : contextEntry.getValue().entrySet()) { - EquipmentInventorySlot slot = slotEntry.getKey(); - LinkedHashSet slotItems = slotEntry.getValue(); - log.debug(" Slot {}: {} items", slot, slotItems.size()); - for (ItemRequirement item : slotItems) { - log.debug(" - {} (ID: {}, Priority: {}, Rating: {})", - item.getName(), item.getId(), item.getPriority(), item.getRating()); - } - } - } - log.debug("=== END EQUIPMENT GROUPING DEBUG ==="); - } - - // Second pass: create logical requirements from grouped items - //createLogicalRequirementsFromGroups(equipmentByContextAndSlot, inventoryByContextAndSlot, - // eitherByContext, shopByContext, lootByContext, newEquipmentCache, - // newShopCache, newLootCache, newInventorySlotCache); - - // Sort all caches - //sortAllCaches(newEquipmentCache, newShopCache, newLootCache, newInventorySlotCache); - - return new CacheSet(newEquipmentCache, newInventorySlotCache,newAnyInventorySlotCacheByContext,newShopCache, newLootCache, newConditionalCache); - } - - /** - * Invalidates cached views, forcing them to be rebuilt on next access. - */ - private void invalidateCache() { - cacheValid = false; - } - - /** - * Invalidates external cached views, forcing them to be rebuilt on next access. - */ - private void invalidateExternalCache() { - externalCacheValid = false; - } - - /** - * Rebuilds all cached views from the central requirements storage. - * This method groups compatible requirements into logical requirements for better organization. - * - * The new approach: - * - Equipment items for the same slot become OR requirements - * - Individual requirements are wrapped in logical requirements for consistency - * - Already logical requirements are categorized appropriately - */ - private synchronized void rebuildCache() { - if (cacheValid) { - return; // Another thread already rebuilt the cache - } - if(requirements ==null || requirements.isEmpty()) { - log.debug("No requirements to rebuild cache for - initializing empty caches"); - // Initialize empty caches when no requirements exist - equipmentItemsCache = new HashMap<>(); - shopRequirementsCache = new HashMap<>(); - lootRequirementsCache = new HashMap<>(); - inventorySlotRequirementsCache = new HashMap<>(); - anyInventorySlotRequirementsCache = new HashMap<>(); - conditionalItemRequirementsCache = new HashMap<>(); - - // Initialize empty collections for each context - for (TaskContext context : TaskContext.values()) { - equipmentItemsCache.put(context, new HashMap<>()); - shopRequirementsCache.put(context, new LinkedHashSet<>()); - lootRequirementsCache.put(context, new LinkedHashSet<>()); - inventorySlotRequirementsCache.put(context, new HashMap<>()); - anyInventorySlotRequirementsCache.put(context, new LinkedHashSet<>()); - conditionalItemRequirementsCache.put(context, new LinkedHashSet<>()); - } - - cacheValid = true; - return; - } - log.debug("Rebuilding requirement caches..."); - - // Use unified rebuild logic for standard requirements - CacheSet newCaches = rebuildCacheUnified(requirements.values(), "standard"); - - // Atomically update caches - equipmentItemsCache = newCaches.equipmentCache; - shopRequirementsCache = newCaches.shopCache; - lootRequirementsCache = newCaches.lootCache; - inventorySlotRequirementsCache = newCaches.inventorySlotCache; - anyInventorySlotRequirementsCache = newCaches.anyInventorySlotCache; - conditionalItemRequirementsCache = newCaches.conditionalCache; - - cacheValid = true; - log.debug("Rebuilt requirement caches with {} total requirements", requirements.size()); - } - - /** - * Decomposes a logical requirement into its child requirements and adds them to the appropriate maps - * for priority-based grouping. This method handles LogicalRequirements that cannot be kept as-is. - */ - private void decomposeLogicalRequirement(LogicalRequirement logical, - Map>> newEquipmentCache, - Map>> newInventorySlotCache, - Map> newAnyInventorySlotCacheByContext, - Map> newShopCache, - Map> newLootCache) { - - // Decompose the logical requirement's child requirements into individual requirements - for (Requirement child : logical.getChildRequirements()) { - if (child instanceof ItemRequirement) { - ItemRequirement itemReq = (ItemRequirement) child; - TaskContext context = itemReq.getTaskContext(); - int slot = -2; - - switch (itemReq.getRequirementType()) { - case EQUIPMENT: - slot = itemReq.getInventorySlot(); - if (slot != -2) { - throw new IllegalArgumentException("Equipment requirement must not specify specific inventory slot"); - } - if (itemReq.getEquipmentSlot() != null) { - newEquipmentCache - .computeIfAbsent(context, k -> new HashMap<>()) - .computeIfAbsent(itemReq.getEquipmentSlot(), k -> new LinkedHashSet<>()) - .add(itemReq); - } - log.debug("Decomposed EQUIPMENT requirement: {} for context {}", itemReq.getName(), context); - break; - case INVENTORY: - slot = itemReq.hasSpecificInventorySlot() ? itemReq.getInventorySlot() : -1; - if (slot != -1) { - newInventorySlotCache - .computeIfAbsent(context, k -> new HashMap<>()) - .computeIfAbsent(slot, k -> new LinkedHashSet<>()) - .add(itemReq); - } else { - // Flexible inventory item - wrap in OrRequirement - OrRequirement orReq = new OrRequirement(itemReq.getPriority(), itemReq.getRating(), - itemReq.getName(), context, ItemRequirement.class); - orReq.addRequirement(itemReq); - newAnyInventorySlotCacheByContext.computeIfAbsent(context, k -> new LinkedHashSet<>()) - .add(orReq); - } - break; - case EITHER: - slot = itemReq.hasSpecificInventorySlot() ? itemReq.getInventorySlot() : -1; - EquipmentInventorySlot equipmentSlot = itemReq.getEquipmentSlot(); - if (equipmentSlot == null && slot == -2) { - throw new IllegalArgumentException("Either requirement must specify either equipment slot or specific inventory slot"); - } - - // Add to equipment cache if equipment slot is specified - if (equipmentSlot != null) { - newEquipmentCache - .computeIfAbsent(context, k -> new HashMap<>()) - .computeIfAbsent(equipmentSlot, k -> new LinkedHashSet<>()) - .add(itemReq); - } - - // Add to inventory cache based on slot - if (slot != -1) { - newInventorySlotCache - .computeIfAbsent(context, k -> new HashMap<>()) - .computeIfAbsent(slot, k -> new LinkedHashSet<>()) - .add(itemReq); - } else { - // Flexible EITHER requirement - wrap in OrRequirement - OrRequirement orReq = new OrRequirement(itemReq.getPriority(), itemReq.getRating(), - itemReq.getName(), context, ItemRequirement.class); - orReq.addRequirement(itemReq); - newAnyInventorySlotCacheByContext.computeIfAbsent(context, k -> new LinkedHashSet<>()) - .add(orReq); - } - break; - default: - log.info("Skipping non-slot requirement type in decompose: {}", itemReq.getRequirementType()); - break; - } - } else if (child instanceof ShopRequirement) { - ShopRequirement shopReq = (ShopRequirement) child; - TaskContext context = shopReq.getTaskContext(); - - if (context == null) { - log.warn("ShopRequirement without context during decompose: {}", shopReq); - continue; - } - - OrRequirement orReq = new OrRequirement(shopReq.getPriority(), 0, - shopReq.getName(), context, ShopRequirement.class); - orReq.addRequirement(shopReq); - newShopCache.computeIfAbsent(context, k -> new LinkedHashSet<>()).add(orReq); - } else if (child instanceof LootRequirement) { - LootRequirement lootReq = (LootRequirement) child; - TaskContext context = lootReq.getTaskContext(); - - if (context == null) { - log.warn("LootRequirement without context during decompose: {}", lootReq); - continue; - } - - OrRequirement orReq = new OrRequirement(lootReq.getPriority(), 0, - lootReq.getName(), context, LootRequirement.class); - orReq.addRequirement(lootReq); - newLootCache.computeIfAbsent(context, k -> new LinkedHashSet<>()).add(orReq); - } else if (child instanceof LogicalRequirement) { - // Recursively decompose nested logical requirements - decomposeLogicalRequirement((LogicalRequirement) child, newEquipmentCache, - newInventorySlotCache, newAnyInventorySlotCacheByContext, newShopCache, newLootCache); - } - } - } - - /** - * Checks if an OR requirement contains only ItemRequirements. - * OR requirements created from ItemRequirement.createOrRequirement() have "total amount" semantics - * that should be preserved rather than being decomposed and regrouped. - * - * @param orReq The OR requirement to check - * @return true if the OR requirement contains only ItemRequirements, false otherwise - */ - private boolean isItemOnlyOrRequirement(OrRequirement orReq) { - for (Requirement child : orReq.getChildRequirements()) { - if (!(child instanceof ItemRequirement)) { - return false; - } - } - return true; - } - - /** - * Adds an OR requirement directly to the appropriate cache based on the type of items it contains. - * This preserves the original total amount semantics for OR requirements like "5 food from any combination". - * - * @param orReq The OR requirement to add directly to cache - * @param equipmentCache Equipment cache to update - * @param inventorySlotCache Inventory slot cache to update - */ - private void addOrRequirementDirectlyToCache(OrRequirement orReq, - Map> equipmentCache, - Map> inventorySlotCache) { - - // Determine the appropriate cache location based on the first child requirement - // All items in an OR requirement should target the same slot type - ItemRequirement firstItem = (ItemRequirement) orReq.getChildRequirements().get(0); - - switch (firstItem.getRequirementType()) { - case EQUIPMENT: - if (firstItem.getEquipmentSlot() != null) { - equipmentCache.computeIfAbsent(firstItem.getEquipmentSlot(), k -> new LinkedHashSet<>()).add(orReq); - log.debug("Added OR requirement directly to equipment slot {}: {}", - firstItem.getEquipmentSlot(), orReq.getName()); - } - break; - case INVENTORY: - int slot = firstItem.hasSpecificInventorySlot() ? firstItem.getInventorySlot() : -1; - inventorySlotCache.computeIfAbsent(slot, k -> new LinkedHashSet<>()).add(orReq); - log.debug("Added OR requirement directly to inventory slot {}: {}", - slot == -1 ? "any" : String.valueOf(slot), orReq.getName()); - break; - case EITHER: - // For EITHER requirements, add to both equipment and inventory slots as appropriate - if (firstItem.getEquipmentSlot() != null) { - equipmentCache.computeIfAbsent(firstItem.getEquipmentSlot(), k -> new LinkedHashSet<>()).add(orReq); - log.debug("Added EITHER OR requirement to equipment slot {}: {}", - firstItem.getEquipmentSlot(), orReq.getName()); - } - int invSlot = firstItem.hasSpecificInventorySlot() ? firstItem.getInventorySlot() : -1; - inventorySlotCache.computeIfAbsent(invSlot, k -> new LinkedHashSet<>()).add(orReq); - log.debug("Added EITHER OR requirement to inventory slot {}: {}", - invSlot == -1 ? "any" : String.valueOf(invSlot), orReq.getName()); - break; - default: - log.warn("Cannot add OR requirement to cache - unsupported requirement type: {}", - firstItem.getRequirementType()); - break; - } - } - - /** - * Wraps an individual ItemRequirement in an OrRequirement for consistency with cache structure. - * This allows all items in the cache to be treated uniformly as logical requirements. - * - * @param itemReq The ItemRequirement to wrap - * @return An OrRequirement containing the single ItemRequirement - */ - private OrRequirement wrapItemRequirementInOr(ItemRequirement itemReq) { - return new OrRequirement( - itemReq.getPriority(), - itemReq.getRating(), - itemReq.getDescription(), - itemReq.getTaskContext(), - itemReq - ); - } - - /** - * Legacy method - kept for potential future use but no longer used in main flow. - * Adds a logical requirement to the appropriate cache based on its content. - * This method analyzes the child requirements to determine the best placement. - */ - private void addLogicalRequirementToCache(LogicalRequirement logical, - Map> equipmentCache, - LinkedHashSet shopCache, - LinkedHashSet lootCache, - Map> inventorySlotCache) { - - // Analyze child requirements to determine placement - boolean hasEquipment = false; - boolean hasInventory = false; - boolean hasEither = false; - boolean hasShop = false; - boolean hasLoot = false; - EquipmentInventorySlot equipmentSlot = null; - Integer specificInventorySlot = null; - - for (Requirement child : logical.getChildRequirements()) { - if (child instanceof ItemRequirement) { - ItemRequirement item = (ItemRequirement) child; - switch (item.getRequirementType()) { - case EQUIPMENT: - hasEquipment = true; - if (equipmentSlot == null) { - equipmentSlot = item.getEquipmentSlot(); - } - break; - case INVENTORY: - hasInventory = true; - if (item.hasSpecificInventorySlot() && specificInventorySlot == null) { - specificInventorySlot = item.getInventorySlot(); - } - break; - case EITHER: - hasEither = true; - if (item.getEquipmentSlot() != null && equipmentSlot == null) { - equipmentSlot = item.getEquipmentSlot(); - } - if (item.hasSpecificInventorySlot() && specificInventorySlot == null) { - specificInventorySlot = item.getInventorySlot(); - } - break; - case PLAYER_STATE: - case LOCATION: - case GAME_CONFIG: - case OR_LOGICAL: - // These don't affect cache placement for items - break; - case SHOP: - case CONDITIONAL: - case LOOT: - case CUSTOM: - // These types are not expected for ItemRequirement - log.warn("Unexpected requirement type for ItemRequirement in logical: {}", item.getRequirementType()); - break; - } - } else if (child instanceof ShopRequirement) { - hasShop = true; - } else if (child instanceof LootRequirement) { - hasLoot = true; - } else if (child instanceof LogicalRequirement) { - // For nested logical requirements, recursively add them - addLogicalRequirementToCache((LogicalRequirement) child, equipmentCache, - shopCache, lootCache, inventorySlotCache); - return; // Don't add the parent logical requirement - } - } - - // Place in the most appropriate cache(s) - // EITHER items can be placed in multiple caches based on their capabilities - if (hasEquipment && equipmentSlot != null) { - equipmentCache.computeIfAbsent(equipmentSlot, k -> new LinkedHashSet<>()).add(logical); - } - - if (hasShop) { - shopCache.add(logical); - } else if (hasLoot) { - lootCache.add(logical); - } else if (hasInventory || hasEither) { - // Add to inventory slot cache (specific slot or -1 for any slot) - if (specificInventorySlot != null) { - // Add to specific slot cache - inventorySlotCache.computeIfAbsent(specificInventorySlot, k -> new LinkedHashSet<>()).add(logical); - } else { - // Add to any-slot cache (key = -1) - inventorySlotCache.computeIfAbsent(-1, k -> new LinkedHashSet<>()).add(logical); - } - } - } - - /** - * Creates logical requirements from grouped individual requirements organized by priority. - * This creates up to 2 OR requirements per slot: one for MANDATORY priority, one for RECOMMENDED priority. - */ - private void createLogicalRequirementsFromGroups( - Map>> equipmentByContextAndSlot, - Map>> inventoryByContextAndSlot, - Map> eitherByContext, - Map> shopByContext, - Map> lootByContext, - Map> equipmentCache, - LinkedHashSet shopCache, - LinkedHashSet lootCache, - Map> inventorySlotCache) { - - // Process equipment items: create consolidated PRE and POST OR requirements per slot - processEquipmentSlots(equipmentByContextAndSlot, eitherByContext, equipmentCache); - - // Process inventory items: create consolidated PRE and POST OR requirements per slot - processInventorySlots(inventoryByContextAndSlot, eitherByContext, inventorySlotCache); - - // Process shop and loot requirements (these don't need slot consolidation) - processShopAndLootRequirements(shopByContext, lootByContext, shopCache, lootCache); - } - - /** - * Processes equipment slots to create up to 2 OR requirements per slot (one for each priority level). - */ - private void processEquipmentSlots( - Map>> equipmentByContextAndSlot, - Map> eitherByContext, - Map> equipmentCache) { - - // Collect all equipment slots that have requirements - Set allEquipmentSlots = new HashSet<>(); - for (Map> slotMap : equipmentByContextAndSlot.values()) { - allEquipmentSlots.addAll(slotMap.keySet()); - } - - // Add slots from EITHER items - for (List eitherItems : eitherByContext.values()) { - for (ItemRequirement item : eitherItems) { - if (item.getEquipmentSlot() != null) { - allEquipmentSlots.add(item.getEquipmentSlot()); - } - } - } - - // For each equipment slot, create MANDATORY and RECOMMENDED OR requirements - for (EquipmentInventorySlot slot : allEquipmentSlots) { - // Collect items for MANDATORY priority (all schedule contexts) - List mandatoryItems = new ArrayList<>(); - addItemsForPriority(equipmentByContextAndSlot, slot, RequirementPriority.MANDATORY, mandatoryItems); - addEitherItemsForEquipmentSlotPriority(eitherByContext, slot, RequirementPriority.MANDATORY, mandatoryItems); - - // Collect items for RECOMMENDED priority (all schedule contexts) - List recommendedItems = new ArrayList<>(); - addItemsForPriority(equipmentByContextAndSlot, slot, RequirementPriority.RECOMMENDED, recommendedItems); - addEitherItemsForEquipmentSlotPriority(eitherByContext, slot, RequirementPriority.RECOMMENDED, recommendedItems); - - // Create OR requirements if items exist - LinkedHashSet slotRequirements = new LinkedHashSet<>(); - - if (!mandatoryItems.isEmpty()) { - OrRequirement mandatoryReq = createMergedOrRequirement(mandatoryItems, - slot.name() + " equipment (MANDATORY)", determineTaskContext(mandatoryItems)); - slotRequirements.add(mandatoryReq); - log.debug("Created MANDATORY equipment OR requirement for slot {}: {} with {} alternatives", - slot, mandatoryReq.getName(), mandatoryItems.size()); - } - - if (!recommendedItems.isEmpty()) { - OrRequirement recommendedReq = createMergedOrRequirement(recommendedItems, - slot.name() + " equipment (RECOMMENDED)", determineTaskContext(recommendedItems)); - slotRequirements.add(recommendedReq); - log.debug("Created RECOMMENDED equipment OR requirement for slot {}: {} with {} alternatives", - slot, recommendedReq.getName(), recommendedItems.size()); - } - - if (!slotRequirements.isEmpty()) { - equipmentCache.put(slot, slotRequirements); - } - } - } - - /** - * Processes inventory slots to create up to 2 OR requirements per slot (one for each priority level). - */ - private void processInventorySlots( - Map>> inventoryByContextAndSlot, - Map> eitherByContext, - Map> inventorySlotCache) { - - // Collect all inventory slots that have requirements - Set allInventorySlots = new HashSet<>(); - for (Map> slotMap : inventoryByContextAndSlot.values()) { - allInventorySlots.addAll(slotMap.keySet()); - } - - // Add slots from EITHER items - for (List eitherItems : eitherByContext.values()) { - for (ItemRequirement item : eitherItems) { - int invSlot = item.hasSpecificInventorySlot() ? item.getInventorySlot() : -1; - allInventorySlots.add(invSlot); - } - } - - // For each inventory slot, create MANDATORY and RECOMMENDED OR requirements - for (Integer slot : allInventorySlots) { - // Collect items for MANDATORY priority (all schedule contexts) - List mandatoryItems = new ArrayList<>(); - addInventoryItemsForPriority(inventoryByContextAndSlot, slot, RequirementPriority.MANDATORY, mandatoryItems); - addEitherItemsForInventorySlotPriority(eitherByContext, slot, RequirementPriority.MANDATORY, mandatoryItems); - - // Collect items for RECOMMENDED priority (all schedule contexts) - List recommendedItems = new ArrayList<>(); - addInventoryItemsForPriority(inventoryByContextAndSlot, slot, RequirementPriority.RECOMMENDED, recommendedItems); - addEitherItemsForInventorySlotPriority(eitherByContext, slot, RequirementPriority.RECOMMENDED, recommendedItems); - - // Create OR requirements if items exist - LinkedHashSet slotRequirements = new LinkedHashSet<>(); - String slotDescription = slot == -1 ? "any inventory slot" : "inventory slot " + slot; - - if (!mandatoryItems.isEmpty()) { - OrRequirement mandatoryReq = createMergedOrRequirement(mandatoryItems, - slotDescription + " (MANDATORY)", determineTaskContext(mandatoryItems)); - slotRequirements.add(mandatoryReq); - log.debug("Created MANDATORY inventory OR requirement for {}: {} with {} alternatives", - slotDescription, mandatoryReq.getName(), mandatoryItems.size()); - } - - if (!recommendedItems.isEmpty()) { - OrRequirement recommendedReq = createMergedOrRequirement(recommendedItems, - slotDescription + " (RECOMMENDED)", determineTaskContext(recommendedItems)); - slotRequirements.add(recommendedReq); - log.debug("Created RECOMMENDED inventory OR requirement for {}: {} with {} alternatives", - slotDescription, recommendedReq.getName(), recommendedItems.size()); - } - - if (!slotRequirements.isEmpty()) { - inventorySlotCache.put(slot, slotRequirements); - } - } - } - - // Helper methods for collecting items - private void addItemsForContext(Map>> equipmentByContextAndSlot, - EquipmentInventorySlot slot, TaskContext context, List targetList) { - Map> contextMap = equipmentByContextAndSlot.get(context); - if (contextMap != null) { - List items = contextMap.get(slot); - if (items != null) { - targetList.addAll(items); - } - } - } - - private void addInventoryItemsForContext(Map>> inventoryByContextAndSlot, - Integer slot, TaskContext context, List targetList) { - Map> contextMap = inventoryByContextAndSlot.get(context); - if (contextMap != null) { - List items = contextMap.get(slot); - if (items != null) { - targetList.addAll(items); - } - } - } - - private void addEitherItemsForEquipmentSlot(Map> eitherByContext, - EquipmentInventorySlot equipSlot, TaskContext context, List targetList) { - List eitherItems = eitherByContext.get(context); - if (eitherItems != null) { - for (ItemRequirement item : eitherItems) { - if (equipSlot.equals(item.getEquipmentSlot())) { - targetList.add(item); - } - } - } - } - - private void addEitherItemsForInventorySlot(Map> eitherByContext, - Integer invSlot, TaskContext context, List targetList) { - List eitherItems = eitherByContext.get(context); - if (eitherItems != null) { - for (ItemRequirement item : eitherItems) { - int itemInvSlot = item.hasSpecificInventorySlot() ? item.getInventorySlot() : -1; - if (invSlot.equals(itemInvSlot)) { - targetList.add(item); - } - } - } - } - - // Priority-based helper methods for collecting items - private void addItemsForPriority(Map>> equipmentByContextAndSlot, - EquipmentInventorySlot slot, RequirementPriority priority, List targetList) { - // Check all schedule contexts for items with the specified priority - for (Map> contextMap : equipmentByContextAndSlot.values()) { - List items = contextMap.get(slot); - if (items != null) { - for (ItemRequirement item : items) { - if (item.getPriority() == priority) { - targetList.add(item); - } - } - } - } - } - - private void addEitherItemsForEquipmentSlotPriority(Map> eitherByContext, - EquipmentInventorySlot equipSlot, RequirementPriority priority, List targetList) { - // Check all schedule contexts for either items with the specified priority - for (List eitherItems : eitherByContext.values()) { - if (eitherItems != null) { - for (ItemRequirement item : eitherItems) { - if (equipSlot.equals(item.getEquipmentSlot()) && item.getPriority() == priority) { - targetList.add(item); - } - } - } - } - } - - private TaskContext determineTaskContext(List items) { - // Determine the most appropriate TaskContext for a group of items - // Priority: PRE_SCHEDULE -> POST_SCHEDULE -> BOTH - boolean hasPre = false, hasPost = false, hasBoth = false; - - for (ItemRequirement item : items) { - switch (item.getTaskContext()) { - case PRE_SCHEDULE: - hasPre = true; - break; - case POST_SCHEDULE: - hasPost = true; - break; - case BOTH: - hasBoth = true; - break; - } - } - - // If all items have the same context, use that - if (hasPre && !hasPost && !hasBoth) return TaskContext.PRE_SCHEDULE; - if (hasPost && !hasPre && !hasBoth) return TaskContext.POST_SCHEDULE; - if (hasBoth && !hasPre && !hasPost) return TaskContext.BOTH; - - // Mixed contexts - default to BOTH (covers all cases) - return TaskContext.BOTH; - } - - // Priority-based helper methods for inventory items - private void addInventoryItemsForPriority(Map>> inventoryByContextAndSlot, - Integer slot, RequirementPriority priority, List targetList) { - // Check all schedule contexts for items with the specified priority - for (Map> contextMap : inventoryByContextAndSlot.values()) { - List items = contextMap.get(slot); - if (items != null) { - for (ItemRequirement item : items) { - if (item.getPriority() == priority) { - targetList.add(item); - } - } - } - } - } - - private void addEitherItemsForInventorySlotPriority(Map> eitherByContext, - Integer invSlot, RequirementPriority priority, List targetList) { - // Check all schedule contexts for either items with the specified priority - for (List eitherItems : eitherByContext.values()) { - if (eitherItems != null) { - for (ItemRequirement item : eitherItems) { - int itemInvSlot = item.hasSpecificInventorySlot() ? item.getInventorySlot() : -1; - if (invSlot.equals(itemInvSlot) && item.getPriority() == priority) { - targetList.add(item); - } - } - } - } - } - - /** - * Processes shop and loot requirements (these don't need slot-based consolidation). - */ - private void processShopAndLootRequirements( - Map> shopByContext, - Map> lootByContext, - LinkedHashSet shopCache, - LinkedHashSet lootCache) { - - // Process shop requirements grouped by schedule context - for (Map.Entry> contextEntry : shopByContext.entrySet()) { - TaskContext taskContext = contextEntry.getKey(); - List shopReqs = contextEntry.getValue(); - - for (ShopRequirement shop : shopReqs) { - OrRequirement orReq = new OrRequirement(shop.getPriority(), shop.getRating(), - shop.getDescription(), taskContext, shop); - shopCache.add(orReq); - } - } - - // Process loot requirements grouped by schedule context - for (Map.Entry> contextEntry : lootByContext.entrySet()) { - TaskContext taskContext = contextEntry.getKey(); - List lootReqs = contextEntry.getValue(); - - for (LootRequirement loot : lootReqs) { - OrRequirement orReq = new OrRequirement(loot.getPriority(), loot.getRating(), - loot.getDescription(), taskContext, loot); - lootCache.add(orReq); - } - } - } - - /** - * Creates a merged OR requirement from a list of competing items for the same slot. - * Implements proper rating calculation (sum) and priority selection (highest). - * - * @param items List of items competing for the same slot - * @param slotDescription Description of the slot for the OR requirement name - * @param TaskContext The schedule context (must be the same for all items) - * @return A merged OrRequirement with correct rating and priority - */ - private OrRequirement createMergedOrRequirement(List items, String slotDescription, TaskContext taskContext) { - if (items.isEmpty()) { - throw new IllegalArgumentException("Cannot create OR requirement from empty item list"); - } - - // Calculate merged rating as sum of all item ratings - int mergedRating = items.stream().mapToInt(Requirement::getRating).sum(); - - // Calculate merged priority as the highest priority (lowest ordinal value) - RequirementPriority mergedPriority = items.stream() - .map(Requirement::getPriority) - .min(RequirementPriority::compareTo) - .orElse(RequirementPriority.RECOMMENDED); - - // Verify all items have the same schedule context - boolean allSameContext = items.stream() - .map(Requirement::getTaskContext) - .allMatch(context -> context == taskContext); - - if (!allSameContext) { - log.warn("Items for {} have different schedule contexts, using {}", slotDescription, taskContext); - } - - // Create descriptive name showing alternatives count - String name = slotDescription + " options (" + items.size() + " alternatives, rating: " + mergedRating + ")"; - - return new OrRequirement(mergedPriority, mergedRating, name, taskContext, - items.toArray(new Requirement[0])); - } - - /** - * Sorts all cache collections by priority and rating. - */ - private void sortAllCaches( - Map> equipmentCache, - LinkedHashSet shopCache, - LinkedHashSet lootCache, - Map> inventorySlotCache) { - - // Sort equipment items within each slot - for (LinkedHashSet slotRequirements : equipmentCache.values()) { - sortLogicalRequirements(slotRequirements); - } - - // Sort inventory slot requirements - for (LinkedHashSet slotRequirements : inventorySlotCache.values()) { - sortLogicalRequirements(slotRequirements); - } - - // Sort other collections - sortLogicalRequirements(shopCache); - sortLogicalRequirements(lootCache); - } - - /** - * Sorts logical requirements by priority and rating. - */ - private void sortLogicalRequirements(LinkedHashSet requirements) { - List sorted = new ArrayList<>(requirements); - sorted.sort((a, b) -> { - // First sort by priority (MANDATORY > RECOMMENDED > OPTIONAL) - int priorityCompare = a.getPriority().compareTo(b.getPriority()); - if (priorityCompare != 0) { - return priorityCompare; - } - // Then sort by rating (higher rating is better) - return Integer.compare(b.getRating(), a.getRating()); - }); - - requirements.clear(); - requirements.addAll(sorted); - } - - - /** - * Gets equipment logical requirements cache for a specific schedule context, rebuilding if necessary. - * Returns only the LogicalRequirements that match the given context. - * - * @param context The schedule context to filter by (PRE_SCHEDULE or POST_SCHEDULE) - * @return Map of equipment slot to context-specific logical requirements - */ - /** - * Gets standard (non-external) inventory slot requirements cache, rebuilding if necessary. - * This excludes externally added requirements to prevent double processing. - */ - public Map> getInventoryRequirements(TaskContext context) { - if (!cacheValid) { - rebuildCache(); - } - - return inventorySlotRequirementsCache.getOrDefault(context, new LinkedHashMap<>()); - } - - /** - * Gets standard (non-external) inventory slot requirements cache, rebuilding if necessary. - * This excludes externally added requirements to prevent double processing. - */ - public Map getInventorySlotLogicalRequirements(TaskContext context) { - if (!cacheValid) { - rebuildCache(); - } - - // Convert LinkedHashSet to OrRequirement - Map result = new LinkedHashMap<>(); - Map> inventory = inventorySlotRequirementsCache.getOrDefault(context, new LinkedHashMap<>()); - for (Map.Entry> entry : inventory.entrySet()) { - if (!entry.getValue().isEmpty()) { - ItemRequirement first = entry.getValue().iterator().next(); - if (entry.getValue().size() == 1) { - OrRequirement orReq = new OrRequirement(first.getPriority(), first.getRating(), - "Inventory slot " + entry.getKey(), context, ItemRequirement.class); - orReq.addRequirement(first); - result.put(entry.getKey(), orReq); - } else { - OrRequirement orReq = new OrRequirement(first.getPriority(), first.getRating(), - "Inventory slot " + entry.getKey(), context, ItemRequirement.class); - for (ItemRequirement item : entry.getValue()) { - orReq.addRequirement(item); - } - result.put(entry.getKey(), orReq); - } - } - } - return result; - } - - /** - * Gets standard (non-external) inventory slot requirements cache as raw ItemRequirement sets. - * This excludes externally added requirements to prevent double processing. - */ - public Map> getInventorySlotRequirements(TaskContext context) { - if (!cacheValid) { - rebuildCache(); - } - - return inventorySlotRequirementsCache.getOrDefault(context, new LinkedHashMap<>()); - } - - /** - * Gets standard (non-external) equipment logical requirements cache, rebuilding if necessary. - * This excludes externally added requirements to prevent double processing. - */ - public Map> getEquipmentRequirements(TaskContext context) { - if (!cacheValid) { - rebuildCache(); - } - - return equipmentItemsCache.getOrDefault(context, new LinkedHashMap<>()); - } - - - - - - /** - * Gets standard (non-external) shop logical requirements cache, rebuilding if necessary. - * This excludes externally added requirements to prevent double processing. - */ - public LinkedHashSet getShopLogicalRequirements(TaskContext context) { - if (!cacheValid) { - rebuildCache(); - } - - - return shopRequirementsCache.getOrDefault(context, new LinkedHashSet<>()); - } - - /** - * Gets standard (non-external) shop requirements cache for a specific context. - */ - public LinkedHashSet getShopRequirements(TaskContext context) { - return getShopLogicalRequirements(context); - } - - /** - * Gets standard (non-external) loot logical requirements cache, rebuilding if necessary. - * This excludes externally added requirements to prevent double processing. - */ - public LinkedHashSet getLootLogicalRequirements(TaskContext context) { - if (!cacheValid) { - rebuildCache(); - } - - return lootRequirementsCache.getOrDefault(context, new LinkedHashSet<>()); - } - - /** - * Gets standard (non-external) loot requirements cache for a specific context. - */ - public LinkedHashSet getLootRequirements(TaskContext context) { - return getLootLogicalRequirements(context); - } - - /** - * Checks if a logical requirement contains only standard (non-external) child requirements. - * - * @param logical The logical requirement to check - * @return true if all child requirements are standard, false if any are external - */ - private boolean isLogicalRequirement(LogicalRequirement logical) { - for (Requirement child : logical.getChildRequirements()) { - RequirementKey childKey = new RequirementKey(child); - if (externalRequirements.containsKey(childKey)) { - return false; // Contains external requirement - } - // For nested logical requirements, check recursively - if (child instanceof LogicalRequirement) { - if (!isLogicalRequirement((LogicalRequirement) child)) { - return false; - } - } - } - return true; // All child requirements are standard - } - - - /** - * Validates the consistency of the registry. - * - * @return true if the registry is consistent, false otherwise - */ - public boolean validateConsistency() { - try { - // Ensure single-instance requirements are properly referenced - long preSpellbookCount = getRequirements(SpellbookRequirement.class, TaskContext.PRE_SCHEDULE).size(); - long postSpellbookCount = getRequirements(SpellbookRequirement.class, TaskContext.POST_SCHEDULE).size(); - long preLocationCount = getRequirements(LocationRequirement.class, TaskContext.PRE_SCHEDULE).size(); - long postLocationCount = getRequirements(LocationRequirement.class, TaskContext.POST_SCHEDULE).size(); - - if (preSpellbookCount > 1 || postSpellbookCount > 1) { - log.error("Multiple spellbook requirements detected: pre={}, post={}", preSpellbookCount, postSpellbookCount); - return false; - } - - if (preLocationCount > 1 || postLocationCount > 1) { - log.error("Multiple location requirements detected: pre={}, post={}", preLocationCount, postLocationCount); - return false; - } - - // Ensure cache consistency (if cache is valid) - if (cacheValid) { - // Count equipment items across all contexts and slots - int equipmentSize = equipmentItemsCache.values().stream() - .mapToInt(contextMap -> contextMap.values().stream().mapToInt(Set::size).sum()) - .sum(); - - // Count inventory slot items across all contexts and slots - int inventorySize = inventorySlotRequirementsCache.values().stream() - .mapToInt(contextMap -> contextMap.values().stream().mapToInt(Set::size).sum()) - .sum(); - - // Count any inventory slot, shop and loot items across all contexts - int anySlotSize = anyInventorySlotRequirementsCache.values().stream().mapToInt(Set::size).sum(); - int shopSize = shopRequirementsCache.values().stream().mapToInt(Set::size).sum(); - int lootSize = lootRequirementsCache.values().stream().mapToInt(Set::size).sum(); - - int cacheSize = equipmentSize + inventorySize + anySlotSize + shopSize + lootSize; - - // Note: EITHER items are now distributed to equipment and inventory caches, - // so we need to account for potential duplicates in the count - long actualItemCount = requirements.values().stream() - .filter(req -> req instanceof ItemRequirement || - req instanceof ShopRequirement || - req instanceof LootRequirement) - .count(); - - // For now, we'll log cache statistics but not fail validation - // since EITHER items are counted in multiple caches - log.debug("Cache statistics: cache={}, actual={}", cacheSize, actualItemCount); - } - - return true; - } catch (Exception e) { - log.error("Error during consistency validation", e); - return false; - } - } - - /** - * Gets debug information about the registry state. - */ - public String getDebugInfo() { - StringBuilder sb = new StringBuilder(); - sb.append("RequirementRegistry Debug Info:\n"); - sb.append(" Total requirements: ").append(requirements.size()).append("\n"); - sb.append(" Cache valid: ").append(cacheValid).append("\n"); - sb.append(" Pre-schedule spellbook: ").append(preScheduleSpellbookRequirement != null).append("\n"); - sb.append(" Post-schedule spellbook: ").append(postScheduleSpellbookRequirement != null).append("\n"); - sb.append(" Pre-schedule location: ").append(preScheduleLocationRequirement != null).append("\n"); - sb.append(" Post-schedule location: ").append(postScheduleLocationRequirement != null).append("\n"); - - Map, Long> typeCounts = requirements.values().stream() - .collect(Collectors.groupingBy(Object::getClass, Collectors.counting())); - - sb.append(" Requirements by type:\n"); - typeCounts.forEach((type, count) -> - sb.append(" ").append(type.getSimpleName()).append(": ").append(count).append("\n")); - - return sb.toString(); - } - - /** - * Gets a comprehensive validation summary of all requirements in the registry. - * This provides an overall evaluation of requirement fulfillment status organized by priority and context. - * - * @param context The schedule context to filter requirements (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @return A formatted string containing the validation summary - */ - public String getValidationSummary(TaskContext context) { - StringBuilder sb = new StringBuilder(); - sb.append("=== Requirements Validation Summary ===\n"); - - // Get all requirements for the specified context - List contextRequirements = getAllRequirements().stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(Collectors.toList()); - - if (contextRequirements.isEmpty()) { - sb.append("No requirements found for context: ").append(context.name()).append("\n"); - return sb.toString(); - } - - // Group requirements by priority for better organization - Map> byPriority = contextRequirements.stream() - .collect(Collectors.groupingBy(Requirement::getPriority)); - - // Overall statistics - long totalRequirements = contextRequirements.size(); - long fulfilledCount = contextRequirements.stream().mapToLong(req -> req.isFulfilled() ? 1 : 0).sum(); - long notFulfilledCount = totalRequirements - fulfilledCount; - - sb.append("Context: ").append(context.name()).append("\n"); - sb.append("Total Requirements: ").append(totalRequirements).append("\n"); - sb.append("Fulfilled: ").append(fulfilledCount).append(" (") - .append(totalRequirements > 0 ? (fulfilledCount * 100 / totalRequirements) : 0).append("%)\n"); - sb.append("Not Fulfilled: ").append(notFulfilledCount).append(" (") - .append(totalRequirements > 0 ? (notFulfilledCount * 100 / totalRequirements) : 0).append("%)\n\n"); - - // Detailed breakdown by priority - for (RequirementPriority priority : RequirementPriority.values()) { - List priorityRequirements = byPriority.getOrDefault(priority, Collections.emptyList()); - if (priorityRequirements.isEmpty()) { - continue; - } - - long priorityFulfilled = priorityRequirements.stream().mapToLong(req -> req.isFulfilled() ? 1 : 0).sum(); - long priorityNotFulfilled = priorityRequirements.size() - priorityFulfilled; - - sb.append("--- ").append(priority.name()).append(" Requirements ---\n"); - sb.append("Total: ").append(priorityRequirements.size()).append(" | "); - sb.append("Fulfilled: ").append(priorityFulfilled).append(" | "); - sb.append("Not Fulfilled: ").append(priorityNotFulfilled).append("\n"); - - // Group by requirement type for better organization - Map> byType = priorityRequirements.stream() - .collect(Collectors.groupingBy(Requirement::getRequirementType)); - - for (Map.Entry> typeEntry : byType.entrySet()) { - RequirementType type = typeEntry.getKey(); - List typeRequirements = typeEntry.getValue(); - - long typeFulfilled = typeRequirements.stream().mapToLong(req -> req.isFulfilled() ? 1 : 0).sum(); - - sb.append(" ").append(type.name()).append(": ") - .append(typeFulfilled).append("/").append(typeRequirements.size()) - .append(" (").append(typeRequirements.size() > 0 ? (typeFulfilled * 100 / typeRequirements.size()) : 0).append("% fulfilled)\n"); - - // Show validation status for each requirement in this type - typeRequirements.forEach(req -> { - String status = req.isFulfilled() ? "✓" : "✗"; - sb.append(" ").append(status).append(" ") - .append("Rating: ").append(req.getRating()).append("/10") - .append(" | Type: ").append(req.getRequirementType().name()).append("\n"); - }); - } - sb.append("\n"); - } - - // Critical validation status - List mandatoryNotFulfilled = contextRequirements.stream() - .filter(req -> req.getPriority() == RequirementPriority.MANDATORY && !req.isFulfilled()) - .collect(Collectors.toList()); - - if (!mandatoryNotFulfilled.isEmpty()) { - sb.append("⚠ïļ CRITICAL: ").append(mandatoryNotFulfilled.size()) - .append(" mandatory requirements are not fulfilled!\n"); - } else if (byPriority.containsKey(RequirementPriority.MANDATORY)) { - sb.append("✓ All mandatory requirements are fulfilled\n"); - } - - // External requirements summary - List externalContextRequirements = getExternalRequirements(context); - if (!externalContextRequirements.isEmpty()) { - long externalFulfilled = externalContextRequirements.stream().mapToLong(req -> req.isFulfilled() ? 1 : 0).sum(); - sb.append("\n--- External Requirements ---\n"); - sb.append("Total: ").append(externalContextRequirements.size()).append(" | "); - sb.append("Fulfilled: ").append(externalFulfilled).append(" | "); - sb.append("Not Fulfilled: ").append(externalContextRequirements.size() - externalFulfilled).append("\n"); - } - - return sb.toString(); - } - - /** - * Gets a concise validation status summary for quick overview. - * - * @param context The schedule context to evaluate - * @return A brief status summary string - */ - public String getValidationStatusSummary(TaskContext context) { - List contextRequirements = getAllRequirements().stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(Collectors.toList()); - - if (contextRequirements.isEmpty()) { - return "No requirements for " + context.name(); - } - - long totalRequirements = contextRequirements.size(); - long fulfilledCount = contextRequirements.stream().mapToLong(req -> req.isFulfilled() ? 1 : 0).sum(); - long mandatoryCount = contextRequirements.stream().mapToLong(req -> req.getPriority() == RequirementPriority.MANDATORY ? 1 : 0).sum(); - long mandatoryFulfilled = contextRequirements.stream() - .filter(req -> req.getPriority() == RequirementPriority.MANDATORY) - .mapToLong(req -> req.isFulfilled() ? 1 : 0).sum(); - - String mandatoryStatus = mandatoryCount > 0 ? - String.format(" | Mandatory: %d/%d", mandatoryFulfilled, mandatoryCount) : ""; - - return String.format("Requirements [%s]: %d/%d fulfilled (%.0f%%)%s", - context.name(), fulfilledCount, totalRequirements, - totalRequirements > 0 ? (fulfilledCount * 100.0 / totalRequirements) : 0.0, - mandatoryStatus); - } - - /** - * Recursively extracts ItemRequirements from a LogicalRequirement. - */ - private void extractItemRequirements(LogicalRequirement logical, LinkedHashSet items) { - for (Requirement child : logical.getChildRequirements()) { - if (child instanceof ItemRequirement) { - items.add((ItemRequirement) child); - } else if (child instanceof LogicalRequirement) { - extractItemRequirements((LogicalRequirement) child, items); - } - } - } - - /** - * Gets requirements for a specific inventory slot. - * - * @param slot The inventory slot (0-27) - * @return Logical requirements for the specified slot - */ - public LinkedHashSet getInventorySlotRequirement(TaskContext context, int slot) { - if (!cacheValid) { - rebuildCache(); - } - return inventorySlotRequirementsCache.getOrDefault(context, new HashMap<>()).get(slot); - } - - /** - * Gets all inventory slot requirements for a specific schedule context. - * Returns only the LogicalRequirements that match the given context. - * - * @param context The schedule context to filter by (PRE_SCHEDULE or POST_SCHEDULE) - * @return Map of slot to context-specific logical requirements - */ - public Map> getInventorySlotsRequirements(TaskContext context) { - if (!cacheValid) { - rebuildCache(); - } - return inventorySlotRequirementsCache.getOrDefault(context, new HashMap<>()); - } - - - - /** - * Gets equipment items from slot-based cache, extracting ItemRequirements from LogicalRequirements. - * - * @return Map of equipment slot to ItemRequirements - */ - public Map> getEquipmentSlotItems(TaskContext context) { - ensureCacheValid(); - return this.equipmentItemsCache.getOrDefault(context, new HashMap<>()); - } - - /** - * Gets inventory items from slot-based cache, extracting ItemRequirements from LogicalRequirements. - * - * @return Set of inventory ItemRequirements (from slot -1 which represents "any slot") - */ - public Map> getInventorySlotItems(TaskContext context) { - ensureCacheValid(); - return this.inventorySlotRequirementsCache.getOrDefault(context, new HashMap<>()); - } - - /** - * Gets inventory items from slot-based cache, extracting ItemRequirements from LogicalRequirements. - * - * @return Set of inventory ItemRequirements (from slot -1 which represents "any slot") - */ - public LinkedHashSet getAnyInventorySlotItems(TaskContext context) { - ensureCacheValid(); - LinkedHashSet inventoryItems = new LinkedHashSet<>(); - LinkedHashSet logicalReqs = this.anyInventorySlotRequirementsCache.getOrDefault(context, new LinkedHashSet<>()); - - for (LogicalRequirement logical : logicalReqs) { - extractItemRequirements(logical, inventoryItems); - } - - return inventoryItems; - } - - /** - * Gets any-slot logical requirements (OrRequirements) for the InventorySetupPlanner. - * These represent flexible inventory items that can go in any inventory slot. - * - * @param context The schedule context to filter by - * @return LinkedHashSet of OrRequirements for flexible inventory placement - */ - public LinkedHashSet getAnySlotLogicalRequirements(TaskContext context) { - ensureCacheValid(); - return anyInventorySlotRequirementsCache.getOrDefault(context, new LinkedHashSet<>()); - } - - /** - * Gets all EITHER items by aggregating from both equipment and inventory slot caches. - * Since EITHER requirements are now distributed across caches, we need to collect them. - * - * @return Set of all EITHER ItemRequirements - */ - public LinkedHashSet getAllEitherItems(TaskContext context) { - LinkedHashSet eitherItems = new LinkedHashSet<>(); - - // Check equipment slots for EITHER items - Map> equipmentItems = getEquipmentSlotItems(context); - for (LinkedHashSet slotItems : equipmentItems.values()) { - for (ItemRequirement item : slotItems) { - if (RequirementType.EITHER.equals(item.getRequirementType())) { - eitherItems.add(item); - } - } - } - - // Check inventory slot cache for EITHER items - Map> inventoryItems = getInventorySlotItems(context); - for (LinkedHashSet slotItems : inventoryItems.values()) { - for (ItemRequirement item : slotItems) { - if (RequirementType.EITHER.equals(item.getRequirementType())) { - eitherItems.add(item); - } - } - } - // Check any inventory slot cache for EITHER items - LinkedHashSet anyInventoryItems = getAnyInventorySlotItems(context); - for (ItemRequirement item : anyInventoryItems) { - if (RequirementType.EITHER.equals(item.getRequirementType())) { - eitherItems.add(item); - } - } - - return eitherItems; - } - - // === UNIFIED ACCESS API === - - /** - * Gets all logical requirements (equipment + inventory) for a specific schedule context. - * This provides a unified view of all item-related requirements. - * - * @param context The schedule context to filter by (PRE_SCHEDULE or POST_SCHEDULE) - * @return List of all logical requirements for the given context - */ - public LinkedHashSet getAllItemRequirements(TaskContext context) { - LinkedHashSet allItemReqs = new LinkedHashSet<>(); - ensureCacheValid(); - - // Add equipment requirements (flatten the LinkedHashSet collections) - Map> equipmentCache = getEquipmentRequirements(context); - for (LinkedHashSet itemSet : equipmentCache.values()) { - allItemReqs.addAll(itemSet); - } - - // Add inventory slot requirements (flatten the LinkedHashSet collections) - Map> inventoryCache = getInventoryRequirements(context); - for (LinkedHashSet itemSet : inventoryCache.values()) { - allItemReqs.addAll(itemSet); - } - - // Add any inventory slot requirements (these are OrRequirements, extract their ItemRequirements) - LinkedHashSet anySlotCache = anyInventorySlotRequirementsCache.getOrDefault(context, new LinkedHashSet<>()); - for (OrRequirement orReq : anySlotCache) { - for (Object child : orReq.getChildRequirements()) { - if (child instanceof ItemRequirement) { - allItemReqs.add((ItemRequirement) child); - } - } - } - - return allItemReqs; - } - - /** - * Gets the total count of all logical requirements (equipment + inventory) for a specific schedule context. - * This is a convenience method for UI components that need to display counts. - * - * @param context The schedule context to filter by (PRE_SCHEDULE or POST_SCHEDULE) - * @return Total count of logical requirements for the given context - */ - public int getItemCount(TaskContext context) { - int count = 0; - // Count all equipment items per slot - for (LinkedHashSet items : getEquipmentRequirements(context).values()) { - count += items.size(); - } - // Count all inventory items per slot - for (LinkedHashSet items : getInventoryRequirements(context).values()) { - count += items.size(); - } - // Count any-slot inventory items (from OrRequirements) - LinkedHashSet anySlotOrs = anyInventorySlotRequirementsCache.getOrDefault(context, new LinkedHashSet<>()); - for (OrRequirement orReq : anySlotOrs) { - for (Requirement req : orReq.getChildRequirements()) { - if (req instanceof ItemRequirement) { - count++; - } - } - } - return count; - } - - - - - - /** - * Ensures the cache is valid, rebuilding if necessary. - */ - private void ensureCacheValid() { - if (!cacheValid) { - rebuildCache(); - } - } - - /** - * Ensures the external cache is valid, rebuilding if necessary. - */ - private void ensureExternalCacheValid() { - if (!externalCacheValid) { - rebuildExternalCache(); - } - } - - // =============================== - // EXTERNAL REQUIREMENTS METHODS - // =============================== - - /** - * Gets all external requirements of a specific type for a specific schedule context. - */ - @SuppressWarnings("unchecked") - public List getExternalRequirements(Class clazz, TaskContext context) { - return externalRequirements.values().stream() - .filter(clazz::isInstance) - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .map(req -> (T) req) - .collect(Collectors.toList()); - } - - /** - * Gets external equipment logical requirements cache, rebuilding if necessary. - */ - public Map>> getExternalEquipmentLogicalRequirements() { - ensureExternalCacheValid(); - return externalEquipmentItemsCache; - } - - /** - * Gets external inventory logical requirements cache, rebuilding if necessary. - */ - public Map>> getExternalInventoryLogicalRequirements() { - ensureExternalCacheValid(); - return externalInventorySlotRequirementsCache; - } - - /** - * Gets external shop logical requirements cache, rebuilding if necessary. - */ - public Map> getExternalShopLogicalRequirements() { - ensureExternalCacheValid(); - return externalShopRequirementsCache; - } - - /** - * Gets external loot logical requirements cache, rebuilding if necessary. - */ - public Map> getExternalLootLogicalRequirements() { - ensureExternalCacheValid(); - return externalLootRequirementsCache; - } - - /** - * Gets conditional item requirements cache, rebuilding if necessary. - */ - public Map> getConditionalItemRequirements() { - ensureCacheValid(); - return conditionalItemRequirementsCache; - } - - /** - * Gets external conditional item requirements cache, rebuilding if necessary. - */ - public Map> getExternalConditionalItemRequirements() { - ensureExternalCacheValid(); - return externalIConditionalItemRequirementsCache; - } - - /** - * Gets conditional item requirements for a specific schedule context. - * - * @param context The schedule context to filter by - * @return List of conditional requirements for the given context - */ - public List getConditionalItemRequirements(TaskContext context) { - ensureCacheValid(); - return conditionalItemRequirementsCache.getOrDefault(context, new LinkedHashSet<>()).stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(Collectors.toList()); - } - - /** - * Gets external conditional item requirements for a specific schedule context. - * - * @param context The schedule context to filter by - * @return List of external conditional requirements for the given context - */ - public List getExternalConditionalItemRequirements(TaskContext context) { - ensureExternalCacheValid(); - return externalIConditionalItemRequirementsCache.getOrDefault(context, new LinkedHashSet<>()).stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(Collectors.toList()); - } - - /** - * Rebuilds external requirement caches from the external requirements storage. - */ - private synchronized void rebuildExternalCache() { - if (externalCacheValid) { - return; // Another thread already rebuilt the cache - } - - if(externalRequirements == null || externalRequirements.isEmpty()) { - log.debug("No external requirements to rebuild cache for - initializing empty external caches"); - // Initialize empty external caches when no requirements exist - externalEquipmentItemsCache = new HashMap<>(); - externalShopRequirementsCache = new HashMap<>(); - externalLootRequirementsCache = new HashMap<>(); - externalInventorySlotRequirementsCache = new HashMap<>(); - externalAnyInventorySlotRequirementsCache = new HashMap<>(); - externalIConditionalItemRequirementsCache = new HashMap<>(); - - // Initialize empty collections for each context - for (TaskContext context : TaskContext.values()) { - externalEquipmentItemsCache.put(context, new HashMap<>()); - externalShopRequirementsCache.put(context, new LinkedHashSet<>()); - externalLootRequirementsCache.put(context, new LinkedHashSet<>()); - externalInventorySlotRequirementsCache.put(context, new HashMap<>()); - externalAnyInventorySlotRequirementsCache.put(context, new LinkedHashSet<>()); - externalIConditionalItemRequirementsCache.put(context, new LinkedHashSet<>()); - } - - externalCacheValid = true; - return; - } - - log.debug("Rebuilding external requirement caches..."); - - // Use unified rebuild logic for external requirements - CacheSet newCaches = rebuildCacheUnified(externalRequirements.values(), "external"); - - // Atomically update external caches - this.externalEquipmentItemsCache = newCaches.equipmentCache; - this.externalShopRequirementsCache = newCaches.shopCache; - this.externalLootRequirementsCache = newCaches.lootCache; - this.externalInventorySlotRequirementsCache = newCaches.inventorySlotCache; - this.externalAnyInventorySlotRequirementsCache = newCaches.anyInventorySlotCache; - this.externalIConditionalItemRequirementsCache = newCaches.conditionalCache; - - externalCacheValid = true; - log.debug("Rebuilt external requirement caches with {} external requirements", externalRequirements.size()); - } - - /** - * Gets item requirements separated into equipment and inventory categories with detailed slot breakdowns. - * This method uses the processed logical requirements from the caches to provide accurate counts - * that reflect OR requirement grouping (e.g., pickaxes grouped as one requirement). - * - * @param context The schedule context to filter by - * @return A breakdown containing detailed slot-by-slot requirement analysis - */ - public RequirementBreakdown getItemRequirementBreakdown(TaskContext context) { - // Ensure caches are valid - ensureCacheValid(); - - Map> equipmentSlotBreakdown = new LinkedHashMap<>(); - Map> inventorySlotBreakdown = new LinkedHashMap<>(); - - // Process equipment logical requirements by slot - Map> contextEquipmentCache = equipmentItemsCache.get(context); - if (contextEquipmentCache != null) { - for (Map.Entry> entry : contextEquipmentCache.entrySet()) { - EquipmentInventorySlot slot = entry.getKey(); - LinkedHashSet logicalReqs = entry.getValue(); - - Map slotCounts = new EnumMap<>(RequirementPriority.class); - slotCounts.put(RequirementPriority.MANDATORY, 0); - slotCounts.put(RequirementPriority.RECOMMENDED, 0); - slotCounts.put(RequirementPriority.RECOMMENDED, 0); - - // Count logical requirements (not individual items) by priority - for (ItemRequirement logicalReq : logicalReqs) { - if (logicalReq.getTaskContext() == context || logicalReq.getTaskContext() == TaskContext.BOTH) { - RequirementPriority priority = logicalReq.getPriority(); - slotCounts.put(priority, slotCounts.get(priority) + 1); - } - } - - // Only add slots that have requirements - if (slotCounts.values().stream().anyMatch(count -> count > 0)) { - equipmentSlotBreakdown.put(slot, slotCounts); - } - } - } - - // Process inventory logical requirements by slot (EXCLUDE -1 slot for "any slot" items) - Map> contextInventoryCache = inventorySlotRequirementsCache.get(context); - if (contextInventoryCache != null) { - for (Map.Entry> entry : contextInventoryCache.entrySet()) { - int slot = entry.getKey(); - LinkedHashSet logicalReqs = entry.getValue(); - - // Skip -1 slot (any slot items) as requested - if (slot == -1) { - continue; - } - - Map slotCounts = new EnumMap<>(RequirementPriority.class); - slotCounts.put(RequirementPriority.MANDATORY, 0); - slotCounts.put(RequirementPriority.RECOMMENDED, 0); - - // Count logical requirements (not individual items) by priority - for (ItemRequirement itemReq : logicalReqs) { - if (itemReq.getTaskContext() == context || itemReq.getTaskContext() == TaskContext.BOTH) { - RequirementPriority priority = itemReq.getPriority(); - slotCounts.put(priority, slotCounts.get(priority) + 1); - } - } - - // Only add slots that have requirements - if (slotCounts.values().stream().anyMatch(count -> count > 0)) { - inventorySlotBreakdown.put(slot, slotCounts); - } - } - } - - return new RequirementBreakdown(equipmentSlotBreakdown, inventorySlotBreakdown, getConditionalItemRequirements(context)); - } - - /** - * Counts requirements by priority for a specific type and context. - * - * @param clazz The requirement class to count - * @param context The schedule context to filter by - * @return A map of Priority to count - */ - public Map countRequirementsByPriority(Class clazz, TaskContext context) { - return getRequirements(clazz, context).stream() - .collect(Collectors.groupingBy( - Requirement::getPriority, - Collectors.counting() - )); - } - - /** - * Helper class to hold detailed slot-by-slot requirement breakdowns with priority counts. - */ - public static class RequirementBreakdown { - private final Map> equipmentSlotBreakdown; - private final Map> inventorySlotBreakdown; - private final List conditionalRequirements; - - public RequirementBreakdown( - Map> equipmentSlotBreakdown, - Map> inventorySlotBreakdown, - List conditionalRequirements) { - this.equipmentSlotBreakdown = new LinkedHashMap<>(equipmentSlotBreakdown); - this.inventorySlotBreakdown = new LinkedHashMap<>(inventorySlotBreakdown); - this.conditionalRequirements = conditionalRequirements != null ? new ArrayList<>(conditionalRequirements) : List.of(); - } - - /** - * Gets the detailed breakdown of equipment slots. - * @return Map of equipment slot to priority counts - */ - public Map> getEquipmentSlotBreakdown() { - return new LinkedHashMap<>(equipmentSlotBreakdown); - } - - /** - * Gets the detailed breakdown of inventory slots. - * @return Map of inventory slot to priority counts - */ - public Map> getInventorySlotBreakdown() { - return new LinkedHashMap<>(inventorySlotBreakdown); - } - - /** - * Gets total count of equipment requirements by priority across all slots. - */ - public long getEquipmentCount(RequirementPriority priority) { - return equipmentSlotBreakdown.values().stream() - .mapToLong(slotCounts -> slotCounts.getOrDefault(priority, 0)) - .sum(); - } - - /** - * Gets total count of inventory requirements by priority across all slots. - */ - public long getInventoryCount(RequirementPriority priority) { - return inventorySlotBreakdown.values().stream() - .mapToLong(slotCounts -> slotCounts.getOrDefault(priority, 0)) - .sum(); - } - - /** - * Gets total count of all equipment requirements across all slots. - */ - public long getTotalEquipmentCount() { - return equipmentSlotBreakdown.values().stream() - .mapToLong(slotCounts -> slotCounts.values().stream().mapToInt(Integer::intValue).sum()) - .sum(); - } - - /** - * Gets total count of all inventory requirements across all slots. - */ - public long getTotalInventoryCount() { - return inventorySlotBreakdown.values().stream() - .mapToLong(slotCounts -> slotCounts.values().stream().mapToInt(Integer::intValue).sum()) - .sum(); - } - - /** - * Gets a detailed string representation of the breakdown showing slot-by-slot details, including conditional requirements. - */ - public String getDetailedBreakdownString() { - StringBuilder sb = new StringBuilder(); - sb.append("=== DETAILED REQUIREMENT BREAKDOWN ===\n"); - - if (!equipmentSlotBreakdown.isEmpty()) { - sb.append("Equipment Slots:\n"); - for (Map.Entry> entry : equipmentSlotBreakdown.entrySet()) { - EquipmentInventorySlot slot = entry.getKey(); - Map counts = entry.getValue(); - sb.append(String.format(" %s: M=%d, R=%d\n", - slot.name(), - counts.getOrDefault(RequirementPriority.MANDATORY, 0), - counts.getOrDefault(RequirementPriority.RECOMMENDED, 0))); - } - } - - if (!inventorySlotBreakdown.isEmpty()) { - sb.append("Inventory Slots:\n"); - for (Map.Entry> entry : inventorySlotBreakdown.entrySet()) { - Integer slot = entry.getKey(); - Map counts = entry.getValue(); - sb.append(String.format(" Slot %d: M=%d, R=%d\n", - slot, - counts.getOrDefault(RequirementPriority.MANDATORY, 0), - counts.getOrDefault(RequirementPriority.RECOMMENDED, 0))); - } - } - - // Conditional requirements statistics - if (!conditionalRequirements.isEmpty()) { - sb.append("\nConditional Requirements Statistics:\n"); - int totalSteps = 0; - int totalItemReqs = 0; - int totalLogicalReqs = 0; - for (ConditionalRequirement conditionalReq : conditionalRequirements) { - sb.append(" ").append(conditionalReq.getName()).append(":\n"); - int stepIdx = 0; - for (var step : conditionalReq.getSteps()) { - totalSteps++; - sb.append(String.format(" Step %d: %s\n", stepIdx++, step.getDescription())); - var req = step.getRequirement(); - if (req instanceof LogicalRequirement) { - totalLogicalReqs++; - sb.append(" LogicalRequirement\n"); - } else if (req instanceof ItemRequirement) { - totalItemReqs++; - sb.append(" ItemRequirement\n"); - } else { - sb.append(" OtherRequirement\n"); - } - } - } - sb.append(String.format(" Total Conditional Steps: %d\n", totalSteps)); - sb.append(String.format(" Total LogicalRequirements: %d\n", totalLogicalReqs)); - sb.append(String.format(" Total ItemRequirements: %d\n", totalItemReqs)); - } - - return sb.toString(); - } - - /** - * Gets count of requirements for a specific equipment slot and priority. - */ - public int getEquipmentSlotCount(EquipmentInventorySlot slot, RequirementPriority priority) { - return equipmentSlotBreakdown.getOrDefault(slot, Map.of()).getOrDefault(priority, 0); - } - - /** - * Gets count of requirements for a specific inventory slot and priority. - */ - public int getInventorySlotCount(int slot, RequirementPriority priority) { - return inventorySlotBreakdown.getOrDefault(slot, Map.of()).getOrDefault(priority, 0); - } - public boolean isEmpty() { - return equipmentSlotBreakdown.isEmpty() && inventorySlotBreakdown.isEmpty(); - } - } - - - - /** - * Provides a comprehensive string representation of the entire registry. - * Shows all requirements organized by type, slot, and schedule context with proper formatting. - * - * @return A detailed string representation of the registry - */ - @Override - public String toString() { - return getDetailedRegistryString(); - } - - /** - * Gets a detailed string representation of the entire registry. - * Organizes requirements by type and provides clear structure with proper indentation. - * - * @return A comprehensive string showing all registered requirements - */ - public String getDetailedRegistryString() { - StringBuilder sb = new StringBuilder(); - - sb.append("=== Requirement Registry Summary ===\n"); - sb.append("Total Requirements: ").append(requirements.size()).append("\n"); - sb.append("Cache Valid: ").append(cacheValid).append("\n\n"); - - if (requirements.isEmpty()) { - sb.append("No requirements registered.\n"); - return sb.toString(); - } - - // Ensure cache is rebuilt for accurate display - ensureCacheValid(); - // Add detailed breakdown for conditional requirements just before returning - if (!conditionalItemRequirementsCache.isEmpty()) { - sb.append("\n=== CONDITIONAL REQUIREMENTS BREAKDOWN ===\n"); - for (Map.Entry> contextEntry : conditionalItemRequirementsCache.entrySet()) { - if (!contextEntry.getValue().isEmpty()) { - sb.append("Context: ").append(contextEntry.getKey()).append("\n"); - for (ConditionalRequirement conditionalReq : contextEntry.getValue()) { - sb.append(" ").append(formatConditionalRequirement(conditionalReq, " ")).append("\n"); - } - } - } - } - - // Display Equipment Requirements by Slot (per context) - sb.append("=== EQUIPMENT REQUIREMENTS BY SLOT ===\n"); - if (equipmentItemsCache.isEmpty()) { - sb.append("\tNo equipment requirements registered.\n"); - } else { - for (Map.Entry>> contextEntry : equipmentItemsCache.entrySet()) { - if (!contextEntry.getValue().isEmpty()) { - sb.append("\tContext: ").append(contextEntry.getKey()).append("\n"); - for (Map.Entry> slotEntry : contextEntry.getValue().entrySet()) { - sb.append("\t\t").append(slotEntry.getKey().name()).append(":\n"); - for (ItemRequirement itemReq : slotEntry.getValue()) { - sb.append("\t\t\t").append(itemReq.getName()).append(" (ID: ").append(itemReq.getId()).append(")\n"); - } - } - } - } - } - sb.append("\n"); - - // Display Inventory Slot Requirements - sb.append("=== INVENTORY SLOT REQUIREMENTS (PRE-Schedule)===\n"); - Map inventorySlotCache = getInventorySlotLogicalRequirements(TaskContext.PRE_SCHEDULE); - if (inventorySlotCache.isEmpty()) { - sb.append("\tNo specific inventory slot requirements registered.\n"); - } else { - for (Map.Entry entry : inventorySlotCache.entrySet()) { - String slotName = entry.getKey() == -1 ? "ANY_SLOT" : "SLOT_" + entry.getKey(); - sb.append("\t").append(slotName).append(":\n"); - sb.append("\t\t").append(formatLogicalRequirement(entry.getValue(), "\t\t")).append("\n"); - } - } - sb.append("\n"); - - // Display Shop Requirements - sb.append("=== SHOP REQUIREMENTS ===\n"); - boolean hasShop = false; - for (Map.Entry> contextEntry : shopRequirementsCache.entrySet()) { - if (!contextEntry.getValue().isEmpty()) { - hasShop = true; - sb.append("\tContext: ").append(contextEntry.getKey()).append("\n"); - for (OrRequirement orReq : contextEntry.getValue()) { - sb.append("\t\t").append(formatLogicalRequirement(orReq, "\t\t")).append("\n"); - } - } - } - if (!hasShop) { - sb.append("\tNo shop requirements registered.\n"); - } - sb.append("\n"); - - // Display Loot Requirements - sb.append("=== LOOT REQUIREMENTS ===\n"); - boolean hasLoot = false; - for (Map.Entry> contextEntry : lootRequirementsCache.entrySet()) { - if (!contextEntry.getValue().isEmpty()) { - hasLoot = true; - sb.append("\tContext: ").append(contextEntry.getKey()).append("\n"); - for (OrRequirement orReq : contextEntry.getValue()) { - sb.append("\t\t").append(formatLogicalRequirement(orReq, "\t\t")).append("\n"); - } - } - } - if (!hasLoot) { - sb.append("\tNo loot requirements registered.\n"); - } - sb.append("\n"); - - // Display Conditional Requirements - sb.append("=== CONDITIONAL REQUIREMENTS ===\n"); - boolean hasConditional = false; - for (Map.Entry> contextEntry : conditionalItemRequirementsCache.entrySet()) { - if (!contextEntry.getValue().isEmpty()) { - hasConditional = true; - sb.append("\tContext: ").append(contextEntry.getKey()).append("\n"); - for (ConditionalRequirement conditionalReq : contextEntry.getValue()) { - sb.append("\t\t").append(formatConditionalRequirement(conditionalReq, "\t\t")).append("\n"); - } - } - } - if (!hasConditional) { - sb.append("\tNo conditional requirements registered.\n"); - } - sb.append("\n"); - - // Display Single-Instance Requirements - sb.append("=== SINGLE-INSTANCE REQUIREMENTS ===\n"); - sb.append("\tPre-Schedule Spellbook: ").append(preScheduleSpellbookRequirement != null ? - preScheduleSpellbookRequirement.getName() : "None").append("\n"); - sb.append("\tPost-Schedule Spellbook: ").append(postScheduleSpellbookRequirement != null ? - postScheduleSpellbookRequirement.getName() : "None").append("\n"); - sb.append("\tPre-Schedule Location: ").append(preScheduleLocationRequirement != null ? - preScheduleLocationRequirement.getName() : "None").append("\n"); - sb.append("\tPost-Schedule Location: ").append(postScheduleLocationRequirement != null ? - postScheduleLocationRequirement.getName() : "None").append("\n"); - - return sb.toString(); - } - - - - - - -/** - * Gets a detailed string representation of cached requirements for a specific schedule context. - * This shows only the processed logical requirements from the cache for the given context, - * providing a focused view of what will actually be fulfilled. - * - * @param context The schedule context to display (PRE_SCHEDULE or POST_SCHEDULE) - * @return A formatted string showing cached requirements for the context - */ - public String getDetailedCacheStringForContext(TaskContext context) { - StringBuilder sb = new StringBuilder(); - if (context == null) { - sb.append("Invalid context provided.\n"); - return sb.toString(); - } - sb.append("=== Cached Requirements for Context: ").append(context.name()).append(" ===\n"); - - - - // Ensure cache is rebuilt for accurate display - ensureCacheValid(); - - boolean hasAnyRequirements = false; - - // Display Equipment Requirements by Slot for this context - sb.append("=== EQUIPMENT REQUIREMENTS BY SLOT ===\n"); - Map> equipmentCache = getEquipmentRequirements(context); - if (equipmentCache.isEmpty()) { - sb.append("\tNo equipment requirements for context: ").append(context.name()).append("\n"); - } else { - hasAnyRequirements = true; - for (Map.Entry> entry : equipmentCache.entrySet()) { - sb.append("\t").append(entry.getKey().name()).append(":\n"); - for (ItemRequirement itemReq : entry.getValue()) { - sb.append("\t\t").append(itemReq.getName()).append(" (ID: ").append(itemReq.getId()).append(")\n"); - } - } - } - sb.append("\n"); - - // Display Inventory Slot Requirements for this context - sb.append("=== INVENTORY SLOT REQUIREMENTS ===\n"); - Map inventorySlotCache = getInventorySlotLogicalRequirements(context); - if (inventorySlotCache.isEmpty()) { - sb.append("\tNo inventory slot requirements for context: ").append(context.name()).append("\n"); - } else { - hasAnyRequirements = true; - for (Map.Entry entry : inventorySlotCache.entrySet()) { - String slotName = entry.getKey() == -1 ? "ANY_SLOT" : "SLOT_" + entry.getKey(); - sb.append("\t").append(slotName).append(":\n"); - sb.append("\t\t").append(formatLogicalRequirement(entry.getValue(), "\t\t")).append("\n"); - } - } - sb.append("\n"); - - // Display Shop Requirements for this context - sb.append("=== SHOP REQUIREMENTS ===\n"); - LinkedHashSet shopCache = getShopRequirements(context); - if (shopCache.isEmpty()) { - sb.append("\tNo shop requirements for context: ").append(context.name()).append("\n"); - } else { - hasAnyRequirements = true; - for (OrRequirement logicalReq : shopCache) { - sb.append("\t").append(formatLogicalRequirement(logicalReq, "\t")).append("\n"); - } - } - sb.append("\n"); - - // Display Loot Requirements for this context - sb.append("=== LOOT REQUIREMENTS ===\n"); - LinkedHashSet lootCache = getLootRequirements(context); - if (lootCache.isEmpty()) { - sb.append("\tNo loot requirements for context: ").append(context.name()).append("\n"); - } else { - hasAnyRequirements = true; - for (OrRequirement logicalReq : lootCache) { - sb.append("\t").append(formatLogicalRequirement(logicalReq, "\t")).append("\n"); - } - } - sb.append("\n"); - - // Display Conditional Requirements for this context - sb.append("=== CONDITIONAL REQUIREMENTS ===\n"); - List conditionalReqs = getConditionalItemRequirements(context); - if (conditionalReqs.isEmpty()) { - sb.append("\tNo conditional requirements for context: ").append(context.name()).append("\n"); - } else { - hasAnyRequirements = true; - for (ConditionalRequirement conditionalReq : conditionalReqs) { - sb.append(formatConditionalRequirement(conditionalReq, "\t")).append("\n"); - } - } - sb.append("\n"); - - - - // Display Single-Instance Requirements for this context - sb.append("=== SINGLE-INSTANCE REQUIREMENTS ===\n"); - boolean hasSpellbook = false; - boolean hasLocation = false; - - if (context == TaskContext.PRE_SCHEDULE || context == TaskContext.BOTH) { - if (preScheduleSpellbookRequirement != null) { - sb.append("\tPre-Schedule Spellbook: ").append(preScheduleSpellbookRequirement.getName()).append("\n"); - hasSpellbook = true; - hasAnyRequirements = true; - } - if (preScheduleLocationRequirement != null) { - sb.append("\tPre-Schedule Location: ").append(preScheduleLocationRequirement.getName()).append("\n"); - hasLocation = true; - hasAnyRequirements = true; - } - } - - if (context == TaskContext.POST_SCHEDULE || context == TaskContext.BOTH) { - if (postScheduleSpellbookRequirement != null) { - sb.append("\tPost-Schedule Spellbook: ").append(postScheduleSpellbookRequirement.getName()).append("\n"); - hasSpellbook = true; - hasAnyRequirements = true; - } - if (postScheduleLocationRequirement != null) { - sb.append("\tPost-Schedule Location: ").append(postScheduleLocationRequirement.getName()).append("\n"); - hasLocation = true; - hasAnyRequirements = true; - } - } - - if (!hasSpellbook && !hasLocation) { - sb.append("\tNo single-instance requirements for context: ").append(context.name()).append("\n"); - } - sb.append("\n"); - - // Summary - if (!hasAnyRequirements) { - sb.append("=== SUMMARY ===\n"); - sb.append("No cached requirements found for context: ").append(context.name()).append("\n"); - } - - return sb.toString(); - } - - /** - * Formats a ConditionalRequirement for display, including all steps, their conditions, and requirements. - * @param conditionalReq The ConditionalRequirement to format - * @param indent The indentation prefix - * @return A formatted string representation - */ - private String formatConditionalRequirement(ConditionalRequirement conditionalReq, String indent) { - StringBuilder sb2 = new StringBuilder(); - sb2.append(conditionalReq.getName()) - .append(" [Parallel: ").append(conditionalReq.isAllowParallelExecution()) - .append("]\n"); - int stepIdx = 0; - for (var step : conditionalReq.getSteps()) { - boolean conditionMet = false; - try { conditionMet = step.needsExecution(); } catch (Throwable t) { conditionMet = false; } - sb2.append(indent).append("Step ").append(stepIdx++).append(": ") - .append(step.getDescription()) - .append(" [ConditionMet: ").append(conditionMet) - .append(", Optional: ").append(step.isOptional()).append("]\n"); - // Show the requirement for this step - Requirement req = step.getRequirement(); - if (req instanceof LogicalRequirement) { - sb2.append(indent).append(" ").append(formatLogicalRequirement((LogicalRequirement)req, indent + " ")).append("\n"); - } else { - sb2.append(indent).append(" ").append(formatSingleRequirement(req)).append("\n"); - } - } - return sb2.toString(); - } - /** - * Formats a logical requirement for display with proper indentation. - * - * @param logicalReq The logical requirement to format - * @param indent The indentation prefix - * @return A formatted string representation - */ - private String formatLogicalRequirement(LogicalRequirement logicalReq, String indent) { - StringBuilder sb = new StringBuilder(); - sb.append(logicalReq.getClass().getSimpleName()).append(": "); - - if (logicalReq instanceof OrRequirement) { - OrRequirement orReq = (OrRequirement) logicalReq; - sb.append("(").append(orReq.getChildRequirements().size()).append(" options)"); - for (Requirement childReq : orReq.getChildRequirements()) { - sb.append("\n").append(indent).append("\t- ").append(formatSingleRequirement(childReq)); - } - } else { - sb.append(formatSingleRequirement(logicalReq)); - } - - return sb.toString(); - } - - /** - * Formats a single requirement for display. - * - * @param req The requirement to format - * @return A formatted string representation - */ - private String formatSingleRequirement(Requirement req) { - if (req instanceof ItemRequirement) { - ItemRequirement itemReq = (ItemRequirement) req; - return String.format("%s (id:%d amount:%d)[%s, Rating: %d] - %s", - itemReq.getName(), - itemReq.getId(), - itemReq.getAmount(), - itemReq.getPriority().name(), - itemReq.getRating(), - itemReq.getTaskContext().name()); - } - return String.format("%s [%s, Rating: %d] - %s", - req.getName(), - req.getPriority().name(), - req.getRating(), - req.getTaskContext().name()); - } - - /** - * Gets all ConditionalRequirements for a specific schedule context that are NOT just item requirements alone. - * This is used to process only 'mixed' or complex conditional requirements (not simple item wrappers). - * - * @param context The schedule context to filter by - * @return List of ConditionalRequirements that are not just item requirements - */ - public List getMixedConditionalRequirements(TaskContext context) { - List all = getRequirements(ConditionalRequirement.class, context); - List mixed = new ArrayList<>(); - for (ConditionalRequirement req : all) { - if (!req.containsOnlyItemRequirements()) { - mixed.add(req); - } - } - return mixed; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/InventorySetupRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/InventorySetupRequirement.java deleted file mode 100644 index e406784321f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/InventorySetupRequirement.java +++ /dev/null @@ -1,172 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.util.Rs2InventorySetup; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -import java.util.Collections; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Delayed; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; -/** - * Inventory setup requirement that loads a specific inventory setup using Rs2InventorySetup. - * This allows plugins to use predefined inventory configurations instead of progressive equipment management. - */ -@Slf4j -public class InventorySetupRequirement extends Requirement { - - private final String inventorySetupName; - private final boolean bankItemsNotInSetup; - /** - * Creates an inventory setup requirement. - * - * @param inventorySetupName Name of the inventory setup to load - * @param taskContext When to apply this requirement (PRE_SCHEDULE, POST_SCHEDULE, BOTH) - * @param priority Priority level (MANDATORY, RECOMMENDED, OPTIONAL) - * @param rating Effectiveness rating 1-10 - * @param description Human-readable description - */ - public InventorySetupRequirement(String inventorySetupName, TaskContext taskContext, - RequirementPriority priority, int rating, String description, boolean bankItemsNotInSetup) { - super(RequirementType.CUSTOM, priority, rating, description, Collections.emptyList(), taskContext); - this.inventorySetupName = inventorySetupName; - this.bankItemsNotInSetup = bankItemsNotInSetup; - - } - - @Override - public String getName() { - return "Inventory Setup: " + inventorySetupName; - } - - @Override - public boolean fulfillRequirement(CompletableFuture scheduledFuture) { - try { - if (inventorySetupName == null || inventorySetupName.trim().isEmpty()) { - log.warn("Inventory setup name is empty, skipping requirement"); - return !isMandatory(); // fail only if mandatory - } - - log.info("Loading inventory setup: {}", inventorySetupName); - // check if setup was successful by waiting for completion - // rs2inventorysetup handles its own validation and timeout - return execute(scheduledFuture); - - } catch (Exception e) { - log.error("Failed to load inventory setup '{}': {}", inventorySetupName, e.getMessage()); - return !isMandatory(); // only fail if mandatory - } - } - private boolean execute(CompletableFuture scheduledFuture){ - try { - log.info("\n\t-Executing plan using Rs2InventorySetup approach: {}", inventorySetupName); - - // Convert CompletableFuture to ScheduledFuture (simplified conversion) - ScheduledFuture mainScheduler = new ScheduledFuture() { - @Override - public long getDelay(TimeUnit unit) { return 0; } - @Override - public int compareTo(Delayed o) { return 0; } - @Override - public boolean cancel(boolean mayInterruptIfRunning) { return scheduledFuture.cancel(mayInterruptIfRunning); } - @Override - public boolean isCancelled() { return scheduledFuture.isCancelled(); } - @Override - public boolean isDone() { return scheduledFuture.isDone(); } - @Override - public Object get() throws InterruptedException, ExecutionException { return scheduledFuture.get(); } - @Override - public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { return scheduledFuture.get(timeout, unit); } - }; - if (!Rs2InventorySetup.isInventorySetup(inventorySetupName)) { - log.error("Failed to create Rs2InventorySetup"); - return false; - } - Rs2InventorySetup rs2Setup = new Rs2InventorySetup(inventorySetupName, mainScheduler); - - - if(rs2Setup.doesEquipmentMatch() && rs2Setup.doesInventoryMatch()){ - log.info("Plan already matches current inventory and equipment setup, skipping execution"); - return true; // No need to execute if already matches - } - if (!Rs2Bank.isOpen()) { - if (!Rs2Bank.walkToBankAndUseBank() && !Rs2Player.isInteracting() && !Rs2Player.isMoving()) { - log.error("\n\tFailed to open bank for comprehensive item management"); - } - boolean openBank= sleepUntil(() -> Rs2Bank.isOpen(), 5000); - if (!openBank) { - log.error("\n\tFailed to open bank within timeout period,for invntory setup execution \"{}\"", inventorySetupName); - return false; - } - } - - // Bank items not in setup first if requested (excludes teleport items) - if (bankItemsNotInSetup) { - log.info("Banking items not in setup (excluding teleport items) before setting up: {}", inventorySetupName); - if (!rs2Setup.bankAllItemsNotInSetup(true)) { - log.warn("Failed to bank all items not in setup, continuing with setup anyway"); - } - } - // Use existing Rs2InventorySetup methods to fulfill the requirements - boolean equipmentSuccess = rs2Setup.loadEquipment(); - if (!equipmentSuccess) { - log.error("Failed to load equipment using Rs2InventorySetup"); - return false; - } - - boolean inventorySuccess = rs2Setup.loadInventory(); - if (!inventorySuccess) { - log.error("Failed to load inventory using Rs2InventorySetup"); - return false; - } - - // Verify the setup matches - boolean equipmentMatches = rs2Setup.doesEquipmentMatch(); - boolean inventoryMatches = rs2Setup.doesInventoryMatch(); - - if (equipmentMatches && inventoryMatches) { - log.info("Successfully executed plan using Rs2InventorySetup: {}", inventorySetupName); - return true; - } else { - log.warn("Plan execution completed but setup verification failed. Equipment matches: {}, Inventory matches: {}", - equipmentMatches, inventoryMatches); - return false; - } - - } catch (Exception e) { - log.error("Failed to execute plan using Rs2InventorySetup: {}", e.getMessage(), e); - return false; - } - } - - @Override - public boolean isFulfilled() { - // inventory setup is more of an action than a state to check - // we consider it fulfilled if the setup name is valid - return inventorySetupName != null && !inventorySetupName.trim().isEmpty(); - } - - @Override - public String getUniqueIdentifier() { - return String.format("%s:INVENTORY_SETUP:%s", - requirementType.name(), - inventorySetupName != null ? inventorySetupName : "null"); - } - - /** - * Gets the inventory setup name for this requirement. - * - * @return The inventory setup name - */ - public String getInventorySetupName() { - return inventorySetupName; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/Requirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/Requirement.java deleted file mode 100644 index 5bf9907e999..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/Requirement.java +++ /dev/null @@ -1,235 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement; - -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; - -import java.util.List; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -/** - * Abstract base class for all requirement types in the plugin scheduler system. - * This class defines common properties and behaviors for all requirements. - */ -@Getter -@AllArgsConstructor -@Slf4j -public abstract class Requirement implements Comparable { - - /** - * The type of requirement (equipment, inventory, player state, etc.) - */ - protected final RequirementType requirementType; - - /** - * Priority level of this requirement for plugin functionality. - */ - protected final RequirementPriority priority; - - /** - * Effectiveness rating from 1-10 (10 being most effective). - * Used for comparison when multiple valid options are available. - */ - protected int rating; - - /** - * Human-readable description explaining the purpose and effectiveness. - * Should include context about why this requirement is useful for the specific plugin/activity. - */ - protected final String description; - - /** - * List of identifiers for this requirement. - * These could be item IDs, NPC IDs, object IDs, varbit IDs, etc. depending on the requirement type. - * For items, this can represent multiple alternative item IDs that satisfy the same requirement. - * Can be empty if not applicable. - */ - protected List ids; - - /** - * Context for when this requirement should be fulfilled. - * PRE_SCHEDULE means before script execution, POST_SCHEDULE means after completion. - * Can be null for requirements that don't have schedule-specific behavior. - */ - @Setter - protected TaskContext taskContext; - - - /** - * Gets the human-readable name of this requirement. - * This is used for display purposes in overlays and logging. - * - * @return The name of this requirement - */ - public abstract String getName(); - - /** - * Abstract method to fulfill this requirement. - * Each requirement type implements its own fulfillment logic. - * - * @param scheduledFuture The CompletableFuture for cancellation support - * @return true if the requirement was fulfilled successfully, false otherwise - */ - public abstract boolean fulfillRequirement(CompletableFuture scheduledFuture); - /** - * Executes this step's requirement with timeout support. - * - * @param scheduledFuture The CompletableFuture for cancellation support - * @param timeoutSeconds Maximum time allowed for this step - * @return true if successfully fulfilled, false otherwise - */ - public boolean fulfillRequirementWithTimeout(CompletableFuture scheduledFuture, long timeoutSeconds) { - try { - log.debug("Executing ordered step with {}s timeout: {}", timeoutSeconds, description); - - CompletableFuture stepFuture = CompletableFuture.supplyAsync(() -> - fulfillRequirement(scheduledFuture) - ); - - return stepFuture.get(timeoutSeconds, TimeUnit.SECONDS); - } catch (TimeoutException e) { - log.error("Step '{}' timed out after {} seconds", description, timeoutSeconds); - return !isMandatory(); - } catch (Exception e) { - log.error("Error executing ordered step '{}': {}", description, e.getMessage()); - return !isMandatory(); - } - } - /** - * Checks if this requirement is currently fulfilled. - * This is a convenience method that calls fulfillRequirement() for consistency - * with the condition system and logical requirements. - * - * @return true if the requirement is fulfilled, false otherwise - */ - public abstract boolean isFulfilled(); - - - /** - * Compare requirements based on priority first, then rating. - * This allows sorting requirements by importance. - * - * @param other The requirement to compare with - * @return A negative value if this requirement is more important, zero if equally important, - * or a positive value if less important - */ - @Override - public int compareTo(Requirement other) { - // First compare by priority (MANDATORY > RECOMMENDED > OPTIONAL) - int priorityComparison = other.getPriority().ordinal() - this.getPriority().ordinal(); - if (priorityComparison != 0) { - return priorityComparison; - } - - // If same priority, compare by rating (higher rating is better) - return other.getRating() - this.getRating(); - } - - /** - * Check if this is a mandatory requirement. - * - * @return true if this requirement has MANDATORY priority, false otherwise - */ - public boolean isMandatory() { - return priority == RequirementPriority.MANDATORY; - } - - /** - * Check if this is a recommended requirement. - * - * @return true if this requirement has RECOMMENDED priority, false otherwise - */ - public boolean isRecommended() { - return priority == RequirementPriority.RECOMMENDED; - } - - - - /** - * Check if this requirement should be fulfilled before script execution. - * - * @return true if this requirement has PRE_SCHEDULE or BOTH context, false otherwise - */ - public boolean isPreSchedule() { - return taskContext == TaskContext.PRE_SCHEDULE || taskContext == TaskContext.BOTH; - } - - /** - * Check if this requirement should be fulfilled after script completion. - * - * @return true if this requirement has POST_SCHEDULE or BOTH context, false otherwise - */ - public boolean isPostSchedule() { - return taskContext == TaskContext.POST_SCHEDULE || taskContext == TaskContext.BOTH; - } - - /** - * Check if this requirement has a specific schedule context task set. - * Since TaskContext is never null (defaults to BOTH), this always returns true. - * - * @return true always, as all requirements have a schedule context - */ - public boolean hasTaskContext() { - return taskContext != null; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || getClass() != obj.getClass()) return false; - - Requirement that = (Requirement) obj; - return rating == that.rating && - Objects.equals(requirementType, that.requirementType) && - Objects.equals(priority, that.priority) && - Objects.equals(description, that.description) && - Objects.equals(ids, that.ids) && - Objects.equals(taskContext, that.taskContext); - } - - @Override - public int hashCode() { - return Objects.hash(requirementType, priority, rating, description, ids, taskContext); - } - - /** - * Returns a multi-line display string with detailed information about this requirement. - * Uses StringBuilder with tabs for proper formatting. - * - * @return A formatted string containing requirement details - */ - public String displayString() { - StringBuilder sb = new StringBuilder(); - sb.append("=== Requirement Details ===\n"); - sb.append("Name:\t\t").append(getName()).append("\n"); - sb.append("Type:\t\t").append(requirementType.name()).append("\n"); - sb.append("Priority:\t").append(priority.name()).append("\n"); - sb.append("Rating:\t\t").append(rating).append("/10\n"); - sb.append("Schedule:\t").append(taskContext.name()).append("\n"); - sb.append("IDs:\t\t").append(ids != null ? ids.toString() : "[]").append("\n"); - sb.append("Description:\t").append(description != null ? description : "No description").append("\n"); - return sb.toString(); - } - - /** - * Gets a unique identifier for this requirement. - * This is used by the RequirementRegistry to ensure uniqueness. - * The default implementation uses the requirement type, description, and IDs. - * Subclasses can override this for more specific uniqueness logic. - * - * @return A unique identifier string for this requirement - */ - public String getUniqueIdentifier() { - return String.format("%s:%s:%s", - requirementType.name(), - description != null ? description.hashCode() : "null", - ids != null ? ids.hashCode() : "null"); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/SpellbookRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/SpellbookRequirement.java deleted file mode 100644 index fd990e07028..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/SpellbookRequirement.java +++ /dev/null @@ -1,520 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import lombok.EqualsAndHashCode; -import net.runelite.api.gameval.ItemID; -import net.runelite.api.gameval.VarbitID; -import net.runelite.api.Quest; -import net.runelite.api.QuestState; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.shortestpath.Transport; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.magic.Rs2Spellbook; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; -import net.runelite.client.plugins.microbot.util.walker.TransportRouteAnalysis; - -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -import org.slf4j.event.Level; - -/** - * Enhanced SpellbookRequirement that extends from the base Requirement class. - * Integrates with the RequirementCollection system and provides comprehensive - * spellbook switching functionality. - * - * This requirement manages spellbook switching operations including: - * - Checking current spellbook state - * - Determining if switching is required before/after script execution - * - Handling different switching methods (altar prayer, NPC dialogue, Magic cape) - * - Managing travel to switching locations - * - Restoring original spellbook after completion - */ -@Slf4j -@EqualsAndHashCode(callSuper = true) -public class SpellbookRequirement extends Requirement { - - - - @Getter - private final Rs2Spellbook requiredSpellbook; // Spellbook required for this task - - - /** - * Full constructor for SpellbookRequirement - * - * @param requiredSpellbook The spellbook required for optimal plugin performance - * @param TaskContext When this spellbook requirement should be applied (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param priority Priority level of this requirement - * @param rating Effectiveness rating (1-10, 10 being most effective) - * @param description Human-readable description of the requirement - */ - public SpellbookRequirement( - Rs2Spellbook requiredSpellbook, - TaskContext taskContext, - RequirementPriority priority, - int rating, - String description) { - - super(RequirementType.GAME_CONFIG, - priority, - rating, - description != null ? description : generateDefaultDescription(requiredSpellbook, taskContext), - requiredSpellbook != null ? Collections.singletonList(requiredSpellbook.getValue()) : Collections.emptyList(), - taskContext); - - this.requiredSpellbook = requiredSpellbook; - - } - - /** - * Simplified constructor with default settings (applies to both pre and post schedule) - * - * @param requiredSpellbook The spellbook required for the task - * @param priority Priority level of this requirement - * @param rating Effectiveness rating (1-10) - */ - public SpellbookRequirement(Rs2Spellbook requiredSpellbook, TaskContext taskContext, RequirementPriority priority, int rating) { - this(requiredSpellbook, taskContext, priority, rating, null); - } - - /** - * Constructor for specific schedule context - * - * @param requiredSpellbook The spellbook required for the task - * @param TaskContext When this requirement should be applied - */ - public SpellbookRequirement(Rs2Spellbook requiredSpellbook, TaskContext taskContext) { - this(requiredSpellbook, taskContext, RequirementPriority.MANDATORY, 10, null); - } - - - - @Override - public String getName() { - return requiredSpellbook != null ? requiredSpellbook.name() + " Spellbook" : "Unknown Spellbook"; - } - - /** - * Generate a default description based on the required spellbook and schedule context - * - * @param spellbook The required spellbook - * @param context When the requirement applies - * @return A descriptive string explaining the requirement - */ - private static String generateDefaultDescription(Rs2Spellbook spellbook, TaskContext context) { - if (spellbook == null) { - return "No specific spellbook required"; - } - - String contextDescription = ""; - switch (context) { - case PRE_SCHEDULE: - contextDescription = "before script execution"; - break; - case POST_SCHEDULE: - contextDescription = "after script completion"; - break; - case BOTH: - contextDescription = "for optimal plugin performance"; - break; - } - - return String.format("Requires %s spellbook %s. %s", - spellbook.name(), - contextDescription, - spellbook.getDescription()); - } - - /** - * Checks if the player is using the required spellbook. - * - * @return true if no spellbook is required or the player is using the required spellbook, - * false otherwise - */ - public boolean hasRequiredSpellbook() { - if (requiredSpellbook == null) { - return true; // No spellbook requirement - } - - return Rs2Spellbook.getCurrentSpellbook() == requiredSpellbook; - } - public boolean isFulfilled() { - // Check if the required spellbook is currently active - return hasRequiredSpellbook(); - } - - /** - * Checks if the required spellbook is available to the player (unlocked). - * - * @return true if the required spellbook is available, false otherwise - */ - public boolean isRequiredSpellbookAvailable() { - if (requiredSpellbook == null) { - return true; // No spellbook requirement - } - - return requiredSpellbook.isUnlocked(); - } - - /** - * Attempts to switch to the required spellbook if needed. - * This method should be called before starting the script if TaskContext includes PRE_SCHEDULE. - * - * @return true if the switch was successful or no switch was needed, false otherwise - */ - private boolean switchToRequiredSpellbook(CompletableFuture scheduledFuture) { - if (requiredSpellbook == null ){ - return true; // No switch needed - } - - if (hasRequiredSpellbook()) { - return true; // Already using required spellbook - } - - if (!isRequiredSpellbookAvailable()) { - log.error("Required spellbook {} is not unlocked, cannot switch", requiredSpellbook.name()); - return false; - } - if (!travelToSwitchLocation(requiredSpellbook)) { - log.error("Failed to travel to {} spellbook switch location at location {}", requiredSpellbook.name(), requiredSpellbook.getSwitchLocation()); - return false; - } - log.info("Switching to required spellbook: {}....", requiredSpellbook.name()); - // Use the enhanced spellbook switching functionality - return requiredSpellbook.switchTo(); - } - - /** - * Switches the player back to their original spellbook after the script completes. - * This method should be called after the script finishes if TaskContext includes POST_SCHEDULE. - * - * @return true if the switch was successful or no switch was needed, false otherwise - */ - public static boolean switchBackToSpellbook(Rs2Spellbook originalSpellbook) { - if (Microbot.getClient() == null) { - return false; - } - if (Microbot.getClient().isClientThread()) { - log.info("Please run fulfillRequirement() on a non-client thread."); - return false; - } - if (originalSpellbook == null) { - return true; // No switch needed - } - - if (Rs2Spellbook.getCurrentSpellbook() == originalSpellbook) { - return true; // Already using original spellbook - } - if (!originalSpellbook.isUnlocked()) { - log.error("Original spellbook {} is not unlocked, cannot switch back", originalSpellbook.name()); - return false; - } - if (!travelToSwitchLocation(originalSpellbook)) { - log.error("Failed to travel to {} spellbook switch location at location {}", originalSpellbook.name(), originalSpellbook.getSwitchLocation()); - return false; - } - log.info("Switching back to original spellbook: {}....", originalSpellbook.name()); - // Use the enhanced spellbook switching functionality - return originalSpellbook.switchTo(); - } - - /** - * Helper method to travel to the spellbook switching location. - * Uses intelligent pathfinding to determine whether to go directly or via bank first - * for transport items. Analyzes transport requirements and route efficiency. - * Special handling for Lunar Isle access requirements. - * - * @param targetSpellbook The spellbook to switch to - * @return true if travel was successful, false otherwise - */ - private static boolean travelToSwitchLocation(Rs2Spellbook targetSpellbook) { - WorldPoint location = targetSpellbook.getSwitchLocation(); - - if (location == null) { - Microbot.status = "No switch location defined for " + targetSpellbook.name() + " spellbook"; - return false; - } - - // Check if we're already at or very close to the target location - WorldPoint currentLocation = Rs2Player.getWorldLocation(); - if (currentLocation != null && currentLocation.distanceTo(location) <= 3) { - Microbot.status = "Already near " + targetSpellbook.name() + " spellbook switch location"; - return true; - } - - // Special handling for Lunar Isle access (Lunar spellbook) - if (targetSpellbook == Rs2Spellbook.LUNAR) { - log.info("Handling Lunar Isle access for Lunar spellbook switching"); - if (!ensureLunarIsleAccess()) { - log.error("Failed to ensure Lunar Isle access - cannot travel to Lunar spellbook location"); - return false; - } - } - - try { - // Use intelligent transport strategy to determine best route - Microbot.status = "Analyzing route to " + targetSpellbook.name() + " spellbook location..."; - - // Analyze transport requirements for the destination - List missingTransports = Rs2Walker.getTransportsForDestination(location, true); - List missingItemIds = Rs2Walker.getMissingTransportItemIds(missingTransports); - - if (!missingItemIds.isEmpty()) { - Microbot.status = String.format("Found %d missing transport items for %s spellbook location", - missingItemIds.size(), targetSpellbook.name()); - - // Compare direct vs banking routes - TransportRouteAnalysis comparison = Rs2Walker.compareRoutes(location); - - if (comparison.isDirectIsFaster()) { - Microbot.status = String.format("Direct route to %s location is faster (%s)", - targetSpellbook.name(), comparison.getAnalysis()); - } else { - Microbot.status = String.format("Banking route to %s location is more efficient (%s)", - targetSpellbook.name(), comparison.getAnalysis()); - } - } else { - Microbot.status = "No transport items needed, traveling directly to " + targetSpellbook.name() + " location"; - } - - // Execute the travel using intelligent strategy - if (!Rs2Walker.walkWithBankedTransports(location, false)) { - Microbot.status = "Failed to initiate travel to " + targetSpellbook.name() + " spellbook location"; - return false; - } - - Microbot.status = "Traveling to " + targetSpellbook.name() + " spellbook switch location"; - return true; - - } catch (Exception e) { - Microbot.status = "Error during travel planning to " + targetSpellbook.name() + " location: " + e.getMessage(); - log.warn("Error in travelToSwitchLocation for {}: {}", targetSpellbook.name(), e.getMessage()); - - // Fallback to simple walkTo - if (!Rs2Walker.walkTo(location)) { - Microbot.status = "Fallback travel failed to " + targetSpellbook.name() + " spellbook location"; - return false; - } - - return true; - } - } - - /** - * Ensures proper access to Lunar Isle for Lunar spellbook switching. - * Handles the seal of passage requirement and Fremennik Diary elite tier exception. - * - * @return true if Lunar Isle access is ensured, false otherwise - */ - private static boolean ensureLunarIsleAccess() { - try { - // check if lunar diplomacy quest is completed (prerequisite for lunar isle access) - if (Rs2Player.getQuestState(Quest.LUNAR_DIPLOMACY) != QuestState.FINISHED) { - log.error("Lunar Diplomacy quest not completed - cannot access Lunar Isle for spellbook switching"); - return false; - } - - // check fremennik elite diary completion (removes seal of passage requirement) - boolean hasFremennikElite = Microbot.getVarbitValue(VarbitID.FREMENNIK_DIARY_ELITE_COMPLETE) == 1; - if (hasFremennikElite) { - log.info("Fremennik Elite diary completed - seal of passage not required for Lunar Isle access"); - return true; // elite diary completed, no seal needed - } - - log.info("Fremennik Elite diary not completed - checking seal of passage requirement"); - - // check if seal of passage is already equipped - if (Rs2Equipment.isWearing(ItemID.LUNAR_SEAL_OF_PASSAGE)) { - log.info("Seal of passage already equipped - Lunar Isle access confirmed"); - return true; - } - - // check if seal of passage is in inventory - if (Rs2Inventory.hasItem(ItemID.LUNAR_SEAL_OF_PASSAGE)) { - log.info("Seal of passage in inventory, equipping it for Lunar Isle access"); - // equip the seal of passage - if (Rs2Inventory.interact(ItemID.LUNAR_SEAL_OF_PASSAGE, "Wear")) { - boolean equipped = sleepUntil(() -> Rs2Equipment.isWearing(ItemID.LUNAR_SEAL_OF_PASSAGE), 3000); - if (equipped) { - log.info("Successfully equipped seal of passage for Lunar Isle access"); - return true; - } else { - log.error("Failed to equip seal of passage within timeout"); - } - } else { - log.error("Failed to interact with seal of passage to equip"); - } - } - - log.info("Seal of passage not found in inventory, checking bank"); - - // try to get seal of passage from bank - if (!Rs2Bank.isOpen()) { - log.info("Opening bank to retrieve seal of passage"); - if (!Rs2Bank.walkToBankAndUseBank()) { - log.error("Failed to walk to bank and open it"); - return false; - } - - // wait for bank to open - boolean bankOpened = sleepUntil(() -> Rs2Bank.isOpen(), 5000); - if (!bankOpened) { - log.error("Failed to open bank within timeout"); - return false; - } - } - - // check if seal of passage is in bank - if (!Rs2Bank.hasItem(ItemID.LUNAR_SEAL_OF_PASSAGE)) { - log.error("Seal of passage not found in bank - cannot access Lunar Isle without Fremennik Elite diary"); - return false; - } - - log.info("Withdrawing seal of passage from bank"); - - // withdraw seal of passage - if (Rs2Bank.withdrawAllAndEquip(ItemID.LUNAR_SEAL_OF_PASSAGE)) { - if (Rs2Equipment.isWearing(ItemID.LUNAR_SEAL_OF_PASSAGE)){ - log.info("Seal of passage already equipped after withdrawal"); - Rs2Bank.closeBank(); - sleepUntil(() -> !Rs2Bank.isOpen(), 2000); - return true; - } - // wait for withdrawal to complete - boolean withdrawn = sleepUntil(() -> Rs2Inventory.hasItem(ItemID.LUNAR_SEAL_OF_PASSAGE), 3000); - if (!withdrawn) { - log.error("Failed to withdraw seal of passage from bank within timeout"); - return false; - } - - log.info("Successfully withdrew seal of passage, now equipping it"); - - // close bank first - Rs2Bank.closeBank(); - sleepUntil(() -> !Rs2Bank.isOpen(), 2000); - - // equip the seal of passage - if (Rs2Inventory.interact(ItemID.LUNAR_SEAL_OF_PASSAGE, "Wear")) { - boolean equipped = sleepUntil(() -> Rs2Equipment.isWearing(ItemID.LUNAR_SEAL_OF_PASSAGE), 3000); - if (equipped) { - log.info("Successfully equipped seal of passage for Lunar Isle access"); - return true; - } else { - log.error("Failed to equip seal of passage within timeout after withdrawal"); - } - } else { - log.error("Failed to interact with seal of passage to equip after withdrawal"); - } - } else { - log.error("Failed to withdraw seal of passage from bank"); - } - - return false; - - } catch (Exception e) { - log.error("Error ensuring Lunar Isle access: {}", e.getMessage(), e); - return false; - } - } - - - - /** - * Implements the abstract fulfillRequirement method from the base Requirement class. - * Attempts to fulfill this spellbook requirement by switching spellbooks as needed. - * - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if the requirement was successfully fulfilled, false otherwise - */ - @Override - public boolean fulfillRequirement(CompletableFuture scheduledFuture) { - try { - if (Microbot.getClient() == null) { - return false; - } - if (Microbot.getClient().isClientThread()) { - log.error("Please run fulfillRequirement() on a non-client thread."); - return false; - } - // Check if the requirement is already fulfilled - if (hasRequiredSpellbook()) { - return true; - } - - // Check if the required spellbook is available to the player - if (!isRequiredSpellbookAvailable()) { - if (isMandatory()) { - log.error("MANDATORY spellbook requirement cannot be fulfilled: " + getName() + " - Spellbook not unlocked"); - return false; - } else { - log.warn("RECOMMENDED spellbook requirement skipped: " + getName() + " - Spellbook not unlocked"); - return true; // Non-mandatory requirements return true if spellbook isn't available - } - } - - // Determine action based on schedule context - boolean success = false; - success = switchToRequiredSpellbook(scheduledFuture); - - - if (!success && isMandatory()) { - Microbot.log("MANDATORY spellbook requirement failed: " + getName()); - return false; - } - - return true; - - } catch (Exception e) { - Microbot.log("Error fulfilling spellbook requirement " + getName() + ": " + e.getMessage()); - return !isMandatory(); // Don't fail mandatory requirements due to exceptions - } - } - - /** - * Check if the required spellbook is currently available (unlocked via quest completion) - * - * @return true if the spellbook is unlocked and can be used - */ - public boolean isSpellbookUnlocked() { - return requiredSpellbook.isUnlocked(); - } - - /** - * Get the quest required to unlock this spellbook (if any) - * - * @return Quest required for spellbook access, or null if no quest required - */ - public Quest getRequiredQuest() { - return requiredSpellbook.getRequiredQuest(); - } - - /** - * Get the location where spellbook switching can be performed - * - * @return WorldPoint of the spellbook switching location - */ - public WorldPoint getSwitchLocation() { - return requiredSpellbook.getSwitchLocation(); - } - - /** - * Get a description of the spellbook switching method and location - * - * @return String describing how to switch to this spellbook - */ - public String getSwitchDescription() { - return requiredSpellbook.getDescription(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/collection/LootRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/collection/LootRequirement.java deleted file mode 100644 index efaaf6d6f1e..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/collection/LootRequirement.java +++ /dev/null @@ -1,600 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.collection; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import lombok.EqualsAndHashCode; -import net.runelite.api.Constants; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.Microbot; - -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -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.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItem; -import net.runelite.client.plugins.microbot.util.grounditem.models.Rs2SpawnLocation; -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.walker.Rs2Walker; - -import static net.runelite.client.plugins.microbot.util.Global.sleep; -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import org.slf4j.event.Level; - -/** - * Represents an item requirement that can be fulfilled by looting an item from a spawn location. - * Extends the Requirement class directly to add loot-specific functionality. - */ -@Getter -@Slf4j -@EqualsAndHashCode(callSuper = true) -public class LootRequirement extends Requirement { - - /** - * Default item count for this requirement. - * This can be overridden if the plugin requires a specific count. - */ - private final Map amounts; - private final Map collectedAmounts = new HashMap<>();; - - /** - * The spawn locations for this item requirement. - */ - private final Rs2SpawnLocation spawnLocation; - - - /** - * Maximum time to wait for collecting items in milliseconds. - */ - private final Duration timeout ; - /** - * define how far apart spawn locations can be to be considered part of the same cluster. - */ - private final int clusterProximity; - - - /** - * Represents a cluster of nearby spawn locations. - */ - private static class SpawnCluster { - List locations; - WorldPoint center; - double averageDistance; - boolean reachable; - - SpawnCluster(List locations) { - this.locations = locations; - this.center = calculateCenter(locations); - this.averageDistance = calculateAverageDistance(); - this.reachable = false; - } - - private WorldPoint calculateCenter(List points) { - int sumX = points.stream().mapToInt(WorldPoint::getX).sum(); - int sumY = points.stream().mapToInt(WorldPoint::getY).sum(); - int sumPlane = points.stream().mapToInt(WorldPoint::getPlane).sum(); - - return new WorldPoint( - sumX / points.size(), - sumY / points.size(), - sumPlane / points.size() - ); - } - - private double calculateAverageDistance() { - WorldPoint playerLocation = Rs2Player.getWorldLocation(); - if (playerLocation == null) { - return Double.MAX_VALUE; - } - return locations.stream() - .mapToInt(location -> location.distanceTo(playerLocation)) - .average() - .orElse(Double.MAX_VALUE); - } - } - public String getName() { - // Use the first item ID as the name, or "Unknown Item" if no IDs are provided - return (ids.isEmpty() || spawnLocation == null) ? "Unknown Item" : spawnLocation.getItemName(); - } - - /** - * Returns a multi-line display string with detailed loot requirement information. - * Uses StringBuilder with tabs for proper formatting. - * - * @return A formatted string containing loot requirement details - */ - public String displayString() { - StringBuilder sb = new StringBuilder(); - sb.append("=== Loot Requirement Details ===\n"); - sb.append("Name:\t\t\t").append(getName()).append("\n"); - sb.append("Type:\t\t\t").append(getRequirementType().name()).append("\n"); - sb.append("Priority:\t\t").append(getPriority().name()).append("\n"); - sb.append("Rating:\t\t\t").append(getRating()).append("/10\n"); - sb.append("Amounts per id:\t\t\t").append(amounts).append("\n"); - sb.append("Amounts Collected per id:\t").append(collectedAmounts).append("\n"); - sb.append("Item IDs:\t\t").append(getIds().toString()).append("\n"); - sb.append("clusterProximity:\t").append(clusterProximity).append(" tiles\n"); - sb.append("Timeout:\t\t").append(timeout.toSeconds()).append(" seconds\n"); - sb.append("Description:\t\t").append(getDescription() != null ? getDescription() : "No description").append("\n"); - - if (spawnLocation != null) { - sb.append("\n--- Spawn Location Information ---\n"); - sb.append(spawnLocation.displayString()); - - // Add availability check - sb.append("Currently Available:\t").append(isSpawnAvailable() ? "Yes" : "No").append("\n"); - } else { - sb.append("Spawn Location:\t\tNot specified\n"); - } - - return sb.toString(); - } - - /** - * Full constructor for a loot item requirement with schedule context. - */ - public LootRequirement( - List itemIds, - int amount, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext, - Rs2SpawnLocation spawnLocation, - int clusterProximity, - Duration timeout) { - - super(RequirementType.INVENTORY, priority, rating, description, - itemIds, taskContext); - - // Create amounts map with the same amount for all item IDs - Map amountsBuilder = new HashMap<>(itemIds.size()); - for (Integer itemId : itemIds) { - amountsBuilder.put(itemId, amount); - this.collectedAmounts.put(itemId, 0); // Initialize collected amounts to 0 - } - this.amounts = Map.copyOf(amountsBuilder); - this.spawnLocation = spawnLocation; - this.clusterProximity = clusterProximity; - this.timeout = timeout; - } - - /** - * Full constructor for a loot item requirement. - * Defaults to TaskContext.BOTH for backwards compatibility. - */ - public LootRequirement( - List itemIds, - int amount, - RequirementPriority priority, - int rating, - String description, - Rs2SpawnLocation spawnLocation, - int clusterProximity, - Duration timeout) { - - super(RequirementType.INVENTORY, priority, rating, description, - itemIds, TaskContext.BOTH); // Default to BOTH for backwards compatibility - - // Create amounts map with the same amount for all item IDs - Map amountsBuilder = new HashMap<>(); - for (Integer itemId : itemIds) { - amountsBuilder.put(itemId, amount); - collectedAmounts.put(itemId, 0); // Initialize collected amounts to 0 - } - this.amounts = Map.copyOf(amountsBuilder); - this.spawnLocation = spawnLocation; - this.clusterProximity = clusterProximity; - this.timeout = timeout; - } - - /** - * Simple constructor for a loot item requirement with mandatory priority. - */ - public LootRequirement( - int itemId, - int amount, - String description, - Rs2SpawnLocation spawnLocation) { - this( - Arrays.asList(itemId), - amount, - RequirementPriority.MANDATORY, - 5, - description, - spawnLocation, - 20, // Default cluster proximity of 20 tiles - Duration.of(2, java.time.temporal.ChronoUnit.MINUTES) // Default timeout of 2 minutes - ); - - } - - - - /** - * Gets the primary item ID (first in the list). - * Used for backward compatibility with code that expects a single item ID. - * - * @return The primary item ID, or -1 if there are no item IDs - */ - public int getPrimaryItemId() { - return ids.isEmpty() ? -1 : ids.get(0); - } - - /** - * Check if this requirement accepts a specific item ID. - * - * @param itemId The item ID to check - * @return true if this requirement can be fulfilled by the specified item ID, false otherwise - */ - public boolean acceptsItemId(int itemId) { - return ids.contains(itemId); - } - - /** - * Gets the list of item IDs for this requirement. - * Provided for backward compatibility. - * - * @return The list of item IDs - */ - public List getItemIds() { - return ids; - } - - - - /** - * Main loot collection method with configurable cluster proximity. - * Finds the best reachable cluster of spawn locations and efficiently collects items. - * - * @param clusterProximity The maximum distance between spawn locations to be considered part of the same cluster - * @return true if the required amount was successfully collected, false otherwise - */ - private boolean collectLootItems(CompletableFuture scheduledFuture) { - - - if (isFulfilled()) { - return true; - } - - if (spawnLocation == null || spawnLocation.getLocations() == null || spawnLocation.getLocations().isEmpty()) { - log.error("No spawn locations defined for loot requirement: " + getName()); - return false; - } - - try { - // Find the best reachable cluster - SpawnCluster bestCluster = findBestReachableCluster(clusterProximity); - if (bestCluster == null) { - log.error("No reachable spawn clusters found for loot requirement: " + getName()); - return false; - } - log.info("Found cluster with {} spawn locations for {}", bestCluster.locations.size(), getName()); - - // Move to the cluster center - if (!moveToCluster(bestCluster)) { - log.error("Failed to reach cluster center for loot requirement: " + getName()); - return false; - } - - // Collect items from the cluster - return collectFromCluster(scheduledFuture, bestCluster); - - } catch (Exception e) { - log.error("Exception during loot collection for " + getName() + ": " + e.getMessage()); - return false; - } - } - - - - /** - * Finds the best reachable cluster of spawn locations. - */ - private SpawnCluster findBestReachableCluster(int clusterProximity) { - List allLocations = spawnLocation.getLocations(); - List clusters = new ArrayList<>(); - Set processedLocations = new HashSet<>(); - - // Create clusters by grouping nearby locations - for (WorldPoint location : allLocations) { - if (processedLocations.contains(location)) continue; - - List clusterLocations = new ArrayList<>(); - clusterLocations.add(location); - processedLocations.add(location); - - // Find all locations within cluster proximity - for (WorldPoint otherLocation : allLocations) { - if (processedLocations.contains(otherLocation)) continue; - - boolean isNearCluster = clusterLocations.stream() - .anyMatch(clusterLoc -> clusterLoc.distanceTo(otherLocation) <= clusterProximity); - - if (isNearCluster) { - clusterLocations.add(otherLocation); - processedLocations.add(otherLocation); - } - } - - clusters.add(new SpawnCluster(clusterLocations)); - } - - // Check reachability and find the best cluster - SpawnCluster bestCluster = null; - double bestScore = Double.MAX_VALUE; - - for (SpawnCluster cluster : clusters) { - // Check if the cluster center is reachable - cluster.reachable = Rs2Walker.canReach(cluster.center); - - if (cluster.reachable) { - // Score based on distance and cluster size (prefer closer clusters with more spawns) - double score = cluster.averageDistance / Math.max(1, cluster.locations.size()); - - if (score < bestScore) { - bestScore = score; - bestCluster = cluster; - } - } - } - - return bestCluster; - } - - /** - * Moves the player to the cluster center. - */ - private boolean moveToCluster(SpawnCluster cluster) { - WorldPoint currentPosition = Rs2Player.getWorldLocation(); - if (currentPosition == null) { - log.error("Player location is unknown, cannot move to cluster for " + getName()); - return false; - } - // Check if we're already near the cluster - boolean nearCluster = cluster.locations.stream() - .anyMatch(location -> currentPosition.distanceTo(location) <= 15); - - if (nearCluster) { - return true; - } - - // Walk to the cluster center - log.info("Walking to cluster center at {} for {}", cluster.center, getName()); - if (!Rs2Walker.walkTo(cluster.center)) { - return false; - } - - // Wait for arrival - return sleepUntil(() -> { - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - return playerLoc!=null && cluster.locations.stream() - .anyMatch(location -> playerLoc.distanceTo(location) <= 15); - }, 30000); - } - - /** - * Collects items from the cluster with proper banking and respawn handling. - */ - private boolean collectFromCluster(CompletableFuture scheduledFuture,SpawnCluster cluster) { - long startTime = System.currentTimeMillis(); - long lastItemFoundTime = System.currentTimeMillis(); - - while (!isFulfilled() && - (System.currentTimeMillis() - startTime) < timeout.toMillis()) { - if (scheduledFuture != null && (scheduledFuture.isCancelled() || scheduledFuture.isDone())) { - log.error("Loot collection cancelled or completed prematurely: " + getName()); - return false; // Stop if the scheduled future is cancelled or done - } - // Check inventory space and bank if needed - if (Rs2Inventory.isFull() ) { - if (!handleBanking(cluster.center)) { - log.error("Banking failed while collecting loot requirement: " + getName()); - return false; - } - startTime = System.currentTimeMillis(); // Reset start time after banking - - continue; - } - - // Try to loot items in the cluster area - boolean itemFound = false; - for (int itemId : getItemIds()) { - int requiredAmount = amounts.get(itemId); - int currentOverallAmountCollected = collectedAmounts.get(itemId);; //not only in the inventory over the whole collection session with banking - log.info("Looking for item {} in cluster for {} (collected {}/{})", itemId, getName(), currentOverallAmountCollected, requiredAmount); - // Check if we have enough - if (currentOverallAmountCollected >= requiredAmount) { - log.info("Successfully collected required amount of {} for {} already fulfilled", getName(), itemId); - continue; - } - - // Check for items within the cluster area - if (Rs2GroundItem.exists(itemId, 25)) { - final int currentInventoryCount = Rs2Inventory.itemQuantity(itemId); - if (Rs2GroundItem.loot(itemId, 25)) { - // Wait for inventory update - sleepUntil(() -> Rs2Inventory.itemQuantity(itemId) > currentInventoryCount, 3000); - final int gained = Math.max(0, Rs2Inventory.itemQuantity(itemId) - currentInventoryCount); - if (gained <= 0) { - log.warn("Failed to confirm loot of item {} for {}", itemId, getName()); - continue; - } - itemFound = true; - lastItemFoundTime = System.currentTimeMillis(); - currentOverallAmountCollected += gained; // Increment count after successful loot - log.info("Looted item {} for {}, gained {} (collected {}/{})", itemId, getName(),gained, currentOverallAmountCollected, requiredAmount); - collectedAmounts.put(itemId, currentOverallAmountCollected); - break; - } - } - } - - if (itemFound) { - lastItemFoundTime = System.currentTimeMillis(); // Reset last found time - } else { - // No items found, wait for respawn - long timeSinceLastItem = System.currentTimeMillis() - lastItemFoundTime; - Duration itemRespwanTime = spawnLocation.getRespawnTime() != null ? spawnLocation.getRespawnTime() : Duration.ofSeconds(30); - // If we haven't found any items for too long, the cluster might be depleted - if (timeSinceLastItem > 30000) { // 30 seconds - log.info("No items found in cluster for 30s, checking other areas..."); - - // Try checking a bit further from cluster center - boolean foundNearby = false; - for (WorldPoint location : cluster.locations) { - if (Rs2Player.getWorldLocation()!=null && Rs2Player.getWorldLocation().distanceTo(location) <= 30) { - for (int itemId : getItemIds()) { - if (Rs2GroundItem.exists(itemId, 15)) { - Rs2Walker.walkTo(location); - sleepUntil(() -> Rs2Player.getWorldLocation().distanceTo(location) <= 10, 15000); - foundNearby = true; - break; - } - } - if (foundNearby) break; - } - } - - if (!foundNearby) { - log.warn("Cluster appears depleted, waiting for respawn..."); - } - - } - - log.info("Waiting for items to respawn for {} (waited {}s)", getName(), timeSinceLastItem / 1000); - int respawnMs = Math.max(Constants.GAME_TICK_LENGTH*2,(int) itemRespwanTime.toMillis()); - int minMs = Math.max(Constants.GAME_TICK_LENGTH, (int)(itemRespwanTime.toMillis()/2.0)); - sleep(Rs2Random.between(minMs, respawnMs)); - } - } - - // Final check - return isFulfilled(); - } - - /** - * Handles banking when inventory is full. - */ - private boolean handleBanking(WorldPoint returnLocation) { - - try { - final WorldPoint localReturnLocation = returnLocation != null ? returnLocation : Rs2Player.getWorldLocation(); - - - // Find and use nearest bank - if (!Rs2Bank.walkToBankAndUseBank()) { - return false; - } - - //Rs2Bank.depositAllExcept();// transportation related items... we need to impplement these in the future - Rs2Bank.depositAll((item)->getIds().contains(item.getId()) );// transportation related items... we need to impplement these in the future - sleepUntil(() -> getItemIds().stream().allMatch(id -> !Rs2Inventory.hasItem(id) ), 5000); // Wait until all items are deposited - // use the rs2transport or nviation or we have called it to get transportation items to the spwan location .. sowe we dont deposit it. - Rs2Bank.closeBank(); - sleepUntil( () -> !Rs2Bank.isOpen(), 5000); // Wait until bank is closed - // Return to collection area - Rs2Walker.walkTo(localReturnLocation); - return sleepUntil(() -> Rs2Player.getWorldLocation().distanceTo(localReturnLocation) <= 10, 30000); - - } catch (Exception e) { - Microbot.logStackTrace("LootItemRequirement.handleBanking", e); - return false; - } - } - - - - /** - * Checks if this item is currently available to loot. - * - * @return true if the item is available to loot, false otherwise - */ - private boolean isSpawnAvailable() { - if (spawnLocation == null || spawnLocation.getLocations() == null || spawnLocation.getLocations().isEmpty()) { - return false; - } - - // Check if we're near any spawn location - WorldPoint currentPosition = Rs2Player.getWorldLocation(); - if (currentPosition == null) { - return false; - } - for (WorldPoint location : spawnLocation.getLocations()) { - if (location.distanceTo(currentPosition) <= 20) { - // Check if any of our target items are available to loot - for (int itemId : getItemIds()) { - if (Rs2GroundItem.exists(itemId, 15)) { - return true; - } - } - } - } - - return false; - } - - /** - * Implements the abstract fulfillRequirement method from the base Requirement class. - * Attempts to fulfill this loot requirement by collecting items from spawn locations. - * - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if the requirement was successfully fulfilled, false otherwise - */ - @Override - public boolean fulfillRequirement(CompletableFuture scheduledFuture) { - if (Microbot.getClient().isClientThread()) { - Microbot.log("Please run fulfillRequirement() on a non-client thread.", Level.ERROR); - return false; - } - try { - // Check if the requirement is already fulfilled - if (isFulfilled()) { - return true; - } - - // Attempt to collect the required items - boolean success = collectLootItems(scheduledFuture); - - if (!success && isMandatory()) { - Microbot.log("MANDATORY loot requirement failed: " + getName()); - return false; - } - - return true; - - } catch (Exception e) { - Microbot.log("Error fulfilling loot requirement " + getName() + ": " + e.getMessage()); - return !isMandatory(); // Don't fail mandatory requirements due to exceptions - } - } - - /** - * Checks if we already have the required amount of items. - * - * @return true if we have enough items, false otherwise - */ - public boolean isFulfilled() { - boolean hasRequiredAmount = collectedAmounts.entrySet().stream() - .allMatch(entry -> entry.getValue() >= amounts.getOrDefault(entry.getKey(), 0)); - if (hasRequiredAmount) { - log.info("Already have all required items for " + getName()); - return true; - } - return false; - } - - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/conditional/ConditionalRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/conditional/ConditionalRequirement.java deleted file mode 100644 index f70fd245773..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/conditional/ConditionalRequirement.java +++ /dev/null @@ -1,522 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -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.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.LogicalRequirement; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.function.BooleanSupplier; - -/** - * A conditional requirement that executes requirements in sequence based on conditions. - * This addresses the preparation workflows where order matters: - * - * Examples: - * - "If we don't have lunar spellbook AND magic level >= 65, switch to lunar" - * - "First ensure we're at bank, then shop for items, then loot materials, then equip gear" - * - "If missing rune pouches, shop for them, then ensure NPC contact runes" - * - * This is much more powerful than simple AND/OR logic because: - * - Handles temporal dependencies (sequence matters) - * - Represents real OSRS preparation workflows - * - Provides conditional logic based on game state - * - Allows for complex decision trees - */ -@Slf4j -@EqualsAndHashCode(callSuper = true) -public class ConditionalRequirement extends Requirement { - - /** - * Represents a single conditional step in the sequence. - */ - @Getter - public static class ConditionalStep { - private final BooleanSupplier condition; - private final Requirement requirement; - private final String description; - private final boolean isOptional; - - /** - * Creates a mandatory conditional step. - * - * @param condition The condition to check (e.g., () -> !Rs2Player.hasLunarSpellbook()) - * @param requirement The requirement to fulfill if condition is true - * @param description Human-readable description of this step - */ - public ConditionalStep(BooleanSupplier condition, Requirement requirement, String description) { - this(condition, requirement, description, false); - } - - /** - * Creates a conditional step. - * - * @param condition The condition to check - * @param requirement The requirement to fulfill if condition is true - * @param description Human-readable description of this step - * @param isOptional Whether this step can be skipped if it fails - */ - public ConditionalStep(BooleanSupplier condition, Requirement requirement, String description, boolean isOptional) { - this.condition = condition; - this.requirement = requirement; - this.description = description; - this.isOptional = isOptional; - } - - /** - * Checks if this step's condition is met and needs execution. - * - * @return true if the condition is true (step needs to be executed) - */ - public boolean needsExecution() { - try { - return condition.getAsBoolean(); - } catch (Exception e) { - log.warn("Error checking condition for step '{}': {}", description, e.getMessage()); - return false; - } - } - - /** - * Executes this step's requirement. - * - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if successfully fulfilled, false otherwise - */ - public boolean execute(CompletableFuture scheduledFuture) { - try { - log.debug("Executing conditional step: {}", description); - return requirement.fulfillRequirement(scheduledFuture); - } catch (Exception e) { - log.error("Error executing conditional step '{}': {}", description, e.getMessage()); - return isOptional; // Optional steps return true on error, mandatory steps return false - } - } - } - - @Getter - private final List steps = new ArrayList<>(); - - @Getter - private final boolean allowParallelExecution; - - // Execution state tracking - private volatile int currentStepIndex = 0; - private volatile boolean allStepsCompleted = false; - private volatile String lastFailureReason = null; - - /** - * Creates a conditional requirement with sequential execution. - * - * @param priority Priority level for this conditional requirement - * @param rating Effectiveness rating (1-10) - * @param description Human-readable description - * @param TaskContext When this requirement should be fulfilled - * @param stopOnFirstFailure Whether to stop execution on first failure - */ - public ConditionalRequirement(RequirementPriority priority, int rating, String description, - TaskContext taskContext) { - this(priority, rating, description, taskContext, false); - } - - /** - * Creates a conditional requirement. - * - * @param priority Priority level for this conditional requirement - * @param rating Effectiveness rating (1-10) - * @param description Human-readable description - * @param TaskContext When this requirement should be fulfilled - * @param stopOnFirstFailure Whether to stop execution on first failure - * @param allowParallelExecution Whether steps can be executed in parallel (when conditions don't depend on each other) - */ - public ConditionalRequirement(RequirementPriority priority, int rating, String description, - TaskContext taskContext, - boolean allowParallelExecution) { - super(RequirementType.CONDITIONAL, priority, rating, description, List.of(), taskContext); - this.allowParallelExecution = allowParallelExecution; - } - - /** - * Adds a conditional step to this requirement. - * Steps are executed in the order they are added. - * - * @param condition The condition to check - * @param requirement The requirement to fulfill if condition is true - * @param description Description of this step - * @return This ConditionalRequirement for method chaining - */ - public ConditionalRequirement addStep(BooleanSupplier condition, Requirement requirement, String description) { - return addStep(condition, requirement, description, false); - } - - /** - * Adds a conditional step to this requirement. - * - * @param condition The condition to check - * @param requirement The requirement to fulfill if condition is true - * @param description Description of this step - * @param isOptional Whether this step can be skipped if it fails - * @return This ConditionalRequirement for method chaining - */ - public ConditionalRequirement addStep(BooleanSupplier condition, Requirement requirement, - String description, boolean isOptional) { - steps.add(new ConditionalStep(condition, requirement, description, isOptional)); - return this; - } - - /** - * Adds a step that always executes (unconditional). - * - * @param requirement The requirement to always fulfill - * @param description Description of this step - * @return This ConditionalRequirement for method chaining - */ - public ConditionalRequirement addAlwaysStep(Requirement requirement, String description) { - return addStep(() -> true, requirement, description, false); - } - - /** - * Adds a step that executes only if a condition is NOT met. - * - * @param condition The condition to check (step executes if this is false) - * @param requirement The requirement to fulfill if condition is false - * @param description Description of this step - * @return This ConditionalRequirement for method chaining - */ - public ConditionalRequirement addIfNotStep(BooleanSupplier condition, Requirement requirement, String description) { - return addStep(() -> !condition.getAsBoolean(), requirement, description, false); - } - - @Override - public String getName() { - return "Conditional: " + getDescription(); - } - - @Override - public boolean fulfillRequirement(CompletableFuture scheduledFuture) { - log.debug("Starting conditional requirement fulfillment: {}", getName()); - - // Reset state - currentStepIndex = 0; - allStepsCompleted = false; - lastFailureReason = null; - if (steps.isEmpty()) { - log.warn("No steps defined for conditional requirement: {}", getName()); - return true; // Empty requirement is considered fulfilled - } - - // Execute steps in sequence - for (int i = 0; i < steps.size(); i++) { - if( scheduledFuture != null && (scheduledFuture.isCancelled() || scheduledFuture.isDone())) { - log.warn("Conditional requirement execution cancelled or completed prematurely: {}", getName()); - return false; // Stop if the scheduled future is cancelled or done - } - currentStepIndex = i; - ConditionalStep step = steps.get(i); - - // Check if this step needs execution - if (!step.needsExecution()) { - log.debug("Skipping step {} (condition not met): {}", i, step.getDescription()); - continue; - } - - // Execute the step - log.debug("Executing step {}: {}", i, step.getDescription()); - boolean success = step.execute(scheduledFuture); - - if (!success) { - lastFailureReason = "Step " + i + " failed: " + step.getDescription(); - log.warn("Conditional requirement step failed: {}", lastFailureReason); - - if (!step.isOptional()) { - log.error("Stopping conditional requirement due to mandatory step failure: {}", lastFailureReason); - return false; - } - - } - } - - allStepsCompleted = true; - log.debug("Conditional requirement completed successfully: {}", getName()); - return true; - } - - /** - * Checks if this conditional requirement is currently fulfilled. - * This checks if all mandatory steps have been executed successfully. - */ - @Override - public boolean isFulfilled() { - // If we haven't started or haven't completed all steps, not fulfilled - if (!allStepsCompleted) { - return false; - } - - // Check if any mandatory steps still need execution - for (ConditionalStep step : steps) { - if (step.needsExecution() && !step.isOptional()) { - return false; - } - } - - return true; - } - - /** - * Gets the current execution progress as a percentage. - * - * @return Progress from 0.0 to 1.0 - */ - public double getExecutionProgress() { - if (steps.isEmpty()) { - return 1.0; - } - return (double) currentStepIndex / steps.size(); - } - - /** - * Gets the current step being executed. - * - * @return The current step, or null if not started or completed - */ - public ConditionalStep getCurrentStep() { - if (currentStepIndex >= 0 && currentStepIndex < steps.size()) { - return steps.get(currentStepIndex); - } - return null; - } - /** - * Gets all requirements that are currently active and need to be fulfilled. - * - * This method is crucial for the @InventorySetupPlanner integration. For example, we have a - * conditional requirement with 2 steps, but only one of the steps is relevant at any given time. - * - * Example use case: Items for alching - we need either fire runes in inventory OR any kind of fire staff - * (normal, lava, battlestaff, mystic) equipped or in inventory. - * - * Setup: - * - Step 1: ItemRequirement for fire runes, with condition: - * () -> (!Rs2Equipment.hasFireStaff() && !Rs2Inventory.hasFireStaff() && !Rs2Bank.hasFireStaff()) - * This step is active when we don't have any fire staff available - * - * - Step 2: OrRequirement with multiple ItemRequirements for different fire staffs, - * with condition: () -> Rs2Equipment.hasFireStaff() || Rs2Inventory.hasFireStaff() || Rs2Bank.hasFireStaff() - * This step is active when we have a fire staff available - * - * The RequirementRegistry caching system should: - * 1. Detect ConditionalRequirements that have only ItemRequirement steps (or LogicalRequirements composed of ItemRequirements) - * 2. Create separate cache entries for ConditionalRequirements to enable efficient active requirement lookup - * 3. Use containsOnlyItemRequirements() method from LogicalRequirement to validate cache compatibility - * 4. Save the allowed child type information to enable type-safe caching optimizations - * - * TODO: Implement ConditionalRequirement handling in: - * - RequirementSelector (to process active requirements) - * - RequirementRegistry (separate cache for conditional requirements with type tracking) - * - InventorySetupPlanner (to use getActiveRequirements() for planning) - * - * @return List of requirements that are currently active and need fulfillment - */ - public List getActiveRequirements() { - List activeRequirements = new ArrayList<>(); - for (ConditionalStep step : steps) { - if (step.needsExecution()) { - activeRequirements.add(step.getRequirement()); - } - } - return activeRequirements; - } - - /** - * Gets all ItemRequirements from currently active steps only. - * This method extracts ItemRequirements from steps that currently need execution, - * flattening any nested LogicalRequirements that contain ItemRequirements. - * - * This is essential for InventorySetupPlanner integration, as it needs to know - * which specific items are required for the current conditional state. - * - * Example: If we have a conditional with two steps: - * - Step 1 (active): Requires fire runes - * - Step 2 (inactive): Requires fire staff - * - * This method will return only the fire runes ItemRequirement since - * that's the only currently active step. - * - * @return List of ItemRequirements from currently active conditional steps - */ - public List getActiveItemRequirements() { - List activeItemRequirements = new ArrayList<>(); - - for (ConditionalStep step : steps) { - if (step.needsExecution()) { - Requirement stepRequirement = step.getRequirement(); - - if (stepRequirement instanceof ItemRequirement) { - activeItemRequirements.add((ItemRequirement) stepRequirement); - } else if (stepRequirement instanceof LogicalRequirement) { - LogicalRequirement logicalReq = (LogicalRequirement) stepRequirement; - activeItemRequirements.addAll(logicalReq.getAllItemRequirements()); - } - } - } - - return activeItemRequirements; - } - - /** - * Checks if this conditional requirement contains only ItemRequirements (or LogicalRequirements that contain only ItemRequirements). - * This is useful for RequirementRegistry caching to identify conditional requirements that can be processed - * alongside other item-based requirements for inventory planning. - * - * @return true if all steps in this conditional requirement contain only ItemRequirements - */ - public boolean containsOnlyItemRequirements() { - for (ConditionalStep step : steps) { - Requirement stepRequirement = step.getRequirement(); - - if (stepRequirement instanceof ItemRequirement) { - // ItemRequirement is allowed - continue; - } else if (stepRequirement instanceof LogicalRequirement) { - // Check if LogicalRequirement contains only ItemRequirements - LogicalRequirement logicalReq = (LogicalRequirement) stepRequirement; - if (!logicalReq.containsOnlyItemRequirements()) { - return false; - } - } else { - // Any other requirement type means this is not an item-only conditional - return false; - } - } - - return true; - } - - /** - * Gets all ItemRequirements from all steps in this conditional requirement, flattening any nested logical requirements. - * This is useful for RequirementRegistry to extract all potential item requirements for caching purposes. - * - * @return List of all ItemRequirements across all steps - */ - public List getAllItemRequirements() { - List allItemRequirements = new ArrayList<>(); - - for (ConditionalStep step : steps) { - Requirement stepRequirement = step.getRequirement(); - - if (stepRequirement instanceof ItemRequirement) { - allItemRequirements.add((ItemRequirement) stepRequirement); - } else if (stepRequirement instanceof LogicalRequirement) { - LogicalRequirement logicalReq = (LogicalRequirement) stepRequirement; - allItemRequirements.addAll(logicalReq.getAllItemRequirements()); - } - } - - return allItemRequirements; - } - - /** - * Gets the reason for the last failure, if any. - * - * @return Failure reason or null if no failure - */ - public String getLastFailureReason() { - return lastFailureReason; - } - - /** - * Gets a detailed status string for display/debugging. - * - * @return Status string with progress and current step info - */ - public String getDetailedStatus() { - StringBuilder sb = new StringBuilder(); - sb.append(getName()).append(" - "); - - if (allStepsCompleted) { - sb.append("Completed"); - } else { - sb.append("Progress: ").append(String.format("%.0f%%", getExecutionProgress() * 100)); - - ConditionalStep currentStep = getCurrentStep(); - if (currentStep != null) { - sb.append(" - Current: ").append(currentStep.getDescription()); - } - } - - if (lastFailureReason != null) { - sb.append(" - Last failure: ").append(lastFailureReason); - } - - return sb.toString(); - } - - @Override - public String toString() { - return "ConditionalRequirement{" + - "name='" + getName() + '\'' + - ", steps=" + steps.size() + - ", progress=" + String.format("%.0f%%", getExecutionProgress() * 100) + - ", completed=" + allStepsCompleted + - '}'; - } - - /** - * Returns a detailed display string with conditional requirement information. - * - * @return A formatted string containing conditional requirement details - */ - @Override - public String displayString() { - StringBuilder sb = new StringBuilder(); - sb.append("=== Conditional Requirement Details ===\n"); - sb.append("Name:\t\t\t").append(getName()).append("\n"); - sb.append("Type:\t\t\t").append(getRequirementType().name()).append("\n"); - sb.append("Priority:\t\t").append(getPriority().name()).append("\n"); - sb.append("Rating:\t\t\t").append(getRating()).append("/10\n"); - sb.append("Schedule Context:\t").append(getTaskContext().name()).append("\n"); - sb.append("Parallel Execution:\t").append(allowParallelExecution ? "Yes" : "No").append("\n"); - sb.append("Total Steps:\t\t").append(steps.size()).append("\n"); - sb.append("Progress:\t\t").append(String.format("%.1f%%", getExecutionProgress() * 100)).append("\n"); - sb.append("Completed:\t\t").append(allStepsCompleted ? "Yes" : "No").append("\n"); - sb.append("Description:\t\t").append(getDescription() != null ? getDescription() : "No description").append("\n"); - - // Add step details - sb.append("\n--- Steps Details ---\n"); - for (int i = 0; i < steps.size(); i++) { - ConditionalStep step = steps.get(i); - sb.append("Step ").append(i + 1).append(":\t\t").append(step.getDescription()).append("\n"); - sb.append("\t\t\tOptional: ").append(step.isOptional() ? "Yes" : "No").append("\n"); - sb.append("\t\t\tRequirement: ").append(step.getRequirement().getName()).append("\n"); - if (i < currentStepIndex) { - sb.append("\t\t\tStatus: Completed\n"); - } else if (i == currentStepIndex) { - sb.append("\t\t\tStatus: Current Step\n"); - } else { - sb.append("\t\t\tStatus: Pending\n"); - } - } - - if (lastFailureReason != null) { - sb.append("\n--- Last Failure ---\n"); - sb.append("Reason:\t\t\t").append(lastFailureReason).append("\n"); - } - - return sb.toString(); - } - - /** - * Enhanced toString method that uses displayString for comprehensive output. - * - * @return A comprehensive string representation of this conditional requirement - */ - public String toDetailedString() { - return displayString(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/conditional/OrderedRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/conditional/OrderedRequirement.java deleted file mode 100644 index 2d0b8ed4f37..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/conditional/OrderedRequirement.java +++ /dev/null @@ -1,427 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ScheduledExecutorService; - -/** - * An ordered requirement that executes requirements in strict sequence. - * Unlike ConditionalRequirement which uses conditions, this executes ALL steps in order. - * - * Perfect for workflows where order matters: - * - First shop for supplies - * - Then loot materials - * - Then equip gear - * - Then go to location - * - * Each step must complete successfully before proceeding to the next. - */ -@Slf4j -@EqualsAndHashCode(callSuper = true) -public class OrderedRequirement extends Requirement { - - /** - * Represents a single ordered step in the sequence. - */ - @Getter - public static class OrderedStep { - private final Requirement requirement; - private final String description; - private final boolean isMandatory; - - /** - * Creates a mandatory ordered step. - * - * @param requirement The requirement to fulfill - * @param description Human-readable description of this step - */ - public OrderedStep(Requirement requirement, String description) { - this(requirement, description, true); - } - - /** - * Creates an ordered step. - * - * @param requirement The requirement to fulfill - * @param description Human-readable description of this step - * @param isMandatory Whether this step must succeed for the sequence to continue - */ - public OrderedStep(Requirement requirement, String description, boolean isMandatory) { - this.requirement = requirement; - this.description = description; - this.isMandatory = isMandatory; - } - - /** - * Executes this step's requirement. - * - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if successfully fulfilled, false otherwise - */ - public boolean execute(CompletableFuture scheduledFuture) { - try { - log.debug("Executing ordered step: {}", description); - return requirement.fulfillRequirement(scheduledFuture); - } catch (Exception e) { - log.error("Error executing ordered step '{}': {}", description, e.getMessage()); - return false; // Defer optional skip policy to the caller (allowSkipOptional) - } - } - - /** - * Checks if this step is currently fulfilled. - * - * @return true if the requirement is fulfilled - */ - public boolean isFulfilled() { - try { - return requirement.isFulfilled(); - } catch (Exception e) { - log.warn("Error checking fulfillment for step '{}': {}", description, e.getMessage()); - return false; - } - } - } - - @Getter - private final List steps = new ArrayList<>(); - - @Getter - private final boolean allowSkipOptional; - - @Getter - private final boolean resumeFromLastFailed; - - // Execution state tracking - private volatile int currentStepIndex = 0; - private volatile int lastCompletedStep = -1; - private volatile boolean allStepsCompleted = false; - private volatile String lastFailureReason = null; - - /** - * Creates an ordered requirement. - * - * @param priority Priority level for this ordered requirement - * @param rating Effectiveness rating (1-10) - * @param description Human-readable description - * @param TaskContext When this requirement should be fulfilled - * @param allowSkipOptional Whether optional steps can be skipped on failure - * @param resumeFromLastFailed Whether to resume from the last failed step or restart - */ - public OrderedRequirement(RequirementPriority priority, int rating, String description, - TaskContext taskContext, boolean allowSkipOptional, - boolean resumeFromLastFailed) { - super(RequirementType.CONDITIONAL, priority, rating, description, List.of(), taskContext); - this.allowSkipOptional = allowSkipOptional; - this.resumeFromLastFailed = resumeFromLastFailed; - } - - /** - * Creates an ordered requirement with default settings. - * - * @param priority Priority level for this ordered requirement - * @param rating Effectiveness rating (1-10) - * @param description Human-readable description - * @param TaskContext When this requirement should be fulfilled - */ - public OrderedRequirement(RequirementPriority priority, int rating, String description, TaskContext taskContext) { - this(priority, rating, description, taskContext, true, true); - } - - /** - * Adds a mandatory step to this ordered requirement. - * Steps are executed in the order they are added. - * - * @param requirement The requirement to fulfill - * @param description Description of this step - * @return This OrderedRequirement for method chaining - */ - public OrderedRequirement addStep(Requirement requirement, String description) { - return addStep(requirement, description, true); - } - - /** - * Adds an optional step to this ordered requirement. - * - * @param requirement The requirement to fulfill - * @param description Description of this step - * @return This OrderedRequirement for method chaining - */ - public OrderedRequirement addOptionalStep(Requirement requirement, String description) { - return addStep(requirement, description, false); - } - - /** - * Adds a step to this ordered requirement. - * - * @param requirement The requirement to fulfill - * @param description Description of this step - * @param isMandatory Whether this step must succeed - * @return This OrderedRequirement for method chaining - */ - public OrderedRequirement addStep(Requirement requirement, String description, boolean isMandatory) { - steps.add(new OrderedStep(requirement, description, isMandatory)); - return this; - } - - @Override - public String getName() { - return "Ordered: " + getDescription(); - } - - @Override - public boolean fulfillRequirement(CompletableFuture scheduledFuture) { - log.debug("Starting ordered requirement fulfillment: {}", getName()); - - // Determine starting point - int startIndex = resumeFromLastFailed ? Math.max(0, lastCompletedStep + 1) : 0; - - if (startIndex == 0) { - // Fresh start - reset state - currentStepIndex = 0; - lastCompletedStep = -1; - allStepsCompleted = false; - lastFailureReason = null; - } - - if (steps.isEmpty()) { - log.warn("No steps defined for ordered requirement: {}", getName()); - return true; // Empty requirement is considered fulfilled - } - - // Execute steps in strict order starting from determined index - for (int i = startIndex; i < steps.size(); i++) { - if( scheduledFuture!= null && (scheduledFuture.isCancelled() || scheduledFuture.isDone())) { - log.warn("Ordered requirement execution cancelled or completed prematurely: {}", getName()); - return false; // Stop if the scheduled future is cancelled or done - } - currentStepIndex = i; - OrderedStep step = steps.get(i); - - log.debug("Executing ordered step {}: {}", i, step.getDescription()); - boolean success = step.execute(scheduledFuture); - - if (success) { - lastCompletedStep = i; - log.debug("Completed step {}: {}", i, step.getDescription()); - } else { - lastFailureReason = "Step " + i + " failed: " + step.getDescription(); - log.warn("Ordered requirement step failed: {}", lastFailureReason); - - if (step.isMandatory()) { - log.error("Stopping ordered requirement due to mandatory step failure: {}", lastFailureReason); - return false; - } else if (!allowSkipOptional) { - log.error("Stopping ordered requirement due to optional step failure (skip not allowed): {}", lastFailureReason); - return false; - } else { - log.warn("Skipping optional step failure: {}", lastFailureReason); - lastCompletedStep = i; // Mark as completed even though it failed (optional) - } - } - } - - allStepsCompleted = true; - log.debug("Ordered requirement completed successfully: {}", getName()); - return true; - } - - /** - * Checks if this ordered requirement is currently fulfilled. - * This checks if all mandatory steps have been completed. - */ - @Override - public boolean isFulfilled() { - // If we haven't completed all steps, check current state - if (!allStepsCompleted) { - return false; - } - - // Check if all mandatory steps are still fulfilled - for (OrderedStep step : steps) { - if (step.isMandatory() && !step.isFulfilled()) { - return false; - } - } - - return true; - } - - /** - * Gets the current execution progress as a percentage. - * - * @return Progress from 0.0 to 1.0 - */ - public double getExecutionProgress() { - if (steps.isEmpty()) { - return 1.0; - } - return (double) (lastCompletedStep + 1) / steps.size(); - } - - /** - * Gets the current step being executed. - * - * @return The current step, or null if not started or completed - */ - public OrderedStep getCurrentStep() { - if (currentStepIndex >= 0 && currentStepIndex < steps.size()) { - return steps.get(currentStepIndex); - } - return null; - } - - /** - * Gets the next step to be executed. - * - * @return The next step, or null if all steps completed - */ - public OrderedStep getNextStep() { - int nextIndex = lastCompletedStep + 1; - if (nextIndex >= 0 && nextIndex < steps.size()) { - return steps.get(nextIndex); - } - return null; - } - - /** - * Gets the number of completed steps. - * - * @return Number of successfully completed steps - */ - public int getCompletedStepCount() { - return lastCompletedStep + 1; - } - - /** - * Gets the number of remaining steps. - * - * @return Number of steps still to be executed - */ - public int getRemainingStepCount() { - return Math.max(0, steps.size() - getCompletedStepCount()); - } - - /** - * Resets the execution state to start from the beginning. - */ - public void reset() { - currentStepIndex = 0; - lastCompletedStep = -1; - allStepsCompleted = false; - lastFailureReason = null; - log.debug("Reset ordered requirement: {}", getName()); - } - - /** - * Gets the reason for the last failure, if any. - * - * @return Failure reason or null if no failure - */ - public String getLastFailureReason() { - return lastFailureReason; - } - - /** - * Gets a detailed status string for display/debugging. - * - * @return Status string with progress and current step info - */ - public String getDetailedStatus() { - StringBuilder sb = new StringBuilder(); - sb.append(getName()).append(" - "); - - if (allStepsCompleted) { - sb.append("Completed (").append(steps.size()).append("/").append(steps.size()).append(" steps)"); - } else { - sb.append("Progress: ").append(getCompletedStepCount()).append("/").append(steps.size()).append(" steps"); - - OrderedStep nextStep = getNextStep(); - if (nextStep != null) { - sb.append(" - Next: ").append(nextStep.getDescription()); - } - } - - if (lastFailureReason != null) { - sb.append(" - Last failure: ").append(lastFailureReason); - } - - return sb.toString(); - } - - @Override - public String toString() { - return "OrderedRequirement{" + - "name='" + getName() + '\'' + - ", steps=" + steps.size() + - ", completed=" + getCompletedStepCount() + - ", allCompleted=" + allStepsCompleted + - '}'; - } - - /** - * Returns a detailed display string with ordered requirement information. - * - * @return A formatted string containing ordered requirement details - */ - @Override - public String displayString() { - StringBuilder sb = new StringBuilder(); - sb.append("=== Ordered Requirement Details ===\n"); - sb.append("Name:\t\t\t").append(getName()).append("\n"); - sb.append("Type:\t\t\t").append(getRequirementType().name()).append("\n"); - sb.append("Priority:\t\t").append(getPriority().name()).append("\n"); - sb.append("Rating:\t\t\t").append(getRating()).append("/10\n"); - sb.append("Schedule Context:\t").append(getTaskContext().name()).append("\n"); - sb.append("Allow Skip Optional:\t").append(allowSkipOptional ? "Yes" : "No").append("\n"); - sb.append("Resume from Failed:\t").append(resumeFromLastFailed ? "Yes" : "No").append("\n"); - sb.append("Total Steps:\t\t").append(steps.size()).append("\n"); - sb.append("Completed Steps:\t").append(getCompletedStepCount()).append("/").append(steps.size()).append("\n"); - sb.append("Progress:\t\t").append(String.format("%.1f%%", getExecutionProgress() * 100)).append("\n"); - sb.append("All Completed:\t\t").append(allStepsCompleted ? "Yes" : "No").append("\n"); - sb.append("Description:\t\t").append(getDescription() != null ? getDescription() : "No description").append("\n"); - - // Add step details - sb.append("\n--- Steps Details ---\n"); - for (int i = 0; i < steps.size(); i++) { - OrderedStep step = steps.get(i); - sb.append("Step ").append(i + 1).append(":\t\t").append(step.getDescription()).append("\n"); - sb.append("\t\t\tMandatory: ").append(step.isMandatory() ? "Yes" : "No").append("\n"); - sb.append("\t\t\tRequirement: ").append(step.getRequirement().getName()).append("\n"); - - // Show execution status - if (i < currentStepIndex) { - sb.append("\t\t\tStatus: Completed\n"); - } else if (i == currentStepIndex && !allStepsCompleted) { - sb.append("\t\t\tStatus: Current Step\n"); - } else { - sb.append("\t\t\tStatus: Pending\n"); - } - } - - if (lastFailureReason != null) { - sb.append("\n--- Last Failure ---\n"); - sb.append("Reason:\t\t\t").append(lastFailureReason).append("\n"); - } - - return sb.toString(); - } - - /** - * Enhanced toString method that uses displayString for comprehensive output. - * - * @return A comprehensive string representation of this ordered requirement - */ - public String toDetailedString() { - return displayString(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/InventorySetupPlanner.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/InventorySetupPlanner.java deleted file mode 100644 index 556340c3622..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/InventorySetupPlanner.java +++ /dev/null @@ -1,3097 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item; - -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.api.Skill; -import net.runelite.client.plugins.microbot.inventorysetups.InventorySetup; -import net.runelite.client.plugins.microbot.inventorysetups.InventorySetupsItem; -import net.runelite.client.plugins.microbot.inventorysetups.InventorySetupsStackCompareID; -import net.runelite.client.plugins.microbot.inventorysetups.InventorySetupsVariationMapping; -import net.runelite.client.plugins.microbot.inventorysetups.MInventorySetupsPlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.LogicalRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.OrRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.ConditionalRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.OrRequirementMode; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.registry.RequirementRegistry; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.util.RequirementSelector; -import net.runelite.client.plugins.microbot.util.Rs2InventorySetup; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Delayed; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.stream.Collectors; -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -/** - * Represents a comprehensive inventory and equipment layout plan. - * This class manages the optimal placement of items across equipment slots and inventory slots, - * considering constraints, priorities, and availability. - */ -@Slf4j -@Getter -public class InventorySetupPlanner { - - // Equipment slot assignments (fixed positions) - private final Map equipmentAssignments = new HashMap<>(); - - // Specific inventory slot assignments (0-27) - private final Map inventorySlotAssignments = new HashMap<>(); - - // Flexible inventory items that can be placed in any available slot - private final List flexibleInventoryItems = new ArrayList<>(); - - // Missing mandatory items that could not be fulfilled - private final List missingMandatoryItems = new ArrayList<>(); - - // Missing mandatory equipment slots - private final Map> missingMandatoryEquipment = new HashMap<>(); - - // Optional: Requirements planning support (for new planning approach) - private RequirementRegistry registry; - private TaskContext taskContext; - private OrRequirementMode orRequirementMode = OrRequirementMode.ANY_COMBINATION; // Default mode - - // Flag to control whether to log recommended/optional missing items in comprehensive analysis - private boolean logOptionalMissingItems = true; // Default to true for backward compatibility - - /** - * Default constructor for backward compatibility. - */ - public InventorySetupPlanner() { - // Default constructor - uses existing functionality - } - - /** - * Enhanced constructor for requirements-based planning. - * - * @param registry The requirement registry containing all requirements - * @param TaskContext The schedule context (PRE_SCHEDULE or POST_SCHEDULE) - * @param orRequirementMode How to handle OR requirements (ANY_COMBINATION or SINGLE_TYPE) - */ - public InventorySetupPlanner(RequirementRegistry registry, TaskContext taskContext, OrRequirementMode orRequirementMode) { - this.registry = registry; - this.taskContext = taskContext; - this.orRequirementMode = orRequirementMode; - this.logOptionalMissingItems = true; // Default to logging optional items - } - - /** - * Enhanced constructor for requirements-based planning with optional item logging control. - * - * @param registry The requirement registry containing all requirements - * @param TaskContext The schedule context (PRE_SCHEDULE or POST_SCHEDULE) - * @param orRequirementMode How to handle OR requirements (ANY_COMBINATION or SINGLE_TYPE) - * @param logOptionalMissingItems Whether to log recommended/optional missing items in comprehensive analysis - */ - public InventorySetupPlanner(RequirementRegistry registry, TaskContext taskContext, OrRequirementMode orRequirementMode, boolean logOptionalMissingItems) { - this.registry = registry; - this.taskContext = taskContext; - this.orRequirementMode = orRequirementMode; - this.logOptionalMissingItems = logOptionalMissingItems; - } - - /** - * Adds an equipment slot assignment. - */ - public void addEquipmentSlotAssignment(EquipmentInventorySlot slot, ItemRequirement item) { - equipmentAssignments.put(slot, item); - log.debug("Assigned {} to equipment slot {}", item.getName(), slot); - } - - /** - * Adds a specific inventory slot assignment with stackability validation. - */ - public void addInventorySlotAssignment(int slot, ItemRequirement item) { - if (slot < 0 || slot > 27) { - throw new IllegalArgumentException("Invalid inventory slot: " + slot); - } - - // Validate stackability constraints - if (item.getAmount() > 1 && !item.isStackable()) { - log.warn("Cannot assign non-stackable item {} with amount {} to single slot {}", - item.getName(), item.getAmount(), slot); - // For non-stackable items with amount > 1, we need to handle differently - // This should be caught earlier in the planning process - return; - } - - inventorySlotAssignments.put(slot, item); - log.info("Assigned {} (amount: {}, stackable: {}) to inventory slot {}", - item.getName(), item.getAmount(), item.isStackable(), slot); - } - - /** - * Adds a flexible inventory item with stackability considerations. - */ - public void addFlexibleInventoryItem(ItemRequirement item) { - // Validate that the item can fit in inventory - if (!item.canFitInInventory()) { - log.warn("Item {} requires {} slots but inventory is full", - item.getName(), item.getRequiredInventorySlots()); - if (item.isMandatory()) { - addMissingMandatoryInventoryItem(item); - } - return; - } - - flexibleInventoryItems.add(item); - log.debug("Added flexible inventory item: {} (requires {} slots)", - item.getName(), item.getRequiredInventorySlots()); - } - - /** - * Adds a missing mandatory item. - */ - public void addMissingMandatoryInventoryItem(ItemRequirement item) { - missingMandatoryItems.add(item); - log.debug("Missing mandatory item: {}", item.getName()); - } - - /** - * Adds a missing mandatory equipment slot. - */ - public void addMissingMandatoryEquipment(EquipmentInventorySlot slot, ItemRequirement item) { - missingMandatoryEquipment.computeIfAbsent(slot, k -> new ArrayList<>()).add(item); - log.warn("Missing mandatory equipment for slot: {}", slot); - } - - /** - * Checks if an equipment slot is already occupied in this plan. - */ - public boolean isEquipmentSlotOccupied(EquipmentInventorySlot slot) { - return equipmentAssignments.containsKey(slot); - } - - /** - * Checks if an inventory slot is already occupied in this plan. - */ - public boolean isInventorySlotOccupied(int slot) { - return inventorySlotAssignments.containsKey(slot); - } - - /** - * Checks if the plan is feasible (no missing mandatory items). - */ - public boolean isFeasible() { - return missingMandatoryItems.isEmpty() && missingMandatoryEquipment.isEmpty(); - } - - /** - * Gets the total number of inventory slots that will be occupied. - * Properly accounts for stackability and amounts. - */ - public int getTotalInventorySlotsNeeded() { - int slotsNeeded = inventorySlotAssignments.size(); - - // Calculate slots needed for flexible items considering stackability - for (ItemRequirement item : flexibleInventoryItems) { - slotsNeeded += item.getRequiredInventorySlots(); - } - - return slotsNeeded; - } - - /** - * Checks if the plan fits within inventory capacity (28 slots). - */ - public boolean fitsInInventory() { - return getTotalInventorySlotsNeeded() <= 28; - } - - /** - * Gets all occupied inventory slots. - */ - public Set getOccupiedInventorySlots() { - Set occupied = new HashSet<>(inventorySlotAssignments.keySet()); - - // For flexible items, we'd need to simulate placement - // For now, just assume they'll fit in remaining slots - int flexibleItemsPlaced = 0; - for (int slot = 0; slot < 28 && flexibleItemsPlaced < flexibleInventoryItems.size(); slot++) { - if (!occupied.contains(slot)) { - occupied.add(slot); - flexibleItemsPlaced++; - } - } - - return occupied; - } - - /** - * Optimizes the placement of flexible inventory items. - * This method attempts to find the best slots for items that don't have specific slot requirements. - * Considers stackability, space constraints, and item consolidation opportunities. - */ - public void optimizeFlexibleItemPlacement() { - if (flexibleInventoryItems.isEmpty()) { - return; - } - - // First, try to consolidate stackable items of the same type - consolidateStackableItems(); - - // Sort flexible items by priority and rating - flexibleInventoryItems.sort((a, b) -> { - int priorityCompare = a.getPriority().compareTo(b.getPriority()); - if (priorityCompare != 0) { - return priorityCompare; - } - return Integer.compare(b.getRating(), a.getRating()); - }); - - // Check if we have enough space - if (!fitsInInventory()) { - log.warn("Not enough inventory space for all items. Need {} slots, but only 28 available.", - getTotalInventorySlotsNeeded()); - - // Attempt space optimization before removing items - if (attemptSpaceOptimization()) { - log.info("Space optimization successful - all items now fit"); - } else { - // Remove lowest priority items until we fit, considering stackability - removeItemsUntilFit(); - } - } - - log.info("Optimized placement for {} flexible inventory items (total slots needed: {})", - flexibleInventoryItems.size(), getTotalInventorySlotsNeeded()); - } - - /** - * Consolidates stackable items of the same type to save space. - */ - private void consolidateStackableItems() { - Map, ItemRequirement> stackableItems = new HashMap<>(); - List toRemove = new ArrayList<>(); - List toAdd = new ArrayList<>(); - - for (ItemRequirement item : flexibleInventoryItems) { - if (item.isStackable()) { - List itemIds = item.getIds(); - if (stackableItems.containsKey(itemIds)) { - // Found another item of the same type - consolidate - ItemRequirement existing = stackableItems.get(itemIds); - int newAmount = existing.getAmount() + item.getAmount(); - - // Create consolidated item - ItemRequirement consolidated = existing.copyWithAmount(newAmount); - - // Mark items for replacement - toRemove.add(existing); - toRemove.add(item); - toAdd.add(consolidated); - - // Update the map - stackableItems.put(itemIds, consolidated); - - log.info("Consolidated stackable items: {} + {} = {} (total: {})", - existing.getName(), item.getName(), consolidated.getName(), newAmount); - } else { - stackableItems.put(itemIds, item); - } - } - } - - // Apply consolidation - flexibleInventoryItems.removeAll(toRemove); - flexibleInventoryItems.addAll(toAdd); - } - - /** - * Attempts various space optimization strategies. - * @return true if optimization freed enough space - */ - private boolean attemptSpaceOptimization() { - int originalSlotsNeeded = getTotalInventorySlotsNeeded(); - - // Strategy 1: Look for items that could be moved to specific slots to free flexible space - optimizeSlotUtilization(); - - // Strategy 2: Consolidate any remaining stackable items - consolidateStackableItems(); - - int newSlotsNeeded = getTotalInventorySlotsNeeded(); - boolean improved = newSlotsNeeded < originalSlotsNeeded; - - if (improved) { - log.info("Space optimization reduced slot usage from {} to {}", originalSlotsNeeded, newSlotsNeeded); - } - - return fitsInInventory(); - } - - /** - * Optimizes slot utilization by moving flexible items to specific slots when beneficial. - */ - private void optimizeSlotUtilization() { - // Find unused specific slots that could accommodate flexible items - Set usedSlots = new HashSet<>(inventorySlotAssignments.keySet()); - List availableSlots = new ArrayList<>(); - - for (int slot = 0; slot < 28; slot++) { - if (!usedSlots.contains(slot)) { - availableSlots.add(slot); - } - } - - // Try to move single-slot flexible items to specific slots - Iterator flexIterator = flexibleInventoryItems.iterator(); - while (flexIterator.hasNext() && !availableSlots.isEmpty()) { - ItemRequirement item = flexIterator.next(); - - // Only move items that need exactly 1 slot - if (item.getRequiredInventorySlots() == 1) { - int targetSlot = availableSlots.remove(0); - - // Create slot-specific copy - ItemRequirement slotSpecific = item.copyWithSpecificSlot(targetSlot); - - // Move to specific slot assignment - inventorySlotAssignments.put(targetSlot, slotSpecific); - flexIterator.remove(); - - log.info("Moved flexible item {} to specific slot {} for optimization", - item.getName(), targetSlot); - } - } - } - - /** - * Removes lowest priority items until the plan fits in inventory. - * CRITICAL: Never removes MANDATORY items - they are protected from removal. - */ - private void removeItemsUntilFit() { - List toRemove = new ArrayList<>(); - - // Sort flexible items by priority (mandatory items should not be removed) - flexibleInventoryItems.sort((a, b) -> { - // Mandatory items always stay (higher priority) - int priorityCompare = a.getPriority().compareTo(b.getPriority()); - if (priorityCompare != 0) { - return priorityCompare; - } - // For same priority, prefer higher rating - return Integer.compare(b.getRating(), a.getRating()); - }); - - while (!fitsInInventory() && !flexibleInventoryItems.isEmpty()) { - // Find the lowest priority non-mandatory item to remove - ItemRequirement itemToRemove = null; - for (int i = flexibleInventoryItems.size() - 1; i >= 0; i--) { - ItemRequirement item = flexibleInventoryItems.get(i); - if (!item.isMandatory()) { - itemToRemove = item; - break; - } - } - - if (itemToRemove != null) { - flexibleInventoryItems.remove(itemToRemove); - toRemove.add(itemToRemove); - log.info("Removed optional item due to space constraints: {} (needs {} slots)", - itemToRemove.getName(), itemToRemove.getRequiredInventorySlots()); - } else { - // All remaining items are mandatory - cannot remove any more - log.error("Cannot fit all mandatory items in inventory. Need {} slots but only 28 available.", - getTotalInventorySlotsNeeded()); - - // Mark remaining mandatory items as missing if they don't fit - for (ItemRequirement mandatoryItem : flexibleInventoryItems) { - if (mandatoryItem.isMandatory()) { - missingMandatoryItems.add(mandatoryItem); - log.error("Cannot fit mandatory item: {} (needs {} slots)", - mandatoryItem.getName(), mandatoryItem.getRequiredInventorySlots()); - } - } - - // Clear all remaining flexible items since they can't fit - flexibleInventoryItems.clear(); - break; - } - } - - if (!toRemove.isEmpty()) { - log.info("Removed {} optional items to fit inventory constraints", toRemove.size()); - } - } - - /** - * Gets a summary of the layout plan. - */ - public String getSummary() { - StringBuilder sb = new StringBuilder(); - sb.append("=== Inventory Layout Plan Summary ===\n"); - sb.append("Equipment assignments: ").append(equipmentAssignments.size()).append("\n"); - sb.append("Specific inventory slots: ").append(inventorySlotAssignments.size()).append("\n"); - sb.append("Flexible inventory items: ").append(flexibleInventoryItems.size()).append("\n"); - sb.append("Total inventory slots needed: ").append(getTotalInventorySlotsNeeded()).append("/28\n"); - sb.append("Plan feasible: ").append(isFeasible()).append("\n"); - sb.append("Fits in inventory: ").append(fitsInInventory()).append("\n"); - - if (!missingMandatoryItems.isEmpty()) { - sb.append("Missing mandatory items: "); - missingMandatoryItems.forEach(item -> sb.append(item.getName()).append(", ")); - sb.append("\n"); - } - - if (!missingMandatoryEquipment.isEmpty()) { - sb.append("Missing mandatory equipment slots: "); - missingMandatoryEquipment.forEach((slot, items) -> sb.append("\n\t"+slot.name()+": ") - .append(items.stream().map(ItemRequirement::getName).collect(Collectors.joining(", "))) - .append("")); - sb.append("\n"); - } - - return sb.toString(); - } - - /** - * Gets a detailed string representation of the inventory setup plan. - * Shows equipment assignments, inventory slot assignments, and flexible items. - * - * @return A comprehensive string describing the planned setup - */ - public String getDetailedPlanString() { - StringBuilder sb = new StringBuilder(); - sb.append("=== Inventory Setup Plan Details ===\n"); - sb.append("Plan Feasible:\t\t").append(isFeasible() ? "Yes" : "No").append("\n"); - sb.append("Fits in Inventory:\t").append((getTotalInventorySlotsNeeded() <= 28) ? "Yes" : "No").append("\n"); - sb.append("Total Slots Needed:\t").append(getTotalInventorySlotsNeeded()).append("/28\n\n"); - - // Equipment assignments - sb.append("=== EQUIPMENT ASSIGNMENTS ===\n"); - if (equipmentAssignments.isEmpty()) { - sb.append("\tNo equipment assignments.\n"); - } else { - for (Map.Entry entry : equipmentAssignments.entrySet()) { - sb.append("\t").append(entry.getKey().name()).append(":\t"); - sb.append(formatItemRequirement(entry.getValue())).append("\n"); - } - } - sb.append("\n"); - - // Specific inventory slot assignments - sb.append("=== SPECIFIC INVENTORY SLOT ASSIGNMENTS ===\n"); - if (inventorySlotAssignments.isEmpty()) { - sb.append("\tNo specific slot assignments.\n"); - } else { - for (int slot = 0; slot < 28; slot++) { - if (inventorySlotAssignments.containsKey(slot)) { - ItemRequirement item = inventorySlotAssignments.get(slot); - sb.append("\tSlot ").append(slot).append(":\t\t"); - sb.append(formatItemRequirement(item)).append("\n"); - } - } - } - sb.append("\n"); - - // Flexible inventory items - sb.append("=== FLEXIBLE INVENTORY ITEMS ===\n"); - if (flexibleInventoryItems.isEmpty()) { - sb.append("\tNo flexible items.\n"); - } else { - for (int i = 0; i < flexibleInventoryItems.size(); i++) { - ItemRequirement item = flexibleInventoryItems.get(i); - sb.append("\t").append(i + 1).append(". ").append(formatItemRequirement(item)); - sb.append(" (needs ").append(item.getRequiredInventorySlots()).append(" slots)\n"); - } - } - sb.append("\n"); - - // Missing mandatory items - if (!missingMandatoryItems.isEmpty()) { - sb.append("=== MISSING MANDATORY ITEMS ===\n"); - for (int i = 0; i < missingMandatoryItems.size(); i++) { - ItemRequirement item = missingMandatoryItems.get(i); - sb.append("\t").append(i + 1).append(". ").append(formatItemRequirement(item)).append("\n"); - } - sb.append("\n"); - } - - // Missing mandatory equipment - if (!missingMandatoryEquipment.isEmpty()) { - sb.append("=== MISSING MANDATORY EQUIPMENT SLOTS ===\n"); - missingMandatoryEquipment.forEach((slot, items) -> { - sb.append("\t").append(slot.name()).append(": "); - if (items != null && !items.isEmpty()) { - sb.append(items.stream() - .map(ItemRequirement::getName) - .collect(Collectors.joining(", "))); - } else { - sb.append("No items listed"); - } - sb.append("\n"); - }); - sb.append("\n"); - } - - return sb.toString(); - } - - /** - * Formats an ItemRequirement for display. - * - * @param item The item requirement to format - * @return A formatted string representation - */ - private String formatItemRequirement(ItemRequirement item) { - StringBuilder sb = new StringBuilder(); - sb.append(item.getName()); - if (item.getAmount() > 1) { - sb.append(" x").append(item.getAmount()); - } - sb.append(" [").append(item.getPriority().name()).append(", Rating: ").append(item.getRating()).append("]"); - if (item.isStackable()) { - sb.append(" (stackable)"); - } - return sb.toString(); - } - - /** - * Gets a summary of occupied slots for logging. - */ - public String getOccupiedSlotsSummary() { - StringBuilder sb = new StringBuilder(); - sb.append("=== SLOT UTILIZATION SUMMARY ===\n"); - - // Equipment slots - sb.append("Equipment slots occupied: ").append(equipmentAssignments.size()).append("\n"); - if (!equipmentAssignments.isEmpty()) { - sb.append(" Slots: ").append(equipmentAssignments.keySet().stream() - .map(Object::toString) - .collect(Collectors.joining(", "))).append("\n"); - } - - // Specific inventory slots - sb.append("Specific inventory slots: ").append(inventorySlotAssignments.size()).append("\n"); - if (!inventorySlotAssignments.isEmpty()) { - sb.append(" Slots: ").append(inventorySlotAssignments.keySet().stream() - .sorted() - .map(Object::toString) - .collect(Collectors.joining(", "))).append("\n"); - } - - // Available inventory slots for flexible items - Set occupiedSlots = new HashSet<>(inventorySlotAssignments.keySet()); - int availableSlots = 28 - occupiedSlots.size(); - int flexibleSlotsNeeded = flexibleInventoryItems.stream() - .mapToInt(ItemRequirement::getRequiredInventorySlots) - .sum(); - - sb.append("Available inventory slots: ").append(availableSlots).append("\n"); - sb.append("Flexible items slots needed: ").append(flexibleSlotsNeeded).append("\n"); - sb.append("Slot surplus/deficit: ").append(availableSlots - flexibleSlotsNeeded).append("\n"); - - return sb.toString(); - } - - /** - * Enhanced toString that provides detailed plan information. - */ - @Override - public String toString() { - return getDetailedPlanString(); - } - - // ========== REQUIREMENTS PLANNING METHODS ========== - - /** - * Creates an optimized inventory setup plan from a requirement registry. - * This is the main entry point for the new requirements-based planning approach. - * - * @return true if planning was successful, false if mandatory requirements could not be fulfilled - */ - public boolean createPlanFromRequirements() { - if (registry == null || taskContext == null) { - throw new IllegalStateException("Cannot create plan from requirements: registry or TaskContext is null"); - } - - StringBuilder planningLog = new StringBuilder(); - planningLog.append("Starting comprehensive requirement analysis for context: ").append(taskContext) - .append(" with OR mode: ").append(orRequirementMode).append("\n"); - - // Track already planned items to avoid double-processing EITHER requirements - Set alreadyPlanned = new HashSet<>(); - - // Get all requirements filtered by context using new context-aware methods - Map> equipmentReqs = - registry.getEquipmentRequirements(taskContext); - Map> slotSpecificReqs = - registry.getInventoryRequirements(taskContext); - LinkedHashSet anySlotReqs = registry.getAnySlotLogicalRequirements(taskContext); - // Get conditional requirements that contain only ItemRequirements - List conditionalReqs = registry.getConditionalItemRequirements(taskContext); - List externalConditionalReqs = registry.getExternalConditionalItemRequirements(taskContext); - - // Process conditional requirements to get active ItemRequirements and merge them - integrateConditionalRequirements(conditionalReqs, externalConditionalReqs, equipmentReqs, slotSpecificReqs,anySlotReqs); - - // Step 1: Plan equipment slots (these have fixed positions) - planningLog.append("\n=== STEP 1: EQUIPMENT PLANNING ===\n"); - if (!planEquipmentSlotsFromCache(equipmentReqs, alreadyPlanned)) { - planningLog.append("FAILED: Mandatory equipment cannot be fulfilled\n"); - log.debug(planningLog.toString()); - return false; // Early exit if mandatory equipment cannot be fulfilled - } - - // Log status after equipment planning - planningLog.append("Equipment assignments: ").append(equipmentAssignments.size()).append("\n"); - planningLog.append("Items in alreadyPlanned: ").append(alreadyPlanned.size()).append("\n"); - planningLog.append("Missing mandatory items so far: ").append(missingMandatoryItems.size()).append("\n"); - - for (ItemRequirement planned : alreadyPlanned) { - planningLog.append(" - Already planned: ").append(planned.getName()) - .append(" (IDs: ").append(planned.getIds()).append(")\n"); - } - - // Step 2: Plan specific inventory slots (0-27, excluding items already planned in equipment) - planningLog.append("\n=== STEP 2: SPECIFIC SLOT PLANNING ===\n"); - planSpecificInventorySlots(slotSpecificReqs, alreadyPlanned); - - // Log status after specific slot planning - planningLog.append("Specific inventory slots: ").append(inventorySlotAssignments.size()).append("\n"); - planningLog.append("Items in alreadyPlanned: ").append(alreadyPlanned.size()).append("\n"); - planningLog.append("Missing mandatory items so far: ").append(missingMandatoryItems.size()).append("\n"); - - // Step 3: Plan flexible inventory items (any slot allowed, from the any-slot cache) - planningLog.append("\n=== STEP 3: FLEXIBLE PLANNING===\n"); - planFlexibleInventoryItems(anySlotReqs, alreadyPlanned); - - // Log status after flexible planning - planningLog.append("Flexible inventory items: ").append(flexibleInventoryItems.size()).append("\n"); - planningLog.append("Items in alreadyPlanned: ").append(alreadyPlanned.size()).append("\n"); - planningLog.append("Missing mandatory items final: ").append(missingMandatoryItems.size()).append("\n"); - - for (ItemRequirement missing : missingMandatoryItems) { - planningLog.append(" - Missing: ").append(missing.getName()) - .append(" (IDs: ").append(missing.getIds()) - .append(", Priority: ").append(missing.getPriority()).append(")\n"); - } - - // Step 4: Analyze and log comprehensive requirement status - planningLog.append(getComprehensiveRequirementAnalysis(true)); - - // Step 5: Optimize and validate the entire plan - planningLog.append("\n=== STEP 4: OPTIMIZATION AND VALIDATION ===\n"); - boolean planValid = optimizeAndValidatePlan(this); - - if (!planValid) { - planningLog.append("FAILED: Plan optimization and validation failed - see comprehensive analysis above for details\n"); - log.error(planningLog.toString()); - return false; - } - - planningLog.append("SUCCESS: Created and validated optimal layout plan for context: ").append(taskContext).append("\n"); - - // Output all planning logs at once - log.info(planningLog.toString()); - - return true; - } - - /** - * Integrates conditional requirements into the equipment and slot-specific requirements. - * This method evaluates active conditional requirements and merges their ItemRequirements - * into the appropriate LogicalRequirements for each slot. - * - * @param conditionalReqs Standard conditional requirements containing only ItemRequirements - * @param externalConditionalReqs External conditional requirements containing only ItemRequirements - * @param equipmentReqs Equipment requirements map to be updated - * @param slotSpecificReqs Slot-specific requirements map to be updated - */ - private void integrateConditionalRequirements( - List conditionalReqs, - List externalConditionalReqs, - Map> equipmentReqs, - Map> slotSpecificReqs, - LinkedHashSet anySlotReqs - ) { - - log.debug("Integrating {} standard and {} external conditional requirements", - conditionalReqs.size(), externalConditionalReqs.size()); - - // Process standard conditional requirements - for (ConditionalRequirement conditionalReq : conditionalReqs) { - processConditionalRequirement(conditionalReq, equipmentReqs, slotSpecificReqs, anySlotReqs,"standard"); - } - - // Process external conditional requirements - for (ConditionalRequirement conditionalReq : externalConditionalReqs) { - processConditionalRequirement(conditionalReq, equipmentReqs, slotSpecificReqs,anySlotReqs, "external"); - } - - log.debug("Completed integration of conditional requirements"); - } - - /** - * Processes a single conditional requirement and integrates its active ItemRequirements - * into the appropriate LogicalRequirements. - * - * @param conditionalReq The conditional requirement to process - * @param equipmentReqs Equipment requirements map to be updated - * @param slotSpecificReqs Slot-specific requirements map to be updated - * @param source Source type for logging ("standard" or "external") - */ - private void processConditionalRequirement( - ConditionalRequirement conditionalReq, - Map> equipmentReqs, - Map> slotSpecificReqs, - LinkedHashSet anySlotReqs, - String source) { - - try { - // Get active requirements from the conditional requirement - List activeRequirements = conditionalReq.getActiveRequirements(); - - if (activeRequirements.isEmpty()) { - log.debug("No active requirements for {} conditional requirement: {}", source, conditionalReq.getName()); - return; - } - - log.info("Processing {} active requirements from {} conditional requirement: {}", - activeRequirements.size(), source, conditionalReq.getName()); - - // Process each active requirement - for (Requirement activeReq : activeRequirements) { - if (activeReq instanceof ItemRequirement) { - ItemRequirement itemReq = (ItemRequirement) activeReq; - log.debug("Integrating active ItemRequirement: {}", itemReq.getName()); - integrateActiveItemRequirement(itemReq, equipmentReqs, slotSpecificReqs,anySlotReqs); - } else if (activeReq instanceof LogicalRequirement) { - LogicalRequirement logicalReq = (LogicalRequirement) activeReq; - if (logicalReq.containsOnlyItemRequirements()) { - // Extract all ItemRequirements from the LogicalRequirement - List itemRequirements = logicalReq.getAllItemRequirements(); - for (ItemRequirement itemReq : itemRequirements) { - log.debug("Integrating ItemRequirement from LogicalRequirement: {}", itemReq.getName()); - integrateActiveItemRequirement(itemReq, equipmentReqs, slotSpecificReqs,anySlotReqs); - } - } else { - log.error("\n\tSkipping LogicalRequirement with mixed requirement types in conditional: {} - consider correct impllementation of the condtional requirement: {}", - conditionalReq.getName()); - } - } else { - log.warn("Unexpected requirement type in conditional requirement: {} (type: {})", - activeReq.getClass().getSimpleName(), activeReq.getName()); - } - } - - } catch (Exception e) { - log.error("Error processing {} conditional requirement '{}': {}", - source, conditionalReq.getName(), e.getMessage(), e); - } - } - - /** - * Integrates a single active ItemRequirement into the appropriate slot requirements. - * - * @param itemReq The active ItemRequirement to integrate - * @param equipmentReqs Equipment requirements map to be updated - * @param slotSpecificReqs Slot-specific requirements map to be updated - */ - private void integrateActiveItemRequirement( - ItemRequirement itemReq, - Map> equipmentReqs, - Map> slotSpecificReqs, - LinkedHashSet anyslotSpecificReqs - ) { - - try { - switch (itemReq.getRequirementType()) { - case EQUIPMENT: - if (itemReq.getEquipmentSlot() != null) { - integrateIntoEquipmentSlot(itemReq, equipmentReqs); - } - break; - - case INVENTORY: - int slot = itemReq.hasSpecificInventorySlot() ? itemReq.getInventorySlot() : -1; - if (slot!=-1){ - integrateIntoInventorySlot(itemReq, slotSpecificReqs, slot); - }else{ - // Flexible inventory item, add to anyslotSpecificReqs - log.debug("Adding flexible ItemRequirement '{}' to anyslotSpecificReqs", itemReq.getName()); - anyslotSpecificReqs.add(new OrRequirement( - itemReq.getPriority(), - itemReq.getRating(), - "Flexible requirement for inventory. based on conditional requirement: " + itemReq.getName(), - itemReq.getTaskContext(), - ItemRequirement.class, - itemReq - )); - } - break; - - case EITHER: - // For EITHER requirements, we can choose the best placement - // For now, prefer equipment slot if available, otherwise flexible inventory - OrRequirement newOrReq = new OrRequirement( - itemReq.getPriority(), - itemReq.getRating(), - "Conditional requirement for inventory flexible", - itemReq.getTaskContext(), - ItemRequirement.class, - itemReq - ); - anyslotSpecificReqs.add(newOrReq); - break; - - default: - log.debug("Skipping ItemRequirement with non-slot type: {} ({})", - itemReq.getName(), itemReq.getRequirementType()); - break; - } - - } catch (Exception e) { - log.error("Error integrating active ItemRequirement '{}': {}", itemReq.getName(), e.getMessage(), e); - } - } - - /** - * Integrates an ItemRequirement into an equipment slot. - * Properly merges conditional requirements with existing OR requirements. - * - * @param itemReq The ItemRequirement to integrate - * @param equipmentReqs Equipment requirements map to be updated - */ - private void integrateIntoEquipmentSlot( - ItemRequirement itemReq, - Map> equipmentReqs) { - - EquipmentInventorySlot slot = itemReq.getEquipmentSlot(); - LinkedHashSet existingLogical = equipmentReqs.getOrDefault(slot , new LinkedHashSet<>()); - - - // If existing is not an OrRequirement or we couldn't merge, try direct addition - try { - existingLogical.add(itemReq); - log.debug("Added conditional ItemRequirement '{}' to existing equipment slot {}", - itemReq.getName(), slot); - } catch (IllegalArgumentException e) { - log.error("Cannot integrate conditional ItemRequirement '{}' into equipment slot {} - incompatible types: {}", - itemReq.getName(), slot, e.getMessage()); - // This should not happen in a well-designed system, but we log the error and continue - throw e; // Rethrow to indicate failure - } - equipmentReqs.put(slot, existingLogical); - - } - - /** - * Integrates an ItemRequirement into an inventory slot. - * Properly merges conditional requirements with existing OR requirements. - * - * @param itemReq The ItemRequirement to integrate - * @param slotSpecificReqs Slot-specific requirements map to be updated - * @param slot The inventory slot (-1 for flexible, 0-27 for specific) - */ - private void integrateIntoInventorySlot( - ItemRequirement itemReq, - Map> slotSpecificReqs, - int slot) { - - LinkedHashSet existingLogical = slotSpecificReqs.getOrDefault(slot , new LinkedHashSet<>()); - - if (existingLogical != null) { - - // If we can't merge into existing requirement, try to add directly - try { - existingLogical.add(itemReq); - log.debug("Added conditional ItemRequirement '{}' to existing inventory slot {}", - itemReq.getName(), slot == -1 ? "flexible" : String.valueOf(slot)); - } catch (IllegalArgumentException e) { - log.error("Cannot integrate conditional ItemRequirement '{}' into inventory slot {} - incompatible types: {}", - itemReq.getName(), slot == -1 ? "flexible" : String.valueOf(slot), e.getMessage()); - // This should not happen in a well-designed system, but we log the error and continue - throw e; // Rethrow to indicate failure - } - }else { - throw new IllegalArgumentException("No existing logical requirement for inventory slot " + - (slot == -1 ? "flexible" : String.valueOf(slot))); - } - slotSpecificReqs.put(slot, existingLogical); - } - - /** - * Plans equipment slots from the new cache structure (LinkedHashSet per slot). - * Treats multiple ItemRequirements in the same slot as alternatives (OR logic). - * - * @param equipmentReqs Map of equipment slot to set of ItemRequirements (alternatives for that slot) - * @param alreadyPlanned Set to track already planned items - * @return true if successful, false if mandatory equipment cannot be fulfilled - */ - private boolean planEquipmentSlotsFromCache(Map> equipmentReqs, - Set alreadyPlanned) { - for (Map.Entry> entry : equipmentReqs.entrySet()) { - EquipmentInventorySlot slot = entry.getKey(); - LinkedHashSet slotItems = entry.getValue(); - - if (slotItems.isEmpty()) { - log.debug("No requirements for equipment slot {}", slot); - continue; - } - - log.debug("Planning equipment slot {} with {} alternatives", slot, slotItems.size()); - - // Convert to list for compatibility with existing selector logic - List itemList = new ArrayList<>(slotItems); - - // Use enhanced item selection for equipment slots with proper slot and skill validation - ItemRequirement bestItem = RequirementSelector.findBestAvailableItemForEquipmentSlot( - itemList, slot, alreadyPlanned); - - if (bestItem != null) { - addEquipmentSlotAssignment(slot, bestItem); - alreadyPlanned.addAll(slotItems); // Mark as planned to avoid double-processing - - log.info("Assigned {} (type: {}) to equipment slot {}", - bestItem.getName(), bestItem.getRequirementType(), slot); - } else { - // Check if any requirement was mandatory - boolean hasMandatory = slotItems.stream().anyMatch(ItemRequirement::isMandatory); - - if (hasMandatory) { - for (ItemRequirement item : slotItems) { - if (item.isMandatory()) { - addMissingMandatoryEquipment(slot, item); - } - } - //addMissingMandatoryEquipment(slot); - log.warn("Cannot fulfill mandatory equipment requirement for slot {}", slot); - log.error("Planning failed: Missing mandatory equipment for slot {}", slot); - return false; // Early exit for mandatory equipment failure - } else { - log.debug("Optional equipment not available for slot {}", slot); - } - } - } - return true; // All mandatory equipment successfully planned - } - - - /** - * Plans specific inventory slots from context-filtered logical requirements. - * With the new cache structure, each slot has exactly one LogicalRequirement for the given context. - * - * @param slotSpecificReqs Slot-specific requirements (one LogicalRequirement per slot) - * @param alreadyPlanned Set to track already planned items - */ - private void planSpecificInventorySlots(Map> slotSpecificReqs, - Set alreadyPlanned) { - for (Map.Entry> entry : slotSpecificReqs.entrySet()) { - int slot = entry.getKey(); - LinkedHashSet itemSlotReq = entry.getValue(); - - // Skip the "any slot" entry (-1) as we'll handle it in planFlexibleInventoryItems - if (slot == -1) { - continue; - } - - - - ItemRequirement bestItem = RequirementSelector.findBestAvailableItemNotAlreadyPlannedForInventory( - itemSlotReq, this); - - if (bestItem != null) { - // Enhanced validation for slot assignment - if (!ItemRequirement.canAssignToSpecificSlot(bestItem, slot)) { - log.warn("Cannot assign item {} to slot {} due to constraints. Moving to flexible items.", - bestItem.getName(), slot); - throw new IllegalArgumentException( - "Item " + bestItem.getName() + " cannot be assigned to specific slot " + slot); - // Handle the item as flexible instead - //handleItemAsFlexible(bestItem, this, alreadyPlanned); - } else { - // Item can be assigned to specific slot - ItemRequirement slotSpecificItem = bestItem.copyWithSpecificSlot(slot); - addInventorySlotAssignment(slot, slotSpecificItem); - alreadyPlanned.addAll(itemSlotReq); // Mark all alternatives as planned - //for (ItemRequirement item : itemSlotReq) { - - - //} - log.info("Assigned {} to specific slot {} (amount: {}, stackable: {})", - bestItem.getName(), slot, bestItem.getAmount(), bestItem.isStackable()); - } - } else { - // Handle missing mandatory items - convert LinkedHashSet to OrRequirement - if (!itemSlotReq.isEmpty()) { - ItemRequirement firstItem = itemSlotReq.iterator().next(); - OrRequirement slotOrRequirement = new OrRequirement( - firstItem.getPriority(), - firstItem.getRating(), - "Slot " + slot + " requirement alternatives", - firstItem.getTaskContext(), - ItemRequirement.class, - itemSlotReq.toArray(new ItemRequirement[0]) - ); - handleMissingMandatoryItem(Collections.singletonList(slotOrRequirement), this, "inventory slot " + slot); - } - } - } - } - - /** - * Plans flexible inventory items from the any-slot cache (new cache structure). - * These items can be placed in any available inventory slot. - * - * @param anySlotReqs Set of OrRequirements for flexible inventory placement - * @param alreadyPlanned Set to track already planned items - */ - private void planFlexibleInventoryItems(LinkedHashSet anySlotReqs, - Set alreadyPlanned) { - if (anySlotReqs == null || anySlotReqs.isEmpty()) { - log.debug("No flexible inventory requirements to plan"); - return; - } - - log.debug("Planning {} flexible inventory OrRequirements", anySlotReqs.size()); - - for (OrRequirement orReq : anySlotReqs) { - log.debug("Planning flexible OR requirement: {}", orReq.getName()); - - // Extract ItemRequirements from the OrRequirement - List orItems = LogicalRequirement.extractItemRequirementsFromLogical(orReq); - - if (orItems.isEmpty()) { - log.warn("OrRequirement has no ItemRequirements: {}", orReq.getName()); - continue; - } - - // Check if this OR requirement has already been satisfied by equipment or specific slots - int alreadySatisfiedAmount = calculateAlreadySatisfiedAmount(orItems, alreadyPlanned); - int totalNeeded = orItems.get(0).getAmount(); // All items in OR should have same amount - - if (alreadySatisfiedAmount >= totalNeeded) { - log.debug("OR requirement already fully satisfied by equipment/specific slots: {} satisfied out of {} needed", - alreadySatisfiedAmount, totalNeeded); - continue; // Skip this OR requirement as it's already satisfied - } - - // Calculate remaining amount needed for inventory - int remainingAmountNeeded = totalNeeded - alreadySatisfiedAmount; - log.debug("OR requirement needs additional {} items for inventory (total needed: {}, already satisfied: {})", - remainingAmountNeeded, totalNeeded, alreadySatisfiedAmount); - // Plan remaining amount needed for inventory (pass remaining amount to avoid double calculation) - List plannedOrItems = planOrRequirement(orItems, remainingAmountNeeded, alreadyPlanned); - - // Check if the OR requirement is fully satisfied after planning - int totalPlannedInventory = plannedOrItems.stream().mapToInt(ItemRequirement::getAmount).sum(); - int totalPlanned = totalPlannedInventory + alreadySatisfiedAmount; - - if (totalPlanned < totalNeeded) { - if (orReq.getPriority() == RequirementPriority.MANDATORY) { - log.warn("Mandatory flexible OR requirement not fully satisfied: {} planned out of {} needed", - totalPlanned, totalNeeded); - missingMandatoryItems.addAll(orItems); - } else { - log.debug("Optional flexible OR requirement partially satisfied: {} planned out of {} needed", - totalPlanned, totalNeeded); - } - } else { - log.debug("Flexible OR requirement fully satisfied: {} planned (including {} already satisfied)", - totalPlanned, alreadySatisfiedAmount); - } - } - - log.debug("Finished planning flexible inventory items. Total flexible items: {}", flexibleInventoryItems.size()); - } - - - /** - * Plans an OR requirement by selecting the best combination of available items to fulfill the specified amount needed. - * This handles OR requirements according to the configured mode (ANY_COMBINATION or SINGLE_TYPE). - * - * @param orItems All items in the OR requirement group - * @param amountNeeded Amount still required (after accounting for equipment assignments) - * @param alreadyPlanned Set of already planned items to avoid conflicts - * @return List of ItemRequirements that were successfully planned - */ - private List planOrRequirement(List orItems, int amountNeeded, Set alreadyPlanned) { - if (orItems.isEmpty()) { - return new ArrayList<>(); - } - - log.debug("Planning OR requirement: {} amount needed from {} alternatives using mode: {}", - amountNeeded, orItems.size(), orRequirementMode); - - // If no amount needed, requirement is already satisfied - if (amountNeeded <= 0) { - log.info("OR requirement already fully satisfied - no additional inventory items needed"); - return new ArrayList<>(); - } - - switch (orRequirementMode) { - case SINGLE_TYPE: - return planOrRequirementSingleType(orItems, amountNeeded, alreadyPlanned); - case ANY_COMBINATION: - default: - return planOrRequirementAnyCombination(orItems, amountNeeded, alreadyPlanned); - } - //log.debug("OR requirement planning completed with allready planned items: {}", alreadyPlanned.size()); - } - - /** - * Calculates how much of an OR requirement has already been satisfied by equipment assignments or already planned items. - * This prevents double-counting when an item can be equipped but also fulfill inventory requirements. - * - * @param orItems All items in the OR requirement group - * @param alreadyPlanned Set of already planned items - * @return The amount already satisfied (0 if none) - */ - private int calculateAlreadySatisfiedAmount(List orItems, Set alreadyPlanned) { - int satisfiedAmount = 0; - - log.debug("Calculating already satisfied amount for OR group with {} items", orItems.size()); - log.debug("Current equipment assignments: {}", equipmentAssignments.size()); - log.debug("AlreadyPlanned set size: {}", alreadyPlanned.size()); - - // Check if any item from the OR group has been assigned to equipment - for (ItemRequirement orItem : orItems) { - log.debug("Checking OR item: {} (IDs: {})", orItem.getName(), orItem.getIds()); - - // Check if this specific item is in already planned (marked during equipment assignment) - if (alreadyPlanned.contains(orItem)) { - satisfiedAmount += orItem.getAmount(); - log.debug("Found OR item {} already planned with amount {}", orItem.getName(), orItem.getAmount()); - continue; - } - - // Also check if any equipment assignment matches this item by ID - for (Map.Entry equipEntry : equipmentAssignments.entrySet()) { - ItemRequirement equippedItem = equipEntry.getValue(); - log.debug("Comparing with equipped item: {} (IDs: {}) in slot {}", - equippedItem.getName(), equippedItem.getIds(), equipEntry.getKey().name()); - - // Check if the equipped item has the same ID as any of the OR alternatives - if (orItem.getIds().stream().anyMatch(id -> equippedItem.getIds().contains(id))) { - satisfiedAmount += equippedItem.getAmount(); - log.debug("MATCH FOUND! OR requirement satisfied by equipment: {} in slot {} with amount {}", - equippedItem.getName(), equipEntry.getKey().name(), equippedItem.getAmount()); - break; // Only count once per OR item - } else { - log.debug("No match: OR item IDs {} vs equipped item IDs {}", orItem.getIds(), equippedItem.getIds()); - } - } - } - log.info("Total satisfied amount calculated: {}", satisfiedAmount); - return satisfiedAmount; - } - - /** - * Checks if an item has already been planned in equipment slots, specific inventory slots, or the alreadyPlanned set. - * This prevents double-planning the same item in flexible inventory when it's already assigned elsewhere. - * - * @param item The item to check - * @param alreadyPlanned Set of items already marked as planned - * @return true if the item is already planned elsewhere, false otherwise - */ - private boolean isItemAlreadyPlannedElsewhere(ItemRequirement item, Set alreadyPlanned) { - // Check if it's in the alreadyPlanned set - if (alreadyPlanned.contains(item)) { - return true; - } - - // Check if any of the item's IDs match equipment assignments - for (ItemRequirement equippedItem : equipmentAssignments.values()) { - if (item.getIds().stream().anyMatch(id -> equippedItem.getIds().contains(id))) { - return true; - } - } - - // Check if any of the item's IDs match specific inventory slot assignments - for (ItemRequirement slotItem : inventorySlotAssignments.values()) { - if (item.getIds().stream().anyMatch(id -> slotItem.getIds().contains(id))) { - return true; - } - } - - return false; - } - - /** - * Plans an OR requirement using SINGLE_TYPE mode - must fulfill with exactly one item type. - * - * @param orItems All items in the OR requirement group - * @param amountNeeded Amount still required (after accounting for equipment assignments) - * @param alreadyPlanned Set of already planned items - * @return List of planned items (will contain at most one item type) - */ - private List planOrRequirementSingleType(List orItems, - int amountNeeded, - Set alreadyPlanned) { - List plannedItems = new ArrayList<>(); - - // If no amount needed, requirement is already satisfied - if (amountNeeded <= 0) { - return plannedItems; - } - - // Calculate what we have available for each item type - Map availableCounts = new HashMap<>(); - for (ItemRequirement item : orItems) { - if (alreadyPlanned.contains(item)) { - continue; // Skip already planned items - } - - int inventoryQuantity= Rs2Inventory.itemQuantity(item.getId()); - int bankCount = Rs2Bank.count(item.getUnNotedId()); - int totalAvailable = inventoryQuantity + bankCount; - - if (totalAvailable >= amountNeeded) { - availableCounts.put(item, totalAvailable); - } - } - - if (availableCounts.isEmpty()) { - log.warn("SINGLE_TYPE mode: No single item type has enough quantity ({} needed)", amountNeeded); - return plannedItems; - } - - // Sort available items by preference (priority, then rating, then amount available) - List> sortedAvailable = availableCounts.entrySet().stream() - .sorted((a, b) -> { - ItemRequirement itemA = a.getKey(); - ItemRequirement itemB = b.getKey(); - - // First by priority (MANDATORY > RECOMMENDED > OPTIONAL) - int priorityCompare = itemA.getPriority().compareTo(itemB.getPriority()); - if (priorityCompare != 0) { - return priorityCompare; - } - - // Then by rating (higher is better) - int ratingCompare = Integer.compare(itemB.getRating(), itemA.getRating()); - if (ratingCompare != 0) { - return ratingCompare; - } - - // Finally by available amount (more is better) - return Integer.compare(b.getValue(), a.getValue()); - }) - .collect(Collectors.toList()); - - // Select the best single item type that can fulfill the entire requirement - Map.Entry bestChoice = sortedAvailable.get(0); - ItemRequirement chosenItem = bestChoice.getKey(); - - // Create a copy with the exact amount needed - ItemRequirement plannedItem = chosenItem.copyWithAmount(amountNeeded); - handleItemAsFlexible(plannedItem, this, alreadyPlanned); - plannedItems.add(plannedItem); // Keep for tracking, but avoid duplicate addition later - - log.info("SINGLE_TYPE mode: Selected {} x{} (available: {}) for OR requirement", - chosenItem.getName(), amountNeeded, bestChoice.getValue()); - - return plannedItems; - } - - /** - * Plans an OR requirement using ANY_COMBINATION mode - can fulfill with any combination of items. - * This is the original behavior from PrePostScheduleRequirements. - * - * @param orItems All items in the OR requirement group - * @param amountNeeded Amount still required (after accounting for equipment assignments) - * @param alreadyPlanned Set of already planned items - * @return List of planned items (can be multiple types) - */ - private List planOrRequirementAnyCombination(List orItems, - int amountNeeded, - Set alreadyPlanned) { - List plannedItems = new ArrayList<>(); - - // If no amount needed, requirement is already satisfied - if (amountNeeded <= 0) { - return plannedItems; - } - - // Calculate what we have available for each item type - Map availableCounts = new HashMap<>(); - for (ItemRequirement item : orItems) { - if (alreadyPlanned.contains(item)) { - continue; // Skip already planned items - } - - int inventoryQuantity = Rs2Inventory.itemQuantity(item.getId()); - int bankCount = Rs2Bank.count(item.getUnNotedId()); - int totalAvailable = inventoryQuantity + bankCount; - - if (totalAvailable > 0) { - availableCounts.put(item, totalAvailable); - } - } - - // Sort available items by preference (priority, then rating, then amount available) - List> sortedAvailable = availableCounts.entrySet().stream() - .sorted((a, b) -> { - ItemRequirement itemA = a.getKey(); - ItemRequirement itemB = b.getKey(); - - // First by priority (MANDATORY > RECOMMENDED > OPTIONAL) - int priorityCompare = itemA.getPriority().compareTo(itemB.getPriority()); - if (priorityCompare != 0) { - return priorityCompare; - } - - // Then by rating (higher is better) - int ratingCompare = Integer.compare(itemB.getRating(), itemA.getRating()); - if (ratingCompare != 0) { - return ratingCompare; - } - - // Finally by available amount (more is better) - return Integer.compare(b.getValue(), a.getValue()); - }) - .collect(Collectors.toList()); - log.debug("ANY_COMBINATION mode: Sorted available items by preference ({} total)", sortedAvailable.size()); - log.debug("Sorted available items: {}", sortedAvailable.stream() - .map(e -> String.format("%s (available: %d)", e.getKey().getName(), e.getValue())) - .collect(Collectors.joining(", "))); - // Select items to fulfill the total amount needed (or as much as possible) - int remainingNeeded = amountNeeded; - - for (Map.Entry entry : sortedAvailable) { - if (remainingNeeded <= 0) { - break; - } - - ItemRequirement item = entry.getKey(); - int available = entry.getValue(); - int amountToTake = Math.min(remainingNeeded, available); - - // Create a copy of the item with the actual amount we're planning to take - ItemRequirement plannedItem = item.copyWithAmount(amountToTake); - - handleItemAsFlexible(plannedItem, this, alreadyPlanned); - plannedItems.add(plannedItem); // Keep for tracking, but remove duplicate addition later - - remainingNeeded -= amountToTake; - - log.debug("ANY_COMBINATION mode: Planned {} x{} for OR requirement (remaining needed: {})\n\t item: \n\t\t{}", - item.getName(), amountToTake, remainingNeeded,item); - } - - // Log the result - if (remainingNeeded > 0) { - log.warn("ANY_COMBINATION mode: OR requirement partially satisfied - planned {}/{} items from available options", - amountNeeded - remainingNeeded, amountNeeded); - - // Add a single "collective shortage" item to represent the unmet need - if (!plannedItems.isEmpty()) { - // Create a special shortage indicator using the first item as template - ItemRequirement firstItem = orItems.get(0); - String shortageDescription = String.format("OR requirement shortage: need %d more from any of %d item types", - remainingNeeded, orItems.size()); - - //ItemRequirement shortageItem = new ItemRequirement( - // -2, // Special shortage indicator ID - // remainingNeeded, - // firstItem.getEquipmentSlot(), - // firstItem.getInventorySlot(), - // firstItem.getPriority(), - // firstItem.getRating(), - // shortageDescription, - // firstItem.getTaskContext() - //); - - //addMissingMandatoryInventoryItem(shortageItem); - } - } else { - log.debug("ANY_COMBINATION mode: Successfully planned OR requirement: {} items from {} alternatives", - amountNeeded, plannedItems.size()); - } - - return plannedItems; - } - - /** - * Determines whether a missing item should be logged in the comprehensive analysis based on its priority and the flag. - * - * @param item The item requirement to check - * @return true if the item should be logged, false otherwise - */ - private boolean shouldLogMissingItem(ItemRequirement item) { - // Always log mandatory items - if (item.getPriority() == RequirementPriority.MANDATORY) { - return true; - } - - // Log recommended/optional items only if the flag is enabled - return logOptionalMissingItems; - } - - /** - * Logs a comprehensive analysis of all requirements including quantities, availability, and missing items. - * This provides a single, detailed summary instead of multiple scattered log messages. - */ - private String getComprehensiveRequirementAnalysis(boolean verbose) { - StringBuilder analysis = new StringBuilder(); - analysis.append("\n").append("=".repeat(80)); - analysis.append("\n\tCOMPREHENSIVE REQUIREMENT ANALYSIS - ").append(taskContext); - analysis.append("\n\tOR Requirement Mode: ").append(orRequirementMode); - analysis.append("\n").append("=".repeat(80)); - - // Equipment Analysis - int plannedEquipment = equipmentAssignments.size(); - int missingEquipment = missingMandatoryEquipment.size(); - analysis.append("\n\nðŸ“Ķ EQUIPMENT ANALYSIS:"); - analysis.append("\n ✓ Successfully planned: ").append(plannedEquipment).append(" slots"); - if (missingEquipment > 0) { - analysis.append("\n ❌ Missing mandatory: ").append(missingEquipment).append(" slots"); - for (Map.Entry> entry : missingMandatoryEquipment.entrySet()) { - EquipmentInventorySlot slot = entry.getKey(); - List items = entry.getValue(); - if (items != null && !items.isEmpty()) { - analysis.append("\n - Slot: ").append(slot.name()).append(" (missing "); - for (ItemRequirement item : items) { - if (verbose) { - analysis.append("\n ").append(item.displayString()); - } else { - int available = 0; - try { - available = Rs2Inventory.itemQuantity(item.getId()) + Rs2Bank.count(item.getUnNotedId()); - } catch (Exception e) { - // ignore, just show 0 - } - analysis.append(item.getName()) - .append(" [id:").append(item.getId()).append("]") - .append(", need: ").append(item.getAmount()) - .append(", available: ").append(available); - if (item.isStackable()) analysis.append(", stackable"); - if (item.getEquipmentSlot() != null) - analysis.append(", slot: ").append(item.getEquipmentSlot().name()); - if (item.getRequirementType() == RequirementType.EITHER) - analysis.append(", flexible"); - analysis.append("; "); - } - } - analysis.append(")\n"); - } - } - } - - // Inventory Analysis with quantities and availability - int plannedSpecificSlots = inventorySlotAssignments.size(); - int plannedFlexibleItems = flexibleInventoryItems.size(); - int missingMandatoryItems = this.missingMandatoryItems.size(); - int totalPlannedInventory = plannedSpecificSlots + plannedFlexibleItems; - - analysis.append("\n\n🎒 INVENTORY ANALYSIS:"); - analysis.append("\n ✓ Successfully planned: ").append(totalPlannedInventory).append(" items"); - analysis.append("\n - Specific slots: ").append(plannedSpecificSlots); - analysis.append("\n - Flexible items: ").append(plannedFlexibleItems); - - // DEBUG: Show what flexible items are planned - if (!flexibleInventoryItems.isEmpty()) { - analysis.append("\n 📋 DEBUG - Flexible items planned:"); - for (ItemRequirement flexItem : flexibleInventoryItems) { - analysis.append("\n - ").append(flexItem.getName()).append(" (IDs: ").append(flexItem.getIds()).append(", Priority: ").append(flexItem.getPriority()).append(")"); - } - } - - if (missingMandatoryItems > 0) { - analysis.append("\n ❌ Missing mandatory items: ").append(missingMandatoryItems); - - // Group missing items by their logical requirement to handle OR requirements properly - Map> groupedMissingItems = new HashMap<>(); - - for (ItemRequirement missingItem : this.missingMandatoryItems) { - // Try to find the logical requirement this item belongs to - String groupKey = findLogicalRequirementGroupKey(missingItem); - groupedMissingItems.computeIfAbsent(groupKey, k -> new ArrayList<>()).add(missingItem); - } - - // Analyze each group separately - for (Map.Entry> group : groupedMissingItems.entrySet()) { - String groupKey = group.getKey(); - List groupItems = group.getValue(); - - if (groupKey.startsWith("OR:")) { - // Handle OR requirement group - analyze as collective need - // Check if we should log this based on priority and flag - if (shouldLogMissingItem(groupItems.get(0))) { - analyzeOrRequirementGroup(analysis, groupKey, groupItems); - } - } else { - // Handle individual requirements - for (ItemRequirement missingItem : groupItems) { - if (shouldLogMissingItem(missingItem)) { - analyzeIndividualRequirement(analysis, missingItem); - } - } - } - } - } - - analysis.append("\n\n📊 SUMMARY:"); - analysis.append("\n Plan Feasible: ").append(isFeasible() ? "✅ YES" : "❌ NO"); - analysis.append("\n Fits in Inventory: ").append(fitsInInventory() ? "✅ YES" : "❌ NO"); - analysis.append("\n Total Slots Needed: ").append(getTotalInventorySlotsNeeded()).append("/28"); - analysis.append("\n").append("=".repeat(80)); - - return analysis.toString(); - } - - /** - * Finds the logical requirement group key for a given item requirement. - * This helps identify if an item belongs to an OR requirement group. - * - * @param item The item requirement to analyze - * @return A group key string for grouping related requirements - */ - private String findLogicalRequirementGroupKey(ItemRequirement item) { - if (registry == null) { - return "INDIVIDUAL:" + item.getName(); - } - - // Search through current context logical requirements to find which OR group this item belongs to - Map slotSpecificReqs = registry.getInventorySlotLogicalRequirements(taskContext); - - for (Map.Entry entry : slotSpecificReqs.entrySet()) { - OrRequirement logicalReq = entry.getValue(); - - if (logicalReq instanceof OrRequirement) { - List orItems = OrRequirement.extractItemRequirementsFromLogical(logicalReq); - - // Check if this item belongs to this OR requirement - for (ItemRequirement orItem : orItems) { - if (orItem.getIds().equals(item.getIds()) && - orItem.getAmount() == item.getAmount() && - Objects.equals(orItem.getEquipmentSlot(), item.getEquipmentSlot()) && - Objects.equals(orItem.getInventorySlot(), item.getInventorySlot())) { - return "OR:" + logicalReq.getDescription(); - } - } - } - } - - return "INDIVIDUAL:" + item.getName(); - } - - /** - * Analyzes an OR requirement group to calculate total available vs. total required items. - * This provides a more accurate analysis for OR requirements like "5 food items from any combination". - * Now includes detailed skill requirement checking and usability analysis. - * - * @param analysis The analysis string builder - * @param groupKey The group key identifying the OR requirement - * @param groupItems All items in the OR requirement group - */ - private void analyzeOrRequirementGroup(StringBuilder analysis, String groupKey, List groupItems) { - String orDescription = groupKey.substring(3); // Remove "OR:" prefix - - analysis.append("\n 🍎 OR Requirement Group: ").append(orDescription); - analysis.append("\n Mode: ").append(orRequirementMode); - - // Calculate total required amount (should be same for all items in OR requirement) - int totalRequired = groupItems.get(0).getAmount(); - analysis.append("\n Total Required: ").append(totalRequired); - - if (orRequirementMode == OrRequirementMode.SINGLE_TYPE) { - analysis.append(" (exactly ").append(totalRequired).append(" of ONE type)"); - } else { - analysis.append(" (any combination)"); - } - - // Calculate total available and usable items across all types in the OR requirement - int totalInventoryQuantity = 0; - int totalBankCount = 0; - int totalUsableCount = 0; - Map itemAnalysis = new HashMap<>(); - - for (ItemRequirement item : groupItems) { - int inventoryQuantity = 0; - int inventoryCount = 0; - int bankCount = 0; - - try { - // Safely get inventory and bank counts with error handling - inventoryQuantity = Rs2Inventory.itemQuantity(item.getId()); - bankCount = Rs2Bank.count(item.getUnNotedId()); - inventoryCount = Rs2Inventory.count(item.getId()); - } catch (ArrayIndexOutOfBoundsException e) { - log.warn("ArrayIndexOutOfBoundsException when counting item '{}' (ID: {}): {}", - item.getName(), item.getId(), e.getMessage()); - // Continue with 0 counts to avoid crashing - inventoryQuantity = 0; - inventoryCount = 0; - bankCount = 0; - } catch (Exception e) { - log.warn("Unexpected error when counting item '{}' (ID: {}): {}", - item.getName(), item.getId(), e.getMessage()); - inventoryQuantity = 0; - inventoryCount = 0; - bankCount = 0; - } - - int totalCount = inventoryQuantity + bankCount; - - totalInventoryQuantity += inventoryQuantity; - totalBankCount += bankCount; - - if (totalCount > 0) { - // Analyze usability based on skill requirements and requirement type - boolean canUse = checkItemUsability(item, inventoryQuantity, bankCount); - boolean meetsSkillReqs = item.meetsSkillRequirements(); - - ItemAvailabilityInfo info = new ItemAvailabilityInfo( - inventoryQuantity,inventoryCount, bankCount, canUse, meetsSkillReqs, item.getRequirementType(), - item.getSkillToUse(), item.getMinimumLevelToUse(), - item.getSkillToEquip(), item.getMinimumLevelToEquip() - ); - - itemAnalysis.put(item.getName(), info); - - if (canUse) { - totalUsableCount += totalCount; - } - } - } - - int totalAvailable = totalInventoryQuantity + totalBankCount; - analysis.append("\n Total Available: ").append(totalAvailable).append(" (Inventory: ").append(totalInventoryQuantity).append(", Bank: ").append(totalBankCount).append(")"); - analysis.append("\n Total Usable: ").append(totalUsableCount).append(" (considering skill requirements)"); - - // Show detailed breakdown of available items with usability analysis - if (!itemAnalysis.isEmpty()) { - analysis.append("\n Detailed Item Analysis:"); - for (Map.Entry entry : itemAnalysis.entrySet()) { - String itemName = entry.getKey(); - ItemAvailabilityInfo info = entry.getValue(); - - analysis.append("\n 🔍 ").append(itemName).append(": ") - .append(info.inventoryQuantity + info.bankCount).append(" total") - .append(" (Inv: ").append(info.inventoryQuantity).append("("+info.inventoryCount+")").append(", Bank: ").append(info.bankCount).append(")"); - - if (info.canUse) { - analysis.append(" ✅ USABLE"); - } else { - analysis.append(" ❌ NOT USABLE"); - - if (!info.meetsSkillRequirements) { - analysis.append(" - Skill requirements not met:"); - - if (info.skillToUse != null && info.minimumLevelToUse != null) { - int currentLevel = Rs2Player.getRealSkillLevel(info.skillToUse); - if (currentLevel < info.minimumLevelToUse) { - analysis.append(" Need ").append(info.skillToUse.getName()) - .append(" ").append(info.minimumLevelToUse) - .append(" (current: ").append(currentLevel).append(")"); - } - } - - if (info.skillToEquip != null && info.minimumLevelToEquip != null) { - int currentLevel = Rs2Player.getRealSkillLevel(info.skillToEquip); - if (currentLevel < info.minimumLevelToEquip) { - analysis.append(" Need ").append(info.skillToEquip.getName()) - .append(" ").append(info.minimumLevelToEquip) - .append(" to equip (current: ").append(currentLevel).append(")"); - } - } - } - - // Add requirement type context - if (info.requirementType == RequirementType.EQUIPMENT && info.inventoryCount > 0 && info.bankCount == 0) { - analysis.append(" - Item in inventory but requirement needs it equipped"); - } else if (info.requirementType == RequirementType.INVENTORY && info.inventoryCount == 0 && info.bankCount > 0) { - analysis.append(" - Item in bank but requirement needs it in inventory"); - } - } - } - } - - // Calculate shortage properly for OR requirements based on mode and usability - boolean isSufficient = false; - if (orRequirementMode == OrRequirementMode.SINGLE_TYPE) { - // Check if any single item type has enough usable items - boolean anySingleTypeHasEnoughUsable = itemAnalysis.values().stream() - .anyMatch(info -> info.canUse && (info.inventoryCount + info.bankCount) >= totalRequired); - - if (anySingleTypeHasEnoughUsable) { - analysis.append("\n Status: ✅ SUFFICIENT (at least one usable type has ").append(totalRequired).append("+ items)"); - isSufficient = true; - } else { - int maxUsableSingleType = itemAnalysis.values().stream() - .filter(info -> info.canUse) - .mapToInt(info -> info.inventoryCount + info.bankCount) - .max() - .orElse(0); - int shortage = totalRequired - maxUsableSingleType; - analysis.append("\n Status: ❌ INSUFFICIENT (need ").append(shortage).append(" more usable items of any single type)"); - } - } else { - // ANY_COMBINATION mode - consider only usable items - if (totalUsableCount >= totalRequired) { - analysis.append("\n Status: ✅ SUFFICIENT (have ").append(totalUsableCount).append(" usable, need ").append(totalRequired).append(")"); - isSufficient = true; - } else { - int shortage = totalRequired - totalUsableCount; - analysis.append("\n Status: ❌ INSUFFICIENT (need ").append(shortage).append(" more usable items of any type)"); - } - } - - analysis.append("\n Priority: ").append(groupItems.get(0).getPriority()); - - if (orRequirementMode == OrRequirementMode.SINGLE_TYPE) { - analysis.append("\n Note: Must have exactly ").append(totalRequired).append(" of ONE usable item type"); - } else { - analysis.append("\n Note: Any combination of usable items can fulfill this requirement"); - } - - // Add debugging hint if insufficient - if (!isSufficient) { - analysis.append("\n ðŸ’Ą Debug Hint: Check if items exist but skill requirements aren't met, or items are in wrong location (bank vs inventory)"); - } - } - - /** - * Checks if an item is usable based on its requirement type and skill requirements. - * - * @param item The item requirement to check - * @param inventoryCount Number of items in inventory - * @param bankCount Number of items in bank - * @return true if the item can be used to fulfill the requirement - */ - private boolean checkItemUsability(ItemRequirement item, int inventoryCount, int bankCount) { - // First check if we have the item available - if (inventoryCount + bankCount == 0) { - return false; - } - - // Check skill requirements - if (!item.meetsSkillRequirements()) { - return false; - } - - // Check requirement type constraints - RequirementType reqType = item.getRequirementType(); - switch (reqType) { - case INVENTORY: - // Must be available (can withdraw from bank if needed) - return true; - case EQUIPMENT: - // Must be available and equippable (can equip from inventory or bank) - return true; - case EITHER: - // Can be in inventory or equipped (most flexible) - return true; - default: - return true; - } - } - - /** - * Helper class to store item availability and usability information. - */ - private static class ItemAvailabilityInfo { - final int inventoryCount; - final int inventoryQuantity; - final int bankCount; - final boolean canUse; - final boolean meetsSkillRequirements; - final RequirementType requirementType; - final Skill skillToUse; - final Integer minimumLevelToUse; - final Skill skillToEquip; - final Integer minimumLevelToEquip; - - ItemAvailabilityInfo(int inventoryCount, int inventoryQuantity, int bankCount, boolean canUse, boolean meetsSkillRequirements, - RequirementType requirementType, Skill skillToUse, Integer minimumLevelToUse, - Skill skillToEquip, Integer minimumLevelToEquip) { - this.inventoryCount = inventoryCount; - this.inventoryQuantity = inventoryQuantity; - this.bankCount = bankCount; - this.canUse = canUse; - this.meetsSkillRequirements = meetsSkillRequirements; - this.requirementType = requirementType; - this.skillToUse = skillToUse; - this.minimumLevelToUse = minimumLevelToUse; - this.skillToEquip = skillToEquip; - this.minimumLevelToEquip = minimumLevelToEquip; - } - } - - /** - * Analyzes an individual item requirement (not part of an OR group). - * Now includes detailed skill requirement checking and usability analysis. - * - * @param analysis The analysis string builder - * @param missingItem The individual item requirement to analyze - */ - private void analyzeIndividualRequirement(StringBuilder analysis, ItemRequirement missingItem) { - analysis.append("\n 📋 Item: ").append(missingItem.getName()); - analysis.append("\n Required: ").append(missingItem.getAmount()); - - // Check current inventory and bank for availability with error handling - int getInventoryQuantity = 0; - int bankCount = 0; - - try { - // Safely get inventory and bank counts with error handling - getInventoryQuantity = Rs2Inventory.itemQuantity(missingItem.getId()); - bankCount = Rs2Bank.count(missingItem.getUnNotedId()); - } catch (ArrayIndexOutOfBoundsException e) { - log.warn("ArrayIndexOutOfBoundsException when counting individual item '{}' (ID: {}): {}", - missingItem.getName(), missingItem.getId(), e.getMessage()); - // Continue with 0 counts to avoid crashing - analysis.append("\n ⚠ïļ Error accessing item data - using 0 counts"); - } catch (Exception e) { - log.warn("Unexpected error when counting individual item '{}' (ID: {}): {}", - missingItem.getName(), missingItem.getId(), e.getMessage()); - analysis.append("\n ⚠ïļ Error accessing item data - using 0 counts"); - } - - int totalAvailable = getInventoryQuantity + bankCount; - - analysis.append("\n Available: ").append(totalAvailable).append(" (Inventory: ").append(getInventoryQuantity).append(", Bank: ").append(bankCount).append(")"); - - // Analyze usability based on skill requirements and requirement type - boolean canUse = checkItemUsability(missingItem, getInventoryQuantity, bankCount); - boolean meetsSkillReqs = missingItem.meetsSkillRequirements(); - boolean hasEnoughQuantity = totalAvailable >= missingItem.getAmount(); - - // Enhanced status analysis considering both quantity and usability - if (hasEnoughQuantity && canUse) { - analysis.append("\n Status: ✅ SUFFICIENT AND USABLE"); - analysis.append("\n ðŸ’Ą Debug Hint: Item is available and usable but wasn't selected - check planning logic"); - } else if (hasEnoughQuantity && !canUse) { - analysis.append("\n Status: ⚠ïļ AVAILABLE BUT NOT USABLE"); - - if (!meetsSkillReqs) { - analysis.append("\n ðŸšŦ Skill Requirements NOT Met:"); - - // Check skill to use requirements - if (missingItem.getSkillToUse() != null && missingItem.getMinimumLevelToUse() != null) { - int currentLevel = Rs2Player.getRealSkillLevel(missingItem.getSkillToUse()); - if (currentLevel < missingItem.getMinimumLevelToUse()) { - analysis.append("\n - Need ").append(missingItem.getSkillToUse().getName()) - .append(" ").append(missingItem.getMinimumLevelToUse()) - .append(" to use (current: ").append(currentLevel).append(")"); - } - } - - // Check skill to equip requirements - if (missingItem.getSkillToEquip() != null && missingItem.getMinimumLevelToEquip() != null) { - int currentLevel = Rs2Player.getRealSkillLevel(missingItem.getSkillToEquip()); - if (currentLevel < missingItem.getMinimumLevelToEquip()) { - analysis.append("\n - Need ").append(missingItem.getSkillToEquip().getName()) - .append(" ").append(missingItem.getMinimumLevelToEquip()) - .append(" to equip (current: ").append(currentLevel).append(")"); - } - } - } - - // Add requirement type context - RequirementType reqType = missingItem.getRequirementType(); - if (reqType == RequirementType.EQUIPMENT && getInventoryQuantity > 0 && bankCount == 0) { - analysis.append("\n 📍 Location Issue: Item in inventory but requirement needs it equipped"); - } else if (reqType == RequirementType.INVENTORY && getInventoryQuantity == 0 && bankCount > 0) { - analysis.append("\n 📍 Location Issue: Item in bank but requirement needs it in inventory"); - } - } else { - int shortage = missingItem.getAmount() - totalAvailable; - analysis.append("\n Status: ❌ INSUFFICIENT (need ").append(shortage).append(" more)"); - } - - // Add detailed item properties for debugging - analysis.append("\n Properties:"); - analysis.append("\n - Name: ").append(missingItem.getName()); - analysis.append("\n - ID: ").append(missingItem.getId()); - analysis.append("\n - noted ID: ").append(missingItem.getNotedId()); - analysis.append("\n - is noted item: ").append(missingItem.getNotedId() == missingItem.getId() ); - analysis.append("\n - Amount: ").append(missingItem.getAmount()); - analysis.append("\n - Stackable: ").append(missingItem.isStackable() ? "Yes" : "No"); - analysis.append("\n - Priority: ").append(missingItem.getPriority()); - analysis.append("\n - Requirement Type: ").append(missingItem.getRequirementType()); - - if (missingItem.getEquipmentSlot() != null) { - analysis.append("\n - Equipment Slot: ").append(missingItem.getEquipmentSlot()); - } - - if (missingItem.getInventorySlot() >= 0) { - analysis.append("\n - Specific Slot: ").append(missingItem.getInventorySlot()); - } - - // Add skill requirements summary if present - if (missingItem.getSkillToUse() != null || missingItem.getSkillToEquip() != null) { - analysis.append("\n Skill Requirements:"); - - if (missingItem.getSkillToUse() != null && missingItem.getMinimumLevelToUse() != null) { - int currentUseLevel = Rs2Player.getRealSkillLevel(missingItem.getSkillToUse()); - String useStatus = currentUseLevel >= missingItem.getMinimumLevelToUse() ? "✅" : "❌"; - analysis.append("\n - Use: ").append(useStatus) - .append(" ").append(missingItem.getSkillToUse().getName()) - .append(" ").append(missingItem.getMinimumLevelToUse()) - .append(" (current: ").append(currentUseLevel).append(")"); - } - - if (missingItem.getSkillToEquip() != null && missingItem.getMinimumLevelToEquip() != null) { - int currentEquipLevel = Rs2Player.getRealSkillLevel(missingItem.getSkillToEquip()); - String equipStatus = currentEquipLevel >= missingItem.getMinimumLevelToEquip() ? "✅" : "❌"; - analysis.append("\n - Equip: ").append(equipStatus) - .append(" ").append(missingItem.getSkillToEquip().getName()) - .append(" ").append(missingItem.getMinimumLevelToEquip()) - .append(" (current: ").append(currentEquipLevel).append(")"); - } - } - } - - // ========== PLAN EXECUTION METHODS ========== - - /** - * Banks all equipped and inventory items that are not part of this planned loadout. - * This ensures a clean slate before executing the optimal layout plan. - * - * @return true if banking was successful, false otherwise - */ - public boolean bankItemsNotInPlan(CompletableFuture scheduledFuture) { - try { - log.info("Analyzing current equipment and inventory state..."); - - // Quick check: if plan is empty, bank everything - boolean hasEquipmentPlan = !equipmentAssignments.isEmpty(); - boolean hasInventoryPlan = !inventorySlotAssignments.isEmpty() || !flexibleInventoryItems.isEmpty(); - - if (!hasEquipmentPlan && !hasInventoryPlan) { - log.info("Plan is empty - banking all equipment and inventory items"); - } else { - log.info("Plan has {} equipment assignments, {} specific inventory slots, {} flexible items", - equipmentAssignments.size(), - inventorySlotAssignments.size(), - flexibleInventoryItems.size()); - } - - // Ensure bank is open - if (!Rs2Bank.isOpen()) { - log.warn("Bank is not open for cleanup banking. Opening bank..."); - if (!Rs2Bank.walkToBankAndUseBank()) { - log.error("Failed to open bank for cleanup banking"); - return false; - } - sleepUntil(() -> Rs2Bank.isOpen(), 5000); - } - - boolean bankingSuccess = true; - int itemsBanked = 0; - - // Step 1: Bank equipped items not in the plan - log.info("Banking equipped items not in planned loadout..."); - for (EquipmentInventorySlot equipmentSlot : EquipmentInventorySlot.values()) { - if (scheduledFuture!=null && scheduledFuture.isCancelled()) { - log.info("Banking task cancelled, stopping equipment cleanup."); - return false; - } - // Check if this slot is planned to have an item - ItemRequirement plannedItem = equipmentAssignments.get(equipmentSlot); - - // Get currently equipped item in this slot - Rs2ItemModel equippedItem = Rs2Equipment.get(equipmentSlot); - - if (equippedItem != null) { // Something is equipped in this slot - boolean shouldKeepEquipped = false; - - if (plannedItem != null) { - // Check if the currently equipped item matches the planned item - shouldKeepEquipped = plannedItem.getId() == equippedItem.getId(); - } - - if (!shouldKeepEquipped) { - // Unequip the item (this will move it to inventory, then we can bank it) - log.info("Unequipping item from slot {}: {}", equipmentSlot.name(), equippedItem.getName()); - - if (Rs2Equipment.interact(item -> item.getSlot() == equipmentSlot.getSlotIdx(), "remove")) { - sleepUntil(() -> Rs2Equipment.get(equipmentSlot) == null, 3000); - - // Now bank the item from inventory - if (Rs2Inventory.hasItem(equippedItem.getId())) { - Rs2Bank.depositOne(equippedItem.getId()); - sleepUntil(() -> !Rs2Inventory.hasItem(equippedItem.getId()), 3000); - itemsBanked++; - } - } else { - log.warn("Failed to unequip item from slot {}", equipmentSlot.name()); - bankingSuccess = false; - } - } else { - log.info("Keeping equipped item in slot {}: matches planned loadout", equipmentSlot.name()); - } - } - } - - // Step 2: Bank inventory items not in the plan - log.info("Banking inventory items not in planned loadout..."); - - // Collect all planned item IDs for quick lookup - Set plannedItemIds = new HashSet<>(); - - // Add planned equipment item IDs - for (ItemRequirement equippedItem : equipmentAssignments.values()) { - plannedItemIds.add(equippedItem.getId()); - } - - // Add planned specific slot item IDs - for (ItemRequirement slotItem : inventorySlotAssignments.values()) { - plannedItemIds.add(slotItem.getId()); - } - - // Add planned flexible item IDs - for (ItemRequirement flexibleItem : flexibleInventoryItems) { - plannedItemIds.add(flexibleItem.getId()); - } - - // Check each inventory slot - for (int inventorySlot = 0; inventorySlot < 28; inventorySlot++) { - final int currentSlot = inventorySlot; // Make it effectively final for lambda - Rs2ItemModel currentItem = Rs2Inventory.getItemInSlot(inventorySlot); - if (currentItem != null) { - boolean shouldKeepInInventory = false; - - // Check if this item is part of the planned loadout - if (plannedItemIds.contains(currentItem.getId())) { - // Further check: is this item in the right place according to the plan? - shouldKeepInInventory = isItemInCorrectPlanPosition(currentItem, inventorySlot); - } - - if (!shouldKeepInInventory) { - // Bank the item - log.info("Banking inventory item from slot {}: {} x{}", - inventorySlot, currentItem.getName(), currentItem.getQuantity()); - Rs2Bank.depositOne(currentItem.getId()); - if ( sleepUntil(() -> Rs2Inventory.getItemInSlot(currentSlot) == null || - Rs2Inventory.getItemInSlot(currentSlot).getId() != currentItem.getId(), 3000)) { - itemsBanked++; - } else { - log.warn("Failed to bank item from inventory slot {}: {}", inventorySlot, currentItem.getName()); - bankingSuccess = false; - } - } else { - log.info("Keeping inventory item in slot {}: {} (matches planned position)", - inventorySlot, currentItem.getName()); - } - } - } - - if (bankingSuccess) { - log.info("Successfully banked {} items not in planned loadout", itemsBanked); - } else { - log.warn("Banking completed with some failures. {} items banked.", itemsBanked); - } - - return bankingSuccess; - - } catch (Exception e) { - log.error("Error banking items not in plan: {}", e.getMessage(), e); - return false; - } - } - - /** - * Checks if an item is in the correct position according to this plan. - * This considers both specific slot assignments and flexible item allowances. - * - * @param currentItem The item currently in inventory - * @param currentSlot The slot where the item is currently located - * @return true if the item should stay in its current position, false if it should be banked - */ - private boolean isItemInCorrectPlanPosition(Rs2ItemModel currentItem, int currentSlot) { - int itemId = currentItem.getId(); - int itemQuantity = currentItem.getQuantity(); - - // Check if this slot has a specific assignment that matches - ItemRequirement specificSlotItem = inventorySlotAssignments.get(currentSlot); - if (specificSlotItem != null) { - // Check if the current item matches the planned item for this slot - if (specificSlotItem.getId() == itemId) { - // Check quantity requirements - if (specificSlotItem.getAmount() <= itemQuantity) { - log.info("Item {} in slot {} matches specific slot assignment", currentItem.getName(), currentSlot); - return true; - } else { - log.info("Item {} in slot {} matches ID but insufficient quantity: {} < {}", - currentItem.getName(), currentSlot, itemQuantity, specificSlotItem.getAmount()); - return false; - } - } else { - log.info("Item {} in slot {} doesn't match specific slot assignment", currentItem.getName(), currentSlot); - return false; - } - } - - // Check if this item is among the flexible items - for (ItemRequirement flexibleItem : flexibleInventoryItems) { - if (flexibleItem.getId() == itemId) { - log.info("Item {} in slot {} is a planned flexible item", currentItem.getName(), currentSlot); - return true; // Flexible items can be anywhere - } - } - - // Not found in any planned positions - log.info("Item {} in slot {} is not part of the planned loadout", currentItem.getName(), currentSlot); - return false; - } - - /** - * Executes this inventory and equipment layout plan. - * Withdraws and equips items according to the optimal layout. - * - * @return true if execution was successful - */ - public boolean executePlan(CompletableFuture scheduledFuture) { - try { - log.info("\n--- Executing Inventory and Equipment Layout Plan ---"); - - // Ensure bank is open - if (!Rs2Bank.isOpen()) { - if (!Rs2Bank.walkToBankAndUseBank()) { - log.error("Failed to open bank for plan execution"); - return false; - } - sleepUntil(() -> Rs2Bank.isOpen(), 5000); - } - - boolean success = true; - - // Step 1: Handle equipment assignments - if (!equipmentAssignments.isEmpty()) { - log.info("Executing equipment assignments ({} items)...", equipmentAssignments.size()); - for (Map.Entry entry : equipmentAssignments.entrySet()) { - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.info("Plan execution cancelled, stopping equipment assignments."); - return false; - } - EquipmentInventorySlot slot = entry.getKey(); - ItemRequirement item = entry.getValue(); - - if (!handleEquipmentAssignment(slot, item)) { - log.error("Failed to fulfill equipment assignment: {} -> {}", slot.name(), item.getName()); - success = false; - } - } - } - - // Step 2: Handle specific inventory slot assignments - if (!inventorySlotAssignments.isEmpty()) { - log.info("Executing specific inventory slot assignments ({} items)...", inventorySlotAssignments.size()); - for (Map.Entry entry : inventorySlotAssignments.entrySet()) { - Integer slot = entry.getKey(); - ItemRequirement item = entry.getValue(); - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.info("Plan execution cancelled, stopping inventory slot assignments."); - return false; - } - if (!handleInventorySlotAssignment(slot, item)) { - log.error("Failed to fulfill inventory slot assignment: slot {} -> {}", slot, item.getName()); - success = false; - } - } - } - - // Step 3: Handle flexible inventory items - if (!flexibleInventoryItems.isEmpty()) { - log.info("Executing flexible inventory items ({} items)...", flexibleInventoryItems.size()); - for (ItemRequirement item : flexibleInventoryItems) { - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.info("Plan execution cancelled, stopping flexible inventory items."); - return false; - } - if (!handleFlexibleInventoryItem(item)) { - log.error("Failed to fulfill flexible inventory item: {}", item.getName()); - // Don't mark as failed for optional items - if (item.isMandatory()) { - success = false; - } - } - } - } - - if (success) { - log.info("Successfully executed all plan assignments"); - } else { - log.error("Some plan assignments failed to execute"); - } - - return success; - - } catch (Exception e) { - log.error("Error executing plan: {}", e.getMessage(), e); - return false; - } - } - - /** - * Handles a single equipment assignment. - */ - private boolean handleEquipmentAssignment(EquipmentInventorySlot slot, ItemRequirement item) { - log.info("Handling equipment assignment: {} -> {}", slot.name(), item.getName()); - - // Check if already equipped correctly (considering fuzzy matching for variations) - Rs2ItemModel currentEquipped = Rs2Equipment.get(slot); - if (currentEquipped != null) { - if (item.isFuzzy()) { - // For fuzzy items, check if any variation is equipped - Collection variations = InventorySetupsVariationMapping.getVariations(item.getId()); - if (variations.contains(currentEquipped.getId())) { - log.info("Item {} (or variation) already equipped in slot {}", item.getName(), slot.name()); - return true; - } - } else { - // Exact ID match - if (item.getId() == currentEquipped.getId()) { - log.info("Item {} already equipped in slot {}", item.getName(), slot.name()); - return true; - } - } - } - - // Try to withdraw and equip the item if available - if (item.isAvailableInBank()) { - int itemId = item.getId(); - Rs2Bank.withdrawAndEquip(itemId); - if (sleepUntil(() -> Rs2Equipment.get(slot) != null && - Rs2Equipment.get(slot).getId() == itemId, 5000)) { - log.info("Withdrew {} for equipment slot {}", item.getName(), slot.name()); - log.info("Successfully equipped {} in slot {}", item.getName(), slot.name()); - return true; - } - } - - log.error("Failed to equip {} in slot {}", item.getName(), slot.name()); - return false; - } - - /** - * Handles a single inventory slot assignment. - */ - private boolean handleInventorySlotAssignment(int slot, ItemRequirement item) { - log.info("Handling inventory slot assignment: slot {} -> {}", slot, item.getName()); - - // Use the static utility method from ItemRequirement - return ItemRequirement.withdrawAndPlaceInSpecificSlot(slot, item); - } - - /** - * Handles a single flexible inventory item. - */ - private boolean handleFlexibleInventoryItem(ItemRequirement item) { - log.info("Handling flexible inventory item: {}", item.getName()); - - // Check if item is already in inventory with sufficient amount (handles fuzzy matching) - if (item.isAvailableInInventory()) { - log.info("Flexible item {} already in inventory with sufficient amount", item.getName()); - return true; - } - - // Try to withdraw the item if available in bank - if (item.isAvailableInBank()) { - int itemId = item.getId(); - if (Rs2Bank.withdrawX(itemId, item.getAmount())) { - log.info("Withdrew flexible item: {} x{}", item.getName(), item.getAmount()); - return true; - } else { - log.warn("Failed to withdraw flexible item: {} x{}", item.getName(), item.getAmount()); - } - } - - log.error("Could not withdraw flexible item: {}", item.getName()); - return false; - } - - // ========== VALIDATION AND ANALYSIS METHODS ========== - - /** - * Optimizes and validates the entire plan with comprehensive checks. - * This includes space optimization, conflict resolution, feasibility checking, and comprehensive mandatory validation. - * - * @param plan The inventory setup plan to optimize and validate - * @return true if the plan is valid and feasible, false otherwise - */ - public static boolean optimizeAndValidatePlan(InventorySetupPlanner plan) { - // Step 1: Optimize flexible item placement - plan.optimizeFlexibleItemPlacement(); - - // Step 2: Validate plan feasibility - if (!plan.isFeasible()) { - log.info("Plan is not feasible - has missing mandatory items:"); - plan.getMissingMandatoryItems().forEach(item -> - log.info(" - Missing: {}", item.getName())); - plan.getMissingMandatoryEquipment().forEach((slot,items) -> - log.info(" - Missing equipment slot: {}", slot+ " with items: {}", - items.stream().map(ItemRequirement::getName).collect(Collectors.joining(", ")))); - return false; // Early exit if plan is not feasible - } - - // Step 3: Comprehensive mandatory requirements validation - boolean allMandatorySatisfied = validateAllMandatoryRequirementsSatisfied(plan); - if (!allMandatorySatisfied) { - log.error("CRITICAL: Final validation failed - not all mandatory requirements are satisfied in the plan"); - return false; // Early exit if mandatory requirements are not satisfied - } - - // Step 4: Validate inventory capacity - if (!plan.fitsInInventory()) { - log.warn("Plan does not fit in inventory - needs {} slots but only 28 available", - plan.getTotalInventorySlotsNeeded()); - return false; // Early exit if plan exceeds inventory capacity - } else { - log.info("Plan successfully created - uses {}/28 inventory slots", - plan.getTotalInventorySlotsNeeded()); - } - - // Step 5: Log summary for debugging - if (log.isDebugEnabled()) { - log.info("Plan summary:\n{}", plan.getSummary()); - } - return true; // Plan is valid and feasible - } - - /** - * Silent version of optimizeAndValidatePlan that suppresses logging. - * Used when detailed analysis is already logged elsewhere. - * - * @param plan The inventory setup plan to optimize and validate - * @return true if the plan is valid and feasible, false otherwise - */ - public static boolean optimizeAndValidatePlanSilent(InventorySetupPlanner plan) { - // Step 1: Optimize flexible item placement - plan.optimizeFlexibleItemPlacement(); - - // Step 2: Validate plan feasibility (silent) - if (!plan.isFeasible()) { - return false; // Early exit if plan is not feasible - } - - // Step 3: Comprehensive mandatory requirements validation (silent) - boolean allMandatorySatisfied = validateAllMandatoryRequirementsSatisfied(plan); - if (!allMandatorySatisfied) { - return false; // Early exit if mandatory requirements are not satisfied - } - - // Step 4: Validate inventory capacity (silent) - if (!plan.fitsInInventory()) { - return false; // Early exit if plan exceeds inventory capacity - } - - return true; // Plan is valid and feasible - } - - /** - * Validates that all mandatory requirements are actually satisfied in the final plan. - * This is a comprehensive check that goes beyond just checking for "missing" items. - * - * @param plan The inventory setup plan to validate - * @return true if all mandatory requirements are satisfied, false otherwise - */ - public static boolean validateAllMandatoryRequirementsSatisfied(InventorySetupPlanner plan) { - // This would need access to the registry, so we'll keep this method simpler - // and focus on validating the plan itself rather than cross-referencing requirements - - // Check if all mandatory items in the plan are actually present - boolean allSatisfied = true; - List unsatisfiedRequirements = new ArrayList<>(); - - // Validate equipment assignments - for (Map.Entry entry : plan.getEquipmentAssignments().entrySet()) { - ItemRequirement item = entry.getValue(); - if (item.isMandatory()) { - // Use ItemRequirement's own availability checking which handles fuzzy matching and amounts - if (!item.isAvailableInInventoryOrBank()) { - unsatisfiedRequirements.add(item.getName() + " (equipment slot: " + entry.getKey() + ")"); - allSatisfied = false; - } - } - } - - // Validate inventory assignments - for (ItemRequirement item : plan.getInventorySlotAssignments().values()) { - if (item.isMandatory()) { - // Use ItemRequirement's own availability checking which handles fuzzy matching and amounts - if (!item.isAvailableInInventoryOrBank()) { - unsatisfiedRequirements.add(item.getName() + " (inventory)"); - allSatisfied = false; - } - } - } - - // Validate flexible items - for (ItemRequirement item : plan.getFlexibleInventoryItems()) { - if (item.isMandatory()) { - // Use ItemRequirement's own availability checking which handles fuzzy matching and amounts - if (!item.isAvailableInInventoryOrBank()) { - unsatisfiedRequirements.add("(flexible inventory): "+ item.displayString() ); - allSatisfied = false; - } - } - } - - if (!allSatisfied) { - StringBuilder unsatisfiedRequirementsBuilder = new StringBuilder(); - unsatisfiedRequirementsBuilder.append(String.format("\nFinal validation failed. {} mandatory requirements not satisfied:", unsatisfiedRequirements.size())); - unsatisfiedRequirements.forEach(req -> unsatisfiedRequirementsBuilder.append(String.format("\n - {}", req))); - log.error(unsatisfiedRequirementsBuilder.toString()); - } else { - log.info("Final validation passed - all mandatory requirements in plan are satisfied"); - } - - return allSatisfied; - } - - /** - * Handles an item as flexible inventory item with proper conflict checking. - * - * @param bestItem The item to handle as flexible - * @param plan The inventory setup plan - * @param alreadyPlanned Set of already planned items - */ - public static void handleItemAsFlexible(ItemRequirement bestItem, InventorySetupPlanner plan, Set alreadyPlanned) { - plan.addFlexibleInventoryItem(bestItem); - alreadyPlanned.add(bestItem); - - log.info("Added {} as flexible inventory item (amount: {}, stackable: {})", - bestItem.getName(), bestItem.getAmount(), bestItem.isStackable()); - } - - /** - * Handles missing mandatory items by adding them to the appropriate missing lists. - * - * @param contextReqs The requirements that couldn't be fulfilled - * @param plan The inventory setup plan - * @param location Description of where the item was needed (for logging) - */ - public static void handleMissingMandatoryItem(List contextReqs, InventorySetupPlanner plan, String location) { - // Check if any requirement was mandatory - boolean hasMandatory = LogicalRequirement.hasMandatoryItems(contextReqs); - - if (hasMandatory) { - List mandatoryItems = LogicalRequirement.extractMandatoryItemRequirements(contextReqs); - for (ItemRequirement mandatoryItem : mandatoryItems) { - plan.addMissingMandatoryInventoryItem(mandatoryItem); - log.info("Cannot fulfill mandatory item requirement for {}: {}", location, mandatoryItem.getName()); - } - } - } - - /** - * Converts this plan to an InventorySetup that can be used with Rs2InventorySetup. - * This allows reusing the existing inventory setup loading functionality. - * - * @param setupName The name for the generated setup - * @return InventorySetup generated from this plan, or null if conversion failed - */ - public InventorySetup convertToInventorySetup(String setupName) { - return convertToInventorySetup(setupName, java.awt.Color.RED, true, null, true, false, 0, false, -1); - } - - /** - * Converts this plan to an InventorySetup with customizable configuration. - * This allows reusing the existing inventory setup loading functionality. - * - * @param setupName The name for the generated setup - * @param highlightColor The highlight color for differences - * @param highlightDifference Whether to highlight differences - * @param displayColor The display color (can be null) - * @param filterBank Whether to filter bank - * @param unorderedHighlight Whether to highlight unordered differences - * @param spellbook The spellbook setting - * @param favorite Whether the setup is marked as favorite - * @param iconID The icon ID for the setup - * @return InventorySetup generated from this plan, or null if conversion failed - */ - public InventorySetup convertToInventorySetup(String setupName, java.awt.Color highlightColor, - boolean highlightDifference, java.awt.Color displayColor, boolean filterBank, - boolean unorderedHighlight, int spellbook, boolean favorite, int iconID) { - try { - log.debug("Converting InventorySetupPlanner to InventorySetup: {}", setupName); - - // Create inventory items list - List inventoryItems = - createInventoryItemsList(); - - // Create equipment items list - List equipmentItems = - createEquipmentItemsList(); - - // Create empty containers for special items (rune pouch, bolt pouch, quiver) - List runePouchItems = - createRunePouchItemsList(); - - List boltPouchItems = - createBoltPouchItemsList(); - - List quiverItems = - createQuiverItemsList(); - - // Create the inventory setup using the same pattern as addInventorySetup - return createInventorySetupFromLists( - setupName, - inventoryItems, - equipmentItems, - runePouchItems, - boltPouchItems, - quiverItems, - highlightColor, - highlightDifference, - displayColor, - filterBank, - unorderedHighlight, - spellbook, - favorite, - iconID - ); - - } catch (Exception e) { - log.error("Failed to convert InventorySetupPlanner to InventorySetup: {}", e.getMessage(), e); - return null; - } - } - - /** - * Creates the inventory items list from the plan. - * Fills all 28 slots, using dummy items for empty slots. - */ - private List createInventoryItemsList() { - List inventoryItems = new ArrayList<>(); - - // Initialize all 28 slots with dummy items - for (int i = 0; i < 28; i++) { - inventoryItems.add(InventorySetupsItem.getDummyItem()); - } - - // Fill specific slot assignments - for (Map.Entry entry : inventorySlotAssignments.entrySet()) { - int slot = entry.getKey(); - ItemRequirement item = entry.getValue(); - - if (slot >= 0 && slot < 28) { - inventoryItems.set(slot, createInventorySetupsItem(item, slot)); - log.debug("\n\t Added specific slot assignment: {} -> slot {}", item.getName(), slot); - } - } - - // Fill flexible items in available slots - int currentSlot = 0; - for (ItemRequirement item : flexibleInventoryItems) { - // Find next available slot - while (currentSlot < 28 && !InventorySetupsItem.itemIsDummy(inventoryItems.get(currentSlot))) { - currentSlot++; - } - - if (currentSlot < 28) { - inventoryItems.set(currentSlot, createInventorySetupsItem(item, currentSlot)); - log.debug("\n\t -Added flexible item: {} -> slot {}", item.getName(), currentSlot); - currentSlot++; - } else { - log.warn("No available inventory slot for flexible item: {}", item.getName()); - } - } - - return inventoryItems; - } - - /** - * Creates the equipment items list from the plan. - * Fills all 14 equipment slots, using dummy items for empty slots. - */ - private List createEquipmentItemsList() { - List equipmentItems = new ArrayList<>(); - - // Initialize all 14 equipment slots with dummy items - for (int i = 0; i < 14; i++) { - equipmentItems.add(InventorySetupsItem.getDummyItem()); - } - - // Fill equipment assignments - for (Map.Entry entry : equipmentAssignments.entrySet()) { - EquipmentInventorySlot slot = entry.getKey(); - ItemRequirement item = entry.getValue(); - - int slotIndex = slot.getSlotIdx(); - if (slotIndex >= 0 && slotIndex < 14) { - equipmentItems.set(slotIndex, createInventorySetupsItem(item, slotIndex)); - log.debug("Added equipment assignment: {} -> slot {}", item.getName(), slot.name()); - } - } - - return equipmentItems; - } - - /** - * Creates the rune pouch items list from the plan. - * Detects RunePouchRequirement objects and converts their required runes to InventorySetupsItem objects. - */ - private List createRunePouchItemsList() { - List runePouchItems = new ArrayList<>(); - - // Search for RunePouchRequirement in both inventory slot assignments and flexible items - List runePouchRequirements = new ArrayList<>(); - - // Check inventory slot assignments for RunePouchRequirement instances - for (ItemRequirement item : inventorySlotAssignments.values()) { - if (item instanceof RunePouchRequirement) { - runePouchRequirements.add((RunePouchRequirement) item); - } - } - - // Check flexible inventory items for RunePouchRequirement instances - for (ItemRequirement item : flexibleInventoryItems) { - if (item instanceof RunePouchRequirement) { - runePouchRequirements.add((RunePouchRequirement) item); - } - } - - // Convert rune requirements to InventorySetupsItem objects - if (!runePouchRequirements.isEmpty()) { - log.debug("Found {} RunePouchRequirement(s) in plan", runePouchRequirements.size()); - - // Collect all required runes from all RunePouchRequirements - Map allRequiredRunes = new HashMap<>(); - - for (RunePouchRequirement runePouchReq : runePouchRequirements) { - // Merge rune requirements (taking the maximum quantity for each rune type) - for (Map.Entry entry : runePouchReq.getRequiredRunes().entrySet()) { - net.runelite.client.plugins.microbot.util.magic.Runes rune = entry.getKey(); - int requiredAmount = entry.getValue(); - allRequiredRunes.merge(rune, requiredAmount, Integer::max); - } - } - - // Convert runes to InventorySetupsItem objects - // Rune pouch has 4 slots (0-3), but we'll use all available slots - int slotIndex = 0; - for (Map.Entry entry : allRequiredRunes.entrySet()) { - if (slotIndex >= 4) { - log.warn("Rune pouch can only hold 4 types of runes, skipping extra runes"); - break; - } - - net.runelite.client.plugins.microbot.util.magic.Runes rune = entry.getKey(); - int quantity = entry.getValue(); - - // Create InventorySetupsItem for this rune - InventorySetupsItem runeItem = new InventorySetupsItem( - rune.getItemId(), // itemID - rune.name() + " Rune", // name - quantity, // quantity - false, // fuzzy (runes don't have variations) - InventorySetupsStackCompareID.None, // stackCompare - false, // locked - slotIndex // slot in rune pouch - ); - - runePouchItems.add(runeItem); - slotIndex++; - - log.debug("Added rune to pouch setup: {} x{} in slot {}", - rune.name(), quantity, slotIndex - 1); - } - } - - // Fill remaining slots with dummy items if needed (rune pouch has 4 slots) - while (runePouchItems.size() < 4) { - InventorySetupsItem dummyItem = new InventorySetupsItem( - -1, // dummy item ID - "", // empty name - 0, // no quantity - false, // not fuzzy - InventorySetupsStackCompareID.None, // no stack compare - false, // not locked - runePouchItems.size() // slot index - ); - runePouchItems.add(dummyItem); - } - - log.debug("Created rune pouch items list with {} items", runePouchItems.size()); - return runePouchItems; - } - - /** - * Creates the bolt pouch items list from the plan. - * TODO: Currently returns empty list - can be enhanced to detect bolt pouch requirements - */ - private List createBoltPouchItemsList() { - // For now, return empty list - can be enhanced later to handle bolt pouch requirements - return new ArrayList<>(); - } - - /** - * Creates the quiver items list from the plan. - * TODO: Currently returns empty list - can be enhanced to detect quiver requirements - */ - private List createQuiverItemsList() { - // For now, return empty list - can be enhanced later to handle quiver requirements - return new ArrayList<>(); - } - - /** - * Creates an InventorySetupsItem from an ItemRequirement. - * Uses the same constructor pattern as found in MInventorySetupsPlugin. - */ - private InventorySetupsItem createInventorySetupsItem(ItemRequirement item, int slot) { - // Use fuzzy matching for items that have multiple variations - boolean fuzzy = item.isStackable() || hasItemVariations(item.getId()); - - // Default stack compare type - could be enhanced based on item type - InventorySetupsStackCompareID stackCompare = - InventorySetupsStackCompareID.None; - - // Item is not locked by default - could be enhanced based on requirements - boolean locked = false; - - return new InventorySetupsItem( - item.getId(), - item.getName(), - item.getAmount(), - fuzzy, - stackCompare, - locked, - slot - ); - } - - /** - * Checks if an item has known variations (e.g., degraded equipment). - */ - private boolean hasItemVariations(int itemId) { - // Use the existing variation mapping to check for variations - try { - Collection variations = - InventorySetupsVariationMapping.getVariations(itemId); - return variations.size() > 1; - } catch (Exception e) { - // If variation mapping fails, default to false - return false; - } - } - - /** - * Creates an InventorySetup from item lists, reusing the same pattern as MInventorySetupsPlugin.addInventorySetup. - */ - private InventorySetup createInventorySetupFromLists( - String setupName, - List inventoryItems, - List equipmentItems, - List runePouchItems, - List boltPouchItems, - List quiverItems) { - - // Default settings - could be enhanced to be configurable - java.awt.Color highlightColor = java.awt.Color.RED; - boolean highlightDifference = true; - java.awt.Color displayColor = null; - boolean filterBank = true; - boolean unorderedHighlight = false; - int spellbook = 0; // Standard spellbook - boolean favorite = false; - int iconID = -1; - - return createInventorySetupFromLists(setupName, inventoryItems, equipmentItems, - runePouchItems, boltPouchItems, quiverItems, highlightColor, highlightDifference, - displayColor, filterBank, unorderedHighlight, spellbook, favorite, iconID); - } - - /** - * Creates an InventorySetup from item lists with configurable parameters. - */ - private InventorySetup createInventorySetupFromLists( - String setupName, - List inventoryItems, - List equipmentItems, - List runePouchItems, - List boltPouchItems, - List quiverItems, - java.awt.Color highlightColor, - boolean highlightDifference, - java.awt.Color displayColor, - boolean filterBank, - boolean unorderedHighlight, - int spellbook, - boolean favorite, - int iconID) { - - // Create the inventory setup using the same constructor as in addInventorySetup - return new InventorySetup( - inventoryItems, - equipmentItems, - runePouchItems, - boltPouchItems, - quiverItems, - new java.util.HashMap<>(), // additionalFilteredItems - setupName, - "automatically Generated by the InventorySetupPlanner", // notes - highlightColor, - highlightDifference, - displayColor, - filterBank, - unorderedHighlight, - spellbook, - favorite, - iconID - ); - } - - /** - * Adds this plan as an InventorySetup to the MInventorySetupsPlugin with default settings. - * Returns the created InventorySetup if successful, null otherwise. - */ - public InventorySetup addToInventorySetupsPlugin(String setupName) { - java.awt.Color highlightColor = java.awt.Color.RED; - boolean highlightDifference = true; - java.awt.Color displayColor = null; - boolean filterBank = true; - boolean unorderedHighlight = false; - int spellbook = 0; // Standard spellbook - boolean favorite = false; - int iconID = -1; - - return addToInventorySetupsPlugin(setupName, highlightColor, highlightDifference, - displayColor, filterBank, unorderedHighlight, spellbook, favorite, iconID); - } - - /** - * Adds this plan as an InventorySetup to the MInventorySetupsPlugin with full configuration. - * Returns the created InventorySetup if successful, null otherwise. - */ - public InventorySetup addToInventorySetupsPlugin(String setupName, java.awt.Color highlightColor, - boolean highlightDifference, java.awt.Color displayColor, boolean filterBank, - boolean unorderedHighlight, int spellbook, boolean favorite, int iconID) { - try { - int MAX_SETUP_NAME_LENGTH = MInventorySetupsPlugin.MAX_SETUP_NAME_LENGTH; - if( setupName.length() > MAX_SETUP_NAME_LENGTH) { - // Trim the setup name to the maximum allowed length - setupName = setupName.substring(0, MAX_SETUP_NAME_LENGTH); - } - // Convert this plan to an InventorySetup with all configuration parameters - InventorySetup inventorySetup = convertToInventorySetup(setupName, highlightColor, - highlightDifference, displayColor, filterBank, unorderedHighlight, spellbook, favorite, iconID); - - if (inventorySetup == null) { - log.error("Failed to convert plan to InventorySetup"); - return null; - } - - // Update or add the setup using the same logic as Rs2InventorySetup - updateSetup(inventorySetup); - - log.debug("Successfully added InventorySetup '{}' to MInventorySetupsPlugin", setupName); - return inventorySetup; - - } catch (Exception e) { - log.error("Failed to add plan to MInventorySetupsPlugin: {}", e.getMessage(), e); - return null; - } - } - - /** - * Updates an existing setup or adds a new one if it doesn't exist. - * Uses the same logic as Rs2InventorySetup.updateSetup. - * - * @param newSetup The setup to update/add - */ - private void updateSetup(InventorySetup newSetup) { - InventorySetup existingSetup = getInventorySetup(newSetup.getName()); - if (existingSetup != null) { - MInventorySetupsPlugin.getInventorySetups().remove(existingSetup); - MInventorySetupsPlugin plugin = getMInventorySetupsPlugin(); - if (plugin != null) { - plugin.getCache().removeSetup(existingSetup); - } - } - addSetupToPlugin(newSetup); - } - - /** - * Adds a setup to the plugin's setup list and cache. - * Uses the same logic as Rs2InventorySetup.addSetupToPlugin. - * - * @param setup The setup to add - */ - private void addSetupToPlugin(InventorySetup setup) { - MInventorySetupsPlugin plugin = getMInventorySetupsPlugin(); - log.debug("\n\t Adding setup '{}' (name length{}) to MInventorySetupsPlugin", setup.getName() ,setup.getName().length() ); - if (plugin != null) { - plugin.addInventorySetup(setup); - - Rs2InventorySetup.isInventorySetup(setup.getName()); // Ensure setup is recognized as an inventory setup - sleepUntil( () -> Rs2InventorySetup.isInventorySetup(setup.getName()), 5000); - - //plugin.getCache().addSetup(setup); - //MInventorySetupsPlugin.getInventorySetups().add(setup); - //plugin.getDataManager().updateConfig(true, false); - //Layout setupLayout = plugin.getLayoutUtilities().createSetupLayout(setup); - //plugin.getLayoutManager().saveLayout(setupLayout); - //plugin.getTagManager().setHidden(setupLayout.getTag(), true); - //SwingUtilities.invokeLater(() -> plugin.getPan().redrawOverviewPanel(false)); - } - } - - /** - * Helper method to get an inventory setup by name. - * - * @param setupName The name of the setup to find - * @return The InventorySetup if found, null otherwise - */ - private InventorySetup getInventorySetup(String setupName) { - return MInventorySetupsPlugin.getInventorySetups().stream() - .filter(setup -> setup.getName().equalsIgnoreCase(setupName)) - .findFirst() - .orElse(null); - } - - /** - * Helper method to get the MInventorySetupsPlugin instance. - * - * @return MInventorySetupsPlugin instance or null if not available - */ - private MInventorySetupsPlugin getMInventorySetupsPlugin() { - return (MInventorySetupsPlugin) net.runelite.client.plugins.microbot.Microbot.getPlugin( - MInventorySetupsPlugin.class.getName()); - } - - /** - * Creates an Rs2InventorySetup instance from this planner for execution using existing Rs2 utilities. - * This allows leveraging the existing loadInventory(), loadEquipment(), etc. methods. - * - * @param setupName The name of the setup to create - * @param mainScheduler The scheduler to monitor for cancellation - * @return Rs2InventorySetup instance, or null if creation failed - */ - public Rs2InventorySetup createRs2InventorySetup(String setupName, ScheduledFuture mainScheduler) { - try { - // First, add this plan to the MInventorySetupsPlugin - InventorySetup createdSetup = addToInventorySetupsPlugin(setupName); - if (createdSetup == null) { - log.error("Failed to add inventory setup to plugin"); - return null; - } - - // Create Rs2InventorySetup using the setup name - Rs2InventorySetup rs2Setup = - new Rs2InventorySetup(createdSetup.getName(), mainScheduler); - - log.info("\n\t-Successfully created Rs2InventorySetup from planner: {}", createdSetup.getName()); - return rs2Setup; - - } catch (Exception e) { - log.error("Failed to create Rs2InventorySetup from planner: {}", e.getMessage(), e); - return null; - } - } - - /** - * Executes this plan using the Rs2InventorySetup approach. - * This provides a more integrated solution that reuses existing bank and equipment management. - * - * @param scheduledFuture The future to monitor for cancellation - * @param setupName The name for the temporary setup - * @return true if execution was successful - */ - public boolean executeUsingRs2InventorySetup(CompletableFuture scheduledFuture, String setupName) { - return executeUsingRs2InventorySetup(scheduledFuture, setupName, false); - } - - /** - * Executes this plan using the Rs2InventorySetup approach with optional banking of items not in setup. - * This provides a more integrated solution that reuses existing bank and equipment management. - * - * @param scheduledFuture The future to monitor for cancellation - * @param setupName The name for the temporary setup - * @param bankItemsNotInSetup Whether to bank items not in the setup first (excludes teleport items) - * @return true if execution was successful - */ - public boolean executeUsingRs2InventorySetup(CompletableFuture scheduledFuture, String setupName, boolean bankItemsNotInSetup) { - try { - log.info("\n\t-Executing plan using Rs2InventorySetup approach: {}", setupName); - - // Convert CompletableFuture to ScheduledFuture (simplified conversion) - ScheduledFuture mainScheduler = new ScheduledFuture() { - @Override - public long getDelay(TimeUnit unit) { return 0; } - @Override - public int compareTo(Delayed o) { return 0; } - @Override - public boolean cancel(boolean mayInterruptIfRunning) { return scheduledFuture.cancel(mayInterruptIfRunning); } - @Override - public boolean isCancelled() { return scheduledFuture.isCancelled(); } - @Override - public boolean isDone() { return scheduledFuture.isDone(); } - @Override - public Object get() throws InterruptedException, ExecutionException { return scheduledFuture.get(); } - @Override - public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { return scheduledFuture.get(timeout, unit); } - }; - - // Create Rs2InventorySetup from this plan - Rs2InventorySetup rs2Setup = - createRs2InventorySetup(setupName, mainScheduler); - - if (rs2Setup == null) { - log.error("Failed to create Rs2InventorySetup"); - return false; - } - if(rs2Setup.doesEquipmentMatch() && rs2Setup.doesInventoryMatch()){ - log.info("Plan already matches current inventory and equipment setup, skipping execution"); - return true; // No need to execute if already matches - } - if (!Rs2Bank.isOpen()) { - if (!Rs2Bank.walkToBankAndUseBank() && !Rs2Player.isInteracting() && !Rs2Player.isMoving()) { - log.error("\n\tFailed to open bank for comprehensive item management"); - } - boolean openBank= sleepUntil(() -> Rs2Bank.isOpen(), 5000); - if (!openBank) { - log.error("\n\tFailed to open bank within timeout period,for invntory setup execution \"{}\"", setupName); - return false; - } - } - - // Bank items not in setup first if requested (excludes teleport items) - if (bankItemsNotInSetup) { - log.info("Banking items not in setup (excluding teleport items) before setting up: {}", setupName); - if (!rs2Setup.bankAllItemsNotInSetup(true)) { - log.warn("Failed to bank all items not in setup, continuing with setup anyway"); - } - } - // Use existing Rs2InventorySetup methods to fulfill the requirements - boolean equipmentSuccess = rs2Setup.loadEquipment(); - if (!equipmentSuccess) { - log.error("Failed to load equipment using Rs2InventorySetup"); - return false; - } - - boolean inventorySuccess = rs2Setup.loadInventory(); - if (!inventorySuccess) { - log.error("Failed to load inventory using Rs2InventorySetup"); - return false; - } - - // Verify the setup matches - boolean equipmentMatches = rs2Setup.doesEquipmentMatch(); - boolean inventoryMatches = rs2Setup.doesInventoryMatch(); - - if (equipmentMatches && inventoryMatches) { - log.info("Successfully executed plan using Rs2InventorySetup: {}", setupName); - return true; - } else { - log.warn("Plan execution completed but setup verification failed. Equipment matches: {}, Inventory matches: {}", - equipmentMatches, inventoryMatches); - return false; - } - - } catch (Exception e) { - log.error("Failed to execute plan using Rs2InventorySetup: {}", e.getMessage(), e); - return false; - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/ItemRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/ItemRequirement.java deleted file mode 100644 index 5fc977a84ab..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/ItemRequirement.java +++ /dev/null @@ -1,1947 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item; - -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.api.ItemComposition; -import net.runelite.api.Skill; -import net.runelite.client.game.ItemEquipmentStats; -import net.runelite.client.game.ItemStats; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -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.tasks.requirements.requirement.logical.OrRequirement; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; -import net.runelite.client.plugins.microbot.util.inventory.Rs2FuzzyItem; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import lombok.Getter; -import lombok.EqualsAndHashCode; -import lombok.extern.slf4j.Slf4j; - -import java.util.List; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.concurrent.CompletableFuture; -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -/** - * Enhanced item recommendation that supports multiple item IDs, different requirement types, - * and priority-based selection logic. Generalized to handle both equipment and inventory requirements. - * - * Supports both equipment requirements (must be equipped) and inventory requirements (must be in inventory). - * Also includes charge detection for items like rings of dueling, binding necklaces, etc. - */ -@Getter -@EqualsAndHashCode(callSuper = true) -@Slf4j -public class ItemRequirement extends Requirement { - - - /** - * Default item count for this requirement. - * This can be overridden if the plugin requires a specific count. - */ - private final int amount; - - /** - * The equipment slot this item should occupy (only relevant for EQUIPMENT and EITHER requirement types). - * Null for pure inventory items. - */ - private final EquipmentInventorySlot equipmentSlot; - - /** - * The specific inventory slot this item should occupy (0-27). - * -1 means any available slot. - * Only relevant for INVENTORY and EITHER requirement types. - * For EITHER requirements, this represents the preferred inventory slot if not equipped. - */ - @Getter - private final Integer inventorySlot; - - /** - * Skill name for the minimum level requirement to use this item (e.g., "Attack", "Runecraft"). - * Null if no level requirement. - */ - private final Skill skillToUse; - - /** - * Minimum player level required to use this item, if applicable. - * Used for items that have a level requirement to be used effectively. - * - * Null if no level requirement. - */ - private final Integer minimumLevelToUse; - - /** - * Skill name for the minimum level requirement (e.g., "Attack", "Runecraft"). - * Null if no level requirement. - */ - private final Skill skillToEquip; - - /** - * Minimum player level required to use the highest priority item in the list. - * Used for level-based item selection (e.g., attack level for weapons). - * Null if no level requirement. - */ - private final Integer minimumLevelToEquip; - - /** - * Whether to use fuzzy matching for this item requirement. - * When fuzzy is true, the requirement will match any variation of the same item. - * For example, different charge states of an item (ring of dueling(8), ring of dueling(7), etc.) - * or different variants of the same item (graceful pieces in different colors). - * Uses InventorySetupsVariationMapping under the hood to map between equivalent item IDs. - */ - private final boolean fuzzy; - private ItemComposition itemComposition = null; // Cached item composition for performance - - /** - * Full constructor for item requirement with schedule context and inventory slot support. - * The RequirementType is automatically inferred from the slot parameters: - * - INVENTORY: equipmentSlot is null and inventorySlot is provided (not -1) - * - EQUIPMENT: equipmentSlot is provided and inventorySlot is -1 - * - EITHER: both equipmentSlot and inventorySlot are provided (not -1) - */ - public ItemRequirement( - int itemId, - int amount, - EquipmentInventorySlot equipmentSlot, - Integer inventorySlot, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext, - Skill skillToUse, - Integer minimumLevelToUse, - Skill skillToEquip, - Integer minimumLevelToEquip, - boolean fuzzy) { - - // Call super constructor with inferred RequirementType and resolved item ID - super(inferRequirementType(equipmentSlot, inventorySlot), priority, rating, description, - Arrays.asList(resolveOptimalItemId(itemId, amount)), taskContext); - - // Only override amount for equipment items (not inventory or ammo) - if (equipmentSlot != null && equipmentSlot != EquipmentInventorySlot.AMMO) { - amount = 1; // For non-ammo equipment items, amount is typically 1 - } - this.amount = amount; - this.equipmentSlot = equipmentSlot; - this.inventorySlot = inventorySlot; - this.skillToUse = skillToUse; - this.minimumLevelToUse = minimumLevelToUse; - this.skillToEquip = skillToEquip; - this.minimumLevelToEquip = minimumLevelToEquip; - if (isNoted()){ - this.fuzzy = true; - }else if(Rs2FuzzyItem.isChargedItem(itemId)){ - this.fuzzy = fuzzy; // it can be desired to use the exact item.. - }else{ - this.fuzzy = fuzzy; - } - - // Validate slot assignments - validateSlotAssignments(); - } - - // Convenience constructors - - /** - * Constructor for single item ID with equipment requirement. - */ - public ItemRequirement(int itemId, int amount, EquipmentInventorySlot equipmentSlot, - RequirementPriority priority, int rating, String description, TaskContext taskContext) { - this(itemId, amount, equipmentSlot, -2, priority, rating, description, - taskContext, null, null, null, null, false); - } - - /** - * Constructor for single item ID with equipment requirement with default amount of 1. - */ - public ItemRequirement(int itemId, EquipmentInventorySlot equipmentSlot, - RequirementPriority priority, int rating, String description, TaskContext taskContext) { - this(itemId, 1, equipmentSlot, -2, priority, rating, description, - taskContext, null, null, null, null, false); - } - - /** - * Constructor for single item ID with equipment requirement and skill requirements for use only. - */ - public ItemRequirement(int itemId, int amount, EquipmentInventorySlot equipmentSlot, Integer inventorySlot, - RequirementPriority priority, int rating, String description, TaskContext taskContext, - Skill skillToUse, Integer minimumLevelToUse) { - this(itemId, amount, equipmentSlot, inventorySlot, priority, rating, description, - taskContext, skillToUse, minimumLevelToUse, null, null, false); - } - - /** - * Constructor for single item ID with equipment requirement and skill requirements for use only. - */ - public ItemRequirement(int itemId, int amount, EquipmentInventorySlot equipmentSlot, - RequirementPriority priority, int rating, String description, TaskContext taskContext, - Skill skillToUse, Integer minimumLevelToUse) { - this(itemId, amount, equipmentSlot, -2, priority, rating, description, - taskContext, skillToUse, minimumLevelToUse, null, null, false); - } - - /** - * Constructor for single item ID with equipment requirement and both skill requirements (use and equip). - */ - public ItemRequirement(int itemId, int amount, EquipmentInventorySlot equipmentSlot, Integer inventorySlot, - RequirementPriority priority, int rating, String description, TaskContext taskContext, - Skill skillToUse, Integer minimumLevelToUse, Skill skillToEquip, Integer minimumLevelToEquip) { - this(itemId, amount, equipmentSlot, inventorySlot, priority, rating, description, - taskContext, skillToUse, minimumLevelToUse, skillToEquip, minimumLevelToEquip, false); - } - - /** - * Constructor for single item ID with equipment requirement and both skill requirements (use and equip). - */ - public ItemRequirement(int itemId, int amount, EquipmentInventorySlot equipmentSlot, - RequirementPriority priority, int rating, String description, TaskContext taskContext, - Skill skillToUse, Integer minimumLevelToUse, Skill skillToEquip, Integer minimumLevelToEquip) { - this(itemId, amount, equipmentSlot, -2, priority, rating, description, - taskContext, skillToUse, minimumLevelToUse, skillToEquip, minimumLevelToEquip, false); - } - - /** - * Constructor for single item ID with fuzzy option. - */ - public ItemRequirement(int itemId, EquipmentInventorySlot equipmentSlot, - RequirementPriority priority, int rating, String description, TaskContext taskContext, boolean fuzzy) { - this(itemId, 1, equipmentSlot, -2, priority, rating, description, - taskContext, null, null, null, null, fuzzy); - } - - /** - * Additional constructors for inventory slot specification - */ - - /** - * Constructor for single item ID with specific inventory slot. - */ - public ItemRequirement(int itemId, int amount, Integer inventorySlot, - RequirementPriority priority, int rating, String description, TaskContext taskContext) { - this(itemId, amount, null, inventorySlot, priority, rating, description, - taskContext, null, null, null, null, false); - } - - /** - * Constructor for EITHER requirement with both equipment and inventory slot specification. - */ - public ItemRequirement(int itemId, int amount, EquipmentInventorySlot equipmentSlot, Integer inventorySlot, - RequirementPriority priority, int rating, String description, TaskContext taskContext) { - this(itemId, amount, equipmentSlot, inventorySlot, priority, rating, description, - taskContext, null, null, null, null, false); - } - - @Override - public String getName() { - if (this.itemComposition == null) { - // Lazy load item composition if not already set - setItemComp(getId()); - } - if(isFuzzy()){ - if(this.itemComposition != null){ - boolean isCharged = Rs2FuzzyItem.isChargedItem(getId()); - if(isCharged){ - return Rs2FuzzyItem.getBaseItemNameFromString( this.itemComposition.getName()); - } - return this.itemComposition.getName(); - }else{ - return "Unknown Item (Fuzzy)"; - } - - } - // Use the single item ID as the name - return this.itemComposition != null ? this.itemComposition.getName() : "Unknown Item"; - } - private void setItemComp(int itemId) { - this.itemComposition = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemComposition(itemId)) .orElse(null); - } - - /** - * Checks if this is a dummy item requirement. - * Dummy item requirements have ID of -1 and are used to block inventory/equipment slots. - * Dummy items serve several purposes: - * 1. They represent empty slots in containers - * 2. They maintain consistent array sizes for comparison - * 3. They can be used as placeholders for required "empty" slots - * 4. They're automatically considered "fulfilled" in requirement checking - * - * @return true if this is a dummy item requirement (itemId == -1) - */ - public boolean isDummyItemRequirement() { - return getId() == -1; - } - - /** - * Creates a dummy item requirement for blocking an equipment slot. - * Dummy requirements have MANDATORY priority, rating 10, itemId -1, amount -1, and no skill requirements. - * They are used to reserve slots without specifying actual items. - * - * @param equipmentSlot The equipment slot to block - * @param TaskContext When this requirement applies - * @param description Description for the dummy requirement - * @return A dummy ItemRequirement for the specified equipment slot - */ - public static ItemRequirement createDummyEquipmentRequirement( - EquipmentInventorySlot equipmentSlot, - TaskContext taskContext, - String description) { - if (equipmentSlot == null) { - throw new IllegalArgumentException("Equipment slot cannot be null for dummy equipment requirement"); - } - - return new ItemRequirement( - -1, // dummy item ID - -1, // dummy amount - equipmentSlot, - -2, // equipment-only slot indicator - RequirementPriority.MANDATORY, - 10, // rating - description, - taskContext, - null, // skillToUse - -1, // minimumLevelToUse - null, // skillToEquip - -1, // minimumLevelToEquip - false // fuzzy - ); - } - - /** - * Creates a dummy item requirement for blocking an inventory slot. - * Dummy requirements have MANDATORY priority, rating 10, itemId -1, amount -1, and no skill requirements. - * They are used to reserve slots without specifying actual items. - * - * @param inventorySlot The inventory slot to block (0-27) - * @param TaskContext When this requirement applies - * @param description Description for the dummy requirement - * @return A dummy ItemRequirement for the specified inventory slot - * @throws IllegalArgumentException if inventorySlot is not between 0 and 27 - */ - public static ItemRequirement createDummyInventoryRequirement( - int inventorySlot, - TaskContext taskContext, - String description) { - if (inventorySlot < 0 || inventorySlot > 27) { - throw new IllegalArgumentException("Inventory slot must be between 0 and 27, got: " + inventorySlot); - } - - return new ItemRequirement( - -1, // dummy item ID - -1, // dummy amount - null, // equipmentSlot - inventorySlot, - RequirementPriority.MANDATORY, - 10, // rating - description, - taskContext, - null, // skillToUse - -1, // minimumLevelToUse - null, // skillToEquip - -1, // minimumLevelToEquip - false // fuzzy - ); - } - - // ========== EXISTING FACTORY METHODS ========== - - /** - * Factory method to create an OrRequirement when multiple item IDs are provided. - * Use this instead of ItemRequirement constructors with List when you have multiple alternative items. - * - * @param itemIds List of alternative item IDs - * @param amount Amount required for each item - * @param equipmentSlot Equipment slot if this is equipment - * @param inventorySlot Inventory slot if specific slot required - * @param priority Priority level - * @param rating Effectiveness rating - * @param description Description for the OR requirement - * @param TaskContext When to fulfill this requirement - * @param skillToUse Skill required to use the items - * @param minimumLevelToUse Minimum level to use - * @param skillToEquip Skill required to equip - * @param minimumLevelToEquip Minimum level to equip - * @param fuzzy Whether to prefer lower charge variants - * @return OrRequirement containing individual ItemRequirements for each ID - */ - public static OrRequirement createOrRequirement( - List itemIds, - int amount, - EquipmentInventorySlot equipmentSlot, - Integer inventorySlot, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext, - Skill skillToUse, - Integer minimumLevelToUse, - Skill skillToEquip, - Integer minimumLevelToEquip, - boolean fuzzy) { - - if (itemIds.size() <= 1) { - throw new IllegalArgumentException("Use regular ItemRequirement constructor for single item ID"); - } - - // Create individual ItemRequirements for each ID - ItemRequirement[] requirements = new ItemRequirement[itemIds.size()]; - for (int i = 0; i < itemIds.size(); i++) { - int itemId = itemIds.get(i); - String itemName = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemComposition(itemId).getName()).orElse("Unknown Item"); - String itemDescription = description + " (" + itemName + ")"; - - requirements[i] = new ItemRequirement( - itemId, amount, equipmentSlot, inventorySlot, - priority, rating, itemDescription, taskContext, - skillToUse, minimumLevelToUse, skillToEquip, minimumLevelToEquip, - fuzzy - ); - } - - return new OrRequirement(priority, rating, description, taskContext, ItemRequirement.class,requirements); - } - - /** - * Factory method for equipment OR requirements with default parameters. - */ - public static OrRequirement createOrRequirement( - List itemIds, - EquipmentInventorySlot equipmentSlot, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext) { - return createOrRequirement(itemIds, 1, equipmentSlot, -2, priority, rating, description, - taskContext, null, null, null, null, false); - } - /** - * Factory method for inventory OR requirements with default parameters. - */ - public static OrRequirement createOrRequirement( - List itemIds, - int amount, - Integer inventorySlot, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext) { - return createOrRequirement(itemIds, amount, null, inventorySlot, priority, rating, description, - taskContext, null, null, null, null, false); - } - /** - * Factory method for inventory OR requirements with default parameters. - */ - public static OrRequirement createOrRequirement( - List itemIds, - int amount, - EquipmentInventorySlot equipmentSlot, - Integer inventorySlot, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext) { - return createOrRequirement(itemIds, amount, equipmentSlot, inventorySlot, priority, rating, description, - taskContext, null, null, null, null, false); - } - - - /** - * Factory method for inventory OR requirements with default parameters. - */ - public static OrRequirement createOrRequirement( - List itemIds, - int amount, - EquipmentInventorySlot equipmentSlot, - Integer inventorySlot, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext, - Skill skillToUse, - Integer minimumLevelToUse, - Skill skillToEquip, - Integer minimumLevelToEquip - ) { - return createOrRequirement(itemIds, amount, equipmentSlot, inventorySlot, priority, rating, description, - taskContext, skillToUse, minimumLevelToUse, skillToEquip, minimumLevelToEquip, false); - } - - /** - * Creates a copy of this ItemRequirement with a specific inventory slot. - * Useful for slot-specific placement during layout planning. - * - * @param targetSlot The target inventory slot (0-27) - * @return A new ItemRequirement with the specified slot - */ - public ItemRequirement copyWithSpecificSlot(int targetSlot) { - if (targetSlot < 0 || targetSlot > 27) { - throw new IllegalArgumentException("Invalid inventory slot: " + targetSlot); - } - - return new ItemRequirement( - getId(), - amount, - equipmentSlot, - targetSlot, - priority, - rating, - description, - taskContext, - skillToUse, - minimumLevelToUse, - skillToEquip, - minimumLevelToEquip, - fuzzy - ); - } - - /** - * Creates a copy of this ItemRequirement with a different amount. - * Useful for partial fulfillment scenarios. - * - * @param newAmount The new amount for the requirement - * @return A new ItemRequirement with the specified amount - */ - public ItemRequirement copyWithAmount(int newAmount) { - return new ItemRequirement( - getId(), - newAmount, - equipmentSlot, - inventorySlot, - priority, - rating, - description, - taskContext, - skillToUse, - minimumLevelToUse, - skillToEquip, - minimumLevelToEquip, - fuzzy - ); - } - - /** - * Retrieves the wiki URL for this item based on the URL suffix or item id. - * - * @return the wiki URL as a {@link String}, or {@code null} if not available - */ - // TODO when implented the wikiscrapper - /**@Nullable - public String getWikiUrl() - { - if (getUrlSuffix() != null) { - return "https://oldschool.runescape.wiki/w/" + getUrlSuffix(); - } - - if (getId() != -1) { - return "https://oldschool.runescape.wiki/w/Special:Lookup?type=item&id=" + getId(); - } - - return null; - }**/ - - /** - * Infers the RequirementType from the provided slot parameters. - * - * @param equipmentSlot The equipment slot (can be null) - * @param inventorySlot The inventory slot (-1 for any slot) - * @return The inferred RequirementType - */ - private static RequirementType inferRequirementType(EquipmentInventorySlot equipmentSlot, Integer inventorySlot) { - boolean hasEquipmentSlot = equipmentSlot != null; // null indicates, we dont allow the item placed into any equipment slot, - boolean hasInventorySlot = inventorySlot != -2 || inventorySlot==null; //-2 indicates we dont allow to be in inventory, -1 indicates any slot, 0-27 indicates specific slot - - if (hasEquipmentSlot && hasInventorySlot) { - return RequirementType.EITHER; - } else if (hasEquipmentSlot) { - return RequirementType.EQUIPMENT; - } else { - return RequirementType.INVENTORY; - } - } - - /** - * Checks if this item is available in either the player's inventory or bank. - * Now properly checks the required amount. - * - * @return true if the item is available with sufficient quantity, false otherwise - */ - public boolean isAvailableInInventoryOrBank() { - return getTotalAvailableQuantity() >= amount; - } - - /** - * Checks if this item is available in the player's inventory. - * Now properly checks the required amount. - * - * @return true if the item is in inventory with sufficient quantity, false otherwise - */ - public boolean isAvailableInInventory() { - return getInventoryQuantity() >= amount; - } - - /** - * Checks if this item is available in the player's bank. - * Now properly checks the required amount. - * - * @return true if the item is in bank with sufficient quantity, false otherwise - */ - public boolean isAvailableInBank() { - return getBankCount() >= amount; - } - - public boolean canBeUsed() { - // Check if the item is available in inventory or bank - if (isAvailableInInventoryOrBank()) { - - // Check skill to use if specified - if (skillToUse != null && minimumLevelToUse != null) { - int currentLevel = Rs2Player.getRealSkillLevel(skillToUse); - if (currentLevel < minimumLevelToUse) { - return false; - } - } - - }else{ - return false; - } - return true; - - } - public boolean canBeEquipped() { - // Check if the item is available in inventory or bank - if (isAvailableInInventoryOrBank()) { - // Check skill to equip if specified - if (skillToEquip != null && minimumLevelToEquip != null) { - int currentLevel = Rs2Player.getRealSkillLevel(skillToEquip); - if (currentLevel < minimumLevelToEquip) { - return false; - } - } - - }else{ - return false; - } - return isEquipment(); - - } - /** - * Checks if the player meets the skill requirements to use this item. - * - * @return true if the player meets skill requirements, false otherwise - */ - public boolean meetsSkillRequirements() { - // Check skill to equip if specified - if (skillToEquip != null && minimumLevelToEquip != null) { - int currentLevel = Rs2Player.getRealSkillLevel(skillToEquip); - if (currentLevel < minimumLevelToEquip) { - return false; - } - } - - // Check skill to use if specified - if (skillToUse != null && minimumLevelToUse != null) { - int currentLevel = Rs2Player.getRealSkillLevel(skillToUse); - if (currentLevel < minimumLevelToUse) { - return false; - } - } - - return true; - } - // TODO implement meets requirements --- has the item availble in inventory or bank, and has the required skill level to use it, and for equipment items, has the required skill level to equip it - - - /** - * Attempts to equip this item from inventory or bank with improved logic. - * Handles charged items and proper equipment verification. - * - * @return true if successfully equipped, false otherwise - */ - private boolean equip() { - if (Microbot.getClient().isClientThread()) { - log.error("Please run equip() on a non-client thread."); - return false; - } - - if (equipmentSlot == null) { - return false; // Not an equipment item - } - - if (!meetsSkillRequirements()) { - log.error("Skill requirements not met for " + getDescription()); - return false; - } - - // Check if already equipped - if (hasRequiredItemEquipped()) { - return true; - } - - // Try to equip from inventory first - if (tryEquipFromInventory()) { - return true; - } - - // Try to withdraw from bank and equip - return tryEquipFromBank(); - } - - /** - * Helper method to try equipping from inventory. - */ - private boolean tryEquipFromInventory() { - int itemId = getId(); - return tryEquipSingleItem(itemId); - } - - /** - * Helper method to try equipping from bank. - */ - private boolean tryEquipFromBank() { - // Ensure bank is open - if (!Rs2Bank.isOpen()) { - if (!Rs2Bank.openBank()) { - return false; - } - sleepUntil(()->Rs2Bank.isOpen(), 3000); - if(!Rs2Bank.isOpen()) { - log.error("Failed to open bank for equipping item: " + getDescription()); - return false; - } - } - - int itemId = getId(); - return tryEquipFromBankSingle(itemId); - } - - /** - * Helper method to try equipping a single item from inventory. - */ - private boolean tryEquipSingleItem(Integer itemId) { - if (Rs2Inventory.hasItem(itemId)) { - Rs2ItemModel item = Rs2Inventory.get(itemId); - if (item != null) { - // Check for common equip actions: "wear", "wield", "equip" - List actionList = new ArrayList<>(Arrays.asList(item.getInventoryActions())); - for (String action : actionList) { - if (action != null && (action.equalsIgnoreCase("wear") || - action.equalsIgnoreCase("wield") || - action.equalsIgnoreCase("equip"))) { - Rs2Inventory.interact(item.getSlot(), action); - if (sleepUntil(() -> Rs2Equipment.isWearing(itemId), 1800)) { - return true; - } - break; - } - } - } - } - return false; - } - - /** - * Helper method to try equipping a single item from bank. - */ - private boolean tryEquipFromBankSingle(Integer itemId) { - if (Rs2Bank.hasItem(itemId)) { - Rs2Bank.withdrawAndEquip(itemId); - return sleepUntil(() -> Rs2Equipment.isWearing(itemId), 3000); - } - return false; - } - - /** - * Attempts to withdraw this item from the bank. - * - * @return true if successfully withdrawn, false otherwise - */ - /** - * Attempts to withdraw this item from the bank with improved logic. - * Handles charged items and proper quantity calculation. - * - * @return true if successfully withdrawn, false otherwise - */ - private boolean withdrawFromBank(CompletableFuture scheduledFuture) { - if (Microbot.getClient().isClientThread()) { - log.error("Please run withdrawFromBank() on a non-client thread."); - return false; - } - - if (!meetsSkillRequirements()) { - log.error("Skill requirements not met for " + getDescription()); - return false; - } - - // Check if already have enough in inventory - if (hasRequiredItemInInventory()) { - return true; - } - - // Ensure we're near and have bank open - if (!Rs2Bank.isNearBank(6)) { - log.error("Not near a bank, cannot withdraw item: " + getDescription()); - Rs2Bank.walkToBank(); - } - - if (!Rs2Bank.isOpen()) { - if (!Rs2Bank.openBank()) { - return false; - } - sleepUntil(()->Rs2Bank.isOpen(), 3000); - } - - // Handle charged items with preference logic - since we now only have one item, check if it's charged - int itemId = getId(); - return tryWithdrawItem(itemId, scheduledFuture); - } - - - /** - * Helper method to attempt withdrawing a specific item. - */ - private boolean tryWithdrawItem(Integer itemId, CompletableFuture scheduledFuture) { - - - if (Rs2Bank.hasItem(itemId)) { - int quantity = getOptimalQuantity(itemId); - if (quantity > 0) { - Rs2Bank.withdrawX(itemId, quantity); - } else if (quantity < 0) { - Rs2Bank.depositX(itemId, Math.abs(quantity)); - } - - return sleepUntil(() -> isAvailableInInventory(), 3000); - } - return false; - } - - - - /** - * Checks if this requirement specifies a particular inventory slot. - * - * @return true if a specific inventory slot is specified (0-27), false if any slot (-1) - */ - public boolean hasSpecificInventorySlot() { - return inventorySlot >= 0 && inventorySlot <= 27; - } - - /** - * Checks if this requirement allows any inventory slot. - * - * @return true if any slot is allowed (-1), false if a specific slot is required - */ - public boolean allowsAnyInventorySlot() { - return inventorySlot == -1; - } - - /** - * Checks if this item requirement can be fulfilled by placing the item in the given inventory slot. - * - * @param slot The inventory slot to check (0-27) - * @return true if the item can be placed in this slot, false otherwise - */ - public boolean canBePlacedInInventorySlot(int slot) { - if (slot < 0 || slot > 27) { - return false; - } - - // If requirement type is EQUIPMENT only, it cannot be placed in inventory - if (requirementType == RequirementType.EQUIPMENT) { - return false; - } - - // If specific slot required, check if it matches - if (hasSpecificInventorySlot()) { - return inventorySlot == slot; - } - - // Otherwise, any slot is fine - return true; - } - - /** - * Gets the total count of this item across inventory, equipment, and bank. - * Properly handles fuzzy matching for charged items and item variations. - * - * @return the total available count - */ - public int getTotalAvailableQuantity() { - if (fuzzy) { - // For fuzzy matching, check all variations of the item - return getFuzzyInventoryQuantity() + getFuzzyBankCount() + getFuzzyEquippedCount(); - } else { - // Exact item ID matching - int itemId = getId(); - return Rs2Inventory.itemQuantity(itemId) + Rs2Bank.count(getUnNotedId()) + getEquippedCount(); - } - } - public int getTotalAvailableCount() { - if (fuzzy) { - // For fuzzy matching, check all variations of the item - return getFuzzyInventoryCount() + getFuzzyBankCount() + getFuzzyEquippedCount(); - } else { - // Exact item ID matching - int itemId = getId(); - return Rs2Inventory.count(itemId) + Rs2Bank.count(getBankCount()) + getEquippedCount(); - } - } - - - /** - * Gets the count of this item currently in inventory. - * Properly handles fuzzy matching for charged items and item variations. - * - * @return the inventory count - */ - public int getInventoryQuantity() { - if (fuzzy) { - return getFuzzyInventoryQuantity(); - } else { - return Rs2Inventory.itemQuantity(getId()); - } - } - - /** - * Gets the count of this item currently in bank. - * Properly handles fuzzy matching for charged items and item variations. - * - * @return the bank count - */ - public int getBankCount() { - if (fuzzy) { - return getFuzzyBankCount(); - } else { - return Rs2Bank.count(getUnNotedId()); - } - } - - /** - * Gets the fuzzy count of this item in inventory (includes all variations). - * Uses Rs2FuzzyItem for comprehensive fuzzy matching including ID-based variations - * and name-based charged item variants. - * - * @return the fuzzy inventory count - */ - private int getFuzzyInventoryCount() { - return Rs2FuzzyItem.getFuzzyInventoryCount(getId(), true); - } - private int getFuzzyInventoryQuantity() { - return Rs2FuzzyItem.getFuzzyInventoryQuantity(getId(), true); - } - - - /** - * Gets the fuzzy count of this item in bank (includes all variations). - * Uses Rs2FuzzyItem for comprehensive fuzzy matching including ID-based variations - * and name-based charged item variants. - * - * @return the fuzzy bank count - */ - private int getFuzzyBankCount() { - return Rs2FuzzyItem.getFuzzyBankCount(getId(), true); - } - - /** - * Gets the fuzzy count of this item currently equipped (includes all variations). - * Uses Rs2FuzzyItem for comprehensive fuzzy matching including ID-based variations - * and name-based charged item variants. - * - * @return the fuzzy equipped count (0 or 1 for most items) - */ - private int getFuzzyEquippedCount() { - if (equipmentSlot != null) { - return Rs2FuzzyItem.getFuzzyEquippedCount(getId(), true); - } - return 0; - } - - /** - * Gets the count of this item currently equipped. - * - * @return the equipped count (0 or 1 for most items) - */ - private int getEquippedCount() { - if (equipmentSlot != null) { - return Rs2Equipment.isWearing(getId()) ? 1 : 0; - } - return 0; - } - - /** - * Gets the optimal quantity to withdraw/deposit for this item. - * Considers current inventory amount and desired amount. - * - * @param itemId The specific item ID to calculate for - * @return Positive for withdraw, negative for deposit, 0 for no action needed - */ - private int getOptimalQuantity(Integer itemId) { - int currentAmount = Rs2Inventory.count(itemId); - int targetAmount = this.amount > 0 ? this.amount : 1; - - return targetAmount - currentAmount; - } - - /** - * Gets the primary item ID for this requirement. - * This is usually the first ID in the list, which typically represents the best option. - * - /** - * Gets the resolved item ID for this requirement. - * This returns the auto-resolved ID (potentially noted variant) that should be used for all operations. - * - * @return the resolved item ID for this requirement - */ - public int getId() { - if (ids.isEmpty()) { - throw new IllegalStateException("ItemRequirement must have exactly one item ID"); - } - if (ids.size() > 1) { - throw new IllegalStateException("ItemRequirement has multiple IDs, use createOrRequirement() factory method instead"); - } - return ids.get(0); - } - - - /** - * Checks if the player meets the skill requirements to equip a specific item. - * Validates both the minimum level and required skill for equipping. - * - * @param item The ItemRequirement to check skill requirements for - * @return true if player can equip the item, false if skill requirements aren't met - */ - public static boolean canPlayerEquipItem(ItemRequirement item) { - // Check if item has skill requirements for equipping - if (item.getSkillToEquip() != null && item.getMinimumLevelToEquip() > 0) { - int playerLevel = Rs2Player.getRealSkillLevel(item.getSkillToEquip()); - - if (playerLevel < item.getMinimumLevelToEquip()) { - log.debug("Player " + item.getSkillToEquip().getName() + " level (" + playerLevel + - ") insufficient to equip item requiring level " + item.getMinimumLevelToEquip()); - return false; - } - } - - return true; - } - - /** - * Checks if the player meets the skill requirements to use a specific item. - * Validates both the minimum level and required skill for using. - * - * @param item The ItemRequirement to check skill requirements for - * @return true if player can use the item, false if skill requirements aren't met - */ - public static boolean canPlayerUseItem(ItemRequirement item) { - // Check if item has skill requirements for using - if (item.getSkillToUse() != null && item.getMinimumLevelToUse() > 0) { - int playerLevel = Rs2Player.getRealSkillLevel(item.getSkillToUse()); - - if (playerLevel < item.getMinimumLevelToUse()) { - log.debug("Player " + item.getSkillToUse().getName() + " level (" + playerLevel + - ") insufficient to use item requiring level " + item.getMinimumLevelToUse()); - return false; - } - } - - return true; - } - - /** - * Returns a multi-line display string with detailed item requirement information. - * Uses StringBuilder with tabs for proper formatting. - * - * @return A formatted string containing item requirement details - */ - public String displayString() { - StringBuilder sb = new StringBuilder(); - sb.append("=== Item Requirement Details ===\n"); - sb.append(" -Name:\t\t\t").append(getName()).append("\n"); - sb.append(" -Type:\t\t\t").append(getRequirementType().name()).append("\n"); - sb.append(" -Priority:\t\t").append(getPriority().name()).append("\n"); - sb.append(" -Rating:\t\t\t").append(getRating()).append("/10\n"); - sb.append(" -Description:\t").append(getDescription()).append("\n"); - sb.append(" -Schedule Context:\t").append(getTaskContext().name()).append("\n"); - sb.append(" -Item ID:\t\t").append(getId()).append("\n"); - sb.append(" -Item unnoted ID:\t\t").append(getUnNotedId()).append("\n"); - sb.append(" -ItemModel unnoted ID:\t\t").append(Rs2ItemModel.getUnNotedId(getId())).append("\n"); - sb.append(" -Item noted ID:\t\t").append(getNotedId()).append("\n"); - sb.append(" -ItemModel noted ID:\t\t").append(Rs2ItemModel.getNotedId(getId())).append("\n"); - sb.append(" -Item linked ID:\t").append( getUnNotedId()).append("\n"); - sb.append(" -Amount:\t\t\t").append(amount).append("\n"); - - if (equipmentSlot != null) { - sb.append(" -Equipment Slot:\t").append(equipmentSlot.name()).append("\n"); - } - - if (inventorySlot != null && inventorySlot >= 0) { - sb.append(" -Inventory Slot:\t").append(inventorySlot).append("\n"); - } - - if (skillToUse != null) { - sb.append(" -Skill to Use:\t").append(skillToUse.getName()).append("\n"); - sb.append(" -Min Level to Use:\t").append(minimumLevelToUse != null ? minimumLevelToUse : "N/A").append("\n"); - } - - if (skillToEquip != null) sb.append(" -Skill to Equip:\t").append(skillToEquip.getName()).append("\n"); - if (minimumLevelToEquip != null) sb.append(" -Min Level to Equip:\t").append(minimumLevelToEquip).append("\n"); - - sb.append(" -Fuzzy Charge:\t").append(fuzzy).append("\n"); - sb.append(" -Is Available in Inventory:\t").append(isAvailableInInventory()).append("\n"); - sb.append(" -Is Available in Bank:\t").append(isAvailableInBank()).append("\n"); - sb.append(" -Total Available Count:\t").append(getTotalAvailableCount()).append("\n"); - sb.append(" - Banked: ").append(getBankCount()).append(" Inventory:").append(getFuzzyInventoryCount()).append("\n"); - sb.append(" -Total Available Quantity:\t").append(getTotalAvailableQuantity()).append("\n"); - sb.append(" - Banked: ").append(getBankCount()).append(" Inventory:").append(getInventoryQuantity()).append("\n"); - sb.append(" -Can be Used:\t\t").append(canBeUsed()).append("\n"); - sb.append(" -Can be Equipped:\t").append(canBeEquipped()).append("\n"); - sb.append(" -Meets Skill Req.:\t").append(meetsSkillRequirements()).append("\n"); - sb.append(" -Is Dummy Item:\t\t").append(isDummyItemRequirement()).append("\n"); - - return sb.toString(); - } - - - - /** - * Enhanced toString method that uses displayString for comprehensive output. - * - * @return A comprehensive string representation of this item requirement - */ - @Override - public String toString() { - return displayString(); - } - - /** - * Implements the abstract fulfillRequirement method from the base Requirement class. - * Attempts to fulfill this item requirement by checking availability and managing inventory/equipment. - * - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if the requirement was successfully fulfilled, false otherwise - */ - @Override - public boolean fulfillRequirement(CompletableFuture scheduledFuture) { - try { - if (Microbot.getClient().isClientThread()) { - log.error("Please run fulfillRequirement() on a non-client thread."); - return false; - } - // Dummy items are always considered "fulfilled" since they're just slot placeholders - if (isDummyItemRequirement()) { - log.debug("Dummy item requirement automatically fulfilled: {}", getDescription()); - return true; - } - // Check if the requirement is already fulfilled - if (isRequirementAlreadyFulfilled()) { - return true; - } - - // Check if the item is available in inventory or bank - if (!isAvailableInInventoryOrBank()) { - if (isMandatory()) { - log.error("MANDATORY item requirement cannot be fulfilled: " + getName() + " - Item not available"); - return false; - } else { - log.error("OPTIONAL/RECOMMENDED item requirement skipped: " + getName() + " - Item not available"); - return true; // Non-mandatory requirements return true if item isn't available - } - } - - // Handle equipment requirements - if (requirementType == RequirementType.EQUIPMENT || requirementType == RequirementType.EITHER) { - if (!fulfillEquipmentRequirement(scheduledFuture)) { - return !isMandatory(); // Return false only for mandatory requirements - } - } - - // Handle inventory requirements - if (requirementType == RequirementType.INVENTORY || requirementType == RequirementType.EITHER) { - if (!fulfillInventoryRequirement(scheduledFuture)) { - return !isMandatory(); // Return false only for mandatory requirements - } - } - - return true; - - } catch (Exception e) { - log.error("Error fulfilling item requirement " + getName() + ": " + e.getMessage()); - return !isMandatory(); // Don't fail mandatory requirements due to exceptions - } - } - - /** - * Checks if this requirement is currently fulfilled without attempting to fulfill it. - * This is more efficient than fulfillRequirement() for status checking. - * - * @return true if the requirement is already met, false otherwise - */ - @Override - public boolean isFulfilled() { - return isRequirementAlreadyFulfilled(); - } - - /** - * Checks if this requirement is already fulfilled based on the requirement type. - * - * @return true if the requirement is already met, false otherwise - */ - private boolean isRequirementAlreadyFulfilled() { - switch (requirementType) { - case EQUIPMENT: - return hasRequiredItemEquipped(); - case INVENTORY: - return hasRequiredItemInInventory(); - case EITHER: - return hasRequiredItemEquipped() || hasRequiredItemInInventory(); - default: - return false; - } - } - - /** - * Attempts to fulfill an equipment requirement by equipping the required item. - * - * @param scheduledFuture The CompletableFuture to monitor for cancellation - * @return true if the equipment requirement was fulfilled, false otherwise - */ - private boolean fulfillEquipmentRequirement(CompletableFuture scheduledFuture) { - - return equip(); - } - - /** - * Attempts to fulfill an inventory requirement by ensuring the required item is in inventory. - * Handles specific slot requirements and proper quantity management. - * - * @param scheduledFuture The CompletableFuture to monitor for cancellation - * @return true if the inventory requirement was fulfilled, false otherwise - */ - private boolean fulfillInventoryRequirement(CompletableFuture scheduledFuture) { - - if (hasSpecificInventorySlot()) { - return withdrawAndPlaceInSpecificSlot(scheduledFuture); - } else { - return withdrawFromBank(scheduledFuture); - } - } - - /** - * Withdraws the item and places it in the specific inventory slot if required. - * Creates a proper copy of the requirement with the target slot. - * - * @param scheduledFuture The CompletableFuture to monitor for cancellation - * @return true if successfully placed in the specific slot, false otherwise - */ - private boolean withdrawAndPlaceInSpecificSlot(CompletableFuture scheduledFuture) { - if (Microbot.getClient().isClientThread()) { - log.error("Please run withdrawAndPlaceInSpecificSlot() on a non-client thread."); - return false; - } - - // Check if already have the item in the correct slot - if (hasItemInSpecificSlot(inventorySlot)) { - return true; - } - - // Ensure we have the item available - if (!isAvailableInInventoryOrBank()) { - return false; - } - - // Handle case where item is already in inventory but wrong slot - if (isAvailableInInventory()) { - return moveToSpecificSlot(); - } - - // Withdraw from bank to specific slot - return withdrawFromBankToSpecificSlot(); - } - - /** - * Checks if the required item is in the specific inventory slot with correct amount. - * - * @param slot The inventory slot to check - * @return true if the item is in the slot with sufficient quantity - */ - private boolean hasItemInSpecificSlot(int slot) { - Rs2ItemModel item = Rs2Inventory.get(slot); - if (item == null) { - return false; - } - - for (Integer itemId : ids) { - if (item.getId() == itemId && item.getQuantity() >= amount) { - return true; - } - } - return false; - } - - /** - * Moves the item from its current inventory position to the specific slot. - * - * @return true if successfully moved, false otherwise - */ - private boolean moveToSpecificSlot() { - // Find the item in inventory - for (Integer itemId : ids) { - Rs2ItemModel item = Rs2Inventory.get(itemId); - if (item != null && item.getQuantity() >= amount) { - // Use the available moveItemToSlot method - return Rs2Inventory.moveItemToSlot(item, inventorySlot); - } - } - return false; - } - - /** - * Withdraws the item from bank directly to the specific inventory slot. - * Since withdrawToSlot doesn't exist, we'll withdraw and then move. - * - * @return true if successfully withdrawn to slot, false otherwise - */ - private boolean withdrawFromBankToSpecificSlot() { - if (!Rs2Bank.isOpen()) { - if (!Rs2Bank.openBank()) { - return false; - } - sleepUntil(() -> Rs2Bank.isOpen(), 3000); - } - - // Find best available item in bank - int itemId = getUnNotedId(); - if (Rs2Bank.count(itemId) >= amount) { - // Clear target slot if needed by depositing the item there - Rs2ItemModel targetSlotItem = Rs2Inventory.get(inventorySlot); - if (targetSlotItem != null) { - Rs2Bank.depositOne(targetSlotItem.getId()); - sleepUntil(() -> Rs2Inventory.get(inventorySlot) == null, 2000); - } - - // Withdraw the item (it will go to any available slot) - Rs2Bank.withdrawX(itemId, amount); - sleepUntil(() -> Rs2Inventory.itemQuantity(itemId) >= amount, 3000); - - // Now move to the specific slot - Rs2ItemModel withdrawnItem = Rs2Inventory.get(itemId); - if (withdrawnItem != null) { - return Rs2Inventory.moveItemToSlot(withdrawnItem, inventorySlot); - } - } - - return false; - } - - /** - * Checks if the required item is equipped in the correct slot (if specified). - * - * @return true if the required item is equipped, false otherwise - */ - private boolean hasRequiredItemEquipped() { - int itemId = getId(); - if (equipmentSlot != null) { - return Rs2Equipment.isWearing(itemId); - } else { - return Rs2Equipment.isWearing(itemId); - } - } - - /** - * Checks if the required item is in inventory with the correct amount. - * - * @return true if the required item is in inventory with sufficient quantity, false otherwise - */ - private boolean hasRequiredItemInInventory() { - return Rs2Inventory.count(getId()) >= amount; - } - - /** - * Validates slot assignments to ensure consistency between requirement type and slot specifications. - * Throws IllegalArgumentException if the configuration is invalid. - */ - private void validateSlotAssignments() { - // Validate inventory slot range - if (inventorySlot < -2 || inventorySlot > 27) { - throw new IllegalArgumentException("Inventory slot must be between -1 (any slot) and 27, got: " + inventorySlot); - } - - // Validate requirement type and slot consistency - switch (requirementType) { - case EQUIPMENT: - if (equipmentSlot == null || inventorySlot!= -2) { - throw new IllegalArgumentException("EQUIPMENT requirement must specify an equipment slot"); - } - break; - case INVENTORY: - if (equipmentSlot != null) { - throw new IllegalArgumentException("INVENTORY requirement should not specify an equipment slot"); - } - if( inventorySlot != -1) { - if(!isStackable() && amount > 1) { - throw new IllegalArgumentException("INVENTORY requirement with non-stackable items must have inventory slot -1 (any slot) or amount 1. Item ID: " + getId() + ", Amount: " + amount); - } - } - break; - case EITHER: - if ( !(equipmentSlot != null || inventorySlot>=-1) ) { - throw new IllegalArgumentException("EITHER requirement must specify at least one of equipment or inventory slot"); - } - // EITHER requirements can have equipment slot specified (preferred) and optional inventory slot - break; - default: - throw new IllegalArgumentException("Unsupported requirement type: " + requirementType); - } - } - - /** - * Resolves the optimal item ID for the given amount, automatically detecting when noted variants should be used. - * This method is called during construction to auto-resolve noted items for stackable requirements. - * - * @param originalItemId The original item ID provided to the constructor - * @param amount The amount required - * @return The optimal item ID (potentially noted variant) to use - */ - private static int resolveOptimalItemId(int originalItemId, int amount) { - if (amount <= 1) { - return originalItemId; // Single items don't need noting - } - - try { - ItemComposition composition = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemComposition(originalItemId) - ).orElse(null); - - if (composition == null) { - log.warn("Could not get item composition for item ID: {}, using original ID", originalItemId); - return originalItemId; - } - - if (composition.isStackable()) { - return originalItemId; // Already stackable, no need to change - } - - // Check if this item has a noted variant - int linkedNoteId = composition.getLinkedNoteId(); - if (linkedNoteId != originalItemId) { - ItemComposition notedComposition = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemComposition(linkedNoteId) - ).orElse(null); - - if (notedComposition != null && notedComposition.isStackable()) { - log.debug("Auto-resolved {} (ID: {}) to noted variant {} (ID: {}) for amount {}", - composition.getName(), originalItemId, - notedComposition.getName(), linkedNoteId, amount); - return linkedNoteId; // Use noted version - } - } - - // If we reach here, item is not stackable and has no noted variant - log.debug("Item {} (ID: {}) with amount {} has no stackable noted variant, keeping original", - composition.getName(), originalItemId, amount); - return originalItemId; - - } catch (Exception e) { - log.error("Error resolving optimal item ID for {} with amount {}: {}", - originalItemId, amount, e.getMessage()); - return originalItemId; // Fall back to original on error - } - } - - private static int getNotedItemId(ItemComposition composition) { - try { - - if (composition == null) { - return -1; - } - - int itemId = composition.getId(); - // If already stackable, return original ID - if (composition.isStackable()) { - return itemId; - } - // Check if this item has a noted variant - int linkedNoteId = composition.getLinkedNoteId(); - ItemComposition linkedComposition = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemComposition(linkedNoteId) - ).orElse(null); - - if (linkedComposition.isStackable() && !composition.isStackable()) { - log.debug("Found noted variant for {} (ID: {}) -> {} (ID: {})", - composition.getName(), itemId, - linkedComposition.getName(), linkedNoteId); - return linkedNoteId; // Use noted version - - } - if (linkedComposition.isStackable() && composition.isStackable()) { - log.debug("Item {} (ID: {}) has only a stackable variant", - composition.getName(), itemId, - linkedComposition.getName(), linkedNoteId); - return linkedNoteId < itemId ? linkedNoteId : itemId; // Use noted version if it has lower ID - } - // If we reach here, item is not stackable and has no noted variant - log.debug("Item {} (ID: {}) has no stackable noted variant", - composition.getName(), itemId); - return itemId; - - } catch (Exception e) { - //log.error("Error getting noted item ID for {}: {}", itemId, e.getMessage()); - return -1; // Fall back to original on error - } - } - private static int getUnNotedId(ItemComposition composition) { - try { - - if (composition == null) { - log.warn("Could not get item composition for item ID, returning original ID"); - return -1; - } - int itemId = composition.getId(); - // Check if this item has a noted variant - int linkedNoteId = composition.getLinkedNoteId(); - if (linkedNoteId == -1){ - log.debug("Item {} (ID: {}) has no noted variant, returning original ID", - composition.getName(), itemId); - return itemId; // No noted variant, return original ID - } - ItemComposition linkedComposition = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemComposition(linkedNoteId) - ).orElse(null); - - if (!linkedComposition.isStackable() && composition.isStackable()) { - log.debug("Found unnoted variant for {} (ID: {}) -> {} (ID: {})", - composition.getName(), itemId, - linkedComposition.getName(), linkedNoteId); - return linkedNoteId; // Use noted version - - } - if (linkedComposition.isStackable() && composition.isStackable()) { - log.debug("Item {} (ID: {}) has only a stackable variant", - composition.getName(), itemId, - linkedComposition.getName(), linkedNoteId); - - return linkedNoteId < itemId ? linkedNoteId : itemId; // Use noted version if it has lower ID - } - // If we reach here, item is not stackable and has no noted variant - log.debug("Item {} (ID: {}) has no non stackable variant", - composition.getName(), itemId); - return itemId; - - } catch (Exception e) { - log.error("Error getting unnoted item ID:{}", e.getMessage()); - return -1; // Fall back to original on error - } - } - private static int getLinkedItemId(ItemComposition composition) { - try { - if (composition == null) { - log.warn("no item composition for item ID, returning -1"); - return -1; - } - - - // Check if this item has a noted variant - return composition.getLinkedNoteId(); - - - } catch (Exception e) { - log.error("Error getting linked item ID: {}", e.getMessage()); - return -1; // Fall back to original on error - } - } - - - /** - * Checks if this item is stackable using the resolved item ID. - * - * @return true if this item is stackable, false otherwise - */ - public boolean isStackable() { - int itemId = getId(); - - try { - if (itemComposition == null) { - this.setItemComp(itemId); - } - return itemComposition !=null ? itemComposition.isStackable(): false; - } catch (Exception e) { - log.error("Error checking if item " + itemId + " is stackable: " + e.getMessage()); - return false; - } - } - - /** - * Gets the noted variant of an item ID if it exists and is stackable. - * Returns the original ID if the item is already stackable or has no noted variant. - * Uses lazy loading pattern for ItemComposition. - * - * @param itemId The original item ID - * @return The noted item ID if available and stackable, otherwise the original item ID - */ - public static int getNotedId(int itemId) { - ItemComposition composition = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemComposition(itemId) - ).orElse(null); - return getNotedItemId(composition); - } - public boolean isNoted() { - try { - if (itemComposition == null) { - this.setItemComp(getId()); - } - return itemComposition.getNote() == 799 && itemComposition.isStackable() && itemComposition.getLinkedNoteId() != -1; - } catch (Exception e) { - log.error("Error getting noted ID for item " + getId() + ": " + e.getMessage()); - return false; - } - } - public int getNotedId(){ - int itemId = getId(); - try { - if (itemComposition == null) { - this.setItemComp(itemId); - } - return getNotedItemId( itemComposition); - } catch (Exception e) { - log.error("Error getting noted ID for item " + itemId + ": " + e.getMessage()); - return itemId; // Fallback to original ID on error - } - } - public int getUnNotedId(){ - int itemId = getId(); - try { - if (itemComposition == null) { - this.setItemComp(itemId); - } - return getUnNotedId(itemComposition); - } catch (Exception e) { - log.error("Error getting noted ID for item " + itemId + ": " + e.getMessage()); - return itemId; // Fallback to original ID on error - } - } - public int getLinkedId(){ - int itemId = getId(); - try { - if (itemComposition == null) { - this.setItemComp(itemId); - } - return getLinkedItemId(itemComposition); - } catch (Exception e) { - log.error("Error getting linked ID for item " + itemId + ": " + e.getMessage()); - return itemId; // Fallback to original ID on error - } - } - public boolean isEquipment() { - int itemId = getId(); - try { - if (itemComposition == null) { - this.setItemComp(itemId); - } - - final ItemStats itemStats = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemStats(itemId) - ).orElse(null); - - if (itemStats == null || !itemStats.isEquipable()) { - return false; - } - final ItemEquipmentStats equipmentStats = itemStats.getEquipment(); - if (equipmentStats == null) { - return false; - } - return true; - - } catch (Exception e) { - log.error("Error checking if item " + itemId + " is equipped: " + e.getMessage()); - return false; - } - } - - /** - * Gets the number of inventory slots this requirement would occupy. - * For stackable items with any amount, this is 1 slot. - * For non-stackable items, this equals the amount required. - * - * @return the number of inventory slots needed - */ - public int getRequiredInventorySlots() { - if (requirementType == RequirementType.EQUIPMENT) { - return 0; // Equipment items don't occupy inventory slots when equipped - } - - if (isStackable()) { - return 1; // Stackable items only need one slot regardless of amount - } else { - return amount; // Non-stackable items need one slot per item - } - } - - /** - * Checks if this item requirement can be placed in inventory considering stackability and amount. - * Non-stackable items with amount > 1 need multiple slots. - * - * @return true if the item can be placed in inventory, false if not enough slots - */ - public boolean canFitInInventory() { - return canFitInInventory(Rs2Inventory.emptySlotCount()); - } - - /** - * Checks if this item requirement can fit in the given number of available slots. - * - * @param availableSlots The number of available inventory slots - * @return true if the item can fit, false otherwise - */ - public boolean canFitInInventory(int availableSlots) { - return getRequiredInventorySlots() <= availableSlots; - } - - /** - * Checks if this requirement meets all conditions (availability, skill requirements, etc.). - * - * @return true if all requirements are met, false otherwise - */ - public boolean meetsAllRequirements() { - return isAvailableInInventoryOrBank() && meetsSkillRequirements(); - } - - - // ========== STATIC UTILITY METHODS ========== - - /** - * Checks if an item is present in a specific inventory slot. - * - * @param slot The inventory slot to check (0-27) - * @param item The ItemRequirement to check for - * @return true if the item is in the specified slot - */ - public static boolean hasItemInSpecificSlot(int slot, ItemRequirement item) { - if (slot < 0 || slot > 27) { - log.warn("Invalid inventory slot: {}", slot); - return false; - } - - Rs2ItemModel slotItem = Rs2Inventory.getItemInSlot(slot); - if (slotItem == null) { - return false; - } - - // Check if the slot contains the item's ID - if (slotItem.getId() == item.getId()) { - log.debug("Found {} in slot {}", item.getName(), slot); - return true; - } - - return false; - } - - /** - * Withdraws an item and places it in a specific inventory slot. - * - * @param slot The target inventory slot (0-27) - * @param item The ItemRequirement to withdraw and place - * @return true if successful - */ - public static boolean withdrawAndPlaceInSpecificSlot(int slot, ItemRequirement item) { - if (slot < 0 || slot > 27) { - log.error("Invalid inventory slot: {}", slot); - return false; - } - - log.debug("Attempting to withdraw {} and place in slot {}", item.displayString(), slot); - - // Check if item is already in the correct slot - if (hasItemInSpecificSlot(slot, item)) { - log.debug("Item {} already in slot {}", item.getName(), slot); - return true; - } - - // Ensure slot is empty or contains the same item - Rs2ItemModel currentSlotItem = Rs2Inventory.getItemInSlot(slot); - if (currentSlotItem != null) { - if (currentSlotItem.getId() != item.getId()) { - log.warn("Slot {} contains different item: {}", slot, currentSlotItem.getName()); - return false; - } - } - - // Try to withdraw the item - boolean withdrawSuccess = false; - int itemId = item.getId(); - if (Rs2Bank.hasItem(itemId)) { - if (Rs2Bank.withdrawX(itemId, item.getAmount())) { - withdrawSuccess = true; - log.debug("Successfully withdrew {} (ID: {})", item.getName(), itemId); - } else { - log.warn("Failed to withdraw {} (ID: {})", item.getName(), itemId); - } - } - - if (!withdrawSuccess) { - log.error("Could not withdraw any variant of {}", item.getName()); - return false; - } - - // Wait for withdrawal to complete - sleepUntil(() -> Rs2Inventory.hasItemAmount(item.getId(), item.getAmount(), false), 3000); - - // Verify the item is now in inventory and in the correct slot if needed - if (hasItemInSpecificSlot(slot, item)) { - log.debug("Successfully placed {} in slot {}", item.getName(), slot); - return true; - } else { - log.warn("Item {} not found in expected slot {} after withdrawal", item.getName(), slot); - // Check if it's anywhere in inventory - if (Rs2Inventory.hasItem(item.getId())) { - log.debug("Item {} found in inventory but not in expected slot", item.getName()); - return true; // Close enough for now - } - return false; - } - } - - /** - * Checks if an item can be assigned to a specific slot based on its constraints. - * - * @param item The ItemRequirement to check - * @param slot The target slot number - * @return true if the item can be assigned to the slot - */ - public static boolean canAssignToSpecificSlot(ItemRequirement item, int slot) { - // For inventory slots (0-27), check if the item can fit in inventory - if (slot >= 0 && slot <= 27) { - return item.getRequirementType() != RequirementType.EQUIPMENT && - (item.getInventorySlot() == null || item.getInventorySlot() == -1 || item.getInventorySlot() == slot); - } - - // For equipment slots, check if the item can be equipped - return item.getRequirementType() != RequirementType.INVENTORY && - (item.getEquipmentSlot() != null && item.getEquipmentSlot().getSlotIdx() == slot); - } - - /** - * Validates that an item meets all suitability requirements for use. - * - * @param item The ItemRequirement to validate - * @return true if the item is suitable for use - */ - public static boolean validateItemSuitability(ItemRequirement item) { - log.debug("Validating suitability for item: {}", item.displayString()); - - // Check skill requirements for usage - if (item.getSkillToUse() != null && item.getMinimumLevelToUse() > 0) { - int currentLevel = Rs2Player.getRealSkillLevel(item.getSkillToUse()); - if (currentLevel < item.getMinimumLevelToUse()) { - log.warn("Insufficient {} level for {}: {} < {}", - item.getSkillToUse().getName(), item.getName(), - currentLevel, item.getMinimumLevelToUse()); - return false; - } - } - - // Check skill requirements for equipping - if (item.getSkillToEquip() != null && item.getMinimumLevelToEquip() > 0) { - int currentLevel = Rs2Player.getRealSkillLevel(item.getSkillToEquip()); - if (currentLevel < item.getMinimumLevelToEquip()) { - log.warn("Insufficient {} level to equip {}: {} < {}", - item.getSkillToEquip().getName(), item.getName(), - currentLevel, item.getMinimumLevelToEquip()); - return false; - } - } - - log.debug("Item {} passes all suitability checks", item.getName()); - return true; - } - - // Advanced fuzzy item utility methods - - /** - * Gets the total charges available for this item across all locations. - * Only applicable for charged items. - * - * @return total charges available, or 0 if not a charged item - */ - public int getTotalCharges() { - return Rs2FuzzyItem.getTotalCharges(getId()); - } - - /** - * Gets the count of charged items within a specific charge range in inventory. - * - * @param minCharges minimum charge level (inclusive) - * @param maxCharges maximum charge level (inclusive) - * @return count of charged items in the specified range - */ - public int getChargedInventoryCount(int minCharges, int maxCharges) { - return Rs2FuzzyItem.getChargedInventoryCount(getId(), true, minCharges, maxCharges); - } - public int getChargedInventoryQuantity(int minCharges, int maxCharges) { - return Rs2FuzzyItem.getChargedInventoryQuantity(getId(), true, minCharges, maxCharges); - } - - /** - * Gets the count of charged items within a specific charge range in bank. - * - * @param minCharges minimum charge level (inclusive) - * @param maxCharges maximum charge level (inclusive) - * @return count of charged items in the specified range - */ - public int getChargedBankCount(int minCharges, int maxCharges) { - return Rs2FuzzyItem.getChargedBankCount(getId(), true, minCharges, maxCharges); - } - - /** - * Gets all fuzzy item variations available in inventory with detailed information. - * - * @return list of FuzzyItemInfo objects sorted by charges (highest first) - */ - public List getAllFuzzyItemsInInventory() { - return Rs2FuzzyItem.getAllFuzzyItemsInInventory(getId(), true); - } - - /** - * Gets all fuzzy item variations available in bank with detailed information. - * - * @return list of FuzzyItemInfo objects sorted by charges (highest first) - */ - public List getAllFuzzyItemsInBank() { - return Rs2FuzzyItem.getAllFuzzyItemsInBank(getId(), true); - } - - /** - * Gets the best (highest charged) fuzzy item match in inventory. - * - * @return the best FuzzyItemInfo match, or null if none found - */ - public Rs2FuzzyItem.FuzzyItemInfo getBestFuzzyItemInInventory() { - return Rs2FuzzyItem.getBestFuzzyItemInInventory(getId(), true); - } - - /** - * Gets the best (highest charged) fuzzy item match in bank. - * - * @return the best FuzzyItemInfo match, or null if none found - */ - public Rs2FuzzyItem.FuzzyItemInfo getBestFuzzyItemInBank() { - return Rs2FuzzyItem.getBestFuzzyItemInBank(getId(), true); - } - - /** - * Gets information about the fuzzy item currently equipped. - * - * @return the equipped FuzzyItemInfo, or null if none found - */ - public Rs2FuzzyItem.FuzzyItemInfo getFuzzyItemEquipped() { - return Rs2FuzzyItem.getFuzzyItemEquipped(getId(), true); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/RunePouchRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/RunePouchRequirement.java deleted file mode 100644 index b762a6d3b06..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/item/RunePouchRequirement.java +++ /dev/null @@ -1,328 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item; - -import lombok.Getter; -import lombok.EqualsAndHashCode; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.inventory.Rs2RunePouch; -import net.runelite.client.plugins.microbot.util.inventory.RunePouchType; -import net.runelite.client.plugins.microbot.util.magic.Runes; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; - -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ScheduledExecutorService; -import java.util.stream.Collectors; - -/** - * Represents a rune pouch requirement that extends ItemRequirement to handle both - * the pouch item itself and its required rune contents. - * - * This requirement ensures: - * 1. The player has a rune pouch in their inventory - * 2. The rune pouch contains the specified runes with minimum quantities - * 3. The required runes are available (inventory + bank + already in pouch) - */ -@Getter -@EqualsAndHashCode(callSuper = true) -public class RunePouchRequirement extends ItemRequirement { - - /** - * Map of required runes and their minimum quantities. - */ - private final Map requiredRunes; - - /** - * Whether to allow combination runes to satisfy basic rune requirements. - * For example, dust runes can satisfy both air and earth rune requirements. - */ - private final boolean allowCombinationRunes; - - /** - * Helper method to check if player has any valid rune pouch in inventory. - * @return true if any rune pouch variant is found - */ - private boolean hasAnyRunePouch() { - for (RunePouchType pouchType : RunePouchType.values()) { - if (Rs2Inventory.hasItem(pouchType.getItemId())) { - return true; - } - } - return false; - } - - /** - * Full constructor for rune pouch requirement. - * - * @param requiredRunes Map of runes to their minimum required quantities - * @param allowCombinationRunes Whether combination runes can satisfy basic rune requirements - * @param priority Priority level for this requirement - * @param rating Rating/importance (1-10) - * @param description Human-readable description - * @param TaskContext When this requirement applies (PRE_SCHEDULE, POST_SCHEDULE, BOTH) - */ - public RunePouchRequirement( - Map requiredRunes, - boolean allowCombinationRunes, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext) { - - // Call parent constructor with first rune pouch ID - we'll handle multiple IDs in our override methods - super(RunePouchType.values()[0].getItemId(), // Use first pouch ID as primary - 1, // amount - we need exactly 1 rune pouch - -1, // inventorySlot - any available slot - priority, - rating, - description, - taskContext); - - this.requiredRunes = requiredRunes; - this.allowCombinationRunes = allowCombinationRunes; - } - - /** - * Simplified constructor with default settings. - * - * @param requiredRunes Map of runes to their minimum required quantities - * @param priority Priority level for this requirement - * @param rating Rating/importance (1-10) - * @param description Human-readable description - * @param TaskContext When this requirement applies - */ - public RunePouchRequirement( - Map requiredRunes, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext) { - this(requiredRunes, false, priority, rating, description, taskContext); - } - - @Override - public String getName() { - String runesDescription = requiredRunes.entrySet().stream() - .map(entry -> entry.getValue() + "x " + entry.getKey().name()) - .collect(Collectors.joining(", ")); - return "Rune Pouch (" + runesDescription + ")"; - } - - /** - * Checks if this rune pouch requirement is currently fulfilled. - * A requirement is fulfilled if: - * 1. Player has a rune pouch in inventory - * 2. The rune pouch contains all required runes with sufficient quantities - * - * @return true if requirement is fulfilled, false otherwise - */ - @Override - public boolean isFulfilled() { - // First check if we have any rune pouch - if (!hasAnyRunePouch()) { - return false; - } - - // Then check if the rune pouch has the required runes - return Rs2RunePouch.contains(requiredRunes, allowCombinationRunes); - } - - /** - * Attempts to fulfill this rune pouch requirement. - * - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if successfully fulfilled, false otherwise - */ - @Override - public boolean fulfillRequirement(CompletableFuture scheduledFuture) { - try { - // Check if we already have any rune pouch - if (!hasAnyRunePouch()) { - // Try to get a rune pouch using parent logic (will try to get the first type) - if (!super.fulfillRequirement(scheduledFuture)) { - Microbot.log("Failed to obtain rune pouch"); - return false; - } - } - - // Check if rune pouch already has required runes - if (Rs2RunePouch.contains(requiredRunes, allowCombinationRunes)) { - Microbot.log("Rune pouch already contains required runes"); - return true; - } - - // Check if we have all required runes available (inventory + bank + current pouch) - if (!areRequiredRunesAvailable()) { - Microbot.log("Required runes are not available"); - return false; - } - - // Load the required runes into the pouch - return loadRunesIntoPouch(); - - } catch (Exception e) { - Microbot.log("Error fulfilling rune pouch requirement: " + e.getMessage()); - return false; - } - } - - /** - * Checks if this rune pouch requirement meets all conditions. - * This includes having the pouch available AND having all required runes available. - * - * @return true if all requirements are met, false otherwise - */ - @Override - public boolean meetsAllRequirements() { - // First check if the pouch itself is available - if (!super.meetsAllRequirements()) { - return false; - } - - // Then check if all required runes are available - return areRequiredRunesAvailable(); - } - - /** - * Checks if all required runes are available across inventory, bank, and current pouch contents. - * - * @return true if all runes are available in sufficient quantities - */ - private boolean areRequiredRunesAvailable() { - for (Map.Entry entry : requiredRunes.entrySet()) { - Runes rune = entry.getKey(); - int required = entry.getValue(); - - // Count runes from all sources - int inInventory = Rs2Inventory.count(rune.getItemId()); - int inBank = Rs2Bank.count(rune.getItemId()); - int inPouch = Rs2RunePouch.getQuantity(rune); - - int totalAvailable = inInventory + inBank + inPouch; - - if (allowCombinationRunes) { - // Add combination runes that can provide this base rune - for (Runes combinationRune : Runes.values()) { - - // Convert base runes array to a list for easier contains check - List baseRunesList =Arrays.asList(combinationRune.getBaseRunes()); - if (baseRunesList.contains(rune)) { - totalAvailable += Rs2Inventory.count(combinationRune.getItemId()); - totalAvailable += Rs2Bank.count(combinationRune.getItemId()); - totalAvailable += Rs2RunePouch.getQuantity(combinationRune); - } - } - } - - if (totalAvailable < required) { - Microbot.log("Insufficient " + rune.name() + " runes: need " + required + ", have " + totalAvailable); - return false; - } - } - - return true; - } - - /** - * Loads the required runes into the rune pouch using Rs2RunePouch utility. - * - * @return true if runes were successfully loaded - */ - private boolean loadRunesIntoPouch() { - try { - // Ensure bank is open for rune pouch configuration - if (!Rs2Bank.isOpen()) { - if (!Rs2Bank.openBank()) { - Microbot.log("Failed to open bank for rune pouch configuration"); - return false; - } - } - - // Use Rs2RunePouch.load() method to configure the pouch - boolean success = Rs2RunePouch.load(requiredRunes); - - if (success) { - Microbot.log("Successfully loaded runes into pouch: " + formatRuneMap(requiredRunes)); - } else { - Microbot.log("Failed to load runes into pouch"); - } - - return success; - - } catch (Exception e) { - Microbot.log("Error loading runes into pouch: " + e.getMessage()); - return false; - } - } - - /** - * Formats a rune map for display purposes. - * - * @param runes Map of runes to quantities - * @return Formatted string representation - */ - private String formatRuneMap(Map runes) { - return runes.entrySet().stream() - .map(entry -> entry.getValue() + "x " + entry.getKey().name()) - .collect(Collectors.joining(", ")); - } - - /** - * Returns a detailed display string with rune pouch requirement information. - * - * @return Formatted string containing requirement details - */ - @Override - public String displayString() { - StringBuilder sb = new StringBuilder(); - sb.append("=== Rune Pouch Requirement Details ===\n"); - sb.append("Name:\t\t\t").append(getName()).append("\n"); - sb.append("Type:\t\t\t").append(getRequirementType().name()).append("\n"); - sb.append("Priority:\t\t").append(getPriority().name()).append("\n"); - sb.append("Rating:\t\t\t").append(getRating()).append("/10\n"); - sb.append("Schedule Context:\t").append(getTaskContext().name()).append("\n"); - sb.append("Allow Combination Runes:\t").append(allowCombinationRunes).append("\n"); - sb.append("Description:\t\t").append(getDescription() != null ? getDescription() : "No description").append("\n"); - - sb.append("\n--- Required Runes ---\n"); - for (Map.Entry entry : requiredRunes.entrySet()) { - Runes rune = entry.getKey(); - int quantity = entry.getValue(); - int available = Rs2RunePouch.getQuantity(rune) + - Rs2Inventory.count(rune.getItemId()) + - Rs2Bank.count(rune.getItemId()); - - sb.append(rune.name()).append(":\t\t") - .append("Required: ").append(quantity) - .append(", Available: ").append(available) - .append(available >= quantity ? " ✓" : " ✗") - .append("\n"); - } - - sb.append("\n--- Current Status ---\n"); - sb.append("Has Rune Pouch:\t\t").append(Rs2Inventory.hasRunePouch() ? "Yes" : "No").append("\n"); - sb.append("Runes Available:\t\t").append(areRequiredRunesAvailable() ? "Yes" : "No").append("\n"); - sb.append("Requirement Met:\t\t").append(isFulfilled() ? "Yes" : "No").append("\n"); - - return sb.toString(); - } - - /** - * Creates a unique identity string for this requirement. - * Used for debugging and logging purposes. - * - * @return Unique identity string - */ - public String getUniqueIdentity() { - String runesSignature = requiredRunes.entrySet().stream() - .map(entry -> entry.getKey().name() + ":" + entry.getValue()) - .sorted() - .collect(Collectors.joining("|")); - - return "RunePouch[" + runesSignature + "|combo:" + allowCombinationRunes + "]"; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/LocationOption.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/LocationOption.java deleted file mode 100644 index 5b351e73209..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/LocationOption.java +++ /dev/null @@ -1,148 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location; - -import java.util.HashMap; -import java.util.Map; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Quest; -import net.runelite.api.QuestState; -import net.runelite.api.Skill; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.inventory.Rs2RunePouch; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; - -/** - * Container class for location data with requirements. - */ -@Getter -@Slf4j -public class LocationOption { - private final WorldPoint worldPoint; - private final String name; - private final boolean membersOnly; // Indicates if this location is members-only - private final Map requiredQuests; - private final Map requiredSkills; - private final Map requiredVarbits; - private final Map requiredVarplayer; - private final Map requiredItems; //id key ,and amount value - - - - public LocationOption(WorldPoint worldPoint, String name, boolean membersOnly) { - this(worldPoint, name,membersOnly, new HashMap<>(), new HashMap<>(),new HashMap<>(),new HashMap<>(),new HashMap<>()); - } - - public LocationOption(WorldPoint worldPoint, String name, - boolean membersOnly, - Map requiredQuests, - Map requiredSkills, - Map requiredVarbits, - Map requiredVarplayer, - Map requiredItems - ) { - this.worldPoint = worldPoint; - this.name = name; - this.membersOnly = membersOnly; - this.requiredQuests = requiredQuests != null ? new HashMap<>(requiredQuests) : new HashMap<>(); - this.requiredSkills = requiredSkills != null ? new HashMap<>(requiredSkills) : new HashMap<>(); - this.requiredVarbits = requiredVarbits != null ? new HashMap<>(requiredVarbits) : new HashMap<>(); - this.requiredVarplayer = requiredVarplayer != null ? new HashMap<>(requiredVarplayer) : new HashMap<>(); - this.requiredItems = requiredItems != null ? new HashMap<>(requiredItems) : new HashMap<>(); - - } - public boolean canReach() { - return Rs2Walker.canReach(worldPoint); - } - /** - * Checks if the player meets all requirements for this location. - * Improved implementation using streams for better performance and readability. - */ - public boolean hasRequirements() { - if (Microbot.getClient() == null) { - log.debug("LocationRequirement hasRequirements called outside client thread"); - return false; - } - if(!Microbot.isLoggedIn()){ - log.debug("Player is not logged in, cannot check location requirements"); - return false; - } - // Check quest requirements using streams - boolean questRequirementsMet = requiredQuests.entrySet().stream() - .allMatch(questReq -> { - QuestState currentState = Rs2Player.getQuestState(questReq.getKey()); - QuestState requiredState = questReq.getValue(); - - // If required state is FINISHED, player must have finished - if (requiredState == QuestState.FINISHED) { - return currentState == QuestState.FINISHED; - } - // If required state is IN_PROGRESS, player must have started (IN_PROGRESS or FINISHED) - if (requiredState == QuestState.IN_PROGRESS) { - return currentState == QuestState.IN_PROGRESS || currentState == QuestState.FINISHED; - } - return true; - }); - - if (!questRequirementsMet) { - return false; - } - - // Check skill requirements using streams - boolean skillRequirementsMet = requiredSkills.entrySet().stream() - .allMatch(skillReq -> Rs2Player.getSkillRequirement(skillReq.getKey(), skillReq.getValue())); - - if (!skillRequirementsMet) { - return false; - } - - // Check varbit requirements using streams - boolean varbitRequirementsMet = requiredVarbits.entrySet().stream() - .allMatch(varbitReq -> Microbot.getVarbitValue(varbitReq.getKey()) == varbitReq.getValue()); - - if (!varbitRequirementsMet) { - return false; - } - - // Check varplayer requirements using streams - boolean varplayerRequirementsMet = requiredVarplayer.entrySet().stream() - .allMatch(varplayerReq -> Microbot.getVarbitPlayerValue(varplayerReq.getKey()) == varplayerReq.getValue()); - - if (!varplayerRequirementsMet) { - return false; - } - - // Check item requirements using streams - boolean itemRequirementsMet = requiredItems.entrySet().stream() - .allMatch(itemReq -> { - int itemId = itemReq.getKey(); - int requiredAmount = itemReq.getValue(); - - int numberOfItems = Rs2Inventory.count(itemId) + - (Rs2Equipment.isWearing(itemId) ? 1 : 0); //TODO we must check if we are checking for stackable items.. - int numberOfItemsInPouch = Rs2RunePouch.getQuantity(itemId); - int numberOfItemsInBank = Rs2Bank.count(itemId); - // todo check rune pouches ? when the ids runes.., - // bolt ammo slot ? when the ids is any ammo - - if (numberOfItems+numberOfItemsInPouch +numberOfItemsInBank< requiredAmount) { - log.warn("Missing required item: {} x{} (have {})", itemId, requiredAmount, numberOfItems); - Microbot.log("Missing required item: " + itemId + " x" + requiredAmount + " (have " + numberOfItems + ")"); - return false; - } - return true; - }); - - return itemRequirementsMet; - } - - @Override - public String toString() { - return name + " (" + worldPoint.getX() + ", " + worldPoint.getY() + ", " + worldPoint.getPlane() + ")"; - } - } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/LocationRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/LocationRequirement.java deleted file mode 100644 index 8254eb7e579..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/LocationRequirement.java +++ /dev/null @@ -1,759 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location; - -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import lombok.EqualsAndHashCode; -import net.runelite.api.Constants; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -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.shortestpath.ShortestPathPlugin; -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.util.player.Rs2Player; -import net.runelite.client.plugins.microbot.util.shop.Rs2Shop; -import net.runelite.client.plugins.microbot.util.walker.TransportRouteAnalysis; -import net.runelite.client.plugins.microbot.util.world.Rs2WorldUtil; -import net.runelite.client.plugins.microbot.util.world.WorldHoppingConfig; -import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; - -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.stream.Collectors; - -/** - * Represents a location requirement for pre and post schedule tasks. - * This requirement ensures the player is at a specific location before or after script execution. - * - * LocationRequirement integrates with Rs2Walker for intelligent pathfinding and travel, - * supporting both simple location checks and complex travel operations. - * - * Enhanced to support multiple target locations with quest and skill requirements. - */ -@Slf4j -@Getter -@EqualsAndHashCode(callSuper = true) -public class LocationRequirement extends Requirement { - - - - /** - * List of possible target locations for this requirement. - */ - private final List targetLocations; - - /** - * The acceptable distance from the target location to consider the requirement fulfilled. - * Default is 5 tiles for most operations. - */ - private final int acceptableDistance; - - /** - * Whether to use transport methods (teleports, boats, etc.) when traveling to this location. - * Default is true for efficient travel. - */ - private final boolean useTransports; - /** -1 indicate not a spefic world - * The world hop to the targeted game world if specified. - * - */ - private final int world; - /** - * Configuration for world hopping behavior with exponential backoff and retry limits. - */ - @Setter - private WorldHoppingConfig worldHoppingConfig = WorldHoppingConfig.createDefault(); - @Override - public String getName() { - LocationOption location = getBestAvailableLocation(); - String bestLocationString = location != null ? String.format("Location %s (%d, %d, %d)", location.getName(), - location.getWorldPoint().getX(), - location.getWorldPoint().getY(), - location.getWorldPoint().getPlane()) : "Unknown Location"; - - - if (targetLocations.size() == 1) { - - - - return String.format("\n\tSingle Location -> %s", bestLocationString); - } else { - - return String.format("Multi-Location (%d options), best location -> %s", targetLocations.size(), bestLocationString); - } - } - - /** - * Gets the best available location option based on current player requirements and position. - * Prioritizes accessible locations, then proximity to player. - */ - public LocationOption getBestAvailableLocation() { - WorldPoint playerLocation = Rs2Player.getWorldLocation(); - - // Filter to only accessible locations - List accessibleLocations = targetLocations.stream() - .filter(LocationOption::hasRequirements) - .collect(Collectors.toList()); - - if (accessibleLocations.isEmpty()) { - log.warn("No accessible locations found for requirement: {}", getName()); - // Return the first location even if not accessible (for error reporting) - return targetLocations.isEmpty() ? null : targetLocations.get(0); - } - - if (accessibleLocations.size() == 1) { - return accessibleLocations.get(0); - } - - // If player location is available, find closest accessible location - if (playerLocation != null) { - return accessibleLocations.stream() - .min((loc1, loc2) -> Integer.compare( - playerLocation.distanceTo(loc1.getWorldPoint()), - playerLocation.distanceTo(loc2.getWorldPoint()) - )) - .orElse(accessibleLocations.get(0)); - } - - // Fall back to first accessible location - return accessibleLocations.get(0); - } - - /** - * Gets the best available location option based on a reference point. - * Prioritizes accessible locations, then proximity to reference point. - */ - public LocationOption getBestAvailableLocation(WorldPoint referencePoint) { - // Filter to only accessible locations - List accessibleLocations = targetLocations.stream() - .filter(LocationOption::hasRequirements) - .collect(Collectors.toList()); - - if (accessibleLocations.isEmpty()) { - log.warn("No accessible locations found for requirement: {}", getName()); - return targetLocations.isEmpty() ? null : targetLocations.get(0); - } - - if (accessibleLocations.size() == 1) { - return accessibleLocations.get(0); - } - - // Find closest accessible location to reference point - return accessibleLocations.stream() - .min((loc1, loc2) -> Integer.compare( - referencePoint.distanceTo(loc1.getWorldPoint()), - referencePoint.distanceTo(loc2.getWorldPoint()) - )) - .orElse(accessibleLocations.get(0)); - } - - /** - * Full constructor for LocationRequirement with multiple locations. - * - * @param targetLocations List of possible target locations with requirements - * @param acceptableDistance Distance tolerance for considering requirement fulfilled - * @param useTransports Whether to use teleports and other transport methods - * @param locationName Custom name for this location requirement - * @param TaskContext When this requirement should be fulfilled - * @param priority Priority level of this requirement - * @param rating Effectiveness rating (1-10) - * @param description Human-readable description - */ - public LocationRequirement( - List targetLocations, - int acceptableDistance, - boolean useTransports, - int world, - TaskContext taskContext, - RequirementPriority priority, - int rating, - String description) { - - super(RequirementType.PLAYER_STATE, - priority, - rating, - description != null ? description : generateDefaultDescription(targetLocations, taskContext), - Collections.emptyList(), // Location requirements don't use item IDs - taskContext); - - this.targetLocations = new ArrayList<>(targetLocations); - this.acceptableDistance = acceptableDistance; - this.useTransports = useTransports; - this.world = world; // Default to no specific world - } - - /** - * Constructor for single location requirement (backwards compatibility). - * - * @param targetLocation The world point to travel to - * @param acceptableDistance Distance tolerance for considering requirement fulfilled - * @param useTransports Whether to use teleports and other transport methods - * @param locationName Custom name for this location - * @param TaskContext When this requirement should be fulfilled - * @param priority Priority level of this requirement - * @param rating Effectiveness rating (1-10) - * @param description Human-readable description - */ - public LocationRequirement( - WorldPoint targetLocation, - String locationName, - boolean membersOnly, - int acceptableDistance, - boolean useTransports, - int world, - TaskContext taskContext, - RequirementPriority priority, - int rating, - String description) { - this(Arrays.asList(new LocationOption(targetLocation,locationName, membersOnly)), // Default to non-members for single location - acceptableDistance, useTransports, world,taskContext, priority, rating, description); - } - - /** - * Simplified constructor with common defaults. - * - * @param targetLocation The world point to travel to - * @param useTransports Whether to use teleports and other transport methods - * @param locationName Custom name for this location - * @param TaskContext When this requirement should be fulfilled - * @param priority Priority level of this requirement - */ - public LocationRequirement( - WorldPoint targetLocation, - String locationName, - boolean membersOnly, - int acceptableDistance, - boolean useTransports, - int world, - TaskContext taskContext, - RequirementPriority priority) { - this(targetLocation,locationName, membersOnly,acceptableDistance, useTransports ,world, taskContext, priority, 8, null); - } - - /** - * Basic constructor for mandatory location requirements. - * - * @param targetLocation The world point to travel to - * @param useTransports Whether to use teleports and other transport methods - * @param locationName Custom name for this location - * @param TaskContext When this requirement should be fulfilled - */ - public LocationRequirement( - WorldPoint targetLocation, - String locationName, - boolean membersOnly, - int acceptableDistance, - boolean useTransports, - int world, - TaskContext taskContext) { - this(targetLocation, locationName,membersOnly,acceptableDistance,useTransports, world,taskContext, RequirementPriority.MANDATORY); - } - - /** - * Constructor for bank locations using existing bank infrastructure. - * - * @param bankLocation The bank location to use as target - * @param acceptableDistance Distance tolerance for considering requirement fulfilled - * @param useTransports Whether to use teleports and other transport methods - * @param TaskContext When this requirement should be fulfilled - * @param priority Priority level of this requirement - */ - public LocationRequirement( - BankLocation bankLocation, - int acceptableDistance, - boolean useTransports, - int world, - TaskContext taskContext, - RequirementPriority priority) { - this(Arrays.asList(new LocationOption(bankLocation.getWorldPoint(), - bankLocation.toString(), - bankLocation.isMembers())), - acceptableDistance, useTransports, world,taskContext, priority, 5, bankLocation.getClass().getSimpleName() + " Bank Location Requirement"); - } - public LocationRequirement( - BankLocation bankLocation, - boolean useTransports, - int world, - TaskContext taskContext, - RequirementPriority priority) { - this(bankLocation, 15, useTransports, world,taskContext, priority); - } - /** - * Checks if the player is currently at any of the required locations. - * - * @return true if the player is within acceptable distance of any valid target location - */ - public boolean isAtRequiredLocation() { - WorldPoint currentLocation = Rs2Player.getWorldLocation(); - if (currentLocation == null) { - return false; - } - - if (world != -1 && isWorldHopRequired()){ - log.error("Player is not in the required world: {} (current: {})", world, Rs2Player.getWorld()); - return false; // Player is not in the required world - } - // Check if player is at any accessible location - for (LocationOption location : targetLocations) { - if (location.hasRequirements() && - currentLocation.distanceTo(location.getWorldPoint()) <= acceptableDistance) { - log.debug("Player is at required location: {} (distance: {})", location.getName(), currentLocation.distanceTo(location.getWorldPoint())); - return true; - } - } - List distanceIntegers = targetLocations.stream() - .map(loc -> loc.getWorldPoint().distanceTo(currentLocation)) - .collect(Collectors.toList()); - log.debug("Player is not at any required location.\n\tCurrent location: {}\n\tRequired locations: {}\n\tdistances to each location: {}", currentLocation, targetLocations, distanceIntegers); - - return false; - } - - /** - * Checks if at least one required location is reachable using available methods. - * This method uses Rs2Walker to determine reachability without actually traveling. - * - * @return true if at least one location can be reached, false otherwise - */ - public boolean isLocationReachable() { - // Check if any accessible location is reachable - for (LocationOption location : targetLocations) { - if (location.hasRequirements()) { - try { - if (Rs2Walker.canReach(location.getWorldPoint(), true)) { - return true; - } - } catch (Exception e) { - log.warn("Error checking reachability for location {}: {}", location, e.getMessage()); - } - } - } - - // If no accessible locations are reachable, check if any location is reachable (for non-mandatory requirements) - if (!isMandatory()) { - for (LocationOption location : targetLocations) { - try { - if (Rs2Walker.canReach(location.getWorldPoint(), true)) { - return true; - } - } catch (Exception e) { - log.warn("Error checking reachability for location {}: {}", location, e.getMessage()); - } - } - } - - return false; - } - @Override - public boolean isFulfilled() { - // Check if the player is at any of the required locations - return isAtRequiredLocation(); - } - - /** - * Implements the abstract fulfillRequirement method from the base Requirement class. - * Attempts to fulfill this location requirement by traveling to the target location. - * - * @param executorService The ScheduledExecutorService on which this requirement fulfillment is running - * @return true if the requirement was successfully fulfilled, false otherwise - */ - @Override - public boolean fulfillRequirement(CompletableFuture scheduledFuture) { - boolean currentUseWithBankedItems = Microbot.getConfigManager().getConfiguration(ShortestPathPlugin.CONFIG_GROUP, "walkWithBankedTransports", Boolean.class); - Microbot.getConfigManager().setConfiguration(ShortestPathPlugin.CONFIG_GROUP, "walkWithBankedTransports", useTransports); - try { - if (Microbot.getClient() == null || Microbot.getClient().isClientThread()) { - log.error("Cannot fulfill location requirement outside client thread"); - return false; // Cannot fulfill outside client thread - } - - - log.info("Attempting to fulfill location requirement: {}", getName()); - // Check if the requirement is already fulfilled - if (isAtRequiredLocation()) { - return true; - } - - - log.info("Checking if location is reachable for requirement: {}", getName()); - // Check if the location is reachable - if (!isLocationReachable()) { - if (isMandatory()) { - log.error("MANDATORY location requirement cannot be fulfilled: " + getName() + " - Location not reachable"); - return false; - } else { - log.error("OPTIONAL/RECOMMENDED location requirement skipped: " + getName() + " - Location not reachable"); - return true; // Non-mandatory requirements return true if location isn't reachable - } - } - log.info("Location is reachable, proceeding to travel for requirement: {}", getName()); - - - // Attempt to travel to the location - boolean success = travelToLocation(scheduledFuture); - - if (!success && isMandatory()) { - log.error("MANDATORY location requirement failed: " + getName()); - return false; - } - - return true; - - } catch (Exception e) { - log.error("Error fulfilling location requirement " + getName() + ": " + e.getMessage()); - return !isMandatory(); // Don't fail mandatory requirements due to exceptions - }finally { - // Restore the original setting for banked transports - Microbot.getConfigManager().setConfiguration(ShortestPathPlugin.CONFIG_GROUP, "walkWithBankedTransports", currentUseWithBankedItems); - } - } - - /** - * Attempts to travel to the best available target location using Rs2Walker with movement watchdog. - * Creates its own executor service for the watchdog that gets cleaned up when done. - * - * @param scheduledFuture The CompletableFuture to monitor for cancellation - * @return true if the travel was successful, false otherwise - */ - private boolean travelToLocation(CompletableFuture scheduledFuture) { - ScheduledExecutorService travelExecutorService = null; - ScheduledFuture watchdogFuture = null; - final int MAX_RETRIES = 3; // Maximum retries for walking to the location - AtomicBoolean watchdogTriggered = new AtomicBoolean(false); - if (world !=-1 && !Rs2WorldUtil.canAccessWorld(world)){ - log.warn("Cannot access world {} for requirement: {}", world, getName()); - return false; // Cannot proceed if world is not accessible - } - if( world != -1 && isWorldHopRequired()){ - boolean successWorldHop = Rs2WorldUtil.hopWorld(scheduledFuture, world, 1, worldHoppingConfig); - if (!successWorldHop) { - log.warn("World hop failed for requirement: {}", getName()); - return false; // World hop failed, cannot proceed - } - } - try { - LocationOption bestLocation = getBestAvailableLocation(); - if (bestLocation == null) { - log.warn("No available location found for requirement: {}", getName()); - return false; - } - - WorldPoint targetLocation = bestLocation.getWorldPoint(); - - // Create a dedicated executor service for this travel operation - travelExecutorService = Executors.newSingleThreadScheduledExecutor(r -> { - Thread thread = new Thread(r, "LocationRequirement-Travel-" + getName()); - thread.setDaemon(true); // Daemon thread so it doesn't prevent JVM shutdown - return thread; - }); - - // Start movement watchdog with our own executor service - watchdogFuture = startMovementWatchdog(travelExecutorService, scheduledFuture, watchdogTriggered, getName()); - if (watchdogFuture != null && !watchdogFuture.isDone()) { - log.debug("Movement watchdog started for location: {}", getName()); - - - } - - // Check if we need to get transport items from bank - boolean walkResult = false; - - for (int retries = 0; retries < MAX_RETRIES; retries++) { - if (walkResult==true || (scheduledFuture != null && scheduledFuture.isDone()) || watchdogTriggered.get()){ - break; // Exit loop if we successfully walked or if the future is cancelled - } - Rs2Walker.setTarget(null); // Reset target in Rs2Walker - if (useTransports) { - // This would be enhanced to check for specific transport items - // For now, just ensure we have access to basic travel - walkResult = Rs2Walker.walkWithBankedTransports(targetLocation,acceptableDistance,false); - } else { - // Use Rs2Walker to travel to the location - walkResult = Rs2Walker.walkTo(targetLocation,acceptableDistance); - } - } - - if(isAtRequiredLocation()) { - log.debug("\nSuccessfully reached required location: {}", getName()); - return true; // Already at the required location - } - - if (walkResult && !watchdogTriggered.get()) { - sleepUntil(()-> !Rs2Player.isMoving() , 5000); - return isAtRequiredLocation() && !watchdogTriggered.get(); - } - - return false; - - } catch (Exception e) { - log.error("Error traveling to location " + getName() + ": " + e.getMessage()); - return false; - } finally { - // Always clean up the watchdog first - if (watchdogFuture != null && !watchdogFuture.isDone()) { - watchdogFuture.cancel(true); - } - - // Then shutdown the executor service - if (travelExecutorService != null) { - travelExecutorService.shutdown(); - try { - // Wait a bit for graceful shutdown - if (!travelExecutorService.awaitTermination(2, TimeUnit.SECONDS)) { - // Force shutdown if tasks don't complete quickly - travelExecutorService.shutdownNow(); - if (!travelExecutorService.awaitTermination(1, TimeUnit.SECONDS)) { - log.warn("Travel executor service did not terminate cleanly for {}", getName()); - } - } - } catch (InterruptedException e) { - // Restore interrupted status and force shutdown - Thread.currentThread().interrupt(); - travelExecutorService.shutdownNow(); - } - } - } - } - - /** - * Gets the estimated travel time to the best available location in game ticks. - * This is a rough estimate based on distance and available transport methods. - * - * @return Estimated travel time in game ticks, or -1 if cannot estimate - */ - public int getEstimatedTravelTime() { - WorldPoint currentLocation = Rs2Player.getWorldLocation(); - if (currentLocation == null) { - return -1; - } - - LocationOption bestLocation = getBestAvailableLocation(); - if (bestLocation == null) { - return -1; - } - - WorldPoint targetLocation = bestLocation.getWorldPoint(); - TransportRouteAnalysis result = Rs2Walker.compareRoutes(targetLocation); // Ensure Rs2Walker has the latest pathfinding data - - int distance = Integer.MAX_VALUE; // Default to a large value if no path found - if (result != null && (result.isDirectIsFaster() || !useTransports)) { - distance = result.getDirectDistance(); // Use direct distance if it's faster - } else if (result != null && useTransports) { - distance = result.getBankingRouteDistance(); // Otherwise, use banking route distance - } - - // Walking only, slower travel - return (distance / 2); // when running we can move 2 tiles per game tick, so 2 tiles per Constants.GAME_TICK - } - private boolean isWorldHopRequired() { - // If world is -1, no specific world hop is required - if (world == -1) { - return false; - } - - // Check if the current world matches the target world - return Rs2Player.getWorld() != world; - } - - - /** - * Returns a detailed display string with location requirement information. - * - * @return A formatted string containing location requirement details - */ - @Override - public String displayString() { - StringBuilder sb = new StringBuilder(); - sb.append("=== Location Requirement Details ===\n"); - sb.append("Name:\t\t\t").append(getName()).append("\n"); - sb.append("Type:\t\t\t").append(getRequirementType().name()).append("\n"); - sb.append("Priority:\t\t").append(getPriority().name()).append("\n"); - sb.append("Rating:\t\t\t").append(getRating()).append("/10\n"); - sb.append("Schedule Context:\t").append(taskContext.name()).append("\n"); - sb.append("Acceptable Distance:\t").append(acceptableDistance).append(" tiles\n"); - sb.append("Use Transports:\t\t").append(useTransports ? "Yes" : "No").append("\n"); - sb.append("World:\t\t\t").append(world != -1 ? world : "Any world").append("\n"); - sb.append("canAccessWorld:\t").append(Rs2WorldUtil.canAccessWorld(world) ? "Yes" : "No").append("\n"); - sb.append("Description:\t\t").append(getDescription() != null ? getDescription() : "No description").append("\n"); - - // Add location details - sb.append("\n--- Available Locations (").append(targetLocations.size()).append(") ---\n"); - for (int i = 0; i < targetLocations.size(); i++) { - LocationOption location = targetLocations.get(i); - sb.append("Location ").append(i + 1).append(":\t\t").append(location.getName()).append("\n"); - sb.append(" Coordinates:\t\t").append(String.format("(%d, %d, %d)", - location.getWorldPoint().getX(), - location.getWorldPoint().getY(), - location.getWorldPoint().getPlane())).append("\n"); - sb.append(" Accessible:\t\t").append(location.hasRequirements() ? "Yes" : "No").append("\n"); - - if (!location.getRequiredQuests().isEmpty()) { - sb.append(" Required Quests:\t"); - location.getRequiredQuests().entrySet().stream() - .forEach(entry -> sb.append(entry.getKey().name()).append(" (").append(entry.getValue().name()).append(") ")); - sb.append("\n"); - } - - if (!location.getRequiredSkills().isEmpty()) { - sb.append(" Required Skills:\t"); - location.getRequiredSkills().entrySet().stream() - .forEach(entry -> sb.append(entry.getKey().name()).append(" ").append(entry.getValue()).append(" ")); - sb.append("\n"); - } - } - - // Add current status - sb.append("\n--- Current Status ---\n"); - sb.append("Currently at Location:\t").append(isAtRequiredLocation() ? "Yes" : "No").append("\n"); - sb.append("Location Reachable:\t").append(isLocationReachable() ? "Yes" : "No").append("\n"); - sb.append("Estimated Travel Time:\t").append(getEstimatedTravelTime()).append(" seconds\n"); - - LocationOption bestLocation = getBestAvailableLocation(); - if (bestLocation != null) { - sb.append("Best Available Location:\t").append(bestLocation.toString()).append("\n"); - } - - return sb.toString(); - } - /** - * Generates a default description based on the target locations and context. - * - * @param locations The target locations - * @param context When the requirement should be fulfilled - * @return A descriptive string explaining the requirement - */ - public static String generateDefaultDescription(List locations, TaskContext context) { - if (locations == null || locations.isEmpty()) { - return "Unknown Location Requirement"; - } - - String contextPrefix = (context == TaskContext.PRE_SCHEDULE) ? "Pre-task" : "Post-task"; - - if (locations.size() == 1) { - LocationOption location = locations.get(0); - WorldPoint wp = location.getWorldPoint(); - String bestLocationString = location.getName() != null ? - location.getName() : - String.format("(%d, %d, %d)", wp.getX(), wp.getY(), wp.getPlane()); - - return String.format("%s location requirement: %s", contextPrefix, bestLocationString); - } else { - // Multiple locations - show best available - LocationOption bestLocation = locations.get(0); // First available location - WorldPoint wp = bestLocation.getWorldPoint(); - String bestLocationString = bestLocation.getName() != null ? - bestLocation.getName() : - String.format("(%d, %d, %d)", wp.getX(), wp.getY(), wp.getPlane()); - - return String.format("%s multi-location requirement (%d options), primary: %s", - contextPrefix, locations.size(), bestLocationString); - } - } - - /** - * Checks if the player has moved out of a defined area around the last position. - * This is more robust than checking single coordinates as it accounts for small movements. - * - * @param lastPosition The last recorded position - * @param currentPosition The current position - * @param areaRadius The radius of the area to check - * @return true if the player has moved significantly outside the area - */ - public static boolean hasMovedOutOfArea(WorldPoint lastPosition, WorldPoint currentPosition, int areaRadius) { - if (lastPosition == null || currentPosition == null) { - return false; - } - - // Calculate distance between positions - int distance = lastPosition.distanceTo(currentPosition); - return distance > areaRadius; - } - - /** - * Starts a movement watchdog that monitors player position and stops walking if no movement is detected. - * - * @param executorService The executor service to run the watchdog on - * @param scheduledFuture The future to monitor for cancellation - * @param watchdogTriggered Atomic boolean to signal when watchdog triggers - * @param taskName Name of the task for logging purposes - * @return The scheduled future for the watchdog task - */ - public static ScheduledFuture startMovementWatchdog(ScheduledExecutorService executorService, - CompletableFuture scheduledFuture, - AtomicBoolean watchdogTriggered, - String taskName) { - AtomicReference lastPosition = new AtomicReference<>(Rs2Player.getWorldLocation()); - AtomicReference lastMovementTime = new AtomicReference<>(System.currentTimeMillis()); - long watchdogCheckInterval_ms = Constants.GAME_TICK_LENGTH; // Check every game tick - return executorService.scheduleAtFixedRate(() -> { - try { - // Check for cancellation first - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.info("Movement watchdog cancelled for: {}", taskName); - watchdogTriggered.set(true); - Rs2Walker.setTarget(null); - throw new RuntimeException("Watchdog cancelled - stopping task"); - } - - WorldPoint currentPosition = Rs2Player.getWorldLocation(); - if (currentPosition == null) { - watchdogTriggered.set(true); - // Stop walking by clearing the target - Rs2Walker.setTarget(null); - throw new RuntimeException("Watchdog cancelled - stopping task"); - } - - WorldPoint lastPos = lastPosition.get(); - if (lastPos == null) { - lastPosition.set(currentPosition); - lastMovementTime.set(System.currentTimeMillis()); - return; - } - log.debug("Current position: {}, Last position: {}", currentPosition, lastPos); - // Check if player has moved significantly (using area detection for robustness) - boolean hasMovedSignificantly = hasMovedOutOfArea(lastPos, currentPosition, 2); - - if (hasMovedSignificantly || Rs2Bank.isOpen() || Rs2Shop.isOpen()) { - // Player has moved, update last movement time and position - lastPosition.set(currentPosition); - lastMovementTime.set(System.currentTimeMillis()); - } else { - // Player hasn't moved significantly, check timeout - long timeSinceLastMovement = System.currentTimeMillis() - lastMovementTime.get(); - log.debug ("Time since last movement: {} ms", timeSinceLastMovement); - if (timeSinceLastMovement > watchdogCheckInterval_ms*10) { // more than 5 times the check interval - log.warn("Movement watchdog triggered - no significant movement detected for 1 minute"); - watchdogTriggered.set(true); - - // Stop walking by clearing the target - Rs2Walker.setTarget(null); - - // Cancel this watchdog - throw new RuntimeException("Watchdog triggered - stopping task"); - } - } - } catch (Exception e) { - log.warn("Watchdog error: {}", e.getMessage()); - watchdogTriggered.set(true); - Rs2Walker.setTarget(null); - throw e; // Re-throw to stop the scheduled task - } - }, 0, watchdogCheckInterval_ms, TimeUnit.MILLISECONDS); // Check every ms - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/ResourceLocationOption.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/ResourceLocationOption.java deleted file mode 100644 index 6b549ce92bd..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/location/ResourceLocationOption.java +++ /dev/null @@ -1,145 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Quest; -import net.runelite.api.QuestState; -import net.runelite.api.Skill; -import net.runelite.api.coords.WorldPoint; - -import java.util.Map; - -/** - * Extended LocationOption with resource tracking capabilities. - * This class is specifically designed for resource-based locations like mining rocks, - * fishing spots, or woodcutting trees where the number of available resources matters. - * - * The numberOfResources field allows for intelligent location selection based on - * resource availability, helping to choose locations with sufficient resources - * for efficient skilling activities. - */ -@Getter -@Slf4j -public class ResourceLocationOption extends LocationOption { - - /** - * The number of available resources at this location. - * For example: - * - Mining: Number of rock spawns - * - Fishing: Number of fishing spots - * - Woodcutting: Number of tree spawns - */ - private final int numberOfResources; - - /** - * Constructor with resource count and members-only flag. - * - * @param worldPoint The world coordinates of this location - * @param name The display name of this location - * @param membersOnly Whether this location requires membership - * @param numberOfResources The number of resources available at this location - */ - public ResourceLocationOption(WorldPoint worldPoint, String name, boolean membersOnly, int numberOfResources) { - super(worldPoint, name, membersOnly); - this.numberOfResources = numberOfResources; - } - - /** - * Full constructor with all requirements and resource count. - * - * @param worldPoint The world coordinates of this location - * @param name The display name of this location - * @param membersOnly Whether this location requires membership - * @param numberOfResources The number of resources available at this location - * @param requiredQuests Quest requirements for accessing this location - * @param requiredSkills Skill level requirements for accessing this location - * @param requiredVarbits Varbit requirements for accessing this location - * @param requiredVarplayer Varplayer requirements for accessing this location - * @param requiredItems Item requirements for accessing this location - */ - public ResourceLocationOption(WorldPoint worldPoint, String name, - boolean membersOnly, - int numberOfResources, - Map requiredQuests, - Map requiredSkills, - Map requiredVarbits, - Map requiredVarplayer, - Map requiredItems) { - super(worldPoint, name, membersOnly, requiredQuests, requiredSkills, - requiredVarbits, requiredVarplayer, requiredItems); - this.numberOfResources = numberOfResources; - } - - /** - * Checks if this location has the minimum required number of resources. - * - * @param minResources The minimum number of resources required - * @return true if this location has enough resources, false otherwise - */ - public boolean hasMinimumResources(int minResources) { - return numberOfResources >= minResources; - } - - /** - * Calculates a resource efficiency score based on the number of resources - * and distance from a reference point. - * Higher scores indicate better locations. - * - * @param referencePoint The point to calculate distance from - * @return Efficiency score (higher is better) - */ - public double calculateResourceEfficiencyScore(WorldPoint referencePoint) { - if (referencePoint == null) { - return numberOfResources; // Just return resource count if no reference point - } - - double distance = Math.sqrt( - Math.pow(getWorldPoint().getX() - referencePoint.getX(), 2) + - Math.pow(getWorldPoint().getY() - referencePoint.getY(), 2) - ); - - // Avoid division by zero and give closer locations higher scores - // Formula: resources * (100 / (distance + 1)) - // This gives more weight to resource count while factoring in distance - return numberOfResources * (100.0 / (distance + 1)); - } - - /** - * Determines if this location is better than another based on resource count and requirements. - * Prioritizes accessible locations first, then resource count, then proximity. - * - * @param other The other location to compare against - * @param referencePoint Optional reference point for distance comparison - * @return true if this location is better than the other - */ - public boolean isBetterThan(ResourceLocationOption other, WorldPoint referencePoint) { - if (other == null) return true; - - // First priority: accessibility (meeting requirements) - boolean thisAccessible = this.hasRequirements(); - boolean otherAccessible = other.hasRequirements(); - - if (thisAccessible && !otherAccessible) return true; - if (!thisAccessible && otherAccessible) return false; - - // Second priority: resource count (more resources = better) - if (this.numberOfResources != other.numberOfResources) { - return this.numberOfResources > other.numberOfResources; - } - - // Third priority: efficiency score (considers distance) - if (referencePoint != null) { - return this.calculateResourceEfficiencyScore(referencePoint) > - other.calculateResourceEfficiencyScore(referencePoint); - } - - // Fallback: prefer this location if all else is equal - return true; - } - - @Override - public String toString() { - return getName() + " (" + getWorldPoint().getX() + ", " + getWorldPoint().getY() + - ", " + getWorldPoint().getPlane() + ") - Resources: " + numberOfResources; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/logical/LogicalRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/logical/LogicalRequirement.java deleted file mode 100644 index 082ba45ff1c..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/logical/LogicalRequirement.java +++ /dev/null @@ -1,879 +0,0 @@ - -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.collection.LootRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.ShopRequirement; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; - -/** - * Abstract base class for logical combinations of requirements. - * Provides common functionality for AND and OR requirement combinations. - * - * Logical requirements enforce type homogeneity - all child requirements must be of the same type. - * This prevents mixing different requirement types (e.g., SpellbookRequirement with ItemRequirement) - * which would make caching and optimization difficult. Complex mixed requirements should use - * ConditionalRequirement instead. - * - * Similar to LogicalCondition but adapted for the requirement system. - * Logical requirements can contain other requirements (including other logical requirements) - * and evaluate them according to logical rules. - * - * @see net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LogicalCondition - */ -@Slf4j -@EqualsAndHashCode(callSuper = true) -public abstract class LogicalRequirement extends Requirement { - - @Getter - protected final List childRequirements = new ArrayList<>(); - - /** - * The allowed requirement type for child requirements in this logical group. - * All child requirements must be of this type or be LogicalRequirements that also - * enforce the same child type. This enables efficient caching and type-safe operations. - */ - @Getter - protected final Class allowedChildType; - - /** - * Protected constructor for logical requirements. - * Child classes must call this constructor and provide their own requirement type. - * - * @param requirementType The type of logical requirement (AND_LOGICAL or OR_LOGICAL) - * @param priority Priority level of this logical requirement - * @param rating Effectiveness rating (1-10) - * @param description Human-readable description - * @param TaskContext When this requirement should be fulfilled - * @param allowedChildType The class type that child requirements must be (or null to infer from first requirement) - * @param requirements Child requirements to combine logically - */ - protected LogicalRequirement(RequirementType requirementType, RequirementPriority priority, int rating, - String description, TaskContext taskContext, - Class allowedChildType, - Requirement... requirements) { - super(requirementType, priority, rating, description, List.of(), taskContext); - - // Determine allowed child type - if (allowedChildType != null) { - this.allowedChildType = allowedChildType; - } else if (requirements.length > 0) { - // Infer from first requirement - Requirement firstReq = requirements[0]; - if (firstReq instanceof LogicalRequirement) { - this.allowedChildType = ((LogicalRequirement) firstReq).getAllowedChildType(); - } else { - this.allowedChildType = firstReq.getClass(); - } - } else { - // Default to Requirement if no children and no explicit type - this.allowedChildType = Requirement.class; - } - - // Validate and add requirements - for (Requirement requirement : requirements) { - addRequirement(requirement); - } - } - - protected LogicalRequirement(RequirementType requirementType, RequirementPriority priority, int rating, - String description, TaskContext taskContext, - Class allowedChildType) { - super(requirementType, priority, rating, description, List.of(), taskContext); - - // Determine allowed child type - if (allowedChildType != null) { - this.allowedChildType = allowedChildType; - } else { - // Default to Requirement if no children and no explicit type - this.allowedChildType = Requirement.class; - } - - } - - /** - * Validates that a requirement is compatible with this logical requirement's type constraints. - * - * @param requirement The requirement to validate - * @throws IllegalArgumentException if the requirement is not compatible - */ - private void validateRequirement(Requirement requirement) { - if (requirement == null) { - throw new IllegalArgumentException("Child requirement cannot be null"); - } - - // Check if it's a LogicalRequirement with compatible child type - if (requirement instanceof LogicalRequirement) { - LogicalRequirement logicalReq = (LogicalRequirement) requirement; - if (!allowedChildType.isAssignableFrom(logicalReq.getAllowedChildType()) && - !logicalReq.getAllowedChildType().isAssignableFrom(allowedChildType)) { - throw new IllegalArgumentException(String.format( - "Logical requirement child type %s is not compatible with required type %s", - logicalReq.getAllowedChildType().getSimpleName(), - allowedChildType.getSimpleName())); - } - } else { - // Check if it's assignable to our allowed type - if (!allowedChildType.isAssignableFrom(requirement.getClass())) { - throw new IllegalArgumentException(String.format( - "Requirement type %s is not compatible with required type %s", - requirement.getClass().getSimpleName(), - allowedChildType.getSimpleName())); - } - } - - // Validate schedule context compatibility - if (this.getTaskContext() != null && requirement.getTaskContext() != null) { - if (this.getTaskContext() != requirement.getTaskContext() && - this.getTaskContext() != TaskContext.BOTH && - requirement.getTaskContext() != TaskContext.BOTH) { - throw new IllegalArgumentException(String.format( - "Schedule context mismatch: logical requirement has %s but child has %s", - this.getTaskContext(), requirement.getTaskContext())); - } - } - } - - - - /** - * Adds a child requirement to this logical requirement with type validation. - * - * @param requirement The requirement to add - * @return This logical requirement for method chaining - * @throws IllegalArgumentException if the requirement type is not compatible - */ - public LogicalRequirement addRequirement(Requirement requirement) { - validateRequirement(requirement); - - // Add the requirement to the child requirements list - this was missing! - childRequirements.add(requirement); - - // Merge all child ids into this.ids - always create new mutable list - if (this.ids == null) { - this.ids = new java.util.ArrayList<>(); - } else { - // Always create a new mutable ArrayList to avoid immutable collection issues - this.ids = new java.util.ArrayList<>(this.ids); - } - if (requirement.getIds() != null) { - for (Integer id : requirement.getIds()) { - if (!this.ids.contains(id)) { - this.ids.add(id); - } - } - } - // Update rating to highest among all children - int maxRating = this.rating; - for (Requirement child : childRequirements) { - if (child.getRating() > maxRating) { - maxRating = child.getRating(); - } - } - this.rating = maxRating; - return this; - } - - /** - * Removes a child requirement from this logical requirement. - * - * @param requirement The requirement to remove - * @return true if the requirement was removed, false if it wasn't found - */ - public boolean removeRequirement(Requirement requirement) { - return childRequirements.remove(requirement); - } - - /** - * Checks if this logical requirement contains the specified requirement, - * either directly or within any nested logical requirements. - * - * @param targetRequirement The requirement to search for - * @return true if the requirement exists within this logical structure, false otherwise - */ - public boolean contains(Requirement targetRequirement) { - if (childRequirements.contains(targetRequirement)) { - return true; - } - - // Check nested logical requirements - for (Requirement child : childRequirements) { - if (child instanceof LogicalRequirement) { - if (((LogicalRequirement) child).contains(targetRequirement)) { - return true; - } - } - } - - return false; - } - - /** - * Gets the total number of requirements in this logical structure (including nested). - * - * @return Total count of all requirements - */ - public int getTotalRequirementCount() { - int count = 0; - for (Requirement child : childRequirements) { - if (child instanceof LogicalRequirement) { - count += ((LogicalRequirement) child).getTotalRequirementCount(); - } else { - count++; - } - } - return count; - } - - /** - * Gets the count of fulfilled requirements in this logical structure. - * - * @return Count of fulfilled requirements - */ - public int getFulfilledRequirementCount() { - int count = 0; - for (Requirement child : childRequirements) { - if (child instanceof LogicalRequirement) { - // For logical requirements, check if they are fulfilled as a whole - if (((LogicalRequirement) child).isLogicallyFulfilled()) { - count += ((LogicalRequirement) child).getTotalRequirementCount(); - } - } else { - if (child.isFulfilled()) { - count++; - } - } - } - return count; - } - - /** - * Abstract method to check if this logical requirement is fulfilled. - * AND requirements need all children fulfilled, OR requirements need at least one. - * - * @return true if the logical requirement is fulfilled, false otherwise - */ - public abstract boolean isLogicallyFulfilled(); - - /** - * Abstract method to get requirements that are blocking fulfillment. - * For AND: all unfulfilled requirements block - * For OR: all requirements block if none are fulfilled - * - * @return List of requirements that are preventing fulfillment - */ - public abstract List getBlockingRequirements(); - - /** - * Gets the progress percentage for this logical requirement. - * - * @return Progress percentage (0.0 to 100.0) - */ - public double getProgressPercentage() { - if (childRequirements.isEmpty()) { - return 100.0; - } - - int total = getTotalRequirementCount(); - int fulfilled = getFulfilledRequirementCount(); - - return total > 0 ? (fulfilled * 100.0) / total : 0.0; - } - - /** - * Checks if this logical requirement is fulfilled. - * This method is required by the Requirement base class. - * - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if the logical requirement is fulfilled, false otherwise - */ - @Override - public boolean fulfillRequirement(CompletableFuture scheduledFuture) { - log.debug("Attempting to fulfill logical requirement: {}", getName()); - - // For logical requirements, we don't directly fulfill them - // Instead, we check if they are already fulfilled by their children - - boolean fulfilled = fulfillLogicalRequirement(scheduledFuture); - - if (fulfilled) { - log.debug("Logical requirement {} is already fulfilled", getName()); - } else { - log.debug("Logical requirement {} is not fulfilled. Blocking requirements: {}", - getName(), getBlockingRequirements().size()); - } - - return fulfilled; - } - - /** - * Gets a unique identifier for this logical requirement. - * Includes the logical operator and child requirement identifiers. - * - * @return A unique identifier string - */ - @Override - public String getUniqueIdentifier() { - String childIds = childRequirements.stream() - .map(Requirement::getUniqueIdentifier) - .collect(Collectors.joining(",")); - - return String.format("%s:LOGICAL:%s:[%s]", - requirementType.name(), - getClass().getSimpleName(), - childIds); - } - - /** - * Gets the best available requirement from this logical structure. - * For OR requirements, this returns the highest-rated fulfilled requirement. - * For AND requirements, this returns null if not all are fulfilled. - * - * @return The best available requirement, or null if none available - */ - public Optional getBestAvailableRequirement() { - if (!isLogicallyFulfilled()) { - return Optional.empty(); - } - - // Find the highest-rated fulfilled requirement - return childRequirements.stream() - .filter(req -> { - if (req instanceof LogicalRequirement) { - return ((LogicalRequirement) req).isLogicallyFulfilled(); - } else { - return req.isFulfilled(); - } - }) - .max(Requirement::compareTo); - } - - /** - * Gets all requirements that can fulfill this logical requirement. - * For OR requirements, returns all fulfilled requirements. - * For AND requirements, returns all requirements if all are fulfilled. - * - * @return List of requirements that can fulfill this logical requirement - */ - public List getAvailableRequirements() { - if (!isLogicallyFulfilled()) { - return new ArrayList<>(); - } - - List available = new ArrayList<>(); - - for (Requirement child : childRequirements) { - if (child instanceof LogicalRequirement) { - LogicalRequirement logical = (LogicalRequirement) child; - if (logical.isLogicallyFulfilled()) { - available.addAll(logical.getAvailableRequirements()); - } - } else { - if (child.isFulfilled()) { - available.add(child); - } - } - } - - return available; - } - - /** - * Gets a detailed status string for this logical requirement. - * - * @return Formatted status information - */ - public String getStatusInfo() { - StringBuilder sb = new StringBuilder(); - - sb.append(getName()).append(" (").append(getClass().getSimpleName()).append(")\n"); - sb.append(" Status: ").append(isLogicallyFulfilled() ? "Fulfilled" : "Not Fulfilled").append("\n"); - sb.append(" Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - sb.append(" Child Requirements: ").append(childRequirements.size()).append("\n"); - - for (int i = 0; i < childRequirements.size(); i++) { - Requirement child = childRequirements.get(i); - String prefix = (i == childRequirements.size() - 1) ? " └─ " : " ├─ "; - - if (child instanceof LogicalRequirement) { - sb.append(prefix).append(((LogicalRequirement) child).getStatusInfo().replace("\n", "\n ")); - } else { - sb.append(prefix).append(child.getName()) - .append(" [").append(child.isFulfilled() ? "FULFILLED" : "NOT FULFILLED").append("]\n"); - } - } - - return sb.toString(); - } - - /** - * Checks if this logical requirement contains only ItemRequirements (or LogicalRequirements that contain only ItemRequirements). - * This is useful for RequirementRegistry caching to identify logical requirements that can be processed - * alongside ConditionalRequirements with ItemRequirement steps. - * - * @return true if this logical requirement and all nested logical requirements contain only ItemRequirements - */ - public boolean containsOnlyItemRequirements() { - // Check if our allowed child type is ItemRequirement - if (!ItemRequirement.class.isAssignableFrom(allowedChildType) && allowedChildType != ItemRequirement.class) { - return false; - } - - // Recursively check all child requirements - for (Requirement child : childRequirements) { - if (child instanceof LogicalRequirement) { - if (!((LogicalRequirement) child).containsOnlyItemRequirements()) { - return false; - } - } else if (!(child instanceof ItemRequirement)) { - return false; - } - } - - return true; - } - - /** - * Gets all ItemRequirements from this logical structure, flattening any nested logical requirements. - * This is useful for RequirementRegistry to extract all item requirements for caching purposes. - * - * @return List of all ItemRequirements in this logical structure - */ - public List getAllItemRequirements() { - List itemRequirements = new ArrayList<>(); - - for (Requirement child : childRequirements) { - if (child instanceof LogicalRequirement) { - itemRequirements.addAll(((LogicalRequirement) child).getAllItemRequirements()); - } else if (child instanceof ItemRequirement) { - itemRequirements.add((ItemRequirement) child); - } - } - - return itemRequirements; - } - - @Override - public String toString() { - return String.format("%s[%s children, %s, type: %s]", - getClass().getSimpleName(), - childRequirements.size(), - isLogicallyFulfilled() ? "FULFILLED" : "NOT FULFILLED", - allowedChildType.getSimpleName()); - } - - - - /** - * Attempts to fulfill a single logical requirement. - * - * @param logicalReq The logical requirement to fulfill - * @param preferEquipment If true, prefer equipping items; if false, prefer inventory - * @return true if the logical requirement was fulfilled - */ - private boolean fulfillLogicalRequirement(CompletableFuture scheduledFuture) { - // If already fulfilled, nothing to do - - if (this instanceof OrRequirement) { - return fulfillOrRequirement((OrRequirement) this,scheduledFuture); - } else { - - log.warn("Unknown logical requirement type: {}", this.getClass().getSimpleName()); - return false; - } - } - - /** - * Fulfills an OR logical requirement (at least one child must be fulfilled). - * Sorts child requirements by rating (highest first) and attempts to fulfill - * only the highest-rated available requirement. - * - * @param orReq The OR requirement - * @param preferEquipment If true, prefer equipping items; if false, prefer inventory - * @return true if at least one child requirement was fulfilled - */ - private boolean fulfillOrRequirement(OrRequirement orReq, CompletableFuture scheduledFuture) { - // Sort child requirements by rating (highest first), then by priority - List sortedRequirements = orReq.getChildRequirements().stream() - .sorted((r1, r2) -> { - // First sort by rating (highest first) - int ratingCompare = Integer.compare(r2.getRating(), r1.getRating()); - if (ratingCompare != 0) { - return ratingCompare; - } - // Then by priority (mandatory first) - return r1.getPriority().compareTo(r2.getPriority()); - }) - .collect(Collectors.toList()); - boolean foundFulfilled = false; - // Try to fulfill all requirements in order of rating (highest first), but only need one to succeed - for (Requirement childReq : sortedRequirements) { - if(scheduledFuture.isCancelled() || scheduledFuture.isDone()) { - log.info("Scheduled future was cancelled or completed, stopping OR requirement fulfillment."); - return foundFulfilled; // Stop if future is cancelled or done - } - if (childReq instanceof LogicalRequirement) { - if (childReq.fulfillRequirement(scheduledFuture)) { - log.info("Fulfilled OR requirement using logical child: {} (rating: {})", - childReq.getDescription(), childReq.getRating()); - foundFulfilled = true; - } - } else if (childReq instanceof ItemRequirement) { - ItemRequirement itemReq = (ItemRequirement) childReq; - itemReq.fulfillRequirement(scheduledFuture); - - if (orReq.isFulfilled() || itemReq.isFulfilled()) { - log.info("Fulfilled OR requirement using item: {} (rating: {})", - itemReq.getName(), itemReq.getRating()); - foundFulfilled = true; - } - } else if (childReq instanceof ShopRequirement) { - ShopRequirement shopReq = (ShopRequirement) childReq; - try { - if (shopReq.fulfillRequirement(scheduledFuture)) { - log.info("Fulfilled shop OR requirement: {} (rating: {})", - shopReq.getName(), shopReq.getRating()); - foundFulfilled = true; - } - } catch (Exception e) { - log.info("Failed to fulfill shop requirement {}: {}", shopReq.getName(), e.getMessage()); - } - } else if (childReq instanceof LootRequirement) { - LootRequirement lootReq = (LootRequirement) childReq; - try { - if (lootReq.fulfillRequirement(scheduledFuture)) { - log.debug("Fulfilled loot OR requirement: {} (rating: {})", - lootReq.getName(), lootReq.getRating()); - foundFulfilled = true; - } - } catch (Exception e) { - log.debug("Failed to fulfill loot requirement {}: {}", lootReq.getName(), e.getMessage()); - } - } - } - - log.info("OR requirement {} fulfilled: {}", - orReq.getDescription(), foundFulfilled); - return foundFulfilled; // No child requirements were fulfilled - } - - // ========== STATIC UTILITY METHODS FOR LOGICAL REQUIREMENT PROCESSING ========== - - /** - * Filters logical requirements by schedule context. - * - * @param requirements The logical requirements to filter - * @param context The schedule context to match - * @return List of requirements matching the context - */ - public static List filterByContext(List requirements, TaskContext context) { - return requirements.stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(Collectors.toList()); - } - - /** - * Checks if any requirement within a logical requirement collection has mandatory items. - * - * @param logicalReqs The logical requirements to check - * @return true if any requirement contains mandatory items - */ - public static boolean hasMandatoryItems(List logicalReqs) { - return logicalReqs.stream() - .anyMatch(req -> extractItemRequirementsFromLogical(req).stream() - .anyMatch(ItemRequirement::isMandatory)); - } - - /** - * Checks if any requirement within a logical requirement collection has mandatory shop items. - * - * @param logicalReqs The logical requirements to check - * @return true if any requirement contains mandatory shop items - */ - public static boolean hasMandatoryShopItems(List logicalReqs) { - return logicalReqs.stream() - .anyMatch(req -> extractShopRequirementsFromLogical(req).stream() - .anyMatch(ShopRequirement::isMandatory)); - } - - /** - * Checks if any requirement within a logical requirement collection has mandatory loot items. - * - * @param logicalReqs The logical requirements to check - * @return true if any requirement contains mandatory loot items - */ - public static boolean hasMandatoryLootItems(List logicalReqs) { - return logicalReqs.stream() - .anyMatch(req -> extractLootRequirementsFromLogical(req).stream() - .anyMatch(LootRequirement::isMandatory)); - } - - /** - * Extracts all item requirements from a collection of logical requirements. - * - * @param logicalReqs The logical requirements to process - * @return List of all item requirements found within the logical structure - */ - public static List extractAllItemRequirements(List logicalReqs) { - return logicalReqs.stream() - .flatMap(req -> extractItemRequirementsFromLogical(req).stream()) - .collect(Collectors.toList()); - } - - /** - * Extracts all shop requirements from a collection of logical requirements. - * - * @param logicalReqs The logical requirements to process - * @return List of all shop requirements found within the logical structure - */ - public static List extractAllShopRequirements(List logicalReqs) { - return logicalReqs.stream() - .flatMap(req -> extractShopRequirementsFromLogical(req).stream()) - .collect(Collectors.toList()); - } - - /** - * Extracts all loot requirements from a collection of logical requirements. - * - * @param logicalReqs The logical requirements to process - * @return List of all loot requirements found within the logical structure - */ - public static List extractAllLootRequirements(List logicalReqs) { - return logicalReqs.stream() - .flatMap(req -> extractLootRequirementsFromLogical(req).stream()) - .collect(Collectors.toList()); - } - - /** - * Extracts only mandatory item requirements from a collection of logical requirements. - * - * @param logicalReqs The logical requirements to process - * @return List of mandatory item requirements found within the logical structure - */ - public static List extractMandatoryItemRequirements(List logicalReqs) { - return extractAllItemRequirements(logicalReqs).stream() - .filter(ItemRequirement::isMandatory) - .collect(Collectors.toList()); - } - - /** - * Processes a collection of logical requirements and fulfills them according to their logic. - * Provides common error handling and logging patterns. - * - * @param logicalReqs The logical requirements to fulfill - * @param requirementType Description of the requirement type for logging - * @return true if all mandatory requirements were fulfilled, false otherwise - */ - public static boolean fulfillLogicalRequirements(CompletableFuture scheduledFuture, List logicalReqs, String requirementType) { - if (logicalReqs.isEmpty()) { - log.debug("No {} requirements to fulfill", requirementType); - return true; - } - - boolean success = true; - int fulfilled = 0; - - for (int i = 0; i < logicalReqs.size(); i++) { - LogicalRequirement logicalReq = logicalReqs.get(i); - - try { - log.debug("Processing {} logical requirement {}/{}: {}", - requirementType, i + 1, logicalReqs.size(), logicalReq.getDescription()); - - if (logicalReq.isLogicallyFulfilled()) { - fulfilled++; - continue; - } - - boolean requirementFulfilled = logicalReq.fulfillRequirement(scheduledFuture); - - if (requirementFulfilled) { - fulfilled++; - } else { - // Check if any child requirement was mandatory - boolean hasMandatory = hasMandatoryItems(Arrays.asList(logicalReq)); - - if (hasMandatory) { - log.error("Failed to fulfill mandatory {} requirement: {}", - requirementType, logicalReq.getDescription()); - success = false; - break; // Stop on mandatory failure - } else { - log.debug("Failed to fulfill optional {} requirement: {}", - requirementType, logicalReq.getDescription()); - } - } - } catch (Exception e) { - log.error("Error fulfilling {} requirement {}: {}", - requirementType, logicalReq.getDescription(), e.getMessage()); - - boolean hasMandatory = hasMandatoryItems(Arrays.asList(logicalReq)); - - if (hasMandatory) { - success = false; - } - } - } - - log.debug("{} requirements fulfillment completed. Success: {}, Fulfilled: {}/{}", - requirementType, success, fulfilled, logicalReqs.size()); - return success; - } - - /** - * Recursively extracts all ItemRequirement instances from a logical requirement structure. - * This method handles nested logical requirements and returns a flat list of all item requirements. - * - * @param logicalReq The logical requirement to extract from - * @return List of all ItemRequirement instances found within the logical structure - */ - public static List extractItemRequirementsFromLogical(LogicalRequirement logicalReq) { - List items = new ArrayList<>(); - - for (Requirement child : logicalReq.getChildRequirements()) { - if (child instanceof ItemRequirement) { - items.add((ItemRequirement) child); - } else if (child instanceof LogicalRequirement) { - items.addAll(extractItemRequirementsFromLogical((LogicalRequirement) child)); - } - } - - return items; - } - - /** - * Recursively extracts all ShopRequirement instances from a logical requirement structure. - * This method handles nested logical requirements and returns a flat list of all shop requirements. - * - * @param logicalReq The logical requirement to extract from - * @return List of all ShopRequirement instances found within the logical structure - */ - public static List extractShopRequirementsFromLogical(LogicalRequirement logicalReq) { - List shops = new ArrayList<>(); - - for (Requirement child : logicalReq.getChildRequirements()) { - if (child instanceof ShopRequirement) { - shops.add((ShopRequirement) child); - } else if (child instanceof LogicalRequirement) { - shops.addAll(extractShopRequirementsFromLogical((LogicalRequirement) child)); - } - } - - return shops; - } - - /** - * Recursively extracts all LootRequirement instances from a logical requirement structure. - * This method handles nested logical requirements and returns a flat list of all loot requirements. - * - * @param logicalReq The logical requirement to extract from - * @return List of all LootRequirement instances found within the logical structure - */ - public static List extractLootRequirementsFromLogical(LogicalRequirement logicalReq) { - List loots = new ArrayList<>(); - - for (Requirement child : logicalReq.getChildRequirements()) { - if (child instanceof LootRequirement) { - loots.add((LootRequirement) child); - } else if (child instanceof LogicalRequirement) { - loots.addAll(extractLootRequirementsFromLogical((LogicalRequirement) child)); - } - } - - return loots; - } - - - - /** - * Breaks down all ItemRequirements in a logical requirement tree by the slot(s) they occupy. - * Equipment items are grouped by EquipmentInventorySlot name. - * Inventory items are grouped by inventory slot ("inventory:X") or "inventory:any" for -1. - * EITHER items are grouped in both equipment and inventory as appropriate. - * - * @param logicalReq The logical requirement to analyze - * @return Map of slot key to list of ItemRequirements occupying that slot - */ - public static java.util.Map> breakdownItemRequirementsBySlot(LogicalRequirement logicalReq) { - java.util.List allItems = extractItemRequirementsFromLogical(logicalReq); - java.util.Map> slotMap = new java.util.HashMap<>(); - for (ItemRequirement item : allItems) { - boolean slotted = false; - if (item.getEquipmentSlot() != null) { - String key = "equipment:" + item.getEquipmentSlot().name(); - slotMap.computeIfAbsent(key, k -> new java.util.ArrayList<>()).add(item); - slotted = true; - } - if (item.getInventorySlot() != null) { - if (item.getInventorySlot() >= 0) { - String key = "inventory:" + item.getInventorySlot(); - slotMap.computeIfAbsent(key, k -> new java.util.ArrayList<>()).add(item); - slotted = true; - } else if (item.getInventorySlot() == -1) { - String key = "inventory:any"; - slotMap.computeIfAbsent(key, k -> new java.util.ArrayList<>()).add(item); - slotted = true; - } - } - // If neither slot is set, group under "unslotted" - if (!slotted) { - slotMap.computeIfAbsent("unslotted", k -> new java.util.ArrayList<>()).add(item); - } - } - return slotMap; - } - - /** - * Pretty-prints the slot breakdown for all ItemRequirements in a logical requirement. - * - * @param logicalReq The logical requirement to analyze - * @return A formatted string showing the breakdown per slot - */ - public static String itemSlotBreakdown(LogicalRequirement logicalReq) { - java.util.Map> slotMap = breakdownItemRequirementsBySlot(logicalReq); - StringBuilder sb = new StringBuilder(); - sb.append("Slot Breakdown for LogicalRequirement: ").append(logicalReq.getName()).append("\n"); - for (String slot : slotMap.keySet()) { - sb.append(" [").append(slot).append("] ").append(slotMap.get(slot).size()).append(" item(s):\n"); - for (ItemRequirement item : slotMap.get(slot)) { - sb.append(" - ").append(item.getName()) - .append(" (id:").append(item.getId()).append(", amt:").append(item.getAmount()).append(")\n"); - } - } - return sb.toString(); - } - // private String formatLogicalRequirement(LogicalRequirement logicalReq) { -// if (logicalReq instanceof OrRequirement) { -// OrRequirement orReq = (OrRequirement) logicalReq; -// StringBuilder sb = new StringBuilder(); -// sb.append("OR(").append(orReq.getChildRequirements().size()).append(" options): "); -// boolean first = true; -// for (Requirement childReq : orReq.getChildRequirements()) { -// if (!first) sb.append(" | "); -// sb.append(childReq.getName()).append("[").append(childReq.getPriority().name()).append("]"); -// first = false; -// } -// return sb.toString(); -// } else { -// return String.format("%s [%s, Rating: %d, Context: %s]", -// logicalReq.getName(), -// logicalReq.getPriority().name(), -// logicalReq.getRating(), -// logicalReq.getTaskContext().name()); -// } -// } -// } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/logical/OrRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/logical/OrRequirement.java deleted file mode 100644 index 6809a27f3a0..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/logical/OrRequirement.java +++ /dev/null @@ -1,543 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical; - -import lombok.EqualsAndHashCode; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.api.Skill; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement; -import net.runelite.client.plugins.microbot.Microbot; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** - * OR logical requirement - at least one child requirement must be fulfilled. - * This is useful for situations where multiple alternatives exist but only one is needed. - * - * All child requirements must be of the same type to ensure type safety and enable - * efficient caching. Mixed requirement types should use ConditionalRequirement instead. - * - * Examples: - * - Food items: Any one type of food is sufficient - * - Equipment slots: Any one item for the slot is sufficient - * - Transportation methods: Any one method to reach a location - * - * Similar to OrCondition but adapted for the requirement system. - */ -@EqualsAndHashCode(callSuper = true) -@Slf4j -public class OrRequirement extends LogicalRequirement { - - /** - * Creates an OR requirement with explicit child type specification. - * - * @param priority Priority level of this logical requirement - * @param rating Effectiveness rating (1-10) - * @param description Human-readable description - * @param TaskContext When this requirement should be fulfilled - * @param allowedChildType The class type that child requirements must be - * @param requirements Child requirements - any one of these can fulfill the OR requirement - */ - public OrRequirement(RequirementPriority priority, int rating, String description, - TaskContext taskContext, Class allowedChildType, - Requirement... requirements) { - super(RequirementType.OR_LOGICAL, priority, rating, description, taskContext, allowedChildType, requirements); - } - public OrRequirement(RequirementPriority priority, int rating, String description, - TaskContext taskContext, Class allowedChildType) { - super(RequirementType.OR_LOGICAL, priority, rating, description, taskContext, allowedChildType); - } - - /** - * Creates an OR requirement with the specified parameters, inferring child type from first requirement. - * - * @param priority Priority level of this logical requirement - * @param rating Effectiveness rating (1-10) - * @param description Human-readable description - * @param TaskContext When this requirement should be fulfilled - * @param requirements Child requirements - any one of these can fulfill the OR requirement - */ - public OrRequirement(RequirementPriority priority, int rating, String description, - TaskContext taskContext, Requirement... requirements) { - super(RequirementType.OR_LOGICAL, priority, rating, description, taskContext, - inferChildTypeFromRequirements(requirements), requirements); - } - - /** - * Helper method to infer the child type from the provided requirements. - * If all requirements are ItemRequirements, returns ItemRequirement.class. - * Otherwise returns the most common compatible type. - */ - private static Class inferChildTypeFromRequirements(Requirement... requirements) { - if (requirements.length == 0) { - return Requirement.class; // Default fallback - } - - // Check if all requirements are ItemRequirements - boolean allItemRequirements = true; - for (Requirement req : requirements) { - if (!(req instanceof ItemRequirement)) { - allItemRequirements = false; - break; - } - } - - if (allItemRequirements) { - return ItemRequirement.class; - } - - // Fallback to first requirement's class - Requirement firstReq = requirements[0]; - if (firstReq instanceof LogicalRequirement) { - return ((LogicalRequirement) firstReq).getAllowedChildType(); - } else { - return firstReq.getClass(); - } - } - - /** - * Convenience constructor with default rating of 5, inferring child type from first requirement. - * - * @param priority Priority level of this logical requirement - * @param description Human-readable description - * @param TaskContext When this requirement should be fulfilled - * @param requirements Child requirements - any one of these can fulfill the OR requirement - */ - public OrRequirement(RequirementPriority priority, String description, TaskContext taskContext, - Requirement... requirements) { - this(priority, 5, description, taskContext, requirements); - } - - /** - * Checks if this OR requirement is logically fulfilled. - * Returns true if at least one child requirement is fulfilled. - * - * @return true if any child requirement is fulfilled, false if none are fulfilled - */ - @Override - public boolean isLogicallyFulfilled() { - if (childRequirements.isEmpty()) { - return true; // Empty OR is considered satisfied - } - return childRequirements.stream().anyMatch(req -> { - if (req instanceof LogicalRequirement) { - return ((LogicalRequirement) req).isLogicallyFulfilled(); - } else { - return req.isFulfilled(); - } - }); - } - public boolean isFulfilled() { - // Check if any child requirement is fulfilled - return isLogicallyFulfilled(); - } - - /** - * Gets requirements that are blocking fulfillment of this OR requirement. - * For an OR requirement, all child requirements are blocking if none are fulfilled. - * If at least one is fulfilled, nothing is blocking. - * - * @return List of all child requirements if none are fulfilled, otherwise empty list - */ - @Override - public List getBlockingRequirements() { - // For an OR requirement, if any requirement is fulfilled, nothing is blocking - if (isLogicallyFulfilled()) { - return new ArrayList<>(); - } - - // If we reach here, none are fulfilled, so all requirements are blocking - List blocking = new ArrayList<>(); - - for (Requirement child : childRequirements) { - if (child instanceof LogicalRequirement) { - // Add the logical requirement itself as blocking, not its children - blocking.add(child); - } else { - blocking.add(child); - } - } - - return blocking; - } - - /** - * Gets the name of this OR requirement. - * - * @return A descriptive name for this OR requirement - */ - @Override - public String getName() { - if (childRequirements.isEmpty()) { - return "Empty OR Requirement"; - } - - if (childRequirements.size() == 1) { - return childRequirements.get(0).getName(); - } - - return String.format("OR(%s alternatives)", childRequirements.size()); - } - - /** - * Gets the best fulfilled requirement from this OR requirement. - * Returns the highest-rated fulfilled requirement, or empty if none are fulfilled. - * - * @return The best fulfilled requirement, or empty optional - */ - public java.util.Optional getBestFulfilledRequirement() { - return childRequirements.stream() - .filter(req -> { - if (req instanceof LogicalRequirement) { - return ((LogicalRequirement) req).isLogicallyFulfilled(); - } else { - return req.isFulfilled(); - } - }) - .max(Requirement::compareTo); - } - - /** - * Gets all fulfilled requirements from this OR requirement. - * Useful when multiple alternatives are fulfilled and you want to see all options. - * - * @return List of all fulfilled requirements - */ - public List getAllFulfilledRequirements() { - return childRequirements.stream() - .filter(req -> { - if (req instanceof LogicalRequirement) { - return ((LogicalRequirement) req).isLogicallyFulfilled(); - } else { - return req.isFulfilled(); - } - }) - .collect(java.util.stream.Collectors.toList()); - } - - /** - * Returns a detailed description of the OR requirement with status information. - * - * @return Formatted description with child requirement status - */ - public String getDetailedDescription() { - StringBuilder sb = new StringBuilder(); - - // Basic description - sb.append("OR Requirement: Any requirement can be fulfilled\n"); - - // Status information - boolean fulfilled = isLogicallyFulfilled(); - sb.append("Status: ").append(fulfilled ? "Fulfilled" : "Not fulfilled").append("\n"); - sb.append("Child Requirements: ").append(childRequirements.size()).append("\n"); - - // Progress information - double progress = getProgressPercentage(); - sb.append(String.format("Overall Progress: %.1f%%\n", progress)); - - // Count fulfilled requirements - int fulfilledCount = getAllFulfilledRequirements().size(); - sb.append("Fulfilled Requirements: ").append(fulfilledCount).append("/").append(childRequirements.size()).append("\n\n"); - - // List all child requirements - sb.append("Child Requirements:\n"); - for (int i = 0; i < childRequirements.size(); i++) { - Requirement requirement = childRequirements.get(i); - boolean childFulfilled = requirement instanceof LogicalRequirement ? - ((LogicalRequirement) requirement).isLogicallyFulfilled() : - requirement.isFulfilled(); - - sb.append(String.format("%d. %s [%s]\n", - i + 1, - requirement.getName(), - childFulfilled ? "FULFILLED" : "NOT FULFILLED")); - } - - return sb.toString(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - // Basic information - sb.append("OrRequirement:\n"); - sb.append(" ┌─ Configuration ─────────────────────────────\n"); - sb.append(" │ Type: OR (Any requirement can be fulfilled)\n"); - sb.append(" │ Child Requirements: ").append(childRequirements.size()).append("\n"); - sb.append(" │ Priority: ").append(priority.name()).append("\n"); - sb.append(" │ Rating: ").append(rating).append("/10\n"); - - // Status information - sb.append(" ├─ Status ──────────────────────────────────\n"); - boolean anyFulfilled = isLogicallyFulfilled(); - sb.append(" │ Fulfilled: ").append(anyFulfilled).append("\n"); - - // Count fulfilled requirements - int fulfilledCount = getAllFulfilledRequirements().size(); - sb.append(" │ Fulfilled Requirements: ").append(fulfilledCount).append("/").append(childRequirements.size()).append("\n"); - sb.append(" │ Progress: ").append(String.format("%.1f%%", getProgressPercentage())).append("\n"); - - // Child requirements - if (!childRequirements.isEmpty()) { - sb.append(" ├─ Child Requirements ────────────────────────\n"); - - for (int i = 0; i < childRequirements.size(); i++) { - Requirement requirement = childRequirements.get(i); - String prefix = (i == childRequirements.size() - 1) ? " └─ " : " ├─ "; - - boolean childFulfilled = requirement instanceof LogicalRequirement ? - ((LogicalRequirement) requirement).isLogicallyFulfilled() : - requirement.isFulfilled(); - - sb.append(prefix).append(String.format("Requirement %d: %s [%s]\n", - i + 1, - requirement.getClass().getSimpleName(), - childFulfilled ? "FULFILLED" : "NOT FULFILLED")); - } - } else { - sb.append(" └─ No Child Requirements ───────────────────────\n"); - } - - return sb.toString(); - } - - - /** - * Merges this OrRequirement with another OrRequirement. - * Combines all child requirements, merges ids, and sets rating to highest of both. - * - * @param other The OrRequirement to merge with - * @return A new merged OrRequirement - */ - public static OrRequirement merge(OrRequirement first, OrRequirement other) { - if (!(other instanceof OrRequirement)) { - throw new IllegalArgumentException("Can only merge with another OrRequirement"); - } - OrRequirement otherOr = (OrRequirement) other; - // Combine all children - List mergedChildren = new ArrayList<>(first.childRequirements); - for (Requirement req : otherOr.childRequirements) { - if (!mergedChildren.contains(req)) { - mergedChildren.add(req); - } - } - // Merge ids - List mergedIds = new ArrayList<>(); - if (first.getIds() != null) mergedIds.addAll(first.getIds()); - if (otherOr.ids != null) { - for (Integer id : otherOr.ids) { - if (!mergedIds.contains(id)) mergedIds.add(id); - } - } - // Find max rating - int maxRating = first.getRating(); - for (Requirement req : mergedChildren) { - if (req.getRating() > maxRating) { - maxRating = req.getRating(); - } - } - // Use the first non-empty description, or combine - String desc = (first.getDescription() != null && !first.getDescription().isEmpty()) ? first.getDescription() : otherOr.description; - if (desc == null || desc.isEmpty()) desc = "Merged OR Requirement"; - // Use the higher priority - RequirementPriority mergedPriority = first.getPriority().ordinal() < otherOr.priority.ordinal() ? first.getPriority() : otherOr.priority; - // Use the most general allowedChildType - Class mergedType = first.allowedChildType.isAssignableFrom(otherOr.allowedChildType) ? first.allowedChildType : otherOr.allowedChildType; - // Create merged OrRequirement - OrRequirement merged = new OrRequirement(mergedPriority, maxRating, desc, first.getTaskContext(), mergedType); - merged.ids = mergedIds; - for (Requirement req : mergedChildren) { - merged.addRequirement(req); - } - - return merged; - } - - - // ============================== - // Static Convenience Functions - // ============================== - - // Pattern to detect charged items with numbers in parentheses, e.g., "Amulet of glory(6)" - private static final Pattern CHARGED_ITEM_PATTERN = Pattern.compile(".*\\((\\d+)\\)$"); - - /** - * Creates an OR requirement from a list of item IDs with automatic rating assignment for charged items. - * This convenience function automatically detects charged items in the list and assigns ratings based on charge levels. - * - * @param itemIds List of item IDs to create OR group from - * @param amount Amount of each item required (default: 1) - * @param equipmentSlot Equipment slot for equipment requirements (null for inventory) - * @param inventorySlot Inventory slot (-1 for any slot) - * @param priority Priority level of the OR requirement - * @param baseRating Base rating for non-charged items (1-10) - * @param description Description of the OR requirement - * @param TaskContext When this requirement should be fulfilled - * @param skillToUse Skill required to use items (optional) - * @param minimumLevelToUse Minimum level required to use items (optional) - * @param skillToEquip Skill required to equip items (optional) - * @param minimumLevelToEquip Minimum level required to equip items (optional) - * @param preferLowerCharge If true, lower charge items get higher ratings; if false, higher charge items get higher ratings - * @param chargedItemsOnly If true, only include charged items from the list; if false, include all items - * @return OrRequirement with ItemRequirement children, rated appropriately - */ - public static OrRequirement fromItemIds(List itemIds, int amount, EquipmentInventorySlot equipmentSlot, - int inventorySlot, RequirementPriority priority, int baseRating, - String description, TaskContext taskContext, - Skill skillToUse, Integer minimumLevelToUse, - Skill skillToEquip, Integer minimumLevelToEquip, - boolean preferLowerCharge, boolean chargedItemsOnly) { - - List itemRequirements = new ArrayList<>(); - - for (Integer itemId : itemIds) { - String itemName = getItemNameById(itemId); - if (itemName == null) { - continue; // Skip items with unknown names - } - - boolean isCharged = isChargedItem(itemName); - - // Skip non-charged items if chargedItemsOnly is true - if (chargedItemsOnly && !isCharged) { - continue; - } - - int rating = baseRating; - - // Adjust rating for charged items - if (isCharged) { - int chargeLevel = getChargeLevel(itemName); - if (chargeLevel != Integer.MAX_VALUE) { - // Assign rating based on charge level - // For preferLowerCharge=true: lower charge = higher rating - // For preferLowerCharge=false: higher charge = higher rating - if (preferLowerCharge) { - // Lower charge gets higher rating (inverse relationship) - // Charge 1 = rating 10, Charge 10 = rating 1 - rating = Math.max(1, Math.min(10, 11 - chargeLevel)); - } else { - // Higher charge gets higher rating (direct relationship) - // Charge 1 = rating 1, Charge 10 = rating 10 - rating = Math.max(1, Math.min(10, chargeLevel)); - } - } - } - - // Create individual ItemRequirement for this item - ItemRequirement itemReq = new ItemRequirement( - itemId, amount, equipmentSlot, inventorySlot, - priority, rating, itemName, taskContext, - skillToUse, minimumLevelToUse, skillToEquip, minimumLevelToEquip, preferLowerCharge - ); - - itemRequirements.add(itemReq); - } - - // Convert to array for constructor - Requirement[] reqArray = itemRequirements.toArray(new Requirement[0]); - - return new OrRequirement(priority, baseRating, description, taskContext, reqArray); - } - - /** - * Simplified convenience method for creating OR requirement from item IDs with default parameters. - * Uses preferLowerCharge=false and chargedItemsOnly=false by default. - * - * @param itemIds List of item IDs to create OR group from - * @param amount Amount required of each item - * @param equipmentSlot Equipment slot for equipment requirements (null for inventory) - * @param inventorySlot Inventory slot (-1 for any slot) - * @param priority Priority level of the OR requirement - * @param baseRating Base rating for items (1-10) - * @param description Description of the OR requirement - * @param TaskContext When this requirement should be fulfilled - * @param preferLowerCharge Whether to prefer lower charge variants - * @return OrRequirement with ItemRequirement children - */ - public static OrRequirement fromItemIds(List itemIds, int amount, EquipmentInventorySlot equipmentSlot, - int inventorySlot, RequirementPriority priority, int baseRating, - String description, TaskContext taskContext, boolean preferLowerCharge) { - return fromItemIds(itemIds, amount, equipmentSlot, inventorySlot, priority, baseRating, - description, taskContext, null, null, null, null, preferLowerCharge, false); - } - - /** - * Convenience method for creating OR requirement from varargs item IDs. - * - * @param amount Amount of each item required - * @param equipmentSlot Equipment slot for equipment requirements (null for inventory) - * @param inventorySlot Inventory slot (-1 for any slot) - * @param priority Priority level of the OR requirement - * @param baseRating Base rating for items (1-10) - * @param description Description of the OR requirement - * @param TaskContext When this requirement should be fulfilled - * @param preferLowerCharge Whether to prefer lower charge variants - * @param itemIds Varargs list of item IDs - * @return OrRequirement with ItemRequirement children - */ - public static OrRequirement fromItemIds(int amount, EquipmentInventorySlot equipmentSlot, int inventorySlot, - RequirementPriority priority, int baseRating, String description, - TaskContext taskContext, boolean preferLowerCharge, Integer... itemIds) { - return fromItemIds(Arrays.asList(itemIds), amount, equipmentSlot, inventorySlot, priority, baseRating, - description, taskContext, preferLowerCharge); - } - - /** - * Utility method to get item name by ID. - * - * @param itemId The item ID - * @return Item name or null if not found - */ - private static String getItemNameById(int itemId) { - try { - return Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getItemManager().getItemComposition(itemId).getName() - ).orElse(""); - } catch (Exception e) { - return null; - } - } - - /** - * Checks if an item name represents a charged item. - * - * @param itemName The item name to check - * @return true if the item appears to be charged, false otherwise - */ - private static boolean isChargedItem(String itemName) { - return itemName != null && CHARGED_ITEM_PATTERN.matcher(itemName).matches(); - } - - /** - * Gets the charge level from an item name. - * - * @param itemName The item name to parse - * @return The charge level, or Integer.MAX_VALUE if not a charged item - */ - private static int getChargeLevel(String itemName) { - if (itemName == null) { - return Integer.MAX_VALUE; - } - - Matcher matcher = CHARGED_ITEM_PATTERN.matcher(itemName); - if (matcher.matches()) { - try { - return Integer.parseInt(matcher.group(1)); - } catch (NumberFormatException e) { - return Integer.MAX_VALUE; - } - } - - return Integer.MAX_VALUE; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/ShopItemRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/ShopItemRequirement.java deleted file mode 100644 index c882ce83411..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/ShopItemRequirement.java +++ /dev/null @@ -1,217 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.models.ShopOperation; -import net.runelite.client.plugins.microbot.util.grandexchange.models.TimeSeriesInterval; -import net.runelite.client.plugins.microbot.util.shop.models.Rs2ShopItem; -import net.runelite.client.plugins.microbot.util.shop.models.Rs2ShopType; - -/** - * Represents an individual item requirement within a shop operation. - * Uses unified stock management system based on operation type. - * Enhanced with Grand Exchange time-series pricing configuration. - * - * Key improvements: - * - Extracted from inner class for better reusability - * - Enhanced stock validation logic - * - Better quantity tracking and completion status - * - Time-series pricing configuration for Grand Exchange operations - */ -@Getter -@EqualsAndHashCode() -@Slf4j -public class ShopItemRequirement { - private final Rs2ShopItem shopItem; - private final int amount; - private final int stockTolerance; - private int completedAmount = 0; - - // Grand Exchange time-series configuration - private final TimeSeriesInterval timeSeriesInterval; - private final boolean useTimeSeriesAveraging; - - /** - * Creates a shop item requirement with unified stock management. - * Uses the Rs2ShopItem's baseStock for stock calculations. - * - * @param shopItem The shop item to buy/sell (contains baseStock information) - * @param amount Total amount needed - * @param stockTolerance Acceptable deviation from shopItem's baseStock - * @param timeSeriesInterval Time-series interval for Grand Exchange price averaging - * @param useTimeSeriesAveraging Whether to use time-series averaging for Grand Exchange - * - * For BUY operations: We buy when shop has (baseStock - stockTolerance) or more - * For SELL operations: We sell when shop has (baseStock + stockTolerance) or less - */ - public ShopItemRequirement(Rs2ShopItem shopItem, int amount, int stockTolerance, - TimeSeriesInterval timeSeriesInterval, boolean useTimeSeriesAveraging) { - if (shopItem == null) { - throw new IllegalArgumentException("ShopItem cannot be null"); - } - if (amount <= 0) { - throw new IllegalArgumentException("Amount must be positive, got: " + amount); - } - if (stockTolerance < 0) { - throw new IllegalArgumentException("Stock tolerance cannot be negative, got: " + stockTolerance); - } - - this.shopItem = shopItem; - this.amount = amount; - this.stockTolerance = stockTolerance; - this.timeSeriesInterval = timeSeriesInterval != null ? timeSeriesInterval : TimeSeriesInterval.ONE_HOUR; - this.useTimeSeriesAveraging = useTimeSeriesAveraging; - } - - /** - * Creates a shop item requirement with default time-series configuration. - * Uses 1-hour averaging for Grand Exchange operations by default. - */ - public ShopItemRequirement(Rs2ShopItem shopItem, int amount, int stockTolerance) { - this(shopItem, amount, stockTolerance, TimeSeriesInterval.ONE_HOUR, true); - } - - - - /** - * For BUY operations: Checks if shop has enough stock to allow buying - * For SELL operations: Checks if shop has low enough stock to allow selling - */ - public boolean canProcessInShop(int currentShopStock, ShopOperation operation) { - if (operation == ShopOperation.BUY) { - return currentShopStock >= getMinimumStockForBuying(); - } else { // SELL - return currentShopStock <= getMaximumStockForSelling(); - } - } - - /** - * For BUY operations: Minimum stock required in shop to allow buying - */ - public int getMinimumStockForBuying() { - return Math.max(0, shopItem.getBaseStock() - stockTolerance); - } - - /** - * For SELL operations: Maximum stock allowed in shop to allow selling - */ - public int getMaximumStockForSelling() { - return shopItem.getBaseStock() + stockTolerance; - } - public int allowedToBuy(int currentStock){ - return Math.max(0, currentStock - getMinimumStockForBuying() + 1); - } - public int allowedToSell(int currentStock){ - return Math.max(0, getMaximumStockForSelling() - currentStock ); - } - - - - /** - * Calculates how many items we can safely buy/sell in current shop stock situation - */ - public int getQuantityForCurrentVisit(int currentShopStock, ShopOperation operation) { - int remaining = getRemainingAmount(); - if (operation == ShopOperation.BUY) { - // Can't buy more than available stock - int availableStockForBuying =allowedToBuy(currentShopStock); - log.info(" Remaing {} to buy -- Available stock for buying {}: {}",remaining, shopItem.getItemName(), availableStockForBuying); - - return Math.min(remaining, availableStockForBuying); - } else { // SELL - // Can't sell more than shop can accept - int shopCapacityForSelling =allowedToSell(currentShopStock); - return Math.min( remaining, shopCapacityForSelling); - } - } - - /** - * Legacy compatibility - returns baseStock from shopItem - */ - @Deprecated - public int getMinimumStock() { - return shopItem.getBaseStock(); - } - - /** - * Gets the base stock from the shop item - */ - public int getBaseStock() { - return shopItem.getBaseStock(); - } - - public boolean isCompleted() { - return completedAmount >= amount; - } - - public int getRemainingAmount() { - return Math.max(0, amount - completedAmount); - } - - public void addCompletedAmount(int additionalAmount) { - if (additionalAmount < 0) { - throw new IllegalArgumentException("Additional amount cannot be negative: " + additionalAmount); - } - this.completedAmount = Math.min(this.amount, this.completedAmount + additionalAmount); - } - - public void setCompletedAmount(int newAmount) { - if (newAmount < 0) { - throw new IllegalArgumentException("Completed amount cannot be negative: " + newAmount); - } - this.completedAmount = Math.min(this.amount, newAmount); - } - - public String getItemName() { - return shopItem.getItemName(); - } - - public int getItemId() { - return shopItem.getItemId(); - } - - /** - * Progress percentage (0.0 to 1.0) - */ - public double getProgress() { - return amount > 0 ? (double) completedAmount / amount : 1.0; - } - - /** - * Human readable progress string - */ - public String getProgressString() { - return String.format("%d/%d (%.1f%%)", completedAmount, amount, getProgress() * 100); - } - - /** - * Determines if this item requirement should use time-series pricing. - * Only applies to Grand Exchange operations when enabled. - */ - public boolean shouldUseTimeSeriesPricing() { - return useTimeSeriesAveraging && isGrandExchangeItem(); - } - - /** - * Checks if this item is for Grand Exchange operations. - * This can be determined from the shop item's context or type. - */ - private boolean isGrandExchangeItem() { - return shopItem.getShopType()==Rs2ShopType.GRAND_EXCHANGE; // Default to true for now - can be refined based on shop context - } - - /** - * Gets the recommended time-series interval for pricing this item. - * Returns the configured interval, defaulting to 1-hour if not set. - */ - public TimeSeriesInterval getRecommendedTimeSeriesInterval() { - return timeSeriesInterval; - } - - @Override - public String toString() { - return String.format("ShopItemRequirement{item='%s', amount=%d, completed=%d, stockTolerance=%d, baseStock=%d}", - getItemName(), amount, completedAmount, stockTolerance, getBaseStock()); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/ShopRequirement.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/ShopRequirement.java deleted file mode 100644 index ec6964a598f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/ShopRequirement.java +++ /dev/null @@ -1,2572 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop; -import static net.runelite.client.plugins.microbot.util.Global.sleep; -import static net.runelite.client.plugins.microbot.util.Global.sleepGaussian; -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import org.slf4j.event.Level; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Constants; -import net.runelite.api.GameState; -import net.runelite.api.GrandExchangeOffer; -import net.runelite.api.GrandExchangeOfferState; -import net.runelite.api.NPC; -import net.runelite.api.coords.WorldArea; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.gameval.ItemID; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -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.util.world.Rs2WorldUtil; -import net.runelite.client.plugins.microbot.util.world.WorldHoppingConfig; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.models.CancelledOfferState; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.models.MultiItemConfig; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.models.ShopOperation; -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.util.grandexchange.GrandExchangeSlots; -import net.runelite.client.plugins.microbot.util.grandexchange.Rs2GrandExchange; -import net.runelite.client.plugins.microbot.util.grandexchange.models.GrandExchangeOfferDetails; -import net.runelite.client.plugins.microbot.util.grandexchange.models.TimeSeriesAnalysis; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; -import net.runelite.client.plugins.microbot.util.npc.Rs2Npc; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import net.runelite.client.plugins.microbot.util.security.LoginManager; -import net.runelite.client.plugins.microbot.util.shop.Rs2Shop; -import net.runelite.client.plugins.microbot.util.shop.models.Rs2ShopItem; -import net.runelite.client.plugins.microbot.util.shop.models.Rs2ShopType; -import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; - -/** - * Represents a requirement to buy or sell multiple items from/to the same shop. - * This extends Requirement directly with additional properties specific to shop operations. - * Enhanced with BanksShopper patterns for world hopping, stock tracking, and quantity management. - * - * Supports batch operations for multiple items at the same shop location to optimize efficiency. - */ -@Getter -@EqualsAndHashCode(callSuper = true) -@Slf4j -public class ShopRequirement extends Requirement { - - // Pattern to detect charged items with numbers in parentheses, e.g., "Amulet of glory(6)" - protected static final Pattern CHARGED_ITEM_PATTERN = Pattern.compile(".*\\((\\d+)\\)$"); - - // Track cancelled offers for recovery - private final List cancelledOffers = new ArrayList<>(); - - /** - * Map of shop items to their individual requirements and settings. - * All items must be from the same shop (same location and NPC). - */ - private final Map shopItemRequirements; - - /** - * The primary shop information (location, NPC, type) - derived from the first shop item. - * All shop items must match this shop's location and NPC. - */ - private final Rs2ShopItem primaryShopItem; - - /** - * The shop operation type - either BUY or SELL. - * All items in this requirement must use the same operation. - */ - private final ShopOperation operation; - - /** - * Whether to handle noted items when selling (unnote them if needed). - */ - @Setter - private boolean handleNotedItems = true; - - /** - * Whether to use next world progression or random world selection. - */ - @Setter - private boolean useNextWorld = false; - - /** - * Whether to enable automatic world hopping when stock is low. - */ - @Setter - private boolean enableWorldHopping = true; - - /** - * Whether to bank purchased items automatically. - */ - @Setter - private boolean enableBanking = true; - - @Setter - private int timeout = 120000; // Default timeout for shop operations - - /** - * Configuration for world hopping behavior with exponential backoff and retry limits. - */ - @Setter - private WorldHoppingConfig worldHoppingConfig = WorldHoppingConfig.createDefault(); - - /** - * Set of world IDs that have been tried and failed during this requirement session. - * Used to avoid repeatedly attempting the same problematic worlds. - */ - private final Set excludedWorlds = new HashSet<>(); - - - public String getName() { - if (shopItemRequirements.isEmpty()) { - return "No Shop Items"; - } - if (shopItemRequirements.size() == 1) { - return shopItemRequirements.values().iterator().next().getItemName(); - } - return shopItemRequirements.size() + " items from " + primaryShopItem.getShopNpcName(); - } - - /** - * Returns a multi-line display string with detailed shop requirement information. - * Uses StringBuilder with tabs for proper formatting. - * - * @return A formatted string containing shop requirement details - */ - public String displayString() { - StringBuilder sb = new StringBuilder(); - sb.append("=== Enhanced Multi-Item Shop Requirement Details ===\n"); - sb.append("Name:\t\t\t").append(getName()).append("\n"); - sb.append("Type:\t\t\t").append(getRequirementType().name()).append("\n"); - sb.append("Priority:\t\t").append(getPriority().name()).append("\n"); - sb.append("Rating:\t\t\t").append(getRating()).append("/10\n"); - sb.append("Operation:\t\t").append(operation.name()).append("\n"); - sb.append("Total Items:\t\t").append(shopItemRequirements.size()).append("\n"); - sb.append("Item IDs:\t\t").append(getIds().toString()).append("\n"); - sb.append("Description:\t\t").append(getDescription() != null ? getDescription() : "No description").append("\n"); - - // Enhanced shopping configuration - sb.append("\n--- Shopping Configuration ---\n"); - sb.append("World Hopping:\t\t").append(enableWorldHopping ? "Enabled" : "Disabled").append("\n"); - sb.append("Use Next World:\t\t").append(useNextWorld ? "Yes" : "Random").append("\n"); - sb.append("Auto Banking:\t\t").append(enableBanking ? "Enabled" : "Disabled").append("\n"); - sb.append("Handle Noted:\t\t").append(handleNotedItems ? "Enabled" : "Disabled").append("\n"); - - // Individual item details - sb.append("\n--- Individual Item Requirements ---\n"); - for (ShopItemRequirement itemReq : shopItemRequirements.values()) { - sb.append("Item: ").append(itemReq.getItemName()).append("\n"); - sb.append(" Amount: ").append(itemReq.getAmount()).append(" (completed: ").append(itemReq.getCompletedAmount()).append(")\n"); - sb.append(" Base Stock: ").append(itemReq.getBaseStock()).append(" items\n"); - sb.append(" Stock Tolerance: ").append(itemReq.getStockTolerance()).append(" items\n"); - sb.append(" max sell stock: ").append(itemReq.getMaximumStockForSelling()).append(" items\n"); - sb.append(" min buy stock: ").append(itemReq.getMinimumStockForBuying()).append(" items\n"); - sb.append(" Status: ").append(itemReq.isCompleted() ? "COMPLETED" : "PENDING").append("\n"); - } - - if (primaryShopItem != null) { - sb.append("\n--- Shop Source Information ---\n"); - sb.append(primaryShopItem.displayString()); - } else { - sb.append("Shop Source:\t\tNot specified\n"); - } - - return sb.toString(); - } - - /** - * Creates a new multi-item shop requirement with schedule context. - * - * @param shopItems Map of shop items to their individual requirements - * @param operation Shop operation type (BUY or SELL) - * @param requirementType Where this item should be located (equipment slot, inventory, or either) - * @param priority Priority level of this item for plugin functionality - * @param rating Effectiveness rating from 1-10 (10 being most effective) - * @param description Human-readable description of the item's purpose - * @param TaskContext When this requirement should be fulfilled - */ - public ShopRequirement( - Map shopItems, - ShopOperation operation, - RequirementType requirementType, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext - ) { - - super(requirementType, priority, rating, description, extractItemIds(shopItems), taskContext); - - if (shopItems.isEmpty()) { - throw new IllegalArgumentException("Shop items map cannot be empty"); - } - - this.shopItemRequirements = new HashMap<>(shopItems); - this.operation = operation; - this.primaryShopItem = shopItems.keySet().iterator().next(); - - // Validate all items are from the same shop - validateSameShop(); - } - - /** - * Convenience constructor for single item requirement (backward compatibility). - */ - public ShopRequirement( - Rs2ShopItem shopItem, - int amount, - ShopOperation operation, - RequirementType requirementType, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext - ) { - this(createSingleItemMap(shopItem, amount), operation, requirementType, priority, rating, description, taskContext); - } - - /** - * Helper method to extract item IDs from shop items map. - */ - private static List extractItemIds(Map shopItems) { - return shopItems.keySet().stream() - .map(Rs2ShopItem::getItemId) - .collect(Collectors.toList()); - } - - /** - * Helper method to create single item map for backward compatibility. - */ - private static Map createSingleItemMap(Rs2ShopItem shopItem, int amount) { - Map map = new HashMap<>(); - map.put(shopItem, new ShopItemRequirement(shopItem, amount, 10)); // Default stockTolerance=10 - return map; - } - - /** - * Validates that all shop items are from the same shop (same location and NPC). - */ - private void validateSameShop() { - String primaryShopNpc = primaryShopItem.getShopNpcName(); - Rs2ShopType primaryShopType = primaryShopItem.getShopType(); - - for (Rs2ShopItem item : shopItemRequirements.keySet()) { - if (!item.getShopNpcName().equals(primaryShopNpc) || - !item.getShopType().equals(primaryShopType) || - !item.getLocation().equals(primaryShopItem.getLocation())) { - throw new IllegalArgumentException( - "All shop items must be from the same shop. Found mismatch: " + - item.getShopNpcName() + " vs " + primaryShopNpc - ); - } - } - } - - /** - * Gets the total number of items needed across all shop items. - */ - public int getTotalAmount() { - return shopItemRequirements.values().stream() - .mapToInt(ShopItemRequirement::getAmount) - .sum(); - } - - /** - * Gets the total number of completed items across all shop items. - */ - public int getTotalCompletedAmount() { - return shopItemRequirements.values().stream() - .mapToInt(ShopItemRequirement::getCompletedAmount) - .sum(); - } - - /** - * Resets the excluded worlds set. This can be called when starting a new requirement - * session or when you want to give previously failed worlds another chance. - */ - public void resetExcludedWorlds() { - excludedWorlds.clear(); - log.debug("Reset excluded worlds list for shop requirement: {}", getName()); - } - - /** - * Checks if all items in this requirement are completed. - */ - public boolean isAllItemsCompleted() { - return shopItemRequirements.values().stream() - .allMatch(ShopItemRequirement::isCompleted); - } - /** - * Collects all completed Grand Exchange offers and updates item requirements accordingly. - * This method handles offers from previous sessions and cancelled offers with partial fills. - * - * @return true if any offers were collected, false otherwise - */ - private boolean collectExistingCompletedOffers() { - try { - Map completedOffers = Rs2GrandExchange.getCompletedOffers(); - - if (completedOffers.isEmpty()) { - return false; - } - - log.info("Found {} completed offers to collect", completedOffers.size()); - boolean collectedAny = false; - - for (Map.Entry entry : completedOffers.entrySet()) { - GrandExchangeSlots slot = entry.getKey(); - GrandExchangeOfferDetails details = entry.getValue(); - - // Find matching shop item requirement - ShopItemRequirement matchingReq = null; - for (ShopItemRequirement itemReq : shopItemRequirements.values()) { - log.info("Checking item requirement: {} offer item ID: {} req item ID: {}", - itemReq.getItemName(), details.getItemId(), itemReq.getShopItem().getItemId()); - if (itemReq.getShopItem().getItemId() == details.getItemId()) { - matchingReq = itemReq; - break; - } - } - - if (matchingReq != null) { - // Collect the offer and get exact quantity - int itemsTransacted = Rs2GrandExchange.collectOfferAndGetQuantity(slot, enableBanking, details.getItemId()); - - if (itemsTransacted > 0) { - matchingReq.addCompletedAmount(itemsTransacted); - collectedAny = true; - - String transactionType = details.isSelling() ? "sold" : "bought"; - log.info("Collected previous {} offer: {} {} of {}", - transactionType, itemsTransacted, matchingReq.getItemName(), matchingReq.getItemName()); - - Microbot.status = "Collected " + itemsTransacted + "x " + matchingReq.getItemName() + - " from previous " + transactionType + " offer"; - }else{ - log.warn("No items transacted for slot {} with details: {}", slot, details); - } - }else{ - //collect, to bank to free slot, but we dont need to update any requirements - Rs2GrandExchange.collectOfferAndGetQuantity(slot, enableBanking, details.getItemId()); - } - } - - return collectedAny; - - } catch (Exception e) { - log.error("Error collecting existing completed offers: {}", e.getMessage()); - return false; - } - } - - /** - * Handles purchasing multiple items from the Grand Exchange. - * Implements proper slot management, offer placement, waiting, and collection patterns. - * Enhanced with 8-slot limit tracking and batch processing. - * - * @return true if the purchase was successful, false otherwise - */ - private boolean buyFromGrandExchange(CompletableFuture scheduledFuture) { - try { - Microbot.status = "Buying " + getName() + " from Grand Exchange"; - WorldArea locationArea = primaryShopItem.getLocationArea(); - WorldPoint[] locationCorners = new WorldPoint[] { - locationArea.toWorldPoint(), // Southwest corner (x, y) - new WorldPoint(locationArea.getX() + locationArea.getWidth() - 1, locationArea.getY(), locationArea.getPlane()), // Southeast corner - new WorldPoint(locationArea.getX(), locationArea.getY() + locationArea.getHeight() - 1, locationArea.getPlane()), // Northwest corner - new WorldPoint(locationArea.getX() + locationArea.getWidth() - 1, locationArea.getY() + locationArea.getHeight() - 1, locationArea.getPlane()) // Northeast corner - }; - // Walk to Grand Exchange and open interface - if (!Rs2Walker.isInArea(locationCorners) && !Rs2Walker.walkTo(BankLocation.GRAND_EXCHANGE.getWorldPoint(),10)) { - Microbot.status = "Failed to walk to Grand Exchange"; - return false; - } - - if (!Rs2GrandExchange.openExchange()) { - Microbot.status = "Failed to open Grand Exchange interface"; - return false; - } - - // Wait for interface to stabilize with proper game state checking - if (!sleepUntil(() -> Rs2GrandExchange.isOpen() && Microbot.getClient().getGameState() == GameState.LOGGED_IN, 8000)) { - Microbot.status = "Grand Exchange interface failed to open properly"; - return false; - } - - // First, collect any existing completed offers from previous sessions - collectExistingCompletedOffers(); - - // Check if we have sufficient coins for all items - int totalCost = calculateTotalCost(); - - if (totalCost > 0 && Rs2Inventory.itemQuantity("Coins") + Rs2Bank.count("Coins") < totalCost) { - Microbot.status = "Insufficient coins for Grand Exchange purchase (need " + totalCost + " coins)"; - return false; - } - - // Process each item that still needs to be purchased - List pendingItems = shopItemRequirements.values().stream() - .filter(itemReq -> !itemReq.isCompleted()) - .collect(Collectors.toList()); - - if (pendingItems.isEmpty()) { - Microbot.status = "All items already obtained"; - return true; - } - - // Pre-check GE slot availability for all pending items - int requiredSlots = pendingItems.size(); - if (!ensureGrandExchangeSlots(requiredSlots)) { - Microbot.status = "Cannot allocate sufficient GE slots for " + requiredSlots + " items"; - return false; - } - - - // BATCH PROCESSING: First, place as many offers as possible in available slots - Microbot.status = "Placing batch of Grand Exchange buy offers..."; - - // Track what items we're buying and their slots - Map activeOffers = new HashMap<>(); - Map itemToSlotMap = new HashMap<>(); - Map initialItemCounts = new HashMap<>(); - - int placedOffers = 0; - - // First phase: Place as many offers as possible at once - for (ShopItemRequirement itemReq : pendingItems) { - // Stop if we've used all available GE slots - if (Rs2GrandExchange.getAvailableSlotsCount() == 0) { - Microbot.status = "Maximum GE slots used - proceeding with " + placedOffers + " offers"; - break; - } - - // Calculate offer price using enhanced pricing method - int itemId = itemReq.getShopItem().getItemId(); - int offerPrice = calculateOfferPrice(itemReq); - int remainingAmount = itemReq.getRemainingAmount(); - - // Check for duplicate offers before placing new ones - cancelDuplicateOffers(itemId, itemReq.getItemName()); - - // Find available slot - GrandExchangeSlots availableSlot = Rs2GrandExchange.getAvailableSlot(); - if (availableSlot == null) { - log.warn("No available GE slots for {}", itemReq.getItemName()); - continue; - } - - // Place the buy order - Microbot.status = "Placing offer #" + (placedOffers+1) + ": " + remainingAmount + "x " + - itemReq.getItemName() + " at " + offerPrice + " gp each"; - - boolean offerPlaced = Rs2GrandExchange.buyItem( - itemReq.getItemName(), - offerPrice, - remainingAmount - ); - - if (!offerPlaced) { - Microbot.status = "Failed to place offer for " + itemReq.getItemName(); - continue; - } - - // Track this offer - activeOffers.put(itemId, itemReq); - itemToSlotMap.put(itemId, availableSlot); - - // Record initial item count for comparison later - int initialCount = Rs2Inventory.itemQuantity(itemId) + Rs2Bank.count(itemId); - initialItemCounts.put(itemId, initialCount); - - // Increment counter and brief pause between placing offers - placedOffers++; - sleep(Constants.GAME_TICK_LENGTH / 2); - } - - if (placedOffers == 0) { - Microbot.status = "Failed to place any Grand Exchange offers"; - } else { - // Second phase: Wait for and collect offers - Microbot.status = "Waiting for " + placedOffers + " Grand Exchange offers to complete..."; - log.info( "Waiting for " + placedOffers + " Grand Exchange offers to complete..."); - // Create a set of items that need to be completed - Set pendingItemIds = new HashSet<>(activeOffers.keySet()); - - // Start time for timeout calculations - long startTime = System.currentTimeMillis(); - long maxWaitTime = 120000; // 2 minutes total wait time for all offers - - // Wait for offers to complete using proper game state checking - while ((!pendingItemIds.isEmpty() && System.currentTimeMillis() - startTime < maxWaitTime) - && scheduledFuture != null && !scheduledFuture.isDone() && !scheduledFuture.isCancelled() - ) { - // Use sleepUntil to wait for any offers to complete, with shorter intervals - boolean hasCompletedOffer = sleepUntil(() -> { - return pendingItemIds.stream().anyMatch(itemId -> { - GrandExchangeSlots slot = itemToSlotMap.get(itemId); - return slot != null && Rs2GrandExchange.hasBoughtOffer(slot) && !scheduledFuture.isDone() && !scheduledFuture.isCancelled(); - }); - }, 5000); // Check every 5 seconds - - if (!hasCompletedOffer && System.currentTimeMillis() - startTime >= maxWaitTime) { - log.info("Timeout reached while waiting for offers"); - break; - } - - // Check each pending item for completion - for (Iterator it = pendingItemIds.iterator(); it.hasNext();) { - int itemId = it.next(); - ShopItemRequirement itemReq = activeOffers.get(itemId); - GrandExchangeSlots slot = itemToSlotMap.get(itemId); - - // Check if this offer has completed - boolean slotHasCompletedOffer = (slot != null) && Rs2GrandExchange.hasBoughtOffer(slot); - - // If offer completed, collect it - if (slotHasCompletedOffer) { - Microbot.status = "Offer completed for: " + itemReq.getItemName(); - - // Use the enhanced collection method to get exact quantity - int itemsPurchased = 0; - if (slot != null && Rs2GrandExchange.hasBoughtOffer(slot)) { - // Use the new method to collect and get exact quantity - itemsPurchased = Rs2GrandExchange.collectOfferAndGetQuantity(slot, enableBanking, itemId); - log.debug("Collected {} items from offer for {}", itemsPurchased, itemReq.getItemName()); - } - - // Update completed amount with actual purchased quantity - if (itemsPurchased > 0) { - itemReq.addCompletedAmount(itemsPurchased); - log.info("Updated completed amount for {}: purchased {} items, new total: {}/{}", - itemReq.getItemName(), itemsPurchased, itemReq.getCompletedAmount(), itemReq.getAmount()); - } else { - log.warn("No items collected from offer for {}", itemReq.getItemName()); - return false; // If we couldn't collect, return false - } - - // Remove this item from pending list - it.remove(); - - // Brief pause after collecting - sleep(Constants.GAME_TICK_LENGTH); - } - } - } - - // Handle any remaining pendingItemIds as unsuccessful - if (!pendingItemIds.isEmpty()) { - Microbot.status = "Timed out waiting for " + pendingItemIds.size() + " offers"; - - // List the items that didn't complete in time - for (int itemId : pendingItemIds) { - ShopItemRequirement itemReq = activeOffers.get(itemId); - log.warn("Offer timeout for: {}", itemReq.getItemName()); - } - - // Enhanced timeout handling: If timeout > 0 and we placed offers successfully, - // consider this a partial success rather than complete failure - if (timeout > 0 && placedOffers > 0) { - log.info("Timeout reached but {} offers were placed successfully - treating as partial success", placedOffers); - Microbot.status = "Grand Exchange orders placed but timed out waiting - continuing"; - } - } - } - - - - // If we have any tracked slots that weren't properly cleared during processing, - // make one final check and collection attempt for them - for (Integer itemId : itemToSlotMap.keySet()) { - GrandExchangeSlots slot = itemToSlotMap.get(itemId); - ShopItemRequirement itemReq = activeOffers.get(itemId); - - if (slot != null && itemReq != null) { - // Check if there's an offer in this slot that needs collection - if (Rs2GrandExchange.hasBoughtOffer(slot)) { - Microbot.status = "Final collection for: " + itemReq.getItemName(); - - // Use enhanced collection to get exact quantity - int itemsPurchased = Rs2GrandExchange.collectOfferAndGetQuantity(slot, enableBanking, itemId); - - if (itemsPurchased > 0) { - // Update completed amount with actual purchased quantity - itemReq.addCompletedAmount(itemsPurchased); - log.info("Final collection - Updated completed amount for {}: purchased {} items, new total: {}/{}", - itemReq.getItemName(), itemsPurchased, itemReq.getCompletedAmount(), itemReq.getAmount()); - }else { - log.warn("No items collected from final slot for {}", itemReq.getItemName()); - return false; // If we couldn't collect, return false - } - - } - - // Also check for cancelled offers with partial fills - else if (Rs2GrandExchange.isCancelledOfferWithItems(slot)) { - Microbot.status = "Collecting partial fill from cancelled offer: " + itemReq.getItemName(); - - int itemsPurchased = Rs2GrandExchange.collectOfferAndGetQuantity(slot, enableBanking, itemId); - if (itemsPurchased > 0) { - itemReq.addCompletedAmount(itemsPurchased); - log.info("Collected {} items from cancelled offer for {}", itemsPurchased, itemReq.getItemName()); - } - else { - log.warn("No items collected from cancelled offer for {}", itemReq.getItemName()); - return false; // If we couldn't collect, return false - } - } - } - } - - // No need to clear allocations - using simplified approach - // clearGrandExchangeSlotAllocations(); - - // Bank the items if banking is enabled - if (enableBanking) { - boolean hasItemsToBank = shopItemRequirements.values().stream() - .anyMatch(itemReq -> Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()) > 0); - - if (hasItemsToBank && !bankItems()) { - Microbot.status = "Purchase successful but failed to bank items"; - } - } - - // Attempt to restore any cancelled offers before checking final completion - int restoredOffers = restoreCancelledOffers(); - if (restoredOffers > 0) { - log.info("Restored {} previously cancelled offers after Grand Exchange buy operation", restoredOffers); - } - - // Final completion check - items should already be updated during collection - boolean allCompleted = isAllItemsCompleted(); - boolean partialSuccess = timeout > 0 && placedOffers > 0; - - if (allCompleted) { - Microbot.status = "Successfully purchased all items from Grand Exchange"; - log.info("Successfully purchased all items from Grand Exchange"); - return true; - } else if (partialSuccess) { - Microbot.status = "Grand Exchange orders placed successfully (some may still be pending)"; - log.info("Partial success: {} offers placed, timeout reached but treating as success", placedOffers); - log.warn("Remaining items: {}", - shopItemRequirements.values().stream() - .filter(itemReq -> !itemReq.isCompleted()) - .map(ShopItemRequirement::getItemName) - .collect(Collectors.joining(", "))); - return true; - } else { - Microbot.status = "Some Grand Exchange purchases failed"; - log.warn("Some Grand Exchange purchases failed, remaining items: {}", - shopItemRequirements.values().stream() - .filter(itemReq -> !itemReq.isCompleted()) - .map(ShopItemRequirement::getItemName) - .collect(Collectors.joining(", "))); - return !isMandatory(); - } - - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.buyFromGrandExchange", e); - // Clear tracked offers on error to prevent stale state - clearCancelledOffers(); - return false; - } - } - - /** - * Calculates the optimal offer price for an item using time-series data or fallback methods. - * Uses intelligent pricing patterns from Rs2GrandExchange for better market interaction. - * - * @param itemReq The item requirement to calculate price for - * @return The calculated offer price - */ - private int calculateOfferPrice(ShopItemRequirement itemReq) { - try { - int itemId = itemReq.getShopItem().getItemId(); - int offerPrice; - - if (itemReq.shouldUseTimeSeriesPricing()) { - // Use time-series average price for more intelligent buying - TimeSeriesAnalysis analysis = Rs2GrandExchange.getTimeSeriesData( - itemId, itemReq.getRecommendedTimeSeriesInterval()); - - if (analysis.averagePrice > 0) { - // Use recommended buy price from time-series analysis - offerPrice = analysis.getRecommendedBuyPrice(); - log.info("Using time-series buy price for {}: {} (avg: {}, high: {})", - itemReq.getItemName(), offerPrice, analysis.averagePrice, analysis.averageHighPrice); - } else { - // Fallback to intelligent pricing with current market data - offerPrice = Rs2GrandExchange.getAdaptiveBuyPrice(itemId, itemReq.getShopItem().getPercentageBoughtAt(), 0); - log.info("Time-series unavailable for {}, using adaptive pricing: {}", - itemReq.getItemName(), offerPrice); - } - } else { - // Traditional pricing method - int gePrice = Microbot.getRs2ItemManager().getGEPrice(itemReq.getItemName()); - offerPrice = Math.max((int) (gePrice * 1.1), (int) itemReq.getShopItem().getInitialPriceBuyAt()); - log.debug("Using traditional buy price for {}: {} (GE: {})", - itemReq.getItemName(), offerPrice, gePrice); - } - - // Ensure price respects the maximum limit from shop item configuration - int maxPrice = (int) itemReq.getShopItem().getInitialPriceBuyAt(); - offerPrice = Math.min(offerPrice, maxPrice); - - log.info("Final offer price for {}: {} (max allowed: {})", - itemReq.getItemName(), offerPrice, maxPrice); - - return offerPrice; - - } catch (Exception e) { - log.warn("Error calculating offer price for {}, using fallback: {}", - itemReq.getItemName(), e.getMessage()); - - // Fallback to shop item's initial price - return (int) itemReq.getShopItem().getInitialPriceBuyAt(); - } - } - - /** - * Handles selling multiple items to the Grand Exchange. - * Implements proper slot management, offer placement, waiting, and collection patterns. - * - * @return true if the sale was successful, false otherwise - */ - private boolean sellToGrandExchange() { - try { - Microbot.status = "Selling items to Grand Exchange"; - - // Get items from bank if needed first - if (enableBanking && !getItemsFromBankForSelling()) { - Microbot.status = "Failed to get items from bank for selling"; - return !isMandatory(); - } - - // Check if we have items to sell in inventory - boolean hasItemsToSell = shopItemRequirements.values().stream() - .anyMatch(itemReq -> Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()) > 0); - - if (!hasItemsToSell) { - Microbot.status = "No items to sell"; - return !isMandatory(); - } - - // Walk to Grand Exchange and open interface - if (!Rs2GrandExchange.walkToGrandExchange()) { - Microbot.status = "Failed to walk to Grand Exchange for selling"; - return false; - } - - if (!Rs2GrandExchange.openExchange()) { - Microbot.status = "Failed to open Grand Exchange interface for selling"; - return false; - } - - // Wait for interface to stabilize - if (!sleepUntil(() -> Rs2GrandExchange.isOpen(), 3000)) { - Microbot.status = "Grand Exchange interface failed to stabilize for selling"; - return false; - } - - // First, collect any existing completed offers from previous sessions - collectExistingCompletedOffers(); - - // Process each item that has inventory stock to sell - List sellableItems = shopItemRequirements.values().stream() - .filter(itemReq -> Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()) > 0) - .collect(Collectors.toList()); - - if (sellableItems.isEmpty()) { - Microbot.status = "No items found in inventory to sell"; - return true; - } - - // First, try to free up GE slots if needed - if (Rs2GrandExchange.getAvailableSlotsCount() == 0) { - Microbot.status = "No free GE slots available for selling, attempting to free them"; - - // Try to collect completed offers first - if (Rs2GrandExchange.hasBoughtOffer() || Rs2GrandExchange.hasSoldOffer()) { - Rs2GrandExchange.collectAll(enableBanking); - sleepUntil(() -> Rs2GrandExchange.getAvailableSlotsCount() > 0, 3000); - } else { - Rs2GrandExchange.abortAllOffers(enableBanking); - sleepUntil(() -> Rs2GrandExchange.getAvailableSlotsCount() > 0, 5000); - } - - if (Rs2GrandExchange.getAvailableSlotsCount() == 0) { - Microbot.status = "Failed to free Grand Exchange slots for selling"; - return !isMandatory(); - } - } - - // BATCH PROCESSING: Place as many offers as possible first - Microbot.status = "Placing batch of Grand Exchange sell offers..."; - - // Track what items we're selling and their slots - Map activeOffers = new HashMap<>(); - Map itemToSlotMap = new HashMap<>(); - Map initialInventoryCounts = new HashMap<>(); - - int placedOffers = 0; - - // First phase: Place as many offers as possible at once - for (ShopItemRequirement itemReq : sellableItems) { - // Stop if we've used all available GE slots - if (Rs2GrandExchange.getAvailableSlotsCount() == 0) { - Microbot.status = "Maximum GE slots used - proceeding with " + placedOffers + " offers"; - break; - } - - // Check inventory count for this item - int inventoryCount = Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()); - if (inventoryCount == 0) { - continue; // No items to sell for this item type - } - - // Calculate sell price using time-series data if enabled, fallback to traditional GE price - int sellPrice; - int sellAmount = Math.min(inventoryCount, itemReq.getAmount()); - int itemId = itemReq.getShopItem().getItemId(); - - if (itemReq.shouldUseTimeSeriesPricing()) { - // Use time-series average price for more intelligent selling - TimeSeriesAnalysis analysis = Rs2GrandExchange.getTimeSeriesData( - itemId, itemReq.getRecommendedTimeSeriesInterval()); - - if (analysis.averagePrice > 0) { - // Use recommended sell price from time-series analysis - sellPrice = analysis.getRecommendedSellPrice(); - log.info("Using time-series sell price for {}: {} (avg: {}, low: {})", - itemReq.getItemName(), sellPrice, analysis.averagePrice, analysis.averageLowPrice); - } else { - // Fallback to intelligent pricing with current market data - sellPrice = Rs2GrandExchange.getAdaptiveSellPrice(itemId, itemReq.getShopItem().getInitialPriceSellAt(), 0); - log.info("Time-series unavailable for {}, using intelligent pricing: {}", - itemReq.getItemName(), sellPrice); - } - } else { - // Traditional pricing method - int gePrice = Microbot.getRs2ItemManager().getGEPrice(itemReq.getItemName()); - sellPrice = Math.max((int) (gePrice * 0.9), (int) itemReq.getShopItem().getInitialPriceSellAt()); - log.info("Using traditional sell price for {}: {} (GE: {})", - itemReq.getItemName(), sellPrice, gePrice); - } - - // Ensure minimum price from shop item configuration - sellPrice = Math.max(sellPrice, (int) itemReq.getShopItem().getInitialPriceSellAt()); - - // Check for duplicate offers before placing new ones - cancelDuplicateOffers(itemId, itemReq.getItemName()); - - // Find available slot - GrandExchangeSlots availableSlot = Rs2GrandExchange.getAvailableSlot(); - if (availableSlot == null) { - log.warn("No available GE slots for selling {}", itemReq.getItemName()); - continue; - } - - // Place the sell order - Microbot.status = "Placing offer #" + (placedOffers+1) + ": " + sellAmount + "x " + - itemReq.getItemName() + " at " + sellPrice + " gp each"; - - boolean offerPlaced = Rs2GrandExchange.sellItem( - itemReq.getItemName(), - sellAmount, - sellPrice - ); - - if (!offerPlaced) { - Microbot.status = "Failed to place sell offer for " + itemReq.getItemName(); - continue; - } - - // Track this offer - activeOffers.put(itemId, itemReq); - itemToSlotMap.put(itemId, availableSlot); - - // Record initial inventory count for comparison later - initialInventoryCounts.put(itemId, inventoryCount); - - // Increment counter and brief pause between placing offers - placedOffers++; - sleep(Constants.GAME_TICK_LENGTH / 2); - } - if (placedOffers == 0) { - Microbot.status = "Failed to place any Grand Exchange sell offers"; - } else { - // Second phase: Wait for and collect offers - Microbot.status = "Waiting for " + placedOffers + " Grand Exchange sell offers to complete..."; - - // Create a set of items that need to be completed - Set pendingItemIds = new HashSet<>(activeOffers.keySet()); - - // Start time for timeout calculations - long startTime = System.currentTimeMillis(); - long maxWaitTime = timeout; // 2 minutes total wait time for all offers - - // Wait for offers to complete and collect as they do - while (!pendingItemIds.isEmpty() && System.currentTimeMillis() - startTime < maxWaitTime) { - // Check each pending item - for (Iterator it = pendingItemIds.iterator(); it.hasNext();) { - int itemId = it.next(); - ShopItemRequirement itemReq = activeOffers.get(itemId); - GrandExchangeSlots slot = itemToSlotMap.get(itemId); - int initialInventoryCount = initialInventoryCounts.get(itemId); - - // Check if this offer has completed - boolean slotHasCompletedOffer = (slot != null) && Rs2GrandExchange.hasSoldOffer(slot); - - // Also check if the item count has decreased (items sold) - int currentInventoryCount = Rs2Inventory.itemQuantity(itemId); - boolean itemCountDecreased = currentInventoryCount < initialInventoryCount; - - // If either condition is met, consider the offer complete - if (slotHasCompletedOffer || itemCountDecreased) { - Microbot.status = "Sell offer completed for: " + itemReq.getItemName(); - - // Use enhanced collection to get exact quantity sold - int itemsSold = 0; - if (slot != null && Rs2GrandExchange.hasSoldOffer(slot)) { - // Get the exact number of items sold from the offer - itemsSold = Rs2GrandExchange.getItemsSoldFromOffer(slot); - - // Collect the coins from this slot - Rs2GrandExchange.collectOffer(slot, enableBanking); - sleepUntil(() -> !Rs2GrandExchange.hasSoldOffer(slot) || - Rs2Inventory.itemQuantity("Coins") > 0, 3000); - - log.debug("Collected coins from sale of {} items of {}", itemsSold, itemReq.getItemName()); - } else { - // Fallback: calculate based on inventory change - itemsSold = initialInventoryCount - currentInventoryCount; - } - - // Update completed amount based on how many items were actually sold - if (itemsSold > 0) { - itemReq.addCompletedAmount(itemsSold); - log.debug("Updated completed amount for {}: sold {} items, new total: {}/{}", - itemReq.getItemName(), itemsSold, itemReq.getCompletedAmount(), itemReq.getAmount()); - } - - // Remove this item from pending list - it.remove(); - - // Brief pause after collecting - sleep(Constants.GAME_TICK_LENGTH); - } - } - sleepUntil(() -> Rs2GrandExchange.hasSoldOffer(), (int)maxWaitTime); // Refresh state - - } - - // Handle any remaining pendingItemIds as unsuccessful - if (!pendingItemIds.isEmpty()) { - Microbot.status = "Timed out waiting for " + pendingItemIds.size() + " sell offers"; - log.warn("Timed out waiting for {} sell offers", pendingItemIds.size()); - - // List the items that didn't complete in time - for (int itemId : pendingItemIds) { - ShopItemRequirement itemReq = activeOffers.get(itemId); - log.warn("Sell offer timeout for: {}", itemReq.getItemName()); - } - - // Enhanced timeout handling: If timeout > 0 and we placed offers successfully, - // consider this a partial success rather than complete failure - if (timeout > 0 && placedOffers > 0) { - log.info("Timeout reached but {} sell offers were placed successfully - treating as partial success", placedOffers); - Microbot.status = "Grand Exchange sell orders placed but timed out waiting - continuing"; - } - } - } - - - - - // No need to clear allocations - using simplified approach - // clearGrandExchangeSlotAllocations(); - - - // Bank the coins if banking is enabled and we got them in inventory - if (enableBanking && Rs2Inventory.itemQuantity("Coins") > 0) { - if (!bankCoinsAfterSelling()) { - Microbot.status = "Sale successful but failed to bank coins"; - // Don't fail the requirement just because banking failed - } - } - - // Attempt to restore any cancelled offers before checking final completion - int restoredOffers = restoreCancelledOffers(); - if (restoredOffers > 0) { - log.info("Restored {} previously cancelled offers after Grand Exchange sell operation", restoredOffers); - } - - // Final completion check with enhanced timeout handling - boolean allCompleted = isAllItemsCompleted(); - boolean partialSuccess = timeout > 0 && placedOffers > 0; - - if (allCompleted) { - Microbot.status = "Successfully sold all items to Grand Exchange"; - log.info("Successfully sold all items to Grand Exchange"); - return true; - } else if (partialSuccess) { - Microbot.status = "Grand Exchange sell orders placed successfully (some may still be pending)"; - log.info("Partial success: {} sell offers placed, timeout reached but treating as success", placedOffers); - return true; - } else { - Microbot.status = "Some Grand Exchange sales failed"; - log.warn("Some Grand Exchange sales failed, remaining items: {}", - shopItemRequirements.values().stream() - .filter(itemReq -> !itemReq.isCompleted()) - .map(ShopItemRequirement::getItemName) - .collect(Collectors.joining(", "))); - return !isMandatory(); // Allow continuation if not mandatory - } - - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.sellToGrandExchange", e); - // Clear tracked offers on error to prevent stale state - clearCancelledOffers(); - return false; - } - } - /** - * Opens the shop interface by interacting with the specified NPC. - * - * @param npcName The name of the shop NPC to interact with - * @param exact Whether to match the name exactly or allow partial matches - * @return true if the shop is successfully opened, false otherwise. - */ - public static boolean openShop(String npcName, boolean exact) { - // Delegate to Rs2Shop utility which now handles finding nearest shop NPC with Trade action - return Rs2Shop.openShop(npcName, exact); - } - - /** - * Handles purchasing multiple items from regular shops with stock management and world hopping. - * Supports both single and multi-item operations with individual item requirements. - * - * @return true if the purchase was successful, false otherwise - */ - private boolean buyFromRegularShop(CompletableFuture scheduledFuture) { - try { - Microbot.status = "Buying items from " + primaryShopItem.getShopNpcName(); - - int maxAttempts = enableWorldHopping ? worldHoppingConfig.getMaxWorldHops (): 0; // More attempts if world hopping is enabled - int successiveWorldHopAttempts = 0; - log.info("walking to shop location: x: {}, y: {}", primaryShopItem.getLocation().getX(), primaryShopItem.getLocation().getY()); - - while (!isAllItemsCompleted() && successiveWorldHopAttempts < maxAttempts) { - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - Microbot.status = "Task cancelled, stopping shop purchases"; - log.info("Shop purchase task cancelled, exiting"); - return false; // Exit if task was cancelled - } - - // Walk to the shop - - if (!Rs2Walker.isInArea(primaryShopItem.getLocationArea().toWorldPointList().toArray(new WorldPoint[0])) && !Rs2Walker.walkTo(primaryShopItem.getLocation(),4)) { - Microbot.status = "Failed to walk to shop"; - log.error("Failed to walk to shop: " + primaryShopItem.getLocation()); - return false; // Exit if walking to shop failed - } - - - // Open shop interface - if (!Rs2Shop.isOpen()){ - if (!Rs2Shop.openShop(primaryShopItem.getShopNpcName())) { - Microbot.status = "Failed to open shop"; - log.error("\n\tFailed to open shop: \"{}\"", primaryShopItem.getShopNpcName()); - continue; - } - } - - //sleepUntil(() -> Rs2Shop.isOpen(), Constants.GAME_TICK_LENGTH * 3); // Ensure shop is open - if (!Rs2Shop.isOpen()) { - Microbot.status = "Shop interface not open"; - log.error("Shop interface failed to open for " + primaryShopItem.getShopNpcName()); - return false; // Exit if shop interface failed to open - } - - // Process each item that still needs to be purchased - List pendingItems = shopItemRequirements.values().stream() - .filter(itemReq -> !itemReq.isCompleted()) - .collect(Collectors.toList()); - - if (pendingItems.isEmpty()) { - Rs2Shop.closeShop(); - break; - } - - boolean needWorldHop = false; - boolean needBanking = false; - boolean purchasedAnything = false; - - int currentStock = 0; - for (ShopItemRequirement itemReq : pendingItems) { - if ( scheduledFuture != null && scheduledFuture.isCancelled()) { - Microbot.status = "Task cancelled, stopping shop purchases"; - log.info("Shop purchase task cancelled, exiting"); - return false; // Exit if task was cancelled - } - // Track initial inventory count before attempting purchase - int initialItemCount = Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()); - - if (itemReq.isCompleted()) { - log.info("Skipping completed item: " + itemReq.getItemName()); - continue; // Skip completed items - } - - - // Get current stock level from the shop interface (real-time check) - currentStock = getShopStock(itemReq.getItemName()); - - if (currentStock == -1) { - Microbot.status = itemReq.getItemName() + " not found in shop"; - log.error("Shop item not found: " + itemReq.getItemName()); - Rs2Shop.closeShop(); - return false; - } - if (itemReq.allowedToBuy(currentStock) ==0){ - log.error("We can't fulfill buy operation,when minimum stock requirement is zero, we cant buy items when the minium stock is zero for: " + itemReq.getItemName()); - return false; // Skip items with zero minimum stock requirement - } - // Check if stock is sufficient using new unified logic - if (!itemReq.canProcessInShop(currentStock, ShopOperation.BUY)) { - Microbot.status = "Insufficient stock for " + itemReq.getItemName() + - " (current: " + currentStock + ", minimum: " + itemReq.getMinimumStockForBuying() + ")"; - needWorldHop = true; - log.warn("Insufficient stock for " + itemReq.getItemName() + - " in shop " + primaryShopItem.getShopNpcName() + - " (current: " + currentStock + ", minimum: " + itemReq.getMinimumStockForBuying() + ")"); - continue; // Check other items - } - - // **STOCK MANAGEMENT**: Calculate quantity using unified logic - int quantityThisVisit = itemReq.getQuantityForCurrentVisit(currentStock, ShopOperation.BUY); - log.info(" Calculated quantity to buy for \n\t{}: {}", itemReq.getItemName(), quantityThisVisit); - quantityThisVisit = Math.min(quantityThisVisit, currentStock); - - // Check if item is stackable to determine inventory limit - boolean isStackable = itemReq.getShopItem().getItemComposition() != null && - itemReq.getShopItem().getItemComposition().isStackable(); - if (!isStackable) { - // For non-stackable items, limit by available inventory space - int freeSlots = 28 - Rs2Inventory.count(); - quantityThisVisit = Math.min(quantityThisVisit, freeSlots); - } - - if (quantityThisVisit <= 0) { - needBanking = true; - if (!isStackable && Rs2Inventory.count() >= 28) { - Microbot.status = "Inventory full - banking non-stackable items"; - Rs2Shop.closeShop(); - if (enableBanking) { - log.info( "Banking items to make space for more purchases"); - break; // Restart the shop visit after banking - } - log.error("would not be able to buy " + itemReq.getItemName() + - " due to insufficient inventory space, banking not enabled"); - return false; - } - continue; // Can't buy this item right now, check next - } - - // Purchase items using optimal buying method - boolean purchaseSuccessful = false; - try { - Microbot.status = "Purchasing " + quantityThisVisit + "x " + itemReq.getItemName() + - " (" + itemReq.getCompletedAmount() + "/" + itemReq.getAmount() + " total)"; - - purchaseSuccessful = Rs2Shop.buyItem(itemReq.getItemName(), String.valueOf(quantityThisVisit)); - if (purchaseSuccessful) { - // Wait for purchase to complete - sleepUntil(() -> Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()) > initialItemCount, 3000); - - // Calculate how many items were actually purchased - int finalItemCount = Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()); - int itemsPurchased = finalItemCount - initialItemCount; - - // Update completed amount with actual purchased quantity - itemReq.addCompletedAmount(itemsPurchased); - purchasedAnything = true; - - log.info("Purchased {} items of {}, new completion: {}/{}", - itemsPurchased, itemReq.getItemName(), itemReq.getCompletedAmount(), itemReq.getAmount()); - - Microbot.status = "Successfully purchased " + itemsPurchased + "x " + itemReq.getItemName(); - }else { - Microbot.status = "Failed to purchase " + itemReq.getItemName(); - log.error("Purchase failed for " + itemReq.getItemName()); - } - - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.buyFromRegularShop - Purchase failed for " + itemReq.getItemName(), e); - purchaseSuccessful = false; - } - - // Brief pause between item purchases - if (purchaseSuccessful) { - sleepGaussian(Constants.GAME_TICK_LENGTH, 300); - } - } - - Rs2Shop.closeShop(); - if (purchasedAnything) { - successiveWorldHopAttempts = successiveWorldHopAttempts > 0 ? successiveWorldHopAttempts-1 : 0; // Reset counter if we successfully purchased items - } else { - successiveWorldHopAttempts++; - } - - // Handle world hopping if needed - if (needWorldHop && enableWorldHopping && primaryShopItem.getShopType().supportsWorldHopping()) { - log.info("World hopping due to insufficient stock in shop"); - - // Use enhanced world hopping with retry mechanism - boolean hopSuccess = false; - - if (useNextWorld || worldHoppingConfig.isUseSequentialWorlds()) { - // Try specific world selection first - int world = LoginManager.getNextWorld(Rs2Player.isMember()); - if (world != -1 && !excludedWorlds.contains(world)) { - hopSuccess = Rs2WorldUtil.hopWorld(scheduledFuture, world, successiveWorldHopAttempts, worldHoppingConfig); - if (!hopSuccess) { - excludedWorlds.add(world); // Mark this world as problematic - } - } - } - - // If specific world hop failed or not using sequential, try enhanced world hopping - if (!hopSuccess) { - hopSuccess = Rs2WorldUtil.hopToNextBestWorld(scheduledFuture, - successiveWorldHopAttempts, - worldHoppingConfig, - excludedWorlds); - } - - if (hopSuccess) { - log.info("World hop successful after insufficient stock in shop"); - continue; - } else { - log.error("Failed to hop to a new world after insufficient stock in shop"); - return false; - } - } - - // Check if we should bank items - if (needBanking && enableBanking && Rs2Inventory.count() > 20) { - if (!bankItems()) { - Microbot.status = "Failed to bank items - continuing without banking"; - log.error("Failed to bank items, we can not make space for more items"); - return false; // Exit if banking failed - } - continue; // Restart shop visit after banking - } - if (!needBanking && !needWorldHop && !purchasedAnything) { - Microbot.status = "No items purchased this visit - checking next item"; - log.info("No items purchased this visit, we cant hop worlds or bank items, checking next item"); - break; // No items purchased, check next item - } - // Brief pause between shop visits - sleepGaussian(Constants.GAME_TICK_LENGTH*3, Constants.GAME_TICK_LENGTH); - } - - // Final status update - if (isAllItemsCompleted()) { - log.info("Successfully completed purchase of all items from regular shop"); - } else { - log.warn("Purchase incomplete after {} attempts, remaining items: {}", - successiveWorldHopAttempts, - shopItemRequirements.values().stream() - .filter(itemReq -> !itemReq.isCompleted()) - .map(ShopItemRequirement::getItemName) - .collect(Collectors.joining(", "))); - } - - return isAllItemsCompleted(); - - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.buyFromRegularShop", e); - return false; - } - } - - /** - * Enhanced selling method with BanksShopper patterns for multiple items. - * Implements proper while loop logic for selling multiple items across worlds. - * - * @return true if the sale was successful, false otherwise - */ - private boolean sellToRegularShop(CompletableFuture scheduledFuture) { - try { - Microbot.status = "Selling items to " + primaryShopItem.getShopNpcName(); - - int maxAttempts = enableWorldHopping ? worldHoppingConfig.getMaxWorldHops() : 0; // More attempts if world hopping is enabled - int successiveWorldHopAttempts = 0; - - - while (!isAllItemsCompleted() && successiveWorldHopAttempts < maxAttempts) { - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - Microbot.status = "Task cancelled, stopping shop sales"; - log.info("Shop sale task cancelled, exiting"); - return false; // Exit if task was cancelled - } - - // Walk to the shop - if (!Rs2Walker.isInArea(primaryShopItem.getLocationArea().toWorldPointList().toArray(new WorldPoint[0])) - && !Rs2Walker.walkTo(primaryShopItem.getLocation())) { - log.error("\n\tFailed to walk to shop: " + primaryShopItem.getLocation()); - return false; // Exit if walking to shop failed - } - - // Open shop interface - if (!Rs2Shop.openShop(primaryShopItem.getShopNpcName())) { - log.error("\n\tFailed to open shop for selling: " + primaryShopItem.getShopNpcName()); - return false; // Exit if shop interface failed to open - } - - // Wait for shop data to update - check if shop is properly loaded - if (!sleepUntil(() -> Rs2Shop.isOpen(), 3000)) { - log.error("\n\tShop interface failed to stabilize for selling: " + primaryShopItem.getShopNpcName()); - Rs2Shop.closeShop(); - return false; // Exit if shop interface failed to open - } - - // Process each item that still needs to be sold - List pendingItems = shopItemRequirements.values().stream() - .filter(itemReq -> { - // Check if we have items to sell in inventory - int currentInventoryCount = Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()); - return currentInventoryCount > 0 && !itemReq.isCompleted(); - }) - .collect(Collectors.toList()); - - if (pendingItems.isEmpty()) { - Rs2Shop.closeShop(); - break; - } - - boolean needWorldHop = false; - boolean soldAnything = false; - - for (ShopItemRequirement itemReq : pendingItems) { - if(itemReq.isCompleted()){ - continue; // Skip completed items - } - // Check if we still have items to sell - int currentInventoryCount = Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()); - if (currentInventoryCount == 0) { - continue; // No more of this item to sell - } - - // Get current shop stock and check if we can sell safely using unified API - int currentStock = getShopStock(itemReq.getItemName()); - if (currentStock == -1) { - log.error("\n\tShop item not found: " + itemReq.getItemName()); - Rs2Shop.closeShop(); - return false; - } - - // **STOCK MANAGEMENT**: Use unified stock validation - if (!itemReq.canProcessInShop(currentStock, operation)) { - Microbot.status = "Shop stock too high for " + itemReq.getItemName() + - " (current: " + currentStock + ") - cannot sell"; - needWorldHop = true; - continue; // Check other items, which may have lower stock, and we can sell - } - - // **STOCK MANAGEMENT**: Use unified quantity calculation - int quantityThisVisit = itemReq.getQuantityForCurrentVisit(currentStock, ShopOperation.SELL); - quantityThisVisit = Math.min(quantityThisVisit, currentInventoryCount); - - if (quantityThisVisit <= 0) { - continue; // Cannot sell any items this visit, check next item - } - - Microbot.status = "Selling " + quantityThisVisit + "x " + itemReq.getItemName() + - " (shop stock: " + currentStock + ")"; - - // Sell items using inventory method (standard approach for selling) - boolean sellSuccessful = false; - try { - sellSuccessful = Rs2Inventory.sellItem(itemReq.getItemName(), String.valueOf(quantityThisVisit)); - if (sellSuccessful) { - // Wait for sale to complete - sleepUntil(() -> Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()) < currentInventoryCount, 3000); - - soldAnything = true; - Microbot.status = "Successfully sold " + quantityThisVisit + "x " + itemReq.getItemName(); - int slotItems = currentInventoryCount - Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()); - itemReq.addCompletedAmount(slotItems); - } - - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.sellToRegularShop - Sale failed for " + itemReq.getItemName(), e); - sellSuccessful = false; - } - - // Brief pause between item sales - if (sellSuccessful) { - sleepGaussian(900, 300); - } - } - - Rs2Shop.closeShop(); - if (soldAnything){ - successiveWorldHopAttempts = successiveWorldHopAttempts >0 ? 0 : successiveWorldHopAttempts-1; // Reset if we sold items - }else{ - successiveWorldHopAttempts++; // Increment if no items sold - } - // Handle world hopping if needed - if (needWorldHop && enableWorldHopping && primaryShopItem.getShopType().supportsWorldHopping()) { - log.info("World hopping due to high stock levels in shop for selling"); - - // Use enhanced world hopping with retry mechanism - boolean hopSuccess = false; - - if (useNextWorld || worldHoppingConfig.isUseSequentialWorlds()) { - // Try specific world selection first - int world = LoginManager.getNextWorld(Rs2Player.isMember()); - if (world != -1 && !excludedWorlds.contains(world)) { - hopSuccess = Rs2WorldUtil.hopWorld(scheduledFuture, world, successiveWorldHopAttempts, worldHoppingConfig); - if (!hopSuccess) { - excludedWorlds.add(world); // Mark this world as problematic - } - } - } - - // If specific world hop failed or not using sequential, try enhanced world hopping - if (!hopSuccess) { - hopSuccess = Rs2WorldUtil.hopToNextBestWorld(scheduledFuture, successiveWorldHopAttempts, worldHoppingConfig, excludedWorlds); - } - - if (hopSuccess) { - log.info("World hop successful after high stock levels in shop"); - continue; - } else { - Microbot.status = "Failed to hop worlds - shop stock too high"; - log.error("Failed to hop to a new world due to high stock levels in shop"); - return false; - } - } - // Brief pause between shop visits - sleep(1000, 2000); - } - - // Final status update - if (isAllItemsCompleted()) { - log.info("Successfully completed sale of all items to regular shop"); - } else { - log.warn("Sale incomplete after {} attempts, remaining items: {}", - successiveWorldHopAttempts, - shopItemRequirements.values().stream() - .filter(itemReq -> !itemReq.isCompleted()) - .map(ShopItemRequirement::getItemName) - .collect(Collectors.joining(", "))); - - } - - return isAllItemsCompleted(); - - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.sellToRegularShop", e); - return false; - } - } - - /** - * Banks purchased items for multiple shop items if banking is enabled. - * - * @return true if banking was successful, false otherwise - */ - private boolean bankItems() { - try { - Microbot.status = "Banking purchased items"; - - // Get all item names from our shop requirements - List itemNames = shopItemRequirements.keySet().stream() - .map(Rs2ShopItem::getItemName) - .collect(Collectors.toList()); - - // Use Rs2Bank utility for banking - boolean success = Rs2Bank.bankItemsAndWalkBackToOriginalPosition( - itemNames, - primaryShopItem.getLocation() - ); - - if (success) { - Microbot.status = "Successfully banked items"; - } else { - Microbot.status = "Failed to bank items"; - } - - return success; - - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.bankItems", e); - return false; - } - } - - - - /** - * Calculates the total cost for purchasing all required items with dynamic pricing. - * Uses Rs2ShopItem dynamic pricing calculations based on stock levels. - * - * @return The total cost in coins, or -1 if calculation failed - */ - private int calculateTotalCost() { - if (shopItemRequirements.isEmpty()) { - return -1; - } - - try { - int totalCost = 0; - - for (ShopItemRequirement itemReq : shopItemRequirements.values()) { - if (itemReq.isCompleted()) { - continue; // Skip completed items - } - - int currentCount = Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()) + - Rs2Bank.count(itemReq.getShopItem().getItemId()); - int amountToBuy = Math.max(0, itemReq.getAmount() - currentCount); - - if (amountToBuy > 0) { - int itemCost = itemReq.getShopItem().getCostForBuyingX(itemReq.getAmount(), amountToBuy); - if (itemCost > 0) { - totalCost += itemCost; - } - } - } - - return totalCost; - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.calculateTotalCost", e); - return -1; - } - } - - /** - * Calculates the total value for selling all items with dynamic pricing. - * Uses Rs2ShopItem dynamic pricing calculations based on stock levels. - * - * @return The total sell value in coins, or -1 if calculation failed - */ - @SuppressWarnings("unused") - private int calculateTotalSellValue() { - try { - int totalValue = 0; - - for (ShopItemRequirement itemReq : shopItemRequirements.values()) { - int currentCount = Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()); - - if (currentCount > 0) { - int itemValue = itemReq.getShopItem().getReturnForSellingX(itemReq.getAmount(), currentCount); - if (itemValue > 0) { - totalValue += itemValue; - } - } - } - - return totalValue; - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.calculateTotalSellValue", e); - return -1; - } - } - - /** - * Estimates the number of world hops needed based on stock levels and requirements for multiple items. - * - * @return Estimated world hops needed, or -1 if cannot estimate - */ - public boolean isFulfilled() { - // Check if all items are completed - return shopItemRequirements.values().stream().allMatch(ShopItemRequirement::isCompleted); - } - /** - * Implements the abstract fulfillRequirement method from the base Requirement class. - * Handles both buying and selling operations based on the operation type. - * - * @param executorService The ScheduledExecutorService on which fulfillment is running - * @return true if the requirement was successfully fulfilled, false otherwise - */ - @Override - public boolean fulfillRequirement(CompletableFuture scheduledFuture) { - try { - if (Microbot.getClient().isClientThread()) { - Microbot.log("Please run fulfillRequirement() on a non-client thread.", Level.ERROR); - return false; - } - - // Reset excluded worlds at the start of each fulfillment attempt - resetExcludedWorlds(); - - Microbot.status = "Fulfilling shop requirement: " + operation.name() + " " + getName(); - boolean success = false; - if (isFulfilled()) { - Microbot.status = "Shop requirement not fulfilled, proceeding with " + operation.name(); - log.info("Shop requirement already fulfilled: " + getName() + " (" + operation + ")"); - return true; // Already fulfilled, no need to proceed - } - - // Validate items can be sold (are tradeable) - for (ShopItemRequirement itemReq : shopItemRequirements.values()) { - boolean isAccessible = itemReq.getShopItem().canAccess(); - if (!itemReq.getShopItem().getItemComposition().isTradeable() || !isAccessible) { - Microbot.log("Item " + itemReq.getItemName() + " is not tradeable - cannot sell to shop or shop could not be accessed"); - return !isMandatory(); - } - //updateCompletedAmount(itemReq); - } - switch (operation) { - case BUY: - success = handleBuyOperation(scheduledFuture); - break; - case SELL: - success = handleSellOperation(scheduledFuture); - break; - default: - Microbot.log("Unknown shop operation: " + operation); - return false; - } - - if (!success && isMandatory()) { - Microbot.log("MANDATORY shop requirement failed: " + getName() + " (" + operation + ")"); - return false; - } - - return true; - - } catch (Exception e) { - Microbot.log("Error fulfilling shop requirement " + getName() + ": " + e.getMessage()); - return !isMandatory(); // Don't fail mandatory requirements due to exceptions - } - } - - /** - * Handles buy operations with proper separation between Grand Exchange and regular shops. - * - * @return true if the buy operation was successful, false otherwise - */ - private boolean handleBuyOperation(CompletableFuture scheduledFuture) { - try { - // Check if we already have enough items - if (isAllItemsCompleted()) { - Microbot.status = "Already have required amount of all items"; - log.info("All items already completed for shop requirement: " + getName()); - return true; - } - // Handle Grand Exchange vs Regular Shop differently - if (primaryShopItem.getShopType() == Rs2ShopType.GRAND_EXCHANGE) { - log.info("Handling Grand Exchange buy operation for shop requirement: " + getName()); - return handleGrandExchangeBuyOperation(scheduledFuture); - } else { - log.info("Handling regular shop buy operation for shop requirement: " + getName()); - return handleRegularShopBuyOperation(scheduledFuture); - } - - } catch (Exception e) { - Microbot.log("Error in buy operation: " + e.getMessage()); - return false; - } - } - - /** - * Handles buying multiple items from Grand Exchange - must wait for offers to complete. - */ - private boolean handleGrandExchangeBuyOperation(CompletableFuture scheduledFuture) { - try { - Microbot.status = "Buying items from Grand Exchange"; - - // Get coins from bank if needed - if (enableBanking && !ensureSufficientCoins()) { - Microbot.status = "Failed to get sufficient coins for Grand Exchange"; - log.info("Insufficient coins for Grand Exchange purchase: " + getName()); - return isMandatory() ? false : true; - } - - // Walk to Grand Exchange - if (!Rs2Walker.walkTo(primaryShopItem.getLocation(), primaryShopItem.getLocationArea().getHeight())) { - Microbot.status = "Failed to walk to Grand Exchange"; - log.info("Failed to walk to Grand Exchange for shop requirement: " + getName()); - return false; - } - - // Calculate total cost for all items - int totalCost = calculateTotalCost(); - - if (Rs2Inventory.itemQuantity("Coins") < totalCost) { - Microbot.status = "Insufficient coins for Grand Exchange purchase"; - return !isMandatory(); - } - - // Use the enhanced buyFromGrandExchange method that handles multiple items - return buyFromGrandExchange(scheduledFuture); - - } catch (Exception e) { - Microbot.log("Error in Grand Exchange buy operation: " + e.getMessage()); - return false; - } - } - - /** - * Handles buying multiple items from regular shops with stock management and world hopping. - */ - private boolean handleRegularShopBuyOperation(CompletableFuture scheduledFuture) { - try { - // Get coins from bank if needed - if (enableBanking && !ensureSufficientCoins()) { - Microbot.status = "Failed to get sufficient coins for shop purchase"; - return isMandatory() ? false : true; - } - - // For multi-item regular shop purchases, use the enhanced buyFromRegularShop method - return buyFromRegularShop(scheduledFuture); - - } catch (Exception e) { - Microbot.log("Error in regular shop buy operation: " + e.getMessage()); - return false; - } - } - - /** - * Handles sell operations with proper separation between Grand Exchange and regular shops. - * - * @return true if the sell operation was successful, false otherwise - */ - private boolean handleSellOperation( CompletableFuture scheduledFuture) { - try { - - - // Handle Grand Exchange vs Regular Shop differently - if (primaryShopItem.getShopType() == Rs2ShopType.GRAND_EXCHANGE) { - return handleGrandExchangeSellOperation(); - } else { - return handleRegularShopSellOperation(scheduledFuture); - } - - } catch (Exception e) { - Microbot.log("Error in sell operation for " + getName() + ": " + e.getMessage()); - return false; - } - } - - /** - * Handles selling to Grand Exchange - must wait for offers to complete. - */ - private boolean handleGrandExchangeSellOperation() { - return sellToGrandExchange(); - } - - /** - * Handles selling to regular shops with stock management and world hopping. - */ - private boolean handleRegularShopSellOperation(CompletableFuture scheduledFuture) { - try { - // Get items from bank at the beginning if banking is enabled - if (enableBanking) { - if (!getItemsFromBankForSelling()) { - Microbot.status = "Failed to get items from bank for selling"; - return !isMandatory(); // Not an error for optional requirements - } - } - - // Check if we have any items to sell in inventory - boolean hasItemsToSell = shopItemRequirements.values().stream() - .anyMatch(itemReq -> Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()) > 0); - - if (!hasItemsToSell) { - Microbot.status = "No items to sell"; - return !isMandatory(); // Not an error for optional requirements - } - - // Check if any items are noted and handle them appropriately - for (ShopItemRequirement itemReq : shopItemRequirements.values()) { - boolean hasNotedItems = Rs2Inventory.hasNotedItem(itemReq.getItemName()); - boolean isNoteable = itemReq.getShopItem().isNoteable(); - - if (hasNotedItems) { - if (handleNotedItems && !isNoteable) { - // Items are noted but shouldn't be - this is an error state - Microbot.log("Item " + itemReq.getItemName() + " is noted but is not noteable - inconsistent state"); - return false; - } - - if (!handleNotedItems && !unnoteItemsForSelling()) { - Microbot.status = "Failed to unnote " + itemReq.getItemName() + " for selling"; - return false; - } - } - } - - // Perform the sale to regular shop - boolean sellSuccess = sellToRegularShop(scheduledFuture); - - // After selling, bank coins if banking is enabled - if (sellSuccess && enableBanking) { - if (!bankCoinsAfterSelling()) { - Microbot.status = "Sale successful but failed to bank coins"; - // Don't fail the requirement just because banking failed - } - } - - return sellSuccess; - - } catch (Exception e) { - Microbot.log("Error in regular shop sell operation: " + e.getMessage()); - return false; - } - } - - /** - * Ensures the player has sufficient coins for buying operations. - * - * @return true if sufficient coins are available, false otherwise - */ - private boolean ensureSufficientCoins() { - try { - int totalCost = calculateTotalCost(); - if (totalCost <= 0) { - return true; // No cost or calculation failed - } - - int currentCoins = Rs2Inventory.itemQuantity("Coins") + Rs2Bank.count("Coins"); - if (currentCoins >= totalCost) { - return true; // Already have enough coins - } - - Microbot.status = "Getting coins from bank for " + getName(); - Rs2Bank.walkToBankAndUseBank(); - if (!Rs2Bank.isOpen()) { - Microbot.status = "Failed to open bank for coins"; - return false; - } - - int neededCoins = totalCost - currentCoins; - int bankCoins = Rs2Bank.count("Coins"); - - if (bankCoins < neededCoins) { - Microbot.status = "Insufficient coins in bank. Need " + neededCoins + ", have " + bankCoins; - Rs2Bank.closeBank(); - return false; - } - - Rs2Bank.withdrawX("Coins", neededCoins); - sleepUntil(() -> Rs2Inventory.itemQuantity("Coins") >= totalCost, 5000); - - Rs2Bank.closeBank(); - return Rs2Inventory.itemQuantity("Coins") >= totalCost; - - } catch (Exception e) { - Microbot.log("Error ensuring sufficient coins: " + e.getMessage()); - Rs2Bank.closeBank(); - return false; - } - } - - /** - * Enhanced banking method for selling operations with comprehensive item handling. - * Properly handles stackable/non-stackable and noted/unnoted items. - * - * @return true if items were successfully retrieved, false otherwise - */ - private boolean getItemsFromBankForSelling() { - try { - Microbot.status = "Getting items from bank for selling with enhanced logic"; - - if (!Rs2Bank.openBank()) { - Microbot.status = "Failed to open bank for items"; - return false; - } - - // **BANKING IMPROVEMENT**: Calculate inventory space needed for non-stackable items - int inventorySpaceNeeded = 0; - List itemsNeedingWithdrawal = new ArrayList<>(); - - for (ShopItemRequirement itemReq : shopItemRequirements.values()) { - int currentCount = Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()); - int neededCount = itemReq.getAmount() - currentCount; - - if (neededCount <= 0) { - continue; // Already have enough of this item - } - - // Check if item is available in bank - if (!Rs2Bank.hasItem(itemReq.getItemName())) { - Microbot.status = "Bank does not contain " + itemReq.getItemName() + ", cannot sell"; - continue; - } - - itemsNeedingWithdrawal.add(itemReq); - - // **BANKING IMPROVEMENT**: Count space needed for non-stackable items - boolean isStackable = itemReq.getShopItem().getItemComposition() != null && - itemReq.getShopItem().getItemComposition().isStackable(); - if (!isStackable && !itemReq.getShopItem().isNoteable()) { - inventorySpaceNeeded += Math.min(neededCount, 28); // Cap at inventory limit - } else { - inventorySpaceNeeded += 1; // Stackable or noted items take 1 slot - } - } - - // **BANKING IMPROVEMENT**: Check if we have enough inventory space - int currentInventoryCount = Rs2Inventory.count(); - int availableSpace = 28 - currentInventoryCount; - - if (inventorySpaceNeeded > availableSpace) { - Microbot.status = "Insufficient inventory space: need " + inventorySpaceNeeded + - ", have " + availableSpace + " - depositing items first"; - - // Deposit non-essential items to make space - Rs2Bank.depositAllExcept("Coins"); // Keep coins for potential fees - sleepUntil(() -> Rs2Inventory.count() <= 1, 3000); // Should only have coins - } - - boolean allItemsRetrieved = true; - - for (ShopItemRequirement itemReq : itemsNeedingWithdrawal) { - int currentCount = Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()); - int neededCount = itemReq.getAmount() - currentCount; - - // **BANKING IMPROVEMENT**: Smart withdrawal based on item properties - boolean isStackable = itemReq.getShopItem().getItemComposition() != null && - itemReq.getShopItem().getItemComposition().isStackable(); - boolean isNoteable = itemReq.getShopItem().isNoteable(); - - if (isNoteable && handleNotedItems) { - // **BANKING IMPROVEMENT**: Use noted items for efficient selling - Microbot.status = "Withdrawing " + neededCount + "x " + itemReq.getItemName() + " as noted"; - Rs2Bank.setWithdrawAs(true); // Withdraw as noted - Rs2Bank.withdrawX(itemReq.getShopItem().getItemName(), neededCount); - sleepUntil(() -> Rs2Inventory.itemQuantity(itemReq.getShopItem().getNoteId()) >= neededCount, 5000); - } else if (isStackable) { - // **BANKING IMPROVEMENT**: Stackable items can be withdrawn in full - Microbot.status = "Withdrawing " + neededCount + "x " + itemReq.getItemName() + " (stackable)"; - Rs2Bank.setWithdrawAs(false); // Withdraw as unnoted - Rs2Bank.withdrawX(itemReq.getShopItem().getItemName(), neededCount); - sleepUntil(() -> Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()) >= neededCount, 5000); - } else { - // **BANKING IMPROVEMENT**: Non-stackable items need careful space management - int availableSlots = 28 - Rs2Inventory.count(); - int withdrawAmount = Math.min(neededCount, availableSlots); - - if (withdrawAmount <= 0) { - Microbot.status = "No inventory space for non-stackable " + itemReq.getItemName(); - allItemsRetrieved = false; - continue; - } - - Microbot.status = "Withdrawing " + withdrawAmount + "x " + itemReq.getItemName() + - " (non-stackable, space limited)"; - Rs2Bank.setWithdrawAs(false); // Withdraw as unnoted - Rs2Bank.withdrawX(itemReq.getShopItem().getItemName(), withdrawAmount); - sleepUntil(() -> Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()) >= - (currentCount + withdrawAmount), 5000); - } - - // Brief pause between withdrawals - sleep(500, 200); - } - - Rs2Bank.setWithdrawAs(false); // Reset to default - Rs2Bank.closeBank(); - - return allItemsRetrieved; - - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.getItemsFromBankForSelling", e); - Rs2Bank.closeBank(); - return false; - } - } - - - /** - * Unnotes items for selling if they are noted and shop doesn't accept noted items. - * - * @return true if items were successfully unnoted, false otherwise - */ - private boolean unnoteItemsForSelling() { - try { - Microbot.status = "Unnoting items for selling"; - - boolean allItemsUnnoted = true; - - for (ShopItemRequirement itemReq : shopItemRequirements.values()) { - // Check if we have noted items for this shop item - int notedId = itemReq.getShopItem().getNoteId(); - if (notedId == -1 || !Rs2Inventory.hasItem(notedId)) { - continue; // No noted items or item is not noteable - } - - // For unnoting, we typically need to use the items on a banker or shop keeper - // This is a simplified implementation - in practice you would: - // 1. Find the nearest banker or the shop keeper - // 2. Use the noted items on them to unnote - // 3. Wait for the unnoting to complete - - // For now, we'll try to use the shop keeper from our primary shop source - if (primaryShopItem != null && primaryShopItem.getShopNPC() != null) { - // Walk to shop keeper - if (!Rs2Walker.walkTo(primaryShopItem.getLocation())) { - Microbot.status = "Failed to walk to shop keeper for unnoting " + itemReq.getItemName(); - allItemsUnnoted = false; - continue; - } - - // Find the shop keeper NPC - NPC shopKeeperNpc = Rs2Npc.getNpc(primaryShopItem.getShopNpcName()); - if (shopKeeperNpc == null) { - Microbot.status = "Shop keeper not found for unnoting " + itemReq.getItemName(); - allItemsUnnoted = false; - continue; - } - - Rs2NpcModel shopKeeper = new Rs2NpcModel(shopKeeperNpc); - - // Use noted items on shop keeper (this may need adjustment based on actual game mechanics) - if (Rs2Inventory.use(notedId)) { - sleep(600); // Game tick - if (Rs2Npc.interact(shopKeeper, "Use")) { - sleepUntil(() -> !Rs2Inventory.hasItem(notedId) || Rs2Inventory.itemQuantity(itemReq.getShopItem().getItemId()) > 0, 5000); - - if (Rs2Inventory.hasItem(notedId)) { - Microbot.log("Failed to unnote " + itemReq.getItemName()); - allItemsUnnoted = false; - } - } else { - allItemsUnnoted = false; - } - } else { - allItemsUnnoted = false; - } - } - } - - if (!allItemsUnnoted) { - Microbot.log("Unnoting feature needs specific implementation for some items"); - } - - return allItemsUnnoted; // Return success status based on all items - - } catch (Exception e) { - Microbot.log("Error unnoting items for selling: " + e.getMessage()); - return false; - } - } - - /** - * Banks coins after selling items. - * - * @return true if coins were successfully banked, false otherwise - */ - private boolean bankCoinsAfterSelling() { - try { - Microbot.status = "Banking coins after selling items"; - - if (!Rs2Bank.isNearBank(10)) { - Rs2Bank.walkToBank(); - } - if (!Rs2Bank.openBank()) { - Microbot.status = "Failed to open bank for banking coins"; - return false; - } - - Rs2Bank.depositAll("Coins"); - sleepUntil(() -> Rs2Inventory.itemQuantity("Coins") == 0, 5000); - - Rs2Bank.closeBank(); - return true; - - } catch (Exception e) { - Microbot.log("Error banking coins after selling: " + e.getMessage()); - Rs2Bank.closeBank(); - return false; - } - } - - /** - * Factory method to create a multi-item shop requirement for the same shop. - * All items must be from the same shop (same location and NPC). - * - * @param primaryShopItem The primary shop item (used for shop location/NPC validation) - * @param itemRequirements Map of item IDs to their amounts and individual settings - * @param operation Shop operation type (BUY or SELL) - * @param requirementType Where this item should be located - * @param priority Priority level of this item for plugin functionality - * @param rating Effectiveness rating from 1-10 - * @param description Human-readable description - * @param TaskContext When this requirement should be fulfilled - */ - public static ShopRequirement createMultiItemRequirement( - Rs2ShopItem primaryShopItem, - Map itemRequirements, - ShopOperation operation, - RequirementType requirementType, - RequirementPriority priority, - int rating, - String description, - TaskContext taskContext) { - - Map shopItems = new HashMap<>(); - - for (Map.Entry entry : itemRequirements.entrySet()) { - Rs2ShopItem shopItem = entry.getKey(); - MultiItemConfig config = entry.getValue(); - - shopItems.put(shopItem, new ShopItemRequirement( - shopItem, - config.amount, - config.stockTolerance // **UNIFIED SYSTEM**: Use stockTolerance directly - )); - } - - return new ShopRequirement(shopItems, operation, requirementType, priority, rating, description, taskContext); - } - - - - /** - * Checks if the player has enough coins to buy all required shop items. - * - * @return true if the player has sufficient funds, false otherwise - */ - public boolean hasSufficientCoinsForRequirements() { - int totalCost = calculateTotalCost(); - if (totalCost == -1) { - Microbot.log("Failed to calculate total cost for shop requirements"); - return false; - } - - if (totalCost <= 0) { - return true; - } - final List coinIDs = Arrays.asList(ItemID.COINS, ItemID.COINS_2, ItemID.COINS_3, ItemID.COINS_4,ItemID.COINS_5, ItemID.COINS_25, ItemID.COINS_100, ItemID.COINS_250,ItemID.COINS_1000, ItemID.COINS_10000); - - - - // Check if player has enough coins in inventory - int inventoryCoins =Rs2Inventory.itemQuantity(item -> coinIDs.contains(item.getId())); - boolean hasCoinsInInventory = inventoryCoins >= totalCost; - - if (hasCoinsInInventory) { - return true; - } - - // If not in inventory, check bank - if (Rs2Bank.isOpen() || Rs2Bank.openBank()) { - int bankCoins = Rs2Bank.count("Coins"); - boolean hasCoinsInBank = bankCoins >= (totalCost - inventoryCoins); - - if (hasCoinsInBank) { - // We need to use custom withdraw logic since Rs2Bank doesn't have a method - // to withdraw a specific amount directly - try { - Rs2Bank.withdrawX("Coins", totalCost - inventoryCoins); - - // Check if we now have enough coins - sleepUntil(() -> Rs2Inventory.itemQuantity(item -> coinIDs.contains(item.getId())) >= totalCost, 5000); - return Rs2Inventory.itemQuantity(item -> coinIDs.contains(item.getId())) >= totalCost; - } catch (Exception e) { - Microbot.log("Failed to withdraw coins: " + e.getMessage()); - return false; - } - } - } - - Microbot.log("Insufficient coins for shop requirements. Need " + totalCost + " coins."); - return false; - } - - /** - * Gets the current stock quantity for a given item name in the shop - * @param itemName The name of the item to check stock for - * @return The current stock quantity, or -1 if not found - */ - private int getShopStock(String itemName) { - if (Rs2Shop.shopItems == null || Rs2Shop.shopItems.isEmpty()) { - return -1; - } - - for (Rs2ItemModel item : Rs2Shop.shopItems) { - if (item.getName().equalsIgnoreCase(itemName)) { - return item.getQuantity(); - } - } - return -1; - } - - - - // ===== GRAND EXCHANGE SLOT MANAGEMENT ===== - - /** - * Enhanced Grand Exchange slot management with state tracking and recovery. - * Only clears the minimum number of slots needed and tracks cancelled offers for restoration. - * Uses proper game state checking and sleepUntil patterns for reliability. - * - * @param requiredSlots Number of slots needed for pending operations - * @return true if sufficient slots can be made available, false otherwise - */ - private boolean ensureGrandExchangeSlots(int requiredSlots) { - try { - if (!Rs2GrandExchange.isOpen()) { - Microbot.status = "Grand Exchange not open, cannot manage slots"; - return false; - } - - // Check current slot availability - int availableSlots = Rs2GrandExchange.getAvailableSlotsCount(); - - if (availableSlots >= requiredSlots) { - Microbot.status = "Sufficient GE slots available: " + availableSlots + "/" + requiredSlots; - return true; - } - - Microbot.status = "Need " + requiredSlots + " slots, have " + availableSlots + " - optimizing slot usage"; - - // Step 1: Try to collect completed offers first (most efficient approach) - if (Rs2GrandExchange.hasBoughtOffer() || Rs2GrandExchange.hasSoldOffer()) { - Microbot.status = "Collecting completed offers to free slots"; - Rs2GrandExchange.collectAll(enableBanking); - - // Wait for collection to complete and slots to update - boolean collectionSuccess = sleepUntil(() -> { - return Rs2GrandExchange.getAvailableSlotsCount() >= requiredSlots; - }, 8000); - - if (collectionSuccess) { - Microbot.status = "Freed sufficient slots by collecting completed offers"; - return true; - } - } - - // Step 2: If still insufficient, selectively cancel offers with least progress - int currentAvailable = Rs2GrandExchange.getAvailableSlotsCount(); - int slotsStillNeeded = requiredSlots - currentAvailable; - - if (slotsStillNeeded > 0) { - Microbot.status = "Selectively cancelling " + slotsStillNeeded + " offers with least progress"; - - // Get active offers sorted by progress (least progress first) - List activeSlots = Rs2GrandExchange.getActiveOfferSlotsByProgress(); - - if (activeSlots.size() < slotsStillNeeded) { - log.warn("Not enough active offers to cancel ({} available, need {})", - activeSlots.size(), slotsStillNeeded); - return false; - } - - // Cancel only the required number of slots with least progress - List slotsToCancel = activeSlots.subList(0, slotsStillNeeded); - List> cancelledDetails = Rs2GrandExchange.cancelSpecificOffers(slotsToCancel, enableBanking); - - // Track cancelled offers for potential recovery - trackCancelledOffers(cancelledDetails); - - // Wait for cancellations to complete and slots to become available - boolean cancellationSuccess = sleepUntil(() -> { - return Rs2GrandExchange.getAvailableSlotsCount() >= requiredSlots; - }, 12000); - - if (!cancellationSuccess) { - log.warn("Timeout waiting for slot cancellations to complete"); - return false; - } - } - - // Final verification - int finalAvailable = Rs2GrandExchange.getAvailableSlotsCount(); - boolean success = finalAvailable >= requiredSlots; - - if (success) { - Microbot.status = "Successfully allocated " + finalAvailable + " GE slots"; - if (!cancelledOffers.isEmpty()) { - log.info("Cancelled {} offers for slot allocation", cancelledOffers.size()); - } - } else { - log.error("Failed to allocate sufficient GE slots: need {}, have {}", - requiredSlots, finalAvailable); - } - - return success; - - } catch (Exception e) { - log.error("Error in ensureGrandExchangeSlots: {}", e.getMessage()); - Microbot.logStackTrace("ShopRequirement.ensureGrandExchangeSlots", e); - return false; - } - } - - /** - * Tracks cancelled offers for potential restoration. - * Only tracks offers that were actively BUYING or SELLING (not empty or completed). - * Converts from Rs2GrandExchange format to our CancelledOfferState objects. - * - * @param cancelledDetails List of cancelled offer details from Rs2GrandExchange - */ - private void trackCancelledOffers(List> cancelledDetails) { - for (Map details : cancelledDetails) { - try { - int itemId = (Integer) details.get("itemId"); - String itemName = details.containsKey("itemName") - ? (String) details.get("itemName") - : Microbot.getClient().getItemDefinition(itemId).getName(); - - int totalQuantity = (Integer) details.get("totalQuantity"); - int remainingQuantity = (Integer) details.get("remainingQuantity"); - int price = (Integer) details.get("price"); - boolean isBuyOffer = (Boolean) details.get("isBuyOffer"); - GrandExchangeSlots originalSlot = (GrandExchangeSlots) details.get("slot"); - - // Verify this was an active offer worth tracking - if (remainingQuantity > 0 && price > 0 && itemId > 0) { - CancelledOfferState cancelledOffer = new CancelledOfferState( - itemId, itemName, totalQuantity, remainingQuantity, - price, isBuyOffer, originalSlot - ); - - cancelledOffers.add(cancelledOffer); - log.info("Tracked cancelled {} offer for recovery: {} ({} items remaining at {} gp)", - isBuyOffer ? "BUY" : "SELL", itemName, remainingQuantity, price); - } else { - log.debug("Skipping tracking of empty/invalid offer: itemId={}, remaining={}, price={}", - itemId, remainingQuantity, price); - } - - } catch (Exception e) { - log.warn("Failed to track cancelled offer details: {}", e.getMessage()); - } - } - } - - /** - * Attempts to restore previously cancelled Grand Exchange offers. - * This should be called after completing buy/sell operations. - * Uses proper game state checking and sleepUntil patterns. - * - * @return Number of offers successfully restored - */ - private int restoreCancelledOffers() { - if (cancelledOffers.isEmpty()) { - return 0; - } - - try { - if (!Rs2GrandExchange.isOpen()) { - log.warn("Grand Exchange not open, cannot restore cancelled offers"); - return 0; - } - - Microbot.status = "Attempting to restore " + cancelledOffers.size() + " cancelled GE offers"; - log.info("Attempting to restore {} cancelled offers", cancelledOffers.size()); - - int restoredCount = 0; - Iterator iterator = cancelledOffers.iterator(); - - while (iterator.hasNext()) { - CancelledOfferState cancelledOffer = iterator.next(); - - // Skip offers that are no longer worth restoring - if (!cancelledOffer.isWorthRestoring()) { - log.debug("Skipping offer not worth restoring: {}", cancelledOffer); - iterator.remove(); - continue; - } - - // Check if we have available slots - if (Rs2GrandExchange.getAvailableSlotsCount() == 0) { - log.info("No available slots for restoration, stopping here"); - break; - } - - // Convert to the map format expected by restoreOffer - Map offerDetails = new HashMap<>(); - offerDetails.put("itemId", cancelledOffer.getItemId()); - offerDetails.put("itemName", cancelledOffer.getItemName()); - offerDetails.put("remainingQuantity", cancelledOffer.getRemainingQuantity()); - offerDetails.put("price", cancelledOffer.getPrice()); - offerDetails.put("isBuyOffer", cancelledOffer.isBuyOffer()); - - // Check if original slot is available, otherwise use any available slot - GrandExchangeSlots targetSlot = Rs2GrandExchange.isSlotAvailable(cancelledOffer.getOriginalSlot()) - ? cancelledOffer.getOriginalSlot() : null; - - Microbot.status = "Restoring " + cancelledOffer.getOperationType() + " offer for " + cancelledOffer.getItemName(); - - // Attempt to restore the offer - boolean restored = Rs2GrandExchange.restoreOffer(offerDetails, targetSlot); - - if (restored) { - // Wait for offer to be placed successfully - boolean offerPlaced = sleepUntil(() -> { - return Rs2GrandExchange.getAvailableSlotsCount() < Rs2GrandExchange.getAvailableSlotsCount() + 1; - }, 5000); - - if (offerPlaced) { - restoredCount++; - iterator.remove(); // Remove from our tracking list - log.info("Successfully restored offer: {}", cancelledOffer); - } else { - log.warn("Offer restoration timed out: {}", cancelledOffer); - } - - // Brief pause between restorations to avoid interface conflicts - sleep(Constants.GAME_TICK_LENGTH); - } else { - log.warn("Failed to restore offer: {}", cancelledOffer); - // Don't remove from list immediately, might retry later - } - } - - if (restoredCount > 0) { - Microbot.status = "Restored " + restoredCount + " cancelled GE offers"; - log.info("Successfully restored {} out of {} cancelled offers", - restoredCount, restoredCount + cancelledOffers.size()); - } else if (!cancelledOffers.isEmpty()) { - log.info("No offers could be restored, {} offers remain tracked", cancelledOffers.size()); - } - - return restoredCount; - - } catch (Exception e) { - log.error("Error during offer restoration: {}", e.getMessage()); - Microbot.logStackTrace("ShopRequirement.restoreCancelledOffers", e); - return 0; - } - } - - /** - * Clears all tracked cancelled offers (called when giving up on restoration). - */ - private void clearCancelledOffers() { - if (!cancelledOffers.isEmpty()) { - log.info("Clearing {} tracked cancelled offers", cancelledOffers.size()); - cancelledOffers.clear(); - } - } - - /** - * Allocates a Grand Exchange slot for tracking purposes. - * - * @param itemName Name of the item using the slot - * @param slots Number of slots to allocate - */ - /** - * Cancels any existing Grand Exchange offers for the specified item ID to prevent duplicates. - * Only cancels offers that are actively BUYING or SELLING (not completed or empty). - * Uses proper game state checking and sleepUntil patterns. - * - * @param itemId The item ID to check for existing offers - * @param itemName The item name for logging purposes - * @return true if any offers were cancelled, false otherwise - */ - private boolean cancelDuplicateOffers(int itemId, String itemName) { - if (!Rs2GrandExchange.isOpen()) { - log.warn("Grand Exchange not open, cannot cancel duplicate offers"); - return false; - } - - boolean cancelledAny = false; - - try { - GrandExchangeOffer[] offers = Microbot.getClient().getGrandExchangeOffers(); - - for (int slotIndex = 0; slotIndex < offers.length; slotIndex++) { - final int finalSlotIndex = slotIndex; // Make effectively final for lambda - GrandExchangeOffer offer = offers[slotIndex]; - - // Skip empty slots - if (offer == null || offer.getItemId() == 0) { - continue; - } - - // Check if this offer is for our item AND is actively buying/selling - if (offer.getItemId() == itemId) { - // Only cancel offers that are actively BUYING or SELLING - // Following the pattern from Rs2GrandExchange.java - boolean isActiveBuyOffer = offer.getState() == GrandExchangeOfferState.BUYING; - boolean isActiveSellOffer = offer.getState() == GrandExchangeOfferState.SELLING; - - if (isActiveBuyOffer || isActiveSellOffer) { - GrandExchangeSlots slot = GrandExchangeSlots.values()[slotIndex]; - String offerType = isActiveBuyOffer ? "BUY" : "SELL"; - - Microbot.status = "Cancelling duplicate " + offerType + " offer for " + itemName + " in slot " + (slot.ordinal() + 1); - log.info("Cancelling duplicate {} offer for {} in slot {} (state: {})", - offerType, itemName, slot.ordinal() + 1, offer.getState()); - - // Cancel the offer using Rs2GrandExchange utility - Rs2GrandExchange.abortOffer(itemName, enableBanking); - - // Wait for cancellation to complete with proper game state checking - boolean cancellationSuccess = sleepUntil(() -> { - GrandExchangeOffer[] updatedOffers = Microbot.getClient().getGrandExchangeOffers(); - if (finalSlotIndex >= updatedOffers.length) return false; - - GrandExchangeOffer updatedOffer = updatedOffers[finalSlotIndex]; - return updatedOffer == null || - updatedOffer.getItemId() == 0 || - updatedOffer.getItemId() != itemId || - (updatedOffer.getState() != GrandExchangeOfferState.BUYING && - updatedOffer.getState() != GrandExchangeOfferState.SELLING); - }, 5000); - - if (cancellationSuccess) { - cancelledAny = true; - log.info("Successfully cancelled duplicate {} offer for {}", offerType, itemName); - } else { - log.warn("Timeout waiting for duplicate {} offer cancellation for {}", offerType, itemName); - } - - // Brief pause between cancellations to avoid interface conflicts - sleep(Constants.GAME_TICK_LENGTH); - } else { - log.debug("Found offer for {} in slot {} but it's not active (state: {})", - itemName, slotIndex + 1, offer.getState()); - } - } - } - - // If any offers were cancelled, collect them and wait for interface to update - if (cancelledAny) { - // Allow time for cancellations to be processed - sleep(Constants.GAME_TICK_LENGTH * 2); - - // Collect cancelled offers to clear the interface - if (Rs2GrandExchange.hasBoughtOffer() || Rs2GrandExchange.hasSoldOffer()) { - Rs2GrandExchange.collectAll(enableBanking); - - // Wait for collection to complete - sleepUntil(() -> { - return !Rs2GrandExchange.hasBoughtOffer() && !Rs2GrandExchange.hasSoldOffer(); - }, 5000); - } - } - - } catch (Exception e) { - log.error("Error checking for duplicate offers for {}: {}", itemName, e.getMessage()); - Microbot.logStackTrace("ShopRequirement.cancelDuplicateOffers", e); - } - - return cancelledAny; - } - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/CancelledOfferState.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/CancelledOfferState.java deleted file mode 100644 index ba0106af490..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/CancelledOfferState.java +++ /dev/null @@ -1,171 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.models; - -import lombok.Getter; -import net.runelite.client.plugins.microbot.util.grandexchange.GrandExchangeSlots; - -/** - * Tracks the state of a cancelled Grand Exchange offer for potential recovery. - * This class stores all the necessary information to restore a cancelled offer - * at a later time, including item details, quantities, pricing, and timing. - * - *

Only tracks offers that were actively BUYING or SELLING when cancelled, - * ensuring we only restore meaningful offer states.

- * - *

The class is designed to be immutable to ensure data integrity and - * thread safety when tracking offer states across operations.

- * - * @author Enhanced GE Slot Management System - * @version 1.1 - */ -@Getter -public class CancelledOfferState { - - /** Maximum age for an offer to be considered still relevant (5 minutes) */ - public static final long DEFAULT_MAX_AGE_MS = 300_000L; - - private final int itemId; - private final String itemName; - private final int totalQuantity; - private final int remainingQuantity; - private final int price; - private final boolean isBuyOffer; - private final GrandExchangeSlots originalSlot; - private final long cancelledTime; - - /** - * Creates a new cancelled offer state with all required details. - * - * @param itemId The RuneScape item ID - * @param itemName The display name of the item - * @param totalQuantity The original total quantity in the offer - * @param remainingQuantity The quantity that was not yet bought/sold when cancelled - * @param price The price per item in the offer - * @param isBuyOffer True if this was a buy offer, false for sell offer - * @param originalSlot The Grand Exchange slot this offer was in - */ - public CancelledOfferState(int itemId, String itemName, int totalQuantity, - int remainingQuantity, int price, boolean isBuyOffer, - GrandExchangeSlots originalSlot) { - this.itemId = itemId; - this.itemName = itemName; - this.totalQuantity = totalQuantity; - this.remainingQuantity = remainingQuantity; - this.price = price; - this.isBuyOffer = isBuyOffer; - this.originalSlot = originalSlot; - this.cancelledTime = System.currentTimeMillis(); - } - - /** - * Checks if this cancelled offer is still relevant for restoration. - * Uses the default maximum age of 5 minutes. - * - * @return true if the offer is still recent enough to be relevant - */ - public boolean isStillRelevant() { - return isStillRelevant(DEFAULT_MAX_AGE_MS); - } - - /** - * Checks if this cancelled offer is still relevant for restoration. - * - * @param maxAgeMs Maximum age in milliseconds for the offer to be considered relevant - * @return true if the offer is still within the acceptable age limit - */ - public boolean isStillRelevant(long maxAgeMs) { - return System.currentTimeMillis() - cancelledTime < maxAgeMs; - } - - /** - * Validates if this offer is worth restoring based on game logic. - * Checks for valid item ID, remaining quantity, and reasonable price. - * - * @return true if this offer should be restored - */ - public boolean isWorthRestoring() { - return itemId > 0 && - remainingQuantity > 0 && - price > 0 && - isStillRelevant() && - originalSlot != null; - } - - /** - * Gets the age of this cancelled offer in milliseconds. - * - * @return The time elapsed since the offer was cancelled - */ - public long getAge() { - return System.currentTimeMillis() - cancelledTime; - } - - /** - * Checks if this offer has any remaining quantity worth restoring. - * - * @return true if there are items remaining to be bought/sold - */ - public boolean hasRemainingQuantity() { - return remainingQuantity > 0; - } - - /** - * Calculates the progress percentage of the original offer. - * - * @return A value between 0.0 and 1.0 representing completion percentage - */ - public double getProgressPercentage() { - if (totalQuantity <= 0) { - return 0.0; - } - int completedQuantity = totalQuantity - remainingQuantity; - return (double) completedQuantity / totalQuantity; - } - - /** - * Gets the operation type as a readable string. - * - * @return "BUY" or "SELL" depending on the offer type - */ - public String getOperationType() { - return isBuyOffer ? "BUY" : "SELL"; - } - - /** - * Creates a summary string suitable for logging and debugging. - * - * @return A formatted string containing key offer details - */ - public String getSummary() { - return String.format("%s %d/%d %s at %d gp (slot %s, age: %.1fs)", - getOperationType(), remainingQuantity, totalQuantity, - itemName, price, originalSlot, getAge() / 1000.0); - } - - @Override - public String toString() { - return getSummary(); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || getClass() != obj.getClass()) return false; - - CancelledOfferState that = (CancelledOfferState) obj; - return itemId == that.itemId && - price == that.price && - isBuyOffer == that.isBuyOffer && - originalSlot == that.originalSlot && - cancelledTime == that.cancelledTime; - } - - @Override - public int hashCode() { - int result = itemId; - result = 31 * result + price; - result = 31 * result + (isBuyOffer ? 1 : 0); - result = 31 * result + (originalSlot != null ? originalSlot.hashCode() : 0); - result = 31 * result + (int) (cancelledTime ^ (cancelledTime >>> 32)); - return result; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/MultiItemConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/MultiItemConfig.java deleted file mode 100644 index db43e873240..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/MultiItemConfig.java +++ /dev/null @@ -1,57 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.models; - -import lombok.Getter; -import net.runelite.client.plugins.microbot.Microbot; -/** - * Enhanced configuration class for individual items in a multi-item requirement. - * Updated to use the unified stock management system. - */ -public class MultiItemConfig { - public final int amount; - public final int stockTolerance; // **UNIFIED SYSTEM**: Replaces minimumStock + maxQuantityPerVisit - - /** - * Creates a new MultiItemConfig with unified stock management. - * - * @param amount Total amount needed for this item - * @param stockTolerance Stock tolerance around baseStock (affects both min stock and max per visit) - */ - public MultiItemConfig(int amount, int stockTolerance) { - this.amount = amount; - this.stockTolerance = stockTolerance; - } - - /** - * Creates a new MultiItemConfig with default stock tolerance. - * - * @param amount Total amount needed for this item - */ - public MultiItemConfig(int amount) { - this(amount, 10); // Default tolerance of 10 - } - - /** - * Legacy constructor for backward compatibility. - * Converts old minimumStock/maxQuantityPerVisit to unified stockTolerance. - * - * @param amount Total amount needed - * @param minimumStock Legacy minimum stock (ignored in new system) - * @param maxQuantityPerVisit Legacy max per visit (used as stockTolerance) - */ - @Deprecated - public MultiItemConfig(int amount, int minimumStock, int maxQuantityPerVisit) { - this.amount = amount; - this.stockTolerance = maxQuantityPerVisit; // Use maxQuantityPerVisit as tolerance - - // Log the conversion for debugging - if (minimumStock != 5) { // 5 was the old default - Microbot.log("MultiItemConfig: Converting legacy minimumStock=" + minimumStock + - " to unified stockTolerance=" + this.stockTolerance); - } - } - - @Override - public String toString() { - return String.format("MultiItemConfig{amount=%d, stockTolerance=%d}", amount, stockTolerance); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/ShopOperation.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/ShopOperation.java deleted file mode 100644 index da3bb58ebc3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/requirement/shop/models/ShopOperation.java +++ /dev/null @@ -1,9 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.models; - -/** - * Enum defining the type of shop operation. - */ -public enum ShopOperation { - BUY, // Purchase items from the shop - SELL // Sell items to the shop -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/ConditionalRequirementBuilder.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/ConditionalRequirementBuilder.java deleted file mode 100644 index 6dc1750b214..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/ConditionalRequirementBuilder.java +++ /dev/null @@ -1,232 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.util; - -import net.runelite.api.Skill; -import net.runelite.api.ItemID; -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location.LocationRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.SpellbookRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.ConditionalRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.OrderedRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.OrRequirement; -import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; -import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; -import net.runelite.client.plugins.microbot.util.magic.Rs2Magic; -import net.runelite.client.plugins.microbot.util.magic.Rs2Spellbook; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -import java.util.Arrays; -import java.util.function.BooleanSupplier; -import java.util.stream.Collectors; - -/** - * Utility class for creating common conditional and ordered requirements. - * Provides pre-built conditions and requirement patterns for typical OSRS workflows. - */ -public class ConditionalRequirementBuilder { - - /** - * Creates a spellbook switching conditional requirement. - * Only switches if the player has the required magic level and doesn't already have the spellbook. - * - * @param targetSpellbook The spellbook to switch to - * @param requiredLevel Minimum magic level required - * @param priority Priority level for this requirement - * @param TaskContext When to fulfill this requirement - * @return ConditionalRequirement for spellbook switching - */ - public static ConditionalRequirement createSpellbookSwitcher(Rs2Spellbook targetSpellbook, int requiredLevel, - RequirementPriority priority, TaskContext taskContext) { - ConditionalRequirement spellbookSwitcher = new ConditionalRequirement( - priority, 8, "Smart Spellbook Switching", taskContext, false - ); - - // Only switch if we have the level and don't already have the spellbook - BooleanSupplier needsSpellbookSwitch = () -> - Rs2Player.getRealSkillLevel(Skill.MAGIC) >= requiredLevel && ! Rs2Magic.isSpellbook(targetSpellbook); - - SpellbookRequirement spellbookReq = new SpellbookRequirement( - targetSpellbook, taskContext, priority, 8, - "Switch to " + targetSpellbook + " spellbook" - ); - - spellbookSwitcher.addStep(needsSpellbookSwitch, spellbookReq, - "Check magic level and switch to " + targetSpellbook + " if needed", true); - - return spellbookSwitcher; - } - - /** - * Creates an equipment upgrade conditional requirement. - * Upgrades to better equipment if the player can afford it and doesn't already have it. - * - * @param basicItemIds Basic equipment item IDs (always required) - * @param upgradeItemIds Upgrade equipment item IDs (conditional) - * @param minGpRequired Minimum GP required for upgrade - * @param equipmentSlot Equipment slot type - * @param description Description for the requirement - * @param priority Priority level - * @param TaskContext When to fulfill this requirement - * @return ConditionalRequirement for equipment upgrading - */ - public static ConditionalRequirement createEquipmentUpgrader(int[] basicItemIds, int[] upgradeItemIds, - int minGpRequired, EquipmentInventorySlot equipmentSlot, - String description, RequirementPriority priority, - TaskContext taskContext) { - ConditionalRequirement equipmentUpgrader = new ConditionalRequirement( - priority, 7, "Smart Equipment Upgrading: " + description, taskContext, false - ); - - // Step 1: Ensure we have basic equipment - OrRequirement basicEquipment = ItemRequirement.createOrRequirement( - Arrays.stream(basicItemIds).boxed().collect(Collectors.toList()),1, equipmentSlot,-2, - priority, 6, "Basic " + description, taskContext - ); - - equipmentUpgrader.addStep( - () -> !hasAnyItem(basicItemIds) && !hasAnyItem(upgradeItemIds), - basicEquipment, - "Get basic " + description + " if none available" - ); - - // Step 2: Upgrade if affordable and beneficial - if (upgradeItemIds.length > 0 && minGpRequired > 0) { - OrRequirement upgradeEquipment = ItemRequirement.createOrRequirement( - Arrays.stream(upgradeItemIds).boxed().collect(Collectors.toList()), equipmentSlot, - RequirementPriority.RECOMMENDED, 9, "Upgraded " + description, taskContext - ); - - equipmentUpgrader.addStep( - () -> !hasAnyItem(upgradeItemIds) && hasGP(minGpRequired), - upgradeEquipment, - "Upgrade to better " + description + " if affordable", - true // Optional upgrade - ); - } - - return equipmentUpgrader; - } - - /** - * Creates a shop-then-equip ordered requirement. - * First shops for items, then ensures they are equipped. - * - * @param shopLocation Where to shop - * @param itemIds Items to shop for - * @param itemName Display name for items - * @param quantity How many to buy - * @param priority Priority level - * @param TaskContext When to fulfill this requirement - * @return OrderedRequirement for shop-then-equip workflow - */ - public static OrderedRequirement createShopThenEquip(BankLocation shopLocation, int[] itemIds, - String itemName, int quantity, RequirementPriority priority, - TaskContext taskContext) { - OrderedRequirement shopThenEquip = new OrderedRequirement( - priority, 8, "Shop and Equip: " + itemName, taskContext - ); - - // Step 1: Go to shop location - LocationRequirement location = new LocationRequirement( - shopLocation, true,-1, taskContext, priority - ); - shopThenEquip.addStep(location, "Travel to " + shopLocation.name() + " for shopping"); - - // Step 2: Shop for items (assuming ShopRequirement exists) - // This is a placeholder - you'll need to implement ShopRequirement.createBuyRequirement - // ShopRequirement shopReq = ShopRequirement.createBuyRequirement(itemIds[0], quantity, priority); - // shopThenEquip.addStep(shopReq, "Buy " + quantity + "x " + itemName); - - // Step 3: Equip the items (Note: This is a simplified example - you may need to specify equipment slot) -// ItemRequirement equipmentReq = ItemRequirement.createOrRequirement( - // Arrays.stream(itemIds).boxed().collect(Collectors.toList()), null,null,-1, - // priority, 7, "Equipped " + itemName, TaskContext - // ); - // shopThenEquip.addStep(equipmentReq, "Equip " + itemName); - - return shopThenEquip; - } - - /** - * Creates a bank-preparation ordered requirement. - * Ensures player is at bank, withdraws needed items, and organizes inventory. - * - * @param bankLocation Preferred bank location - * @param withdrawItems Items to withdraw from bank - * @param priority Priority level - * @param TaskContext When to fulfill this requirement - * @return OrderedRequirement for bank preparation - */ - public static OrderedRequirement createBankPreparation(BankLocation bankLocation, ItemRequirement[] withdrawItems, - RequirementPriority priority, TaskContext taskContext) { - OrderedRequirement bankPrep = new OrderedRequirement( - priority, 9, "Bank Preparation", taskContext - ); - - // Step 1: Go to bank - LocationRequirement bankLocationReq = new LocationRequirement( - bankLocation, true, -1,taskContext, priority - ); - bankPrep.addStep(bankLocationReq, "Travel to " + bankLocation.name() + " bank"); - - // Step 2: Open bank - // This would need a custom requirement for opening bank - // bankPrep.addStep(new CustomRequirement(() -> Rs2Bank.openBank()), "Open bank"); - - // Step 3: Withdraw each required item - for (int i = 0; i < withdrawItems.length; i++) { - ItemRequirement item = withdrawItems[i]; - bankPrep.addStep(item, "Withdraw " + item.getName(), !item.isMandatory()); - } - - return bankPrep; - } - - /** - * Creates a level-based conditional requirement. - * Only fulfills the requirement if the player has sufficient level. - * - * @param skill Required skill - * @param requiredLevel Minimum level required - * @param requirement Requirement to fulfill if level is met - * @param description Description for this conditional - * @param priority Priority level - * @param TaskContext When to fulfill this requirement - * @return ConditionalRequirement based on skill level - */ - public static ConditionalRequirement createLevelBasedRequirement(Skill skill, int requiredLevel, - Requirement requirement, String description, - RequirementPriority priority, TaskContext taskContext) { - ConditionalRequirement levelBased = new ConditionalRequirement( - priority, 8, "Level-based: " + description, taskContext, false - ); - - BooleanSupplier hasLevel = () ->Rs2Player.getRealSkillLevel(skill) >= requiredLevel; - - levelBased.addStep(hasLevel, requirement, - description + " (requires " + skill.getName() + " level " + requiredLevel + ")", true); - - return levelBased; - } - - // Helper methods for common conditions - private static boolean isCurrentSpellbook(Rs2Spellbook spellbook) { - return Rs2Magic.isSpellbook(spellbook); - } - - private static boolean hasAnyItem(int[] itemIds) { - for (int itemId : itemIds) { - if (Rs2Inventory.hasItem(itemId)) { - return true; - } - } - return false; - } - - private static boolean hasGP(int amount) { return Rs2Inventory.hasItem(ItemID.COINS) && - Rs2Inventory.itemQuantity(ItemID.COINS) >= amount; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSelector.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSelector.java deleted file mode 100644 index 4ad1036dfb2..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSelector.java +++ /dev/null @@ -1,314 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.util; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementType; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.InventorySetupPlanner; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.item.ItemRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.LogicalRequirement; - -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Utility class for selecting the best available items to fulfill requirements. - * Contains logic for finding optimal items based on availability, priority, and constraints. - */ -@Slf4j -public class RequirementSelector { - - /** - * Finds the best available item from a list of logical requirements. - * - * @param logicalReqs List of logical requirements to evaluate - * @return The best available ItemRequirement, or null if none found - */ - public static ItemRequirement findBestAvailableItemForInventory(List logicalReqs, int inventorySlot) { - log.debug("Finding best available item from {} logical requirements", logicalReqs.size()); - - for (LogicalRequirement logicalReq : logicalReqs) { - List items = LogicalRequirement.extractItemRequirementsFromLogical(logicalReq); - log.debug("Checking {} items in logical requirement: {}", items.size(), logicalReq.displayString()); - - for (ItemRequirement item : items) { - if (isItemAvailable(item) && canPlayerUse(item)) { - log.debug("Found available item: {}", item.displayString()); - return item; - } - } - } - - log.debug("No available items found in any logical requirement"); - return null; - } - - /** - * Finds the best available item that hasn't already been planned for use. - * - * @param logicalReqs List of logical requirements to evaluate - * @param plan Current inventory setup plan - * @return The best available ItemRequirement not already planned, or null if none found - */ - public static ItemRequirement findBestAvailableItemNotAlreadyPlannedForInventory(LinkedHashSet items, InventorySetupPlanner plan) { - - for (ItemRequirement item : items) { - if (isItemAvailable(item) && - canPlayerUse(item) && - !isItemAlreadyPlanned(item, plan)) { - log.debug("Found available item not already planned: {}", item.displayString()); - return item; - } - } - - log.debug("No available items found that aren't already planned"); - return null; - } - - - - /** - * Enhanced method for finding the best available item for a specific equipment slot. - * This method considers already planned items to avoid conflicts and validates equipment slot compatibility. - * - * @param logicalReqs The logical requirements to search through - * @param targetEquipmentSlot The specific equipment slot to find an item for - * @param alreadyPlanned Items already planned (for conflict avoidance) - * @return The best available item for the slot, or null if none found - */ - public static ItemRequirement findBestAvailableItemForEquipmentSlot(List equipmentSlotReqs, - EquipmentInventorySlot targetEquipmentSlot, - Set alreadyPlanned) { - - List items = equipmentSlotReqs; - - // Sort by priority, then by type priority: EQUIPMENT > EITHER > INVENTORY, then by rating - items.sort((a, b) -> { - // First sort by priority (MANDATORY > RECOMMENDED > OPTIONAL) - int priorityCompare = a.getPriority().compareTo(b.getPriority()); - if (priorityCompare != 0) { - return priorityCompare; - } - - // Then sort by type priority: EQUIPMENT > EITHER > INVENTORY - int typePriorityA = getTypePriority(a.getRequirementType()); - int typePriorityB = getTypePriority(b.getRequirementType()); - - if (typePriorityA != typePriorityB) { - return Integer.compare(typePriorityB, typePriorityA); // Higher priority first - } - - // Finally by rating (higher is better) - return Integer.compare(b.getRating(), a.getRating()); - }); - - for (ItemRequirement item : items) { - // Skip if already planned - if (alreadyPlanned.contains(item)) { - continue; - } - - // Validate that this item can be assigned to the target equipment slot and meets skill requirements - if (!canAssignToEquipmentSlot(item, targetEquipmentSlot)) { - log.debug("Item {} cannot be assigned to equipment slot {}, skipping", - item.getName(), targetEquipmentSlot.name()); - continue; - } - - // Check if item is available - if (isItemAvailable(item)) { - log.debug("Found compatible item {} for equipment slot {}", - item.getName(), targetEquipmentSlot.name()); - return item; - } - } - - return null; - } - - /** - * Gets the type priority for sorting equipment requirements. - * Higher numbers = higher priority. - */ - private static int getTypePriority(RequirementType type) { - switch (type) { - case EQUIPMENT: - return 3; - case EITHER: - return 2; - case INVENTORY: - return 1; - default: - return 0; - } - } - - /** - * Validates if two ItemRequirements represent the same logical requirement. - * This accounts for the fact that requirements may be copied or modified during planning. - * - * @param original The original requirement - * @param planned The planned requirement - * @return true if they match, false otherwise - */ - public static boolean itemRequirementMatches(ItemRequirement original, ItemRequirement planned) { - // Check if they have overlapping item IDs - return original.getIds().stream().anyMatch(planned.getIds()::contains) && - original.getAmount() <= planned.getAmount(); - } - - /** - * Finds items that can fulfill multiple requirements simultaneously. - * Useful for optimizing inventory space. - * - * @param logicalReqs The logical requirements to analyze - * @return Map of items to the number of requirements they can fulfill - */ - public static Map findMultiPurposeItems(List logicalReqs) { - Map multiPurposeMap = new HashMap<>(); - List allItems = LogicalRequirement.extractAllItemRequirements(logicalReqs); - - for (ItemRequirement item : allItems) { - int count = 0; - for (ItemRequirement other : allItems) { - if (itemRequirementMatches(item, other)) { - count++; - } - } - if (count > 1) { - multiPurposeMap.put(item, count); - } - } - - return multiPurposeMap; - } - - - - /** - * Checks if an item is currently available (in inventory, equipment, or bank). - * Dummy items are always considered "available" since they're just slot placeholders. - * Uses the ItemRequirement's own availability checking methods which properly handle fuzzy matching and amounts. - * - * @param item ItemRequirement to check - * @return true if the item is available - */ - public static boolean isItemAvailable(ItemRequirement item) { - // Dummy items are always considered available since they're just slot placeholders - if (item.isDummyItemRequirement()) { - return true; - } - - // Use ItemRequirement's own availability checking which handles fuzzy matching and amounts properly - return item.isAvailableInInventoryOrBank(); - } - - /** - * Validates if the player can use or equip an item based on skill requirements. - * Checks both equipment and usage skill requirements. - * - * @param item ItemRequirement to validate - * @return true if player meets all skill requirements for the item - */ - public static boolean canPlayerUse(ItemRequirement item) { - // Dummy items can always be used - if (item.isDummyItemRequirement()) { - return true; - } - // For all items, check usage requirements if specified - if (!ItemRequirement.canPlayerUseItem(item)) { - log.debug("Player cannot use item {} due to skill requirements", item.getName()); - return false; - } - - return true; - } - - /** - * Validates if an item can be assigned to a specific equipment slot and meets skill requirements. - * - * @param item ItemRequirement to validate - * @param targetEquipmentSlot Target equipment slot - * @return true if item can be assigned to the slot and player meets requirements - */ - public static boolean canAssignToEquipmentSlot(ItemRequirement item, EquipmentInventorySlot targetEquipmentSlot) { - // Dummy items can always be assigned - if (item.isDummyItemRequirement()) { - return true; - } - - // Item must be able to be equipped - if (!item.canBeEquipped()) { - log.debug("Item {} cannot be equipped", item.getName()); - return false; - } - - // Check if item matches the equipment slot - if (item.getEquipmentSlot() != null && item.getEquipmentSlot() != targetEquipmentSlot) { - log.debug("Item {} equipment slot mismatch: expected {}, actual {}", - item.getName(), targetEquipmentSlot, item.getEquipmentSlot()); - return false; - } - - // Check skill requirements for equipping - if (!ItemRequirement.canPlayerEquipItem(item)) { - log.debug("Player cannot equip item {} due to skill requirements", item.getName()); - return false; - } - // Check skill requirements for equipping - if (!ItemRequirement.canPlayerUseItem(item)) { - log.debug("Player cannot use item {} due to skill requirements", item.getName()); - return false; - } - - return true; - } - - - - /** - * Checks if an item is already planned for use in the current plan. - * - * @param item ItemRequirement to check - * @param plan Current inventory setup plan - * @return true if the item is already planned - */ - private static boolean isItemAlreadyPlanned(ItemRequirement item, InventorySetupPlanner plan) { - // Check equipment assignments - for (ItemRequirement plannedItem : plan.getEquipmentAssignments().values()) { - if (plannedItem != null && hasMatchingItemId(plannedItem, item)) { - return true; - } - } - - // Check inventory slot assignments - for (ItemRequirement plannedItem : plan.getInventorySlotAssignments().values()) { - if (plannedItem != null && hasMatchingItemId(plannedItem, item)) { - return true; - } - } - - // Check flexible inventory items - for (ItemRequirement plannedItem : plan.getFlexibleInventoryItems()) { - if (hasMatchingItemId(plannedItem, item)) { - return true; - } - } - - return false; - } - - /** - * Checks if two ItemRequirements have matching item IDs. - * - * @param item1 First item requirement - * @param item2 Second item requirement - * @return true if they have at least one matching item ID - */ - private static boolean hasMatchingItemId(ItemRequirement item1, ItemRequirement item2) { - // Since ItemRequirement now only supports single IDs, simply compare them - return item1.getId() == item2.getId(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSolver.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSolver.java deleted file mode 100644 index 66bf02c5e62..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/requirements/util/RequirementSolver.java +++ /dev/null @@ -1,347 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.util; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementMode; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.registry.RequirementRegistry; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.location.LocationRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.ShopRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.state.TaskExecutionState; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.SpellbookRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.ConditionalRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.conditional.OrderedRequirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.logical.LogicalRequirement; -import org.slf4j.event.Level; - -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -/** - * Utility class for solving different types of requirements with common patterns. - * Provides reusable fulfillment logic and error handling patterns. - * - * This is the unified fulfillment system that handles both standard and external requirements - * using the same logic patterns and error handling. - */ -@Slf4j -public class RequirementSolver { - - /** - * Fulfills shop requirements for the specified schedule context. - * Uses the unified filtering system to automatically handle pre/post schedule requirements. - * - * @param shopRequirements The shop requirements to fulfill - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @return true if all shop requirements were fulfilled successfully, false otherwise - */ - public static boolean fulfillShopRequirements(CompletableFuture scheduledFuture,List shopRequirements, TaskContext context) { - List contextReqs = shopRequirements.stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(java.util.stream.Collectors.toList()); - - if (contextReqs.isEmpty()) { - log.debug("No shop requirements for context: {}", context); - return true; - } - - boolean success = true; - int fulfilled = 0; - - for (int i = 0; i < contextReqs.size(); i++) { - ShopRequirement requirement = contextReqs.get(i); - - try { - log.info("Processing shop requirement {}/{}: {}", i + 1, contextReqs.size(), requirement.getName()); - boolean requirementFulfilled = requirement.fulfillRequirement(scheduledFuture); - - if (requirementFulfilled) { - fulfilled++; - } else { - if (requirement.isMandatory()) { - log.error("Failed to fulfill mandatory shop requirement: {}", requirement.getName()); - success = false; - break; // Stop on mandatory failure - } else { - log.debug("Failed to fulfill optional shop requirement: {}", requirement.getName()); - } - } - } catch (Exception e) { - log.error("Error fulfilling shop requirement {}: {}", requirement.getName(), e.getMessage()); - if (requirement.isMandatory()) { - success = false; - } - } - } - - log.info("Shop requirements fulfillment completed. Success: {}, Fulfilled: {}/{}", success, fulfilled, contextReqs.size()); - return success; - } - - /** - * Fulfills loot requirements for the specified schedule context. - * Uses the unified filtering system to automatically handle pre/post schedule requirements. - * - * @param lootLogical The logical loot requirements to fulfill - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @return true if all loot requirements were fulfilled successfully, false otherwise - */ - public static boolean fulfillLootRequirements(CompletableFuture scheduledFuture, List lootLogical, TaskContext context) { - List contextReqs = LogicalRequirement.filterByContext(lootLogical, context); - - if (contextReqs.isEmpty()) { - log.debug("No loot requirements for context: {}", context); - return true; - } - - return LogicalRequirement.fulfillLogicalRequirements(scheduledFuture,contextReqs, "loot"); - } - - /** - * Fulfills location requirements for the specified schedule context. - * Uses the unified filtering system to automatically handle pre/post schedule requirements. - * - * @param locationReqs The location requirements to fulfill - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @return true if all location requirements were fulfilled successfully, false otherwise - */ - public static boolean fulfillLocationRequirements(CompletableFuture scheduledFuture, List locationReqs, TaskContext context) { - List contextReqs = locationReqs.stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(java.util.stream.Collectors.toList()); - - if (contextReqs.isEmpty()) { - log.debug("No location requirements for context: {}", context); - return true; - } - - boolean success = true; - int fulfilled = 0; - - for (int i = 0; i < contextReqs.size(); i++) { - LocationRequirement requirement = contextReqs.get(i); - if (scheduledFuture != null && scheduledFuture.isCancelled()) { - log.warn("Scheduled future is cancelled, skipping location requirement fulfillment: {}", requirement.getName()); - return false; // Skip if scheduled future is cancelled - } - try { - log.debug("Processing location requirement {}/{}: {}", i + 1, contextReqs.size(), requirement.getName()); - boolean requirementFulfilled = requirement.fulfillRequirement(scheduledFuture); - - if (requirementFulfilled) { - fulfilled++; - } else { - if (requirement.isMandatory()) { - log.error("Failed to fulfill mandatory location requirement: {}", requirement.getName()); - success = false; - break; // Stop on mandatory failure - } else { - log.debug("Failed to fulfill optional location requirement: {}", requirement.getName()); - } - } - } catch (Exception e) { - log.error("Error fulfilling location requirement {}: {}", requirement.getName(), e.getMessage()); - if (requirement.isMandatory()) { - success = false; - } - } - } - - log.debug("Location requirements fulfillment completed. Success: {}, Fulfilled: {}/{}", success, fulfilled, contextReqs.size()); - return success; - } - - /** - * Fulfills spellbook requirements for the specified schedule context. - * - * @param spellbookReqs The spellbook requirements to fulfill - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @param saveCurrentSpellbook Whether to save the current spellbook before switching - * @return true if all spellbook requirements were fulfilled successfully, false otherwise - */ - public static boolean fulfillSpellbookRequirements(CompletableFuture scheduledFuture, List spellbookReqs, - TaskContext context, - boolean saveCurrentSpellbook) { - List contextReqs = spellbookReqs.stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(java.util.stream.Collectors.toList()); - - if (contextReqs.isEmpty()) { - log.debug("No spellbook requirements for context: {}", context); - return true; - } - - boolean success = true; - int fulfilled = 0; - - for (int i = 0; i < contextReqs.size(); i++) { - SpellbookRequirement requirement = contextReqs.get(i); - - try { - log.debug("Processing spellbook requirement {}/{}: {}", i + 1, contextReqs.size(), requirement.getName()); - - // Save current spellbook if requested (typically for PRE_SCHEDULE context) - if (saveCurrentSpellbook) { - // This would need to be handled by the calling class as it maintains state - log.debug("Spellbook saving requested (handled by caller)"); - } - - boolean requirementFulfilled = requirement.fulfillRequirement(scheduledFuture); - - if (requirementFulfilled) { - fulfilled++; - } else { - if (requirement.isMandatory()) { - log.error("Failed to fulfill mandatory spellbook requirement: {}", requirement.getName()); - success = false; - break; // Stop on mandatory failure - } else { - log.debug("Failed to fulfill optional spellbook requirement: {}", requirement.getName()); - } - } - } catch (Exception e) { - log.error("Error fulfilling spellbook requirement {}: {}", requirement.getName(), e.getMessage()); - if (requirement.isMandatory()) { - success = false; - } - } - } - - log.debug("Spellbook requirements fulfillment completed. Success: {}, Fulfilled: {}/{}", success, fulfilled, contextReqs.size()); - return success; - } - - /** - * Fulfills conditional requirements for the specified schedule context. - * - * @param conditionalReqs The conditional requirements to fulfill - * @param orderedReqs The ordered requirements to fulfill - * @param context The schedule context (PRE_SCHEDULE, POST_SCHEDULE, or BOTH) - * @return true if all conditional requirements were fulfilled successfully, false otherwise - */ - public static boolean fulfillConditionalRequirements(CompletableFuture scheduledFuture, - TaskExecutionState executionState, - List conditionalReqs, - List orderedReqs, - TaskContext context) { - List contextConditionalReqs = conditionalReqs.stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(java.util.stream.Collectors.toList()); - - List contextOrderedReqs = orderedReqs.stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(java.util.stream.Collectors.toList()); - - if (contextConditionalReqs.isEmpty() && contextOrderedReqs.isEmpty()) { - log.debug("No conditional or ordered requirements to fulfill for context: {}", context); - return true; // No requirements to fulfill - } - - boolean success = true; - int currentIndex = 0; - int totalReqs = contextConditionalReqs.size() + contextOrderedReqs.size(); - - // Process ConditionalRequirements first - for (ConditionalRequirement requirement : contextConditionalReqs) { - try { - log.debug("Processing conditional requirement {}/{}: {}", ++currentIndex, totalReqs, requirement.getName()); - boolean fulfilled = requirement.fulfillRequirement(scheduledFuture); - if (!fulfilled && requirement.getPriority() == RequirementPriority.MANDATORY) { - Microbot.log("Failed to fulfill mandatory conditional requirement: " + requirement.getName(), Level.ERROR); - success = false; - } else if (!fulfilled) { - Microbot.log("Failed to fulfill optional conditional requirement: " + requirement.getName(), Level.WARN); - } - } catch (Exception e) { - log.error("Error fulfilling conditional requirement '{}': {}", requirement.getName(), e.getMessage(), e); - Microbot.log("Error fulfilling conditional requirement " + requirement.getName() + ": " + e.getMessage(), Level.ERROR); - if (requirement.getPriority() == RequirementPriority.MANDATORY) { - success = false; - } - } - } - - // Process OrderedRequirements second - for (OrderedRequirement requirement : contextOrderedReqs) { - try { - log.debug("Processing ordered requirement {}/{}: {}", ++currentIndex, totalReqs, requirement.getName()); - boolean fulfilled = requirement.fulfillRequirement(scheduledFuture); - if (!fulfilled && requirement.getPriority() == RequirementPriority.MANDATORY) { - Microbot.log("Failed to fulfill mandatory ordered requirement: " + requirement.getName(), Level.ERROR); - success = false; - } else if (!fulfilled) { - Microbot.log("Failed to fulfill optional ordered requirement: " + requirement.getName(), Level.WARN); - } - } catch (Exception e) { - log.error("Error fulfilling ordered requirement '{}': {}", requirement.getName(), e.getMessage(), e); - Microbot.log("Error fulfilling ordered requirement " + requirement.getName() + ": " + e.getMessage(), Level.ERROR); - if (requirement.getPriority() == RequirementPriority.MANDATORY) { - success = false; - } - } - } - - log.debug("Conditional requirements fulfillment completed. Success: {}, Total processed: {}", success, totalReqs); - return success; - } - - /** - * Generic requirement fulfillment method with common error handling patterns. - * - * @param requirements List of requirements to fulfill - * @param requirementTypeName Name of the requirement type for logging - * @param context The schedule context - * @param The requirement type - * @return true if all mandatory requirements were fulfilled successfully - */ - public static boolean fulfillRequirements( CompletableFuture scheduledFuture, - List requirements, - String requirementTypeName, - TaskContext context) { - List contextReqs = requirements.stream() - .filter(req -> req.getTaskContext() == context || req.getTaskContext() == TaskContext.BOTH) - .collect(java.util.stream.Collectors.toList()); - - if (contextReqs.isEmpty()) { - log.debug("No {} requirements for context: {}", requirementTypeName, context); - return true; - } - - boolean success = true; - int fulfilled = 0; - - for (int i = 0; i < contextReqs.size(); i++) { - T requirement = contextReqs.get(i); - - try { - log.debug("Processing {} requirement {}/{}: {}", requirementTypeName, i + 1, contextReqs.size(), requirement.getName()); - boolean requirementFulfilled = requirement.fulfillRequirement(scheduledFuture); - - if (requirementFulfilled) { - fulfilled++; - } else { - if (requirement.isMandatory()) { - log.error("Failed to fulfill mandatory {} requirement: {}", requirementTypeName, requirement.getName()); - success = false; - break; // Stop on mandatory failure - } else { - log.debug("Failed to fulfill optional {} requirement: {}", requirementTypeName, requirement.getName()); - } - } - } catch (Exception e) { - log.error("Error fulfilling {} requirement {}: {}", requirementTypeName, requirement.getName(), e.getMessage()); - if (requirement.isMandatory()) { - success = false; - } - } - } - - log.debug("{} requirements fulfillment completed. Success: {}, Fulfilled: {}/{}", - requirementTypeName, success, fulfilled, contextReqs.size()); - return success; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/state/FulfillmentStep.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/state/FulfillmentStep.java deleted file mode 100644 index 366f61ec984..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/state/FulfillmentStep.java +++ /dev/null @@ -1,72 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.state; - -import lombok.Getter; - -/** - * Represents the different steps in the requirement fulfillment process. - * These steps are executed in order for both pre-schedule and post-schedule contexts. - */ -@Getter -public enum FulfillmentStep { - CONDITIONAL(0, "Conditional", "Executing conditional and ordered requirements"), - LOOT(1, "Loot", "Collecting required loot items"), - SHOP(2, "Shop", "Purchasing required shop items"), - ITEMS(3, "Items", "Preparing inventory and equipment"), - SPELLBOOK(4, "Spellbook", "Switching to required spellbook"), - LOCATION(5, "Location", "Moving to required location"), - EXTERNAL_REQUIREMENTS(6, "External", "Fulfilling externally added requirements"); - - private final int order; - private final String displayName; - private final String description; - - FulfillmentStep(int order, String displayName, String description) { - this.order = order; - this.displayName = displayName; - this.description = description; - } - - /** - * Gets the total number of fulfillment steps. - */ - public static int getTotalSteps() { - return values().length; - } - - /** - * Gets the next step in the fulfillment process. - * @return The next step, or null if this is the last step - */ - public FulfillmentStep getNext() { - FulfillmentStep[] values = values(); - if (ordinal() < values.length - 1) { - return values[ordinal() + 1]; - } - return null; - } - - /** - * Gets the previous step in the fulfillment process. - * @return The previous step, or null if this is the first step - */ - public FulfillmentStep getPrevious() { - if (ordinal() > 0) { - return values()[ordinal() - 1]; - } - return null; - } - - /** - * Checks if this is the first step. - */ - public boolean isFirst() { - return ordinal() == 0; - } - - /** - * Checks if this is the last step. - */ - public boolean isLast() { - return ordinal() == values().length - 1; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/state/TaskExecutionState.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/state/TaskExecutionState.java deleted file mode 100644 index b14a64733fa..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/state/TaskExecutionState.java +++ /dev/null @@ -1,441 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.state; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.state.FulfillmentStep; - -/** - * Centralized state tracking system for pre/post schedule task execution and requirement fulfillment. - * This class provides a single source of truth for the current execution state, eliminating - * the redundant state tracking that existed across AbstractPrePostScheduleTasks and PrePostScheduleRequirements. - */ -@Slf4j -public class TaskExecutionState { - - /** - * Represents the overall execution phase - */ - public enum ExecutionPhase { - IDLE("Idle"), - PRE_SCHEDULE("Pre-Schedule"), - MAIN_EXECUTION("Main Execution"), - POST_SCHEDULE("Post-Schedule"); - - @Getter - private final String displayName; - - ExecutionPhase(String displayName) { - this.displayName = displayName; - } - } - - /** - * Represents the current state of task execution - */ - public enum ExecutionState { - STARTING("Starting"), - FULFILLING_REQUIREMENTS("Fulfilling Requirements"), - CUSTOM_TASKS("Custom Tasks"), - COMPLETED("Completed"), - FAILED("Failed"), - ERROR("Error"); - - @Getter - private final String displayName; - - ExecutionState(String displayName) { - this.displayName = displayName; - } - } - - // Current execution state - @Getter - private volatile ExecutionPhase currentPhase = ExecutionPhase.IDLE; - @Getter - private volatile ExecutionState currentState = ExecutionState.STARTING; - - @Getter - private volatile String currentDetails = null; - @Getter - private volatile boolean hasError = false; - @Getter - private volatile String errorMessage = null; - - // Progress tracking - @Getter - private volatile int currentStepNumber = 0; - @Getter - private volatile int totalSteps = 0; - - // Individual requirement tracking within steps - @Getter - private volatile FulfillmentStep currentStep = null; - @Getter - private volatile Object currentRequirement = null; // The specific requirement being processed - @Getter - private volatile String currentRequirementName = null; // readable name of current requirement - @Getter - private volatile int currentRequirementIndex = 0; // Current requirement index within step - @Getter - private volatile int totalRequirementsInStep = 0; // Total requirements in current step - - // Execution phase completion tracking - prevents multiple executions - @Getter - private volatile boolean hasPreTaskStarted = false; - @Getter - private volatile boolean hasPreTaskCompleted = false; - @Getter - private volatile boolean hasMainTaskStarted = false; - @Getter - private volatile boolean hasMainTaskCompleted = false; - @Getter - private volatile boolean hasPostTaskStarted = false; - @Getter - private volatile boolean hasPostTaskCompleted = false; - - /** - * Updates the current execution phase and resets step tracking - */ - public synchronized void update(ExecutionPhase phase, ExecutionState state) { - this.currentPhase = phase; - this.currentState = state; - // Mark phase as started - switch (phase) { - case PRE_SCHEDULE: - this.hasPreTaskStarted = true; - if (state == ExecutionState.COMPLETED|| state == ExecutionState.FAILED || state == ExecutionState.ERROR) { - this.hasPreTaskCompleted = true; - } else { - this.hasPreTaskCompleted = false; - } - break; - case MAIN_EXECUTION: - if (!hasPreTaskCompleted) { - log.warn("Main execution started without pre-schedule tasks. Ensure pre-tasks are executed first."); - }else{ - hasPreTaskCompleted = true; - } - this.hasMainTaskStarted = true; - if (state == ExecutionState.COMPLETED|| state == ExecutionState.FAILED || state == ExecutionState.ERROR) { - this.hasMainTaskCompleted = true; - } else { - this.hasMainTaskCompleted = false; - } - break; - case POST_SCHEDULE: - this.hasPostTaskStarted = true; - if (state == ExecutionState.COMPLETED|| state == ExecutionState.FAILED || state == ExecutionState.ERROR) { - this.hasPostTaskCompleted = true; - } else { - this.hasPostTaskCompleted = false; - } - break; - default: - break; - } - - log.debug("Execution phase updated to: {}", phase.getDisplayName()); - } - - - - /** - * Updates the current fulfillment step and progress - */ - public synchronized void updateFulfillmentStep(FulfillmentStep step, String details) { - this.currentStep = step; - this.currentDetails = details; - this.currentState = ExecutionState.FULFILLING_REQUIREMENTS; - this.currentStepNumber = step != null ? step.getOrder() : 0; - this.totalSteps = FulfillmentStep.getTotalSteps(); - - // Reset requirement tracking when starting new step - this.currentRequirement = null; - this.currentRequirementName = null; - this.currentRequirementIndex = 0; - this.totalRequirementsInStep = 0; - - log.debug("Fulfillment step updated to: {} ({}/{}) - {}", - step != null ? step.getDisplayName() : "None", - currentStepNumber, totalSteps, details); - } - - /** - * Updates the current fulfillment step with requirement counts - */ - public synchronized void updateFulfillmentStep(FulfillmentStep step, String details, int totalRequirementsInStep) { - updateFulfillmentStep(step, details); - this.totalRequirementsInStep = totalRequirementsInStep; - - log.debug("Fulfillment step updated with {} total requirements", totalRequirementsInStep); - } - - /** - * Updates the current requirement being processed within a fulfillment step - */ - public synchronized void updateCurrentRequirement(Object requirement, String requirementName, int requirementIndex) { - this.currentRequirement = requirement; - this.currentRequirementName = requirementName; - this.currentRequirementIndex = requirementIndex; - - // Update details to show current requirement - if (requirementName != null && totalRequirementsInStep > 0) { - this.currentDetails = String.format("Processing: %s (%d/%d)", - requirementName, requirementIndex, totalRequirementsInStep); - } else if (requirementName != null) { - this.currentDetails = "Processing: " + requirementName; - } - - log.debug("Current requirement updated to: {} ({}/{})", - requirementName, requirementIndex, totalRequirementsInStep); - } - - - - /** - * Marks the current execution as failed with an error message - */ - public synchronized void markFailed(String errorMessage) { - this.currentState = ExecutionState.FAILED; - this.hasError = true; - this.errorMessage = errorMessage; - this.currentDetails = errorMessage; - - log.warn("Execution marked as failed: {}", errorMessage); - } - - /** - * Marks the current execution as having an error - */ - public synchronized void markError(String errorMessage) { - this.currentState = ExecutionState.ERROR; - this.hasError = true; - this.errorMessage = errorMessage; - this.currentDetails = errorMessage; - - log.error("Execution marked as error: {}", errorMessage); - } - public synchronized void markCompleted() { - this.currentState = ExecutionState.COMPLETED; - this.hasError = false; - this.errorMessage = null; - this.currentDetails = "Execution completed successfully"; - - log.info("Execution marked as completed"); - } - public synchronized void markIdle() { - this.currentPhase = ExecutionPhase.IDLE; - this.currentState = ExecutionState.STARTING; - this.currentStep = null; - this.currentDetails = null; - this.hasError = false; - this.errorMessage = null; - this.currentStepNumber = 0; - this.totalSteps = 0; - - log.info("Execution state marked as idle"); - } - public synchronized void clearRequirementState() { - // Clear individual requirement tracking - this.currentStep = null; - this.currentRequirement = null; - this.currentRequirementName = null; - this.currentRequirementIndex = 0; - this.totalRequirementsInStep = 0; - - log.debug("Current requirement state cleared"); - } - /** - * Clears all state and returns to idle - */ - public synchronized void clear() { - this.currentPhase = ExecutionPhase.IDLE; - this.currentState = ExecutionState.STARTING; - this.currentStep = null; - this.currentDetails = null; - this.hasError = false; - this.errorMessage = null; - this.currentStepNumber = 0; - this.totalSteps = 0; - this.currentRequirement = null; - this.currentRequirementName = null; - this.currentRequirementIndex = 0; - this.totalRequirementsInStep = 0; - - log.debug("Execution state cleared"); - } - - /** - * Resets all execution tracking to allow tasks to be run again. - * This clears the completion flags but keeps current state if still executing. - */ - public synchronized void reset() { - this.hasPreTaskStarted = false; - this.hasPreTaskCompleted = false; - this.hasMainTaskStarted = false; - this.hasMainTaskCompleted = false; - this.hasPostTaskStarted = false; - this.hasPostTaskCompleted = false; - - // Only clear current state if we're not actively executing - if (currentPhase == ExecutionPhase.IDLE || hasError) { - clear(); - } - - log.debug("\n\t##Task execution state reset - tasks can now be executed again##"); - } - - /** - * Gets a concise status string for overlay display - * @return A formatted status string, or null if idle - */ - public String getDisplayStatus() { - if (currentPhase == ExecutionPhase.IDLE) { - return null; - } - - StringBuilder status = new StringBuilder(); - status.append(currentPhase.getDisplayName()); - - if (currentState == ExecutionState.FULFILLING_REQUIREMENTS && currentStep != null) { - // Show step progress: "Pre-Schedule: Items (3/5)" - status.append(": ").append(currentStep.getDisplayName()) - .append(" (").append(currentStepNumber).append("/").append(totalSteps).append(")"); - - // Add requirement progress if available: "Pre-Schedule: Items (3/5) [2/4]" - if (totalRequirementsInStep > 0 && currentRequirementIndex > 0) { - status.append(" [").append(currentRequirementIndex).append("/").append(totalRequirementsInStep).append("]"); - } - } else if (currentState != ExecutionState.STARTING) { - // Show state: "Pre-Schedule: Custom Tasks" - status.append(": ").append(currentState.getDisplayName()); - } - - return status.toString(); - } - - /** - * Gets a detailed status string including current details and requirement name - * @return A formatted detailed status string, or null if idle - */ - public String getDetailedStatus() { - String displayStatus = getDisplayStatus(); - if (displayStatus == null) { - return null; - } - - StringBuilder detailed = new StringBuilder(displayStatus); - - // Add current requirement name if available - if (currentRequirementName != null && !currentRequirementName.isEmpty()) { - detailed.append(" - ").append(currentRequirementName); - } else if (currentDetails != null && !currentDetails.isEmpty()) { - detailed.append(" - ").append(currentDetails); - } - - return detailed.toString(); - } - - /** - * Checks if any execution is currently in progress - */ - public boolean isExecuting() { - return currentPhase != ExecutionPhase.IDLE; - } - - /** - * Checks if requirements are currently being fulfilled - */ - public boolean isFulfillingRequirements() { - return currentState == ExecutionState.FULFILLING_REQUIREMENTS; - } - - /** - * Checks if the current execution is in an error state - */ - public boolean isInErrorState() { - return hasError; - } - - /** - * Gets the current progress as a percentage (0-100) - */ - public int getProgressPercentage() { - if (totalSteps == 0) { - return 0; - } - return (int) ((double) currentStepNumber / totalSteps * 100); - } - - // Convenience methods for checking task completion and execution states - - /** - * Checks if pre-schedule tasks can be executed (not started or already completed) - */ - public boolean canExecutePreTasks() { - return !hasPreTaskStarted || hasPreTaskCompleted; - } - - /** - * Checks if main task can be executed (pre-tasks completed, main not started or already completed) - */ - public boolean canExecuteMainTask() { - return hasPreTaskCompleted && (!hasMainTaskStarted || hasMainTaskCompleted); - } - - /** - * Checks if post-schedule tasks can be executed (main task completed, post not started or already completed) - */ - public boolean canExecutePostTasks() { - return hasMainTaskStarted && (!hasPostTaskStarted || hasPostTaskCompleted); - } - - /** - * Checks if pre-schedule tasks are currently running - */ - public boolean isPreTaskRunning() { - return hasPreTaskStarted && !hasPreTaskCompleted && currentPhase == ExecutionPhase.PRE_SCHEDULE; - } - - /** - * Checks if main task is currently running - */ - public boolean isMainTaskRunning() { - return hasMainTaskStarted && !hasMainTaskCompleted && currentPhase == ExecutionPhase.MAIN_EXECUTION; - } - - /** - * Checks if post-schedule tasks are currently running - */ - public boolean isPostTaskRunning() { - return hasPostTaskStarted && !hasPostTaskCompleted && currentPhase == ExecutionPhase.POST_SCHEDULE; - } - - /** - * Checks if pre-schedule tasks are completed - */ - public boolean isPreTaskComplete() { - return hasPreTaskCompleted; - } - - /** - * Checks if main task is completed - */ - public boolean isMainTaskComplete() { - return hasMainTaskCompleted; - } - - /** - * Checks if post-schedule tasks are completed - */ - public boolean isPostTaskComplete() { - return hasPostTaskCompleted; - } - - /** - * Checks if all tasks (pre, main, post) are completed - */ - public boolean areAllTasksComplete() { - return hasPreTaskCompleted && hasMainTaskCompleted && hasPostTaskCompleted; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/PrePostScheduleTasksInfoPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/PrePostScheduleTasksInfoPanel.java deleted file mode 100644 index fb55735cb68..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/PrePostScheduleTasksInfoPanel.java +++ /dev/null @@ -1,258 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.ui; - -import net.runelite.client.plugins.microbot.pluginscheduler.api.SchedulablePlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.AbstractPrePostScheduleTasks; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.PrePostScheduleRequirements; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.state.TaskExecutionState; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import java.awt.*; - -/** - * A comprehensive UI component for displaying pre/post schedule task information for a plugin. - * This panel shows both the execution state and requirements information. - */ -public class PrePostScheduleTasksInfoPanel extends JPanel { - - private TaskExecutionStatePanel preTaskStatePanel; - private TaskExecutionStatePanel postTaskStatePanel; - private RequirementsStatusPanel requirementsPanel; - private JLabel pluginNameLabel; - private JLabel tasksEnabledLabel; - - // State tracking - private SchedulablePlugin lastTrackedPlugin; - private AbstractPrePostScheduleTasks lastTrackedTasks; - - public PrePostScheduleTasksInfoPanel() { - setLayout(new BorderLayout(5, 5)); - setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.BRAND_ORANGE, 2), - "Pre/Post Schedule Tasks", - TitledBorder.CENTER, - TitledBorder.TOP, - FontManager.getRunescapeBoldFont(), - Color.WHITE - ), - new EmptyBorder(8, 8, 8, 8) - )); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setOpaque(true); - - // Header panel with plugin info - JPanel headerPanel = createHeaderPanel(); - add(headerPanel, BorderLayout.NORTH); - - // Main content with task states - JPanel contentPanel = createContentPanel(); - add(contentPanel, BorderLayout.CENTER); - - // Initially hidden until a plugin with tasks is set - setVisible(false); - } - - private JPanel createHeaderPanel() { - JPanel panel = new JPanel(new GridBagLayout()); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - panel.setOpaque(true); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(2, 5, 2, 5); - gbc.anchor = GridBagConstraints.WEST; - - // Plugin name - gbc.gridx = 0; gbc.gridy = 0; - gbc.weightx = 0.0; - JLabel pluginTitle = new JLabel("Plugin:"); - pluginTitle.setFont(FontManager.getRunescapeSmallFont()); - pluginTitle.setForeground(Color.LIGHT_GRAY); - panel.add(pluginTitle, gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - pluginNameLabel = new JLabel("None"); - pluginNameLabel.setFont(FontManager.getRunescapeBoldFont()); - pluginNameLabel.setForeground(Color.WHITE); - panel.add(pluginNameLabel, gbc); - - // Tasks enabled status - gbc.gridx = 0; gbc.gridy = 1; - gbc.weightx = 0.0; - gbc.fill = GridBagConstraints.NONE; - JLabel enabledTitle = new JLabel("Tasks:"); - enabledTitle.setFont(FontManager.getRunescapeSmallFont()); - enabledTitle.setForeground(Color.LIGHT_GRAY); - panel.add(enabledTitle, gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - tasksEnabledLabel = new JLabel("Disabled"); - tasksEnabledLabel.setFont(FontManager.getRunescapeSmallFont()); - tasksEnabledLabel.setForeground(Color.RED); - panel.add(tasksEnabledLabel, gbc); - - return panel; - } - - private JPanel createContentPanel() { - JPanel panel = new JPanel(); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - panel.setOpaque(true); - - // Create task state panels - preTaskStatePanel = new TaskExecutionStatePanel("Pre-Schedule Tasks"); - postTaskStatePanel = new TaskExecutionStatePanel("Post-Schedule Tasks"); - - // Create requirements status panel - requirementsPanel = new RequirementsStatusPanel(); - - // Add components with spacing - panel.add(preTaskStatePanel); - panel.add(Box.createVerticalStrut(5)); - panel.add(postTaskStatePanel); - panel.add(Box.createVerticalStrut(5)); - panel.add(requirementsPanel); - - return panel; - } - - /** - * Updates the panel with information from the specified plugin - */ - public void updatePlugin(SchedulablePlugin plugin) { - if (plugin == null) { - setVisible(false); - return; - } - - // Check if this is a different plugin or if tasks changed - AbstractPrePostScheduleTasks tasks = plugin.getPrePostScheduleTasks(); - boolean pluginChanged = plugin != lastTrackedPlugin; - boolean tasksChanged = tasks != lastTrackedTasks; - - if (pluginChanged || tasksChanged) { - updatePluginHeader(plugin, tasks); - lastTrackedPlugin = plugin; - lastTrackedTasks = tasks; - } - - // Update task execution states - if (tasks != null) { - TaskExecutionState executionState = tasks.getExecutionState(); - TaskExecutionState.ExecutionPhase currentPhase = executionState.getCurrentPhase(); - - // Update appropriate panel based on current phase - if (currentPhase == TaskExecutionState.ExecutionPhase.PRE_SCHEDULE) { - preTaskStatePanel.updateState(executionState); - preTaskStatePanel.setVisible(true); - postTaskStatePanel.setVisible(false); - setVisible(true); - } else if (currentPhase == TaskExecutionState.ExecutionPhase.POST_SCHEDULE) { - postTaskStatePanel.updateState(executionState); - postTaskStatePanel.setVisible(true); - preTaskStatePanel.setVisible(false); - setVisible(true); - } else { - // Idle or main execution - hide both task panels and the whole panel if no active tasks - preTaskStatePanel.setVisible(false); - postTaskStatePanel.setVisible(false); - - // Show the panel if tasks are available (even if not executing) to show requirements - // Hide completely only if in IDLE phase and no interesting state to show - if (currentPhase == TaskExecutionState.ExecutionPhase.IDLE) { - setVisible(false); - } else { - setVisible(true); - } - } - - // Update requirements panel with execution state for enhanced progress tracking - PrePostScheduleRequirements requirements = tasks.getRequirements(); - requirementsPanel.updateRequirements(requirements, executionState); - - } else { - // No tasks available at all - hide everything - preTaskStatePanel.setVisible(false); - postTaskStatePanel.setVisible(false); - setVisible(false); - } - } - - private void updatePluginHeader(SchedulablePlugin plugin, AbstractPrePostScheduleTasks tasks) { - // Update plugin name - String pluginName = "Unknown"; - if (plugin instanceof net.runelite.client.plugins.Plugin) { - net.runelite.client.plugins.Plugin p = (net.runelite.client.plugins.Plugin) plugin; - net.runelite.client.plugins.PluginDescriptor descriptor = p.getClass().getAnnotation(net.runelite.client.plugins.PluginDescriptor.class); - if (descriptor != null) { - pluginName = descriptor.name(); - } else { - pluginName = p.getClass().getSimpleName(); - } - } - pluginNameLabel.setText(pluginName); - - // Update tasks enabled status - if (tasks != null) { - tasksEnabledLabel.setText("Enabled"); - tasksEnabledLabel.setForeground(Color.GREEN); - } else { - tasksEnabledLabel.setText("Disabled"); - tasksEnabledLabel.setForeground(Color.RED); - } - } - - /** - * Clears the panel and hides it - */ - public void clear() { - pluginNameLabel.setText("None"); - tasksEnabledLabel.setText("Disabled"); - tasksEnabledLabel.setForeground(Color.RED); - - preTaskStatePanel.reset(); - postTaskStatePanel.reset(); - requirementsPanel.clear(); - - lastTrackedPlugin = null; - lastTrackedTasks = null; - - setVisible(false); - } - - /** - * Forces a refresh of the task states - useful when task execution begins or ends - */ - public void refresh() { - if (lastTrackedTasks != null) { - TaskExecutionState executionState = lastTrackedTasks.getExecutionState(); - - // Reset visibility for both panels - preTaskStatePanel.setVisible(false); - postTaskStatePanel.setVisible(false); - - // Show appropriate panel based on current phase - TaskExecutionState.ExecutionPhase phase = executionState.getCurrentPhase(); - if (phase == TaskExecutionState.ExecutionPhase.PRE_SCHEDULE) { - preTaskStatePanel.updateState(executionState); - preTaskStatePanel.setVisible(true); - } else if (phase == TaskExecutionState.ExecutionPhase.POST_SCHEDULE) { - postTaskStatePanel.updateState(executionState); - postTaskStatePanel.setVisible(true); - } - - // Update requirements panel with execution state - requirementsPanel.updateRequirements(lastTrackedTasks.getRequirements(), executionState); - - repaint(); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/RequirementsStatusPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/RequirementsStatusPanel.java deleted file mode 100644 index d4f2e53e5e9..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/RequirementsStatusPanel.java +++ /dev/null @@ -1,434 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.ui; - -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.PrePostScheduleRequirements; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.registry.RequirementRegistry; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.Requirement; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.RequirementPriority; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.enums.TaskContext; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.state.TaskExecutionState; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import java.awt.*; -import java.util.List; -import java.util.ArrayList; - -/** - * A UI component for displaying the status of pre/post schedule requirements. - * Shows requirement counts, fulfillment status, and progress information. - * Enhanced with context awareness to display requirements specific to current execution phase. - */ -public class RequirementsStatusPanel extends JPanel { - - private final JLabel totalRequirementsLabel; - private final JLabel fulfilledRequirementsLabel; - private final JLabel mandatoryRequirementsLabel; - private final JLabel optionalRequirementsLabel; - private final JProgressBar fulfillmentProgressBar; - private final JLabel currentRequirementLabel; - private final JLabel phaseLabel; - - // State tracking - private PrePostScheduleRequirements lastRequirements; - private int lastTotalRequirements = 0; - private int lastFulfilledRequirements = 0; - private TaskExecutionState lastExecutionState; - private TaskExecutionState.ExecutionPhase lastPhase; - - public RequirementsStatusPanel() { - setLayout(new BorderLayout(5, 5)); - setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.BRAND_ORANGE, 1), - "Requirements Status", - TitledBorder.CENTER, - TitledBorder.TOP, - FontManager.getRunescapeSmallFont(), - Color.WHITE - ), - new EmptyBorder(5, 5, 5, 5) - )); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setOpaque(true); - - // Create content panel - JPanel contentPanel = new JPanel(new GridBagLayout()); - contentPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - contentPanel.setOpaque(true); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(2, 5, 2, 5); - gbc.anchor = GridBagConstraints.WEST; - - // Phase row - shows current execution phase - gbc.gridx = 0; gbc.gridy = 0; - gbc.weightx = 0.0; - JLabel phaseTitle = new JLabel("Phase:"); - phaseTitle.setFont(FontManager.getRunescapeSmallFont()); - phaseTitle.setForeground(Color.LIGHT_GRAY); - contentPanel.add(phaseTitle, gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - phaseLabel = new JLabel("Idle"); - phaseLabel.setFont(FontManager.getRunescapeSmallFont()); - phaseLabel.setForeground(Color.CYAN); - contentPanel.add(phaseLabel, gbc); - - // Total requirements row - gbc.gridx = 0; gbc.gridy = 1; - gbc.weightx = 0.0; - gbc.fill = GridBagConstraints.NONE; - JLabel totalTitle = new JLabel("Total:"); - totalTitle.setFont(FontManager.getRunescapeSmallFont()); - totalTitle.setForeground(Color.LIGHT_GRAY); - contentPanel.add(totalTitle, gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - totalRequirementsLabel = new JLabel("0"); - totalRequirementsLabel.setFont(FontManager.getRunescapeSmallFont()); - totalRequirementsLabel.setForeground(Color.WHITE); - contentPanel.add(totalRequirementsLabel, gbc); - - // Fulfilled requirements row - gbc.gridx = 0; gbc.gridy = 2; - gbc.weightx = 0.0; - gbc.fill = GridBagConstraints.NONE; - JLabel fulfilledTitle = new JLabel("Fulfilled:"); - fulfilledTitle.setFont(FontManager.getRunescapeSmallFont()); - fulfilledTitle.setForeground(Color.LIGHT_GRAY); - contentPanel.add(fulfilledTitle, gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - fulfilledRequirementsLabel = new JLabel("0"); - fulfilledRequirementsLabel.setFont(FontManager.getRunescapeSmallFont()); - fulfilledRequirementsLabel.setForeground(Color.GREEN); - contentPanel.add(fulfilledRequirementsLabel, gbc); - - // Mandatory requirements row - gbc.gridx = 0; gbc.gridy = 3; - gbc.weightx = 0.0; - gbc.fill = GridBagConstraints.NONE; - JLabel mandatoryTitle = new JLabel("Mandatory:"); - mandatoryTitle.setFont(FontManager.getRunescapeSmallFont()); - mandatoryTitle.setForeground(Color.LIGHT_GRAY); - contentPanel.add(mandatoryTitle, gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - mandatoryRequirementsLabel = new JLabel("0"); - mandatoryRequirementsLabel.setFont(FontManager.getRunescapeSmallFont()); - mandatoryRequirementsLabel.setForeground(Color.RED); - contentPanel.add(mandatoryRequirementsLabel, gbc); - - // Optional requirements row - gbc.gridx = 0; gbc.gridy = 4; - gbc.weightx = 0.0; - gbc.fill = GridBagConstraints.NONE; - JLabel optionalTitle = new JLabel("Optional:"); - optionalTitle.setFont(FontManager.getRunescapeSmallFont()); - optionalTitle.setForeground(Color.LIGHT_GRAY); - contentPanel.add(optionalTitle, gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - optionalRequirementsLabel = new JLabel("0"); - optionalRequirementsLabel.setFont(FontManager.getRunescapeSmallFont()); - optionalRequirementsLabel.setForeground(Color.YELLOW); - contentPanel.add(optionalRequirementsLabel, gbc); - - // Current requirement row - shows what's being processed - gbc.gridx = 0; gbc.gridy = 5; - gbc.weightx = 0.0; - gbc.fill = GridBagConstraints.NONE; - JLabel currentTitle = new JLabel("Current:"); - currentTitle.setFont(FontManager.getRunescapeSmallFont()); - currentTitle.setForeground(Color.LIGHT_GRAY); - contentPanel.add(currentTitle, gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - currentRequirementLabel = new JLabel("None"); - currentRequirementLabel.setFont(FontManager.getRunescapeSmallFont()); - currentRequirementLabel.setForeground(Color.ORANGE); - contentPanel.add(currentRequirementLabel, gbc); - - // Progress bar row - gbc.gridx = 0; gbc.gridy = 6; - gbc.gridwidth = 2; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - fulfillmentProgressBar = new JProgressBar(0, 100); - fulfillmentProgressBar.setStringPainted(true); - fulfillmentProgressBar.setString("0%"); - fulfillmentProgressBar.setFont(FontManager.getRunescapeSmallFont()); - fulfillmentProgressBar.setForeground(ColorScheme.BRAND_ORANGE); - fulfillmentProgressBar.setBackground(ColorScheme.DARKER_GRAY_COLOR); - contentPanel.add(fulfillmentProgressBar, gbc); - - add(contentPanel, BorderLayout.CENTER); - - // Initially hidden - setVisible(false); - } - - /** - * Updates the panel with the current requirements status and execution state - */ - public void updateRequirements(PrePostScheduleRequirements requirements) { - updateRequirements(requirements, null); - } - - /** - * Enhanced update method that includes TaskExecutionState for progress tracking - */ - public void updateRequirements(PrePostScheduleRequirements requirements, TaskExecutionState executionState) { - if (requirements == null) { - setVisible(false); - return; - } - - boolean hasChanges = false; - - // Update execution phase display - if (executionState != null) { - TaskExecutionState.ExecutionPhase currentPhase = executionState.getCurrentPhase(); - if (currentPhase != lastPhase) { - phaseLabel.setText(currentPhase.getDisplayName()); - - // Color-code the phase - switch (currentPhase) { - case PRE_SCHEDULE: - phaseLabel.setForeground(Color.CYAN); - break; - case MAIN_EXECUTION: - phaseLabel.setForeground(Color.GREEN); - break; - case POST_SCHEDULE: - phaseLabel.setForeground(Color.ORANGE); - break; - default: - phaseLabel.setForeground(Color.LIGHT_GRAY); - break; - } - - lastPhase = currentPhase; - hasChanges = true; - } - - // Update current requirement display - String currentRequirementName = executionState.getCurrentRequirementName(); - int currentIndex = executionState.getCurrentRequirementIndex(); - int totalInStep = executionState.getTotalRequirementsInStep(); - - if (currentRequirementName != null && totalInStep > 0) { - currentRequirementLabel.setText(String.format("%s (%d/%d)", - currentRequirementName, currentIndex, totalInStep)); - currentRequirementLabel.setForeground(Color.ORANGE); - } else if (currentRequirementName != null) { - currentRequirementLabel.setText(currentRequirementName); - currentRequirementLabel.setForeground(Color.ORANGE); - } else { - currentRequirementLabel.setText("None"); - currentRequirementLabel.setForeground(Color.LIGHT_GRAY); - } - - lastExecutionState = executionState; - } else { - // No execution state - show idle - phaseLabel.setText("Idle"); - phaseLabel.setForeground(Color.LIGHT_GRAY); - currentRequirementLabel.setText("None"); - currentRequirementLabel.setForeground(Color.LIGHT_GRAY); - } - - // Get requirement registry - RequirementRegistry registry = requirements.getRegistry(); - if (registry == null) { - setVisible(false); - return; - } - - // Determine which requirements to show based on current execution phase - List relevantRequirements; - if (executionState != null) { - TaskExecutionState.ExecutionPhase currentPhase = executionState.getCurrentPhase(); - - // Filter requirements based on current execution phase - if (currentPhase == TaskExecutionState.ExecutionPhase.PRE_SCHEDULE) { - // Show PRE_SCHEDULE and BOTH requirements - relevantRequirements = registry.getRequirements(TaskContext.PRE_SCHEDULE).stream() - .collect(ArrayList::new, (list, req) -> { - if (!list.contains(req)) list.add(req); - }, ArrayList::addAll); - - registry.getExternalRequirements(TaskContext.PRE_SCHEDULE).stream() - .filter(req -> !relevantRequirements.contains(req)) - .forEach(relevantRequirements::add); - - // Add BOTH requirements, excluding duplicates - registry.getRequirements(TaskContext.BOTH).stream() - .filter(req -> !relevantRequirements.contains(req)) - .forEach(relevantRequirements::add); - - registry.getExternalRequirements(TaskContext.BOTH).stream() - .filter(req -> !relevantRequirements.contains(req)) - .forEach(relevantRequirements::add); - - } else if (currentPhase == TaskExecutionState.ExecutionPhase.POST_SCHEDULE) { - // Show POST_SCHEDULE and BOTH requirements - relevantRequirements = registry.getRequirements(TaskContext.POST_SCHEDULE).stream() - .collect(ArrayList::new, (list, req) -> { - if (!list.contains(req)) list.add(req); - }, ArrayList::addAll); - - registry.getExternalRequirements(TaskContext.POST_SCHEDULE).stream() - .filter(req -> !relevantRequirements.contains(req)) - .forEach(relevantRequirements::add); - - // Add BOTH requirements, excluding duplicates - registry.getRequirements(TaskContext.BOTH).stream() - .filter(req -> !relevantRequirements.contains(req)) - .forEach(relevantRequirements::add); - - registry.getExternalRequirements(TaskContext.BOTH).stream() - .filter(req -> !relevantRequirements.contains(req)) - .forEach(relevantRequirements::add); - - } else { - // For IDLE or MAIN_EXECUTION, show all requirements - relevantRequirements = registry.getAllRequirements().stream() - .collect(ArrayList::new, (list, req) -> { - if (!list.contains(req)) list.add(req); - }, ArrayList::addAll); - - // Add external requirements for all contexts, excluding duplicates - java.util.Arrays.stream(TaskContext.values()) - .flatMap(context -> registry.getExternalRequirements(context).stream()) - .filter(req -> !relevantRequirements.contains(req)) - .forEach(relevantRequirements::add); - } - } else { - // No execution state - show all requirements - relevantRequirements = registry.getAllRequirements().stream() - .collect(ArrayList::new, (list, req) -> { - if (!list.contains(req)) list.add(req); - }, ArrayList::addAll); - - // Add external requirements for all contexts, excluding duplicates - java.util.Arrays.stream(TaskContext.values()) - .flatMap(context -> registry.getExternalRequirements(context).stream()) - .filter(req -> !relevantRequirements.contains(req)) - .forEach(relevantRequirements::add); - } - - // Calculate requirement counts based on filtered requirements - int totalRequirements = relevantRequirements.size(); - int mandatoryCount = (int) relevantRequirements.stream() - .filter(req -> req.getPriority() == RequirementPriority.MANDATORY) - .count(); - int optionalCount = totalRequirements - mandatoryCount; - - // Calculate fulfilled count from execution state progress - int fulfilledCount = 0; - if (executionState != null) { - // Use execution state step progress to calculate fulfillment - int currentStepNumber = executionState.getCurrentStepNumber(); - int totalSteps = executionState.getTotalSteps(); - int currentRequirementIndex = executionState.getCurrentRequirementIndex(); - int totalRequirementsInStep = executionState.getTotalRequirementsInStep(); - - if (totalSteps > 0 && currentStepNumber > 0) { - // Calculate approximate fulfillment based on step progress - double stepProgress = (double) (currentStepNumber - 1) / totalSteps; - - // Add progress within current step if available - if (totalRequirementsInStep > 0 && currentRequirementIndex > 0) { - double stepCompletionRatio = (double) currentRequirementIndex / totalRequirementsInStep; - stepProgress += stepCompletionRatio / totalSteps; - } - - fulfilledCount = (int) Math.round(stepProgress * totalRequirements); - fulfilledCount = Math.min(fulfilledCount, totalRequirements); // Cap at total - } - } - - // Update total requirements - if (totalRequirements != lastTotalRequirements) { - totalRequirementsLabel.setText(String.valueOf(totalRequirements)); - lastTotalRequirements = totalRequirements; - hasChanges = true; - } - - // Update fulfilled requirements - if (fulfilledCount != lastFulfilledRequirements) { - fulfilledRequirementsLabel.setText(String.valueOf(fulfilledCount)); - lastFulfilledRequirements = fulfilledCount; - hasChanges = true; - } - - // Update mandatory count - mandatoryRequirementsLabel.setText(String.valueOf(mandatoryCount)); - - // Update optional count - optionalRequirementsLabel.setText(String.valueOf(optionalCount)); - - // Update progress bar - int percentage = totalRequirements > 0 ? (int) ((fulfilledCount / (double) totalRequirements) * 100) : 0; - fulfillmentProgressBar.setValue(percentage); - fulfillmentProgressBar.setString(percentage + "%"); - - // Update progress bar color based on fulfillment state - if (percentage == 100) { - fulfillmentProgressBar.setForeground(Color.GREEN); - } else if (percentage > 0) { - fulfillmentProgressBar.setForeground(ColorScheme.BRAND_ORANGE); - } else { - fulfillmentProgressBar.setForeground(Color.RED); - } - - lastRequirements = requirements; - setVisible(true); - - if (hasChanges) { - repaint(); - } - } - - /** - * Clears the panel and hides it - */ - public void clear() { - phaseLabel.setText("Idle"); - phaseLabel.setForeground(Color.LIGHT_GRAY); - totalRequirementsLabel.setText("0"); - fulfilledRequirementsLabel.setText("0"); - mandatoryRequirementsLabel.setText("0"); - optionalRequirementsLabel.setText("0"); - currentRequirementLabel.setText("None"); - currentRequirementLabel.setForeground(Color.LIGHT_GRAY); - fulfillmentProgressBar.setValue(0); - fulfillmentProgressBar.setString("0%"); - fulfillmentProgressBar.setForeground(Color.RED); - - lastRequirements = null; - lastTotalRequirements = 0; - lastFulfilledRequirements = 0; - lastExecutionState = null; - lastPhase = null; - - setVisible(false); - repaint(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/TaskExecutionStatePanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/TaskExecutionStatePanel.java deleted file mode 100644 index 80d6e9e0079..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/tasks/ui/TaskExecutionStatePanel.java +++ /dev/null @@ -1,289 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.tasks.ui; - -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.state.TaskExecutionState; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import java.awt.*; - -/** - * A reusable UI component for displaying the current state of pre/post schedule task execution. - * This panel shows the current phase, execution state, progress, and any error information. - */ -public class TaskExecutionStatePanel extends JPanel { - - private final JLabel phaseLabel; - private final JLabel stateLabel; - private final JLabel detailsLabel; - private final JProgressBar progressBar; - private final JLabel progressLabel; - private final JLabel currentRequirementLabel; - private final JLabel errorLabel; - - // State tracking for optimized updates - private TaskExecutionState.ExecutionPhase lastPhase; - private TaskExecutionState.ExecutionState lastState; - private String lastDetails; - private int lastCurrentStep; - private int lastTotalSteps; - private boolean lastHasError; - private String lastErrorMessage; - private String lastCurrentRequirementName; - - public TaskExecutionStatePanel(String title) { - setLayout(new BorderLayout(5, 5)); - setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.BRAND_ORANGE, 1), - title, - TitledBorder.CENTER, - TitledBorder.TOP, - FontManager.getRunescapeSmallFont(), - Color.WHITE - ), - new EmptyBorder(5, 5, 5, 5) - )); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setOpaque(true); - - // Create main content panel - JPanel contentPanel = new JPanel(new GridBagLayout()); - contentPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - contentPanel.setOpaque(true); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(2, 5, 2, 5); - gbc.anchor = GridBagConstraints.WEST; - - // Phase row - gbc.gridx = 0; gbc.gridy = 0; - gbc.weightx = 0.0; - JLabel phaseTitle = new JLabel("Phase:"); - phaseTitle.setFont(FontManager.getRunescapeSmallFont()); - phaseTitle.setForeground(Color.LIGHT_GRAY); - contentPanel.add(phaseTitle, gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - phaseLabel = new JLabel("Idle"); - phaseLabel.setFont(FontManager.getRunescapeSmallFont()); - phaseLabel.setForeground(Color.WHITE); - contentPanel.add(phaseLabel, gbc); - - // State row - gbc.gridx = 0; gbc.gridy = 1; - gbc.weightx = 0.0; - gbc.fill = GridBagConstraints.NONE; - JLabel stateTitle = new JLabel("State:"); - stateTitle.setFont(FontManager.getRunescapeSmallFont()); - stateTitle.setForeground(Color.LIGHT_GRAY); - contentPanel.add(stateTitle, gbc); - - gbc.gridx = 1; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - stateLabel = new JLabel("Starting"); - stateLabel.setFont(FontManager.getRunescapeSmallFont()); - stateLabel.setForeground(Color.WHITE); - contentPanel.add(stateLabel, gbc); - - // Progress bar row - gbc.gridx = 0; gbc.gridy = 2; - gbc.gridwidth = 2; - gbc.weightx = 1.0; - gbc.fill = GridBagConstraints.HORIZONTAL; - progressBar = new JProgressBar(0, 100); - progressBar.setStringPainted(true); - progressBar.setString("0 / 0"); - progressBar.setFont(FontManager.getRunescapeSmallFont()); - progressBar.setForeground(ColorScheme.BRAND_ORANGE); - progressBar.setBackground(ColorScheme.DARKER_GRAY_COLOR); - contentPanel.add(progressBar, gbc); - - // Progress label row - gbc.gridy = 3; - progressLabel = new JLabel("No progress"); - progressLabel.setFont(FontManager.getRunescapeSmallFont()); - progressLabel.setForeground(Color.LIGHT_GRAY); - contentPanel.add(progressLabel, gbc); - - // Current requirement row - gbc.gridy = 4; - currentRequirementLabel = new JLabel(""); - currentRequirementLabel.setFont(FontManager.getRunescapeSmallFont()); - currentRequirementLabel.setForeground(Color.CYAN); - contentPanel.add(currentRequirementLabel, gbc); - - // Details row - gbc.gridy = 5; - detailsLabel = new JLabel(""); - detailsLabel.setFont(FontManager.getRunescapeSmallFont()); - detailsLabel.setForeground(Color.WHITE); - contentPanel.add(detailsLabel, gbc); - - // Error row - gbc.gridy = 6; - errorLabel = new JLabel(""); - errorLabel.setFont(FontManager.getRunescapeSmallFont()); - errorLabel.setForeground(Color.RED); - contentPanel.add(errorLabel, gbc); - - add(contentPanel, BorderLayout.CENTER); - - // Set initial visibility - setVisible(false); - } - - /** - * Updates the panel with the current task execution state. - * Only redraws components that have actually changed for performance. - */ - public void updateState(TaskExecutionState state) { - if (state == null) { - setVisible(false); - return; - } - - boolean hasChanges = false; - - // Check phase changes - TaskExecutionState.ExecutionPhase currentPhase = state.getCurrentPhase(); - if (currentPhase != lastPhase) { - phaseLabel.setText(currentPhase.getDisplayName()); - lastPhase = currentPhase; - hasChanges = true; - } - - // Check state changes - TaskExecutionState.ExecutionState currentState = state.getCurrentState(); - if (currentState != lastState) { - stateLabel.setText(currentState.getDisplayName()); - lastState = currentState; - hasChanges = true; - } - - // Check details changes - String currentDetails = state.getCurrentDetails(); - if (!java.util.Objects.equals(currentDetails, lastDetails)) { - detailsLabel.setText(currentDetails != null ? currentDetails : ""); - lastDetails = currentDetails; - hasChanges = true; - } - - // Check progress changes - int currentStep = state.getCurrentStepNumber(); - int totalSteps = state.getTotalSteps(); - if (currentStep != lastCurrentStep || totalSteps != lastTotalSteps) { - updateProgressBar(currentStep, totalSteps); - lastCurrentStep = currentStep; - lastTotalSteps = totalSteps; - hasChanges = true; - } - - // Check current requirement changes - String currentRequirementName = state.getCurrentRequirementName(); - if (!java.util.Objects.equals(currentRequirementName, lastCurrentRequirementName)) { - updateCurrentRequirement(currentRequirementName, state.getCurrentRequirementIndex(), state.getTotalRequirementsInStep()); - lastCurrentRequirementName = currentRequirementName; - hasChanges = true; - } - - // Check error state changes - boolean hasError = state.isHasError(); - String errorMessage = state.getErrorMessage(); - if (hasError != lastHasError || !java.util.Objects.equals(errorMessage, lastErrorMessage)) { - updateErrorDisplay(hasError, errorMessage); - lastHasError = hasError; - lastErrorMessage = errorMessage; - hasChanges = true; - } - - // Show panel if it was hidden and we have actual execution happening - if (!isVisible() && currentPhase != TaskExecutionState.ExecutionPhase.IDLE) { - setVisible(true); - hasChanges = true; - } - - // Hide panel if back to idle - if (isVisible() && currentPhase == TaskExecutionState.ExecutionPhase.IDLE) { - setVisible(false); - hasChanges = true; - } - - // Repaint if there were changes - if (hasChanges) { - repaint(); - } - } - - private void updateProgressBar(int current, int total) { - if (total > 0) { - int percentage = (int) ((current / (double) total) * 100); - progressBar.setValue(percentage); - progressBar.setString(current + " / " + total); - progressLabel.setText(String.format("Step %d of %d", current, total)); - } else { - progressBar.setValue(0); - progressBar.setString("0 / 0"); - progressLabel.setText("No steps defined"); - } - } - - private void updateCurrentRequirement(String requirementName, int requirementIndex, int totalRequirements) { - if (requirementName != null && !requirementName.isEmpty()) { - if (totalRequirements > 0) { - currentRequirementLabel.setText(String.format("Requirement: %s (%d/%d)", - requirementName, requirementIndex + 1, totalRequirements)); - } else { - currentRequirementLabel.setText("Requirement: " + requirementName); - } - currentRequirementLabel.setVisible(true); - } else { - currentRequirementLabel.setText(""); - currentRequirementLabel.setVisible(false); - } - } - - private void updateErrorDisplay(boolean hasError, String errorMessage) { - if (hasError && errorMessage != null && !errorMessage.isEmpty()) { - errorLabel.setText("Error: " + errorMessage); - errorLabel.setVisible(true); - } else { - errorLabel.setText(""); - errorLabel.setVisible(false); - } - } - - /** - * Resets the panel to its initial state - */ - public void reset() { - phaseLabel.setText("Idle"); - stateLabel.setText("Starting"); - detailsLabel.setText(""); - progressBar.setValue(0); - progressBar.setString("0 / 0"); - progressLabel.setText("No progress"); - currentRequirementLabel.setText(""); - currentRequirementLabel.setVisible(false); - errorLabel.setText(""); - errorLabel.setVisible(false); - - // Reset state tracking - lastPhase = null; - lastState = null; - lastDetails = null; - lastCurrentStep = 0; - lastTotalSteps = 0; - lastHasError = false; - lastErrorMessage = null; - lastCurrentRequirementName = null; - - setVisible(false); - repaint(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/Antiban/AntibanDialogWindow.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/Antiban/AntibanDialogWindow.java deleted file mode 100644 index 2d638580310..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/Antiban/AntibanDialogWindow.java +++ /dev/null @@ -1,53 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.Antiban; - -import javax.swing.*; - -import net.runelite.client.plugins.microbot.util.antiban.ui.MasterPanel; - -import java.awt.*; - -/** - * A dialog window for displaying the Antiban Master Panel in a separate window. - * This allows users to configure antiban settings without having to switch to the Antiban plugin tab. - */ -public class AntibanDialogWindow extends JDialog { - - /** - * Creates a new dialog window containing the Antiban MasterPanel - * - * @param owner The parent frame for the dialog - */ - public AntibanDialogWindow(Frame owner) { - super(owner, "Antiban Settings", false); - - // Create a new master panel - MasterPanel masterPanel = new MasterPanel(); - - // Add the panel to the dialog - add(masterPanel); - - // Set dialog properties - setSize(new Dimension(320, 600)); - setLocationRelativeTo(owner); - setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); - - // Load initial settings - masterPanel.loadSettings(); - } - - /** - * Static utility method to show the Antiban settings in a new window - * - * @param parent The parent component (used to find the owner frame) - * @return The created dialog window - */ - public static AntibanDialogWindow showAntibanSettings(Component parent) { - // Find the parent frame - Frame parentFrame = JOptionPane.getFrameForComponent(parent); - - // Create and show the dialog - AntibanDialogWindow dialog = new AntibanDialogWindow(parentFrame); - dialog.setVisible(true); - return dialog; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/Antiban/AntibanWindowManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/Antiban/AntibanWindowManager.java deleted file mode 100644 index b3bdaea7b48..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/Antiban/AntibanWindowManager.java +++ /dev/null @@ -1,76 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.Antiban; - - -import javax.swing.*; - -import net.runelite.client.plugins.microbot.util.antiban.ui.MasterPanel; - -/** - * A utility class that manages opening the Antiban MasterPanel in a separate window. - * This allows the MasterPanel to be displayed outside of the normal RuneLite sidebar. - */ -public class AntibanWindowManager { - - private static JFrame antibanWindow; - private static MasterPanel masterPanel; - - /** - * Opens the Antiban MasterPanel in a new window - * - * @param injector The injector to use for creating the MasterPanel - * @return The created window - */ - public static JFrame openAntibanWindow(Object injector) { - // If the window is already open, just bring it to front - if (antibanWindow != null && antibanWindow.isDisplayable()) { - antibanWindow.toFront(); - antibanWindow.requestFocus(); - return antibanWindow; - } - - // Create a new MasterPanel - if (masterPanel == null) { - try { - masterPanel = new MasterPanel(); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - - // Create and set up the window - antibanWindow = new JFrame("Antiban Configuration"); - antibanWindow.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); - antibanWindow.setContentPane(masterPanel); - antibanWindow.pack(); - antibanWindow.setSize(400, 600); // Set an appropriate size - antibanWindow.setLocationRelativeTo(null); // Center on screen - - // Show the window - antibanWindow.setVisible(true); - - // Load the latest settings - masterPanel.loadSettings(); - - return antibanWindow; - } - - /** - * Checks if the Antiban window is currently open - * - * @return true if the window is open, false otherwise - */ - public static boolean isWindowOpen() { - return antibanWindow != null && antibanWindow.isDisplayable(); - } - - /** - * Closes the Antiban window if it's open - */ - public static void closeWindow() { - if (antibanWindow != null) { - antibanWindow.dispose(); - antibanWindow = null; - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/PrioritySpinnerEditor.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/PrioritySpinnerEditor.java deleted file mode 100644 index 27f6a3b5d8d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/PrioritySpinnerEditor.java +++ /dev/null @@ -1,113 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.PluginScheduleEntry; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.table.TableCellEditor; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.util.EventObject; - -/** - * Custom cell editor that provides a spinner for editing plugin priority values - */ -@Slf4j -public class PrioritySpinnerEditor extends AbstractCellEditor implements TableCellEditor, ActionListener { - private JSpinner spinner; - private SpinnerNumberModel spinnerModel; - private PluginScheduleEntry currentEntry; - - public PrioritySpinnerEditor() { - // Create spinner model with min 0, max 100, step 1, initial value 0 - spinnerModel = new SpinnerNumberModel(0, 0, 100, 1); - - // Setup spinner with custom model and editor - spinner = new JSpinner(spinnerModel); - spinner.setBackground(ColorScheme.DARK_GRAY_COLOR); - spinner.setBorder(new EmptyBorder(2, 5, 2, 5)); - - // Style the spinner editor - JSpinner.DefaultEditor editor = (JSpinner.DefaultEditor) spinner.getEditor(); - editor.getTextField().setForeground(Color.WHITE); - editor.getTextField().setBackground(ColorScheme.DARK_GRAY_COLOR); - editor.getTextField().setBorder(BorderFactory.createEmptyBorder()); - - // Add listener for value changes - spinner.addChangeListener(e -> { - // Update the underlying model when the spinner value changes - if (currentEntry != null) { - int newValue = (Integer) spinner.getValue(); - - // If this is a default plugin, don't allow changing priority from 0 - if (currentEntry.isDefault() && newValue != 0) { - spinner.setValue(0); // Reset to 0 - log.debug("Cannot change priority of default plugin {}. Resetting to 0.", currentEntry.getCleanName()); - return; - } - - currentEntry.setPriority(newValue); - log.debug("Updated priority for plugin {} to {}", currentEntry.getCleanName(), newValue); - } - }); - } - - @Override - public Component getTableCellEditorComponent(JTable table, Object value, - boolean isSelected, int row, int column) { - // Get the PluginScheduleEntry for this row - if (table.getModel().getValueAt(row, 0) instanceof PluginScheduleEntry) { - currentEntry = (PluginScheduleEntry) table.getModel().getValueAt(row, 0); - } else { - // If the first column doesn't contain the PluginScheduleEntry, try to get it from the table model - if (table.getModel() instanceof ScheduleTableModel) { - ScheduleTableModel model = (ScheduleTableModel) table.getModel(); - currentEntry = model.getPluginAtRow(row); - } - } - - // Set current value - int priority = value instanceof Integer ? (Integer) value : 0; - spinner.setValue(priority); - - // If this is a default plugin, disable editing - boolean isDefault = false; - if (table.getModel().getValueAt(row, 6) instanceof Boolean) { - isDefault = (Boolean) table.getModel().getValueAt(row, 6); - } - - spinner.setEnabled(!isDefault); - - // Set background color for selection - if (isSelected) { - spinner.setBackground(ColorScheme.MEDIUM_GRAY_COLOR); - JSpinner.DefaultEditor editor = (JSpinner.DefaultEditor) spinner.getEditor(); - editor.getTextField().setBackground(ColorScheme.MEDIUM_GRAY_COLOR); - } else { - spinner.setBackground(ColorScheme.DARK_GRAY_COLOR); - JSpinner.DefaultEditor editor = (JSpinner.DefaultEditor) spinner.getEditor(); - editor.getTextField().setBackground(ColorScheme.DARK_GRAY_COLOR); - } - - return spinner; - } - - @Override - public Object getCellEditorValue() { - return spinner.getValue(); - } - - @Override - public boolean isCellEditable(EventObject anEvent) { - // Allow editing if the current plugin is not a default plugin - return currentEntry != null && !currentEntry.isDefault(); - } - - @Override - public void actionPerformed(ActionEvent e) { - fireEditingStopped(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleFormPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleFormPanel.java deleted file mode 100644 index afea83d2634..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleFormPanel.java +++ /dev/null @@ -1,1339 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.PluginScheduleEntry; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerPlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.api.SchedulablePlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.DayOfWeekCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.SingleTriggerTimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.ui.TimeConditionPanelUtil; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.pluginscheduler.util.PluginFilterUtil; -import net.runelite.client.plugins.Plugin; -import net.runelite.client.plugins.PluginDescriptor; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.CompoundBorder; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.ItemEvent; -import java.time.Duration; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -@Slf4j -public class ScheduleFormPanel extends JPanel { - private final SchedulerPlugin plugin; - - // Add a flag to track if the combo box change is from user or programmatic - private boolean isUserAction = true; - - @Getter - private JComboBox pluginComboBox; - private JComboBox primeFilterComboBox; - private JComboBox primaryFilterComboBox; - private JComboBox secondaryFilterComboBox; - private JComboBox timeConditionTypeComboBox; - private JCheckBox randomSchedulingCheckbox; - private JCheckBox timeBasedStopConditionCheckbox; - private JCheckBox allowContinueCheckbox; // Add new checkbox field - @Getter - private JSpinner prioritySpinner; - private JCheckBox defaultPluginCheckbox; - - // New panel for editing plugin properties when one is selected - private JPanel pluginPropertiesPanel; - private JSpinner selectedPluginPrioritySpinner; - private JCheckBox selectedPluginDefaultCheckbox; - private JCheckBox selectedPluginEnabledCheckbox; - private JCheckBox selectedPluginRandomCheckbox; - private JCheckBox selectedPluginTimeStopCheckbox; - private JCheckBox selectedPluginAllowContinueCheckbox; // Add new checkbox field for properties panel - - // Statistics labels - private JLabel selectedPluginNameLabel; - private JLabel runsLabel; - private JLabel lastRunLabel; - private JLabel lastDurationLabel; - private JLabel lastStopReasonLabel; - private JButton saveChangesButton; - - // Condition config panels - private JPanel conditionConfigPanel; - private JPanel currentConditionPanel; - - private JButton addButton; - private JButton updateButton; - private JButton removeButton; - private JButton controlButton; - private JTabbedPane tabbedPane; - private PluginScheduleEntry selectedPlugin; - - // Flag to prevent update loops - private boolean updatingValues = false; - - // Plugin change tracking - private Set lastKnownPlugins = new HashSet<>(); - - // Constants for time condition types - private static final String CONDITION_DEFAULT = "Run Default"; - private static final String CONDITION_SPECIFIC_TIME = "Run at Specific Time"; - private static final String CONDITION_INTERVAL = "Run at Interval"; - private static final String CONDITION_TIME_WINDOW = "Run in Time Window"; - private static final String CONDITION_DAY_OF_WEEK = "Run on Day of Week"; - private static final String[] TIME_CONDITION_TYPES = { - CONDITION_DEFAULT, - CONDITION_SPECIFIC_TIME, - CONDITION_INTERVAL, - CONDITION_TIME_WINDOW, - CONDITION_DAY_OF_WEEK - }; - - // Add fields and methods for the selection change listener - private Runnable selectionChangeListener; - - public void setSelectionChangeListener(Runnable listener) { - this.selectionChangeListener = listener; - } - - public ScheduleFormPanel(SchedulerPlugin plugin) { - this.plugin = plugin; - - setLayout(new BorderLayout()); - setBorder(BorderFactory.createTitledBorder( - BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(1, 1, 1, 1, ColorScheme.DARK_GRAY_COLOR), - BorderFactory.createEmptyBorder(5, 5, 5, 5)), - "Schedule Configuration", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE)); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Set minimum size - setMinimumSize(new Dimension(350, 300)); - setPreferredSize(new Dimension(400, 500)); - - // Create a tabbed pane to separate plugin selection and properties - tabbedPane = new JTabbedPane(); - tabbedPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - tabbedPane.setForeground(Color.WHITE); - tabbedPane.setFont(FontManager.getRunescapeFont()); - - // Add tabs - tabbedPane.addTab("New Schedule", createScheduleFormPanel()); - tabbedPane.addTab("Properties", createPropertiesPanel()); - - add(tabbedPane, BorderLayout.CENTER); - - // Create button panel - JPanel buttonPanel = new JPanel(new GridLayout(2, 2, 5, 5)); - buttonPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - buttonPanel.setBorder(new EmptyBorder(5, 0, 0, 0)); - - addButton = createButton("Add Schedule", ColorScheme.BRAND_ORANGE_TRANSPARENT); - updateButton = createButton("Update Schedule", ColorScheme.BRAND_ORANGE); - removeButton = createButton("Remove Schedule", ColorScheme.PROGRESS_ERROR_COLOR); - - // Control button (Run Now/Stop) - controlButton = createButton("Run Now", ColorScheme.PROGRESS_COMPLETE_COLOR); - controlButton.addActionListener(this::onControlButtonClicked); - - buttonPanel.add(addButton); - buttonPanel.add(updateButton); - buttonPanel.add(removeButton); - buttonPanel.add(controlButton); - - // Add button panel to the bottom - add(buttonPanel, BorderLayout.SOUTH); - - // Initialize the condition panel - updateConditionPanel(); - } - - /** - * Creates the main schedule form panel for adding new schedules - */ - private JScrollPane createScheduleFormPanel() { - // Create the form panel with GridBagLayout for flexibility - JPanel formPanel = new JPanel(new GridBagLayout()); - formPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(5, 5, 5, 5); - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.weightx = 1.0; // Make components expand horizontally - gbc.anchor = GridBagConstraints.WEST; - - // Filter section - gbc.gridx = 0; - gbc.gridy = 0; - gbc.gridwidth = 4; - JPanel filterPanel = createFilterPanel(); - formPanel.add(filterPanel, gbc); - - // Plugin selection - gbc.gridx = 0; - gbc.gridy = 1; - gbc.gridwidth = 1; - JLabel pluginLabel = new JLabel("Plugin:"); - pluginLabel.setForeground(Color.WHITE); - pluginLabel.setFont(FontManager.getRunescapeFont()); - formPanel.add(pluginLabel, gbc); - - gbc.gridx = 1; - gbc.gridy = 1; - gbc.gridwidth = 3; - pluginComboBox = new JComboBox<>(); - formPanel.add(pluginComboBox, gbc); - - // Initialize filters now that pluginComboBox exists - updateSecondaryFilter(); - - // Initialize known plugins for change detection - lastKnownPlugins = Microbot.getPluginManager().getPlugins().stream() - .filter(plugin -> plugin instanceof SchedulablePlugin) - .collect(Collectors.toSet()); - - updateFilteredPluginList(); - - // Add listener to clear table selection when ComboBox changes - pluginComboBox.addActionListener(e -> { - if (pluginComboBox.getSelectedItem() != null && selectionChangeListener != null && isUserAction) { - //selectionChangeListener.run(); - } - }); - - - - // Plugin settings section with improved UI - gbc.gridx = 0; - gbc.gridy = 2; - gbc.gridwidth = 4; - JPanel pluginSettingsPanel = new JPanel(new BorderLayout()); - pluginSettingsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - pluginSettingsPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Plugin Settings", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - - // Create a panel for the checkboxes with horizontal layout - JPanel checkboxesPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 5)); - checkboxesPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Random scheduling checkbox - randomSchedulingCheckbox = new JCheckBox("Allow random scheduling"); - randomSchedulingCheckbox.setSelected(true); - randomSchedulingCheckbox.setToolTipText( - "When enabled, this plugin can be randomly selected when multiple plugins are due to run.
" + - "If disabled, this plugin will have higher priority than randomizable plugins."); - randomSchedulingCheckbox.setForeground(Color.WHITE); - randomSchedulingCheckbox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - checkboxesPanel.add(randomSchedulingCheckbox); - - // Time-based stop condition checkbox - timeBasedStopConditionCheckbox = new JCheckBox("Requires time-based stop condition"); - timeBasedStopConditionCheckbox.setSelected(false); - timeBasedStopConditionCheckbox.setToolTipText( - "When enabled, the scheduler will prompt you to add a time-based stop condition for this plugin.
" + - "This helps prevent plugins from running indefinitely if other stop conditions don't trigger."); - timeBasedStopConditionCheckbox.setForeground(Color.WHITE); - timeBasedStopConditionCheckbox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - checkboxesPanel.add(timeBasedStopConditionCheckbox); - - // Allow continue checkbox - allowContinueCheckbox = new JCheckBox("Allow continue"); - allowContinueCheckbox.setSelected(false); - allowContinueCheckbox.setToolTipText( - "When enabled, the plugin will automatically resume after being interrupted by a higher-priority plugin.
" + - "This preserves the plugin's state and progress toward stop conditions without needing to re-evaluate start conditions.
" + - "Especially important for default plugins (priority 0) that should continue after higher-priority tasks finish."); - allowContinueCheckbox.setForeground(Color.WHITE); - allowContinueCheckbox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - checkboxesPanel.add(allowContinueCheckbox); - - // Add checkboxes panel to the top - pluginSettingsPanel.add(checkboxesPanel, BorderLayout.NORTH); - - // Priority and Default panel - improved layout with better spacing - JPanel priorityPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 5)); - priorityPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Priority label and spinner in one group - JLabel priorityLabel = new JLabel("Priority:"); - priorityLabel.setForeground(Color.WHITE); - priorityLabel.setFont(FontManager.getRunescapeFont()); - priorityPanel.add(priorityLabel); - - prioritySpinner = new JSpinner(new SpinnerNumberModel(0, 0, 100, 1)); - prioritySpinner.setToolTipText("Higher priority plugins will be scheduled before lower priority ones"); - prioritySpinner.setPreferredSize(new Dimension(60, 28)); - priorityPanel.add(prioritySpinner); - - // Add some spacing - priorityPanel.add(Box.createHorizontalStrut(20)); - - // Default plugin checkbox - defaultPluginCheckbox = new JCheckBox("Set as default plugin"); - defaultPluginCheckbox.setSelected(false); - defaultPluginCheckbox.setToolTipText( - "When enabled, this plugin is marked as a default option.
" + - "Default plugins always have priority 0.
" + - "Non-default plugins will always be scheduled first."); - defaultPluginCheckbox.setForeground(Color.WHITE); - defaultPluginCheckbox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - priorityPanel.add(defaultPluginCheckbox); - - // Synchronize priority and default checkbox - prioritySpinner.addChangeListener(e -> { - if (updatingValues) return; - updatingValues = true; - int value = (Integer) prioritySpinner.getValue(); - defaultPluginCheckbox.setSelected(value == 0); - updatingValues = false; - }); - - defaultPluginCheckbox.addItemListener(e -> { - if (updatingValues) return; - updatingValues = true; - if (e.getStateChange() == ItemEvent.SELECTED) { - prioritySpinner.setValue(0); // Default plugins always have priority 0 - } else if ((Integer) prioritySpinner.getValue() == 0) { - prioritySpinner.setValue(1); // If unchecking default and priority is 0, set to 1 - } - updatingValues = false; - }); - - // Add priority panel to bottom - pluginSettingsPanel.add(priorityPanel, BorderLayout.CENTER); - - formPanel.add(pluginSettingsPanel, gbc); - // Time condition type selection - gbc.gridx = 0; - gbc.gridy = 3; - gbc.gridwidth = 1; - JLabel conditionTypeLabel = new JLabel("Schedule Type:"); - conditionTypeLabel.setForeground(Color.WHITE); - conditionTypeLabel.setFont(FontManager.getRunescapeFont()); - formPanel.add(conditionTypeLabel, gbc); - - gbc.gridx = 1; - gbc.gridy = 3; - gbc.gridwidth = 3; - timeConditionTypeComboBox = new JComboBox<>(TIME_CONDITION_TYPES); - timeConditionTypeComboBox.addActionListener(e -> updateConditionPanel()); - formPanel.add(timeConditionTypeComboBox, gbc); - - // Dynamic condition config panel - gbc.gridx = 0; - gbc.gridy = 4; - gbc.gridwidth = 4; - conditionConfigPanel = new JPanel(new BorderLayout()); - conditionConfigPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - conditionConfigPanel.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(1, 1, 1, 1, ColorScheme.DARK_GRAY_COLOR), - BorderFactory.createEmptyBorder(5, 5, 5, 5))); - conditionConfigPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - conditionConfigPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Main Start Condition", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - formPanel.add(conditionConfigPanel, gbc); - - - - // Wrap the formPanel in a scroll pane - JScrollPane scrollPane = new JScrollPane(formPanel); - scrollPane.setBorder(BorderFactory.createEmptyBorder()); - scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - scrollPane.getVerticalScrollBar().setUnitIncrement(16); - - return scrollPane; - } - - /** - * Creates the filter panel with primary and secondary filter comboboxes - */ - private JPanel createFilterPanel() { - JPanel filterPanel = new JPanel(new GridBagLayout()); - filterPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - filterPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Plugin Filters", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(5, 5, 5, 5); - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.anchor = GridBagConstraints.WEST; - - // Primary filter - gbc.gridx = 0; - gbc.gridy = 0; - gbc.weightx = 0.0; - JLabel primaryFilterLabel = new JLabel("Filter by:"); - primaryFilterLabel.setForeground(Color.WHITE); - primaryFilterLabel.setFont(FontManager.getRunescapeFont()); - filterPanel.add(primaryFilterLabel, gbc); - - gbc.gridx = 1; - gbc.weightx = 0.5; - primaryFilterComboBox = new JComboBox<>(); - for (String category : PluginFilterUtil.getPrimaryFilterCategories()) { - primaryFilterComboBox.addItem(category); - } - primaryFilterComboBox.addActionListener(e -> updateSecondaryFilter()); - filterPanel.add(primaryFilterComboBox, gbc); - - // Secondary filter - gbc.gridx = 2; - gbc.weightx = 0.0; - JLabel secondaryFilterLabel = new JLabel("Sub-filter:"); - secondaryFilterLabel.setForeground(Color.WHITE); - secondaryFilterLabel.setFont(FontManager.getRunescapeFont()); - filterPanel.add(secondaryFilterLabel, gbc); - - gbc.gridx = 3; - gbc.weightx = 0.5; - secondaryFilterComboBox = new JComboBox<>(); - secondaryFilterComboBox.addActionListener(e -> updateFilteredPluginList()); - filterPanel.add(secondaryFilterComboBox, gbc); - - // Don't initialize filters here - wait until pluginComboBox is created - // updateSecondaryFilter() will be called later in initialization - - return filterPanel; - } - - /** - * Updates the secondary filter combobox based on primary filter selection - */ - private void updateSecondaryFilter() { - String selectedPrimary = (String) primaryFilterComboBox.getSelectedItem(); - if (selectedPrimary == null) return; - - secondaryFilterComboBox.removeAllItems(); - - List allPlugins = lastKnownPlugins.stream() - .filter(plugin -> plugin instanceof SchedulablePlugin) - .filter(plugin -> { - net.runelite.client.plugins.PluginDescriptor descriptor = - plugin.getClass().getAnnotation(net.runelite.client.plugins.PluginDescriptor.class); - return descriptor != null && !descriptor.hidden(); - }) - .collect(Collectors.toList()); - List secondaryOptions = PluginFilterUtil.getSecondaryFilterOptions(selectedPrimary, allPlugins); - - for (String option : secondaryOptions) { - secondaryFilterComboBox.addItem(option); - } - - updateFilteredPluginList(); - } - - /** - * Updates the plugin combobox based on current filter selections - */ - private void updateFilteredPluginList() { - String primaryFilter = (String) primaryFilterComboBox.getSelectedItem(); - String secondaryFilter = (String) secondaryFilterComboBox.getSelectedItem(); - - if (primaryFilter == null || pluginComboBox == null) return; - List allPlugins = lastKnownPlugins.stream() - .filter(plugin -> plugin instanceof SchedulablePlugin) - .filter(plugin -> { - PluginDescriptor descriptor = - plugin.getClass().getAnnotation(PluginDescriptor.class); - return descriptor != null && !descriptor.hidden(); - }) - .collect(Collectors.toList()); - - List filteredPlugins = PluginFilterUtil.filterPlugins(allPlugins, primaryFilter, secondaryFilter); - - // Convert to plugin names and update the main plugin combobox - List pluginNames = filteredPlugins.stream() - .map(Plugin::getName) - .sorted() - .collect(Collectors.toList()); - - // Temporarily disable action listeners to prevent feedback loops - ActionListener[] listeners = pluginComboBox.getActionListeners(); - for (ActionListener listener : listeners) { - pluginComboBox.removeActionListener(listener); - } - - pluginComboBox.removeAllItems(); - for (String name : pluginNames) { - pluginComboBox.addItem(name); - } - - // Re-add listeners - for (ActionListener listener : listeners) { - pluginComboBox.addActionListener(listener); - } - } - - /** - * Creates a panel for editing selected plugin properties - */ - private JScrollPane createPropertiesPanel() { - JPanel formPanel = new JPanel(new GridBagLayout()); - formPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 0; - gbc.gridwidth = 2; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.insets = new Insets(5, 5, 5, 5); - gbc.weightx = 1.0; - - // Create a message for when no plugin is selected - JPanel noSelectionPanel = new JPanel(new BorderLayout()); - noSelectionPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - JLabel noSelectionLabel = new JLabel("Select a plugin from the table to edit its properties"); - noSelectionLabel.setForeground(Color.WHITE); - noSelectionLabel.setFont(FontManager.getRunescapeFont()); - noSelectionLabel.setHorizontalAlignment(SwingConstants.CENTER); - noSelectionPanel.add(noSelectionLabel, BorderLayout.CENTER); - - // Create editor panel - JPanel editorPanel = new JPanel(new GridBagLayout()); - editorPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Plugin name header - selectedPluginNameLabel = new JLabel("Plugin Properties"); - selectedPluginNameLabel.setForeground(Color.WHITE); - selectedPluginNameLabel.setFont(FontManager.getRunescapeBoldFont()); - - gbc.gridx = 0; - gbc.gridy = 0; - gbc.gridwidth = 2; - editorPanel.add(selectedPluginNameLabel, gbc); - - // Enabled checkbox - gbc.gridx = 0; - gbc.gridy = 1; - gbc.gridwidth = 2; - selectedPluginEnabledCheckbox = createPropertyCheckbox("Enabled", - "When checked, the plugin is eligible to be scheduled based on its conditions"); - editorPanel.add(selectedPluginEnabledCheckbox, gbc); - - // Default plugin checkbox - gbc.gridx = 0; - gbc.gridy = 2; - gbc.gridwidth = 2; - selectedPluginDefaultCheckbox = createPropertyCheckbox("Default Plugin", - "When checked, this plugin will be treated as a default plugin (priority 0) and scheduled after all others"); - editorPanel.add(selectedPluginDefaultCheckbox, gbc); - - // Priority spinner with improved alignment - JPanel priorityGroupPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - priorityGroupPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel priorityLabel = new JLabel("Priority:"); - priorityLabel.setForeground(Color.WHITE); - priorityGroupPanel.add(priorityLabel); - - SpinnerModel priorityModel = new SpinnerNumberModel(1, 0, 100, 1); - selectedPluginPrioritySpinner = new JSpinner(priorityModel); - selectedPluginPrioritySpinner.setPreferredSize(new Dimension(60, 28)); - selectedPluginPrioritySpinner.setToolTipText("Sets the priority for this plugin.
Higher priority = run first.
0 = default plugin (scheduled after all others)"); - priorityGroupPanel.add(selectedPluginPrioritySpinner); - - gbc.gridx = 0; - gbc.gridy = 3; - gbc.gridwidth = 2; - editorPanel.add(priorityGroupPanel, gbc); - - // Random scheduling checkbox - gbc.gridx = 0; - gbc.gridy = 4; - gbc.gridwidth = 2; - selectedPluginRandomCheckbox = createPropertyCheckbox("Allow Random Scheduling", - "When enabled, the scheduler will apply some randomization to when this plugin runs"); - editorPanel.add(selectedPluginRandomCheckbox, gbc); - - gbc.gridx = 0; - gbc.gridy = 5; - gbc.gridwidth = 2; - selectedPluginTimeStopCheckbox = createPropertyCheckbox("Requires Time-based Stop Condition", - "When enabled, the scheduler will prompt you to add a time-based stop condition for this plugin."); - editorPanel.add(selectedPluginTimeStopCheckbox, gbc); - - // Add Allow Continue checkbox - gbc.gridx = 0; - gbc.gridy = 6; - gbc.gridwidth = 2; - selectedPluginAllowContinueCheckbox = createPropertyCheckbox("Allow Continue After Interruption", - "When enabled, the plugin will automatically resume after being interrupted by a higher-priority plugin.
" + - "This preserves all progress made toward stop conditions without resetting start conditions.
" + - "For default plugins (priority 0) in a cycle, this determines whether the plugin keeps its place
" + - "or must compete with other default plugins based on run counts when it's time to select the next plugin."); - editorPanel.add(selectedPluginAllowContinueCheckbox, gbc); - - // Plugin run statistics - gbc.gridx = 0; - gbc.gridy = 7; - gbc.gridwidth = 2; - JPanel statsPanel = new JPanel(new GridLayout(0, 1, 5, 5)); - statsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - statsPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - "Statistics", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeFont(), - Color.WHITE)); - - // Statistics info labels - runsLabel = new JLabel("Total Runs: 0"); - runsLabel.setForeground(Color.WHITE); - lastRunLabel = new JLabel("Last Run: Never"); - lastRunLabel.setForeground(Color.WHITE); - lastDurationLabel = new JLabel("Last Duration: N/A"); - lastDurationLabel.setForeground(Color.WHITE); - lastStopReasonLabel = new JLabel("Last Stop Reason: N/A"); - lastStopReasonLabel.setForeground(Color.WHITE); - - statsPanel.add(runsLabel); - statsPanel.add(lastRunLabel); - statsPanel.add(lastDurationLabel); - statsPanel.add(lastStopReasonLabel); - - gbc.gridx = 0; - gbc.gridy = 8; - editorPanel.add(statsPanel, gbc); - - // Save changes button with prominent styling - gbc.gridx = 0; - gbc.gridy = 9; - gbc.gridwidth = 2; - saveChangesButton = new JButton("Save Changes"); - saveChangesButton.setBackground(new Color(76, 175, 80)); // Green - saveChangesButton.setForeground(Color.WHITE); - saveChangesButton.setFocusPainted(false); - saveChangesButton.addActionListener(e -> updateSelectedPlugin()); - editorPanel.add(saveChangesButton, gbc); - - // Add property change listeners - selectedPluginEnabledCheckbox.addItemListener(e -> { - if (selectedPlugin != null) { - selectedPlugin.setEnabled(selectedPluginEnabledCheckbox.isSelected()); - if (tabbedPane.getSelectedIndex() == 1) { - updateSelectedPlugin(); - } - updateControlButton(); - updateStatistics(); - } - }); - - selectedPluginRandomCheckbox.addItemListener(e -> { - if (selectedPlugin != null) { - selectedPlugin.setAllowRandomScheduling(selectedPluginRandomCheckbox.isSelected()); - if (tabbedPane.getSelectedIndex() == 1) { - updateSelectedPlugin(); - } - } - }); - - selectedPluginTimeStopCheckbox.addItemListener(e -> { - if (selectedPlugin != null) { - selectedPlugin.setNeedsStopCondition(selectedPluginTimeStopCheckbox.isSelected()); - if (tabbedPane.getSelectedIndex() == 1) { - updateSelectedPlugin(); - } - } - }); - - // Add listener for the new Allow Continue checkbox - selectedPluginAllowContinueCheckbox.addItemListener(e -> { - if (selectedPlugin != null) { - selectedPlugin.setAllowContinue(selectedPluginAllowContinueCheckbox.isSelected()); - if (tabbedPane.getSelectedIndex() == 1) { - updateSelectedPlugin(); - } - } - }); - - selectedPluginDefaultCheckbox.addItemListener(e -> { - if (selectedPlugin != null) { - selectedPlugin.setDefault(selectedPluginDefaultCheckbox.isSelected()); - if (tabbedPane.getSelectedIndex() == 1) { - updateSelectedPlugin(); - } - } - }); - - // Initialize with no selection panel - pluginPropertiesPanel = new JPanel(new BorderLayout()); - pluginPropertiesPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - pluginPropertiesPanel.add(noSelectionPanel, BorderLayout.CENTER); - - // Store panels as client properties for later retrieval - pluginPropertiesPanel.putClientProperty("noSelectionPanel", noSelectionPanel); - pluginPropertiesPanel.putClientProperty("editorPanel", editorPanel); - - // Link default checkbox and priority spinner - selectedPluginDefaultCheckbox.addItemListener(e -> { - if (updatingValues) return; // Skip if we're programmatically updating values - - updatingValues = true; // Set flag to prevent recursive updates - try { - if (selectedPlugin != null) { - if (e.getStateChange() == ItemEvent.SELECTED) { - selectedPluginPrioritySpinner.setValue(0); // Default plugins have priority 0 - } else if ((Integer) selectedPluginPrioritySpinner.getValue() == 0) { - selectedPluginPrioritySpinner.setValue(1); // Non-default get priority 1 - } - - if (tabbedPane.getSelectedIndex() == 1) { - - updateSelectedPlugin(); - } - } - } finally { - updatingValues = false; // Always reset flag - } - }); - - selectedPluginPrioritySpinner.addChangeListener(e -> { - if (updatingValues) return; // Skip if we're programmatically updating values - - updatingValues = true; // Set flag to prevent recursive updates - try { - if (selectedPlugin != null) { - int priority = (Integer) selectedPluginPrioritySpinner.getValue(); - // Update default checkbox based on priority value - boolean shouldBeDefault = priority == 0; - if (selectedPluginDefaultCheckbox.isSelected() != shouldBeDefault) { - selectedPluginDefaultCheckbox.setSelected(shouldBeDefault); - } - if (tabbedPane.getSelectedIndex() == 1) { - updateSelectedPlugin(); - } - - } - } finally { - updatingValues = false; // Always reset flag - } - }); - - // Wrap the formPanel in a scroll pane - JScrollPane scrollPane = new JScrollPane(pluginPropertiesPanel); - scrollPane.setBorder(BorderFactory.createEmptyBorder()); - scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - scrollPane.getVerticalScrollBar().setUnitIncrement(16); - return scrollPane; - } - - /** - * Helper method to create a styled checkbox for properties panel - */ - private JCheckBox createPropertyCheckbox(String text, String tooltip) { - JCheckBox checkbox = new JCheckBox(text); - checkbox.setForeground(Color.WHITE); - checkbox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - checkbox.setToolTipText(tooltip); - checkbox.setFocusPainted(false); - return checkbox; - } - - /** - * Updates the statistics display in the properties panel - */ - private void updateStatistics() { - if (selectedPlugin == null) { - runsLabel.setText("Total Runs: 0"); - lastRunLabel.setText("Last Run: Never"); - lastDurationLabel.setText("Last Duration: N/A"); - lastStopReasonLabel.setText("Last Stop Reason: N/A"); - return; - } - - // Update run count - runsLabel.setText("Total Runs: " + selectedPlugin.getRunCount()); - - // Update last run time - ZonedDateTime lastEndRunTime = selectedPlugin.getLastRunEndTime(); - Duration lastRunTime = selectedPlugin.getLastRunDuration(); - if (lastEndRunTime != null) { - lastRunLabel.setText("Last End: " + lastEndRunTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))+ " (" + lastRunTime.toHoursPart() + ":" + lastRunTime.toMinutesPart() + ":" + lastRunTime.toSecondsPart() + ")"); - } else { - lastRunLabel.setText("Last End: Never"); - } - - // Update duration if available - if (selectedPlugin.getLastRunDuration() != null && !selectedPlugin.getLastRunDuration().isZero()) { - Duration duration = selectedPlugin.getLastRunDuration(); - long hours = duration.toHours(); - long minutes = duration.toMinutesPart(); - long seconds = duration.toSecondsPart(); - lastDurationLabel.setText(String.format("Last Duration: %d:%02d:%02d", hours, minutes, seconds)); - } else { - lastDurationLabel.setText("Last Duration: N/A"); - } - - // Update stop reason - String stopReason = selectedPlugin.getLastStopReason(); - if (stopReason != null && !stopReason.isEmpty()) { - if (stopReason.length() > 40) { - stopReason = stopReason.substring(0, 37) + "..."; - } - lastStopReasonLabel.setText("Last Stop: " + stopReason); - } else { - lastStopReasonLabel.setText("Last Stop: N/A"); - } - } - - /** - * Updates the condition configuration panel based on the selected time condition type - */ - private void updateConditionPanel() { - // Clear existing panel - if (currentConditionPanel != null) { - conditionConfigPanel.remove(currentConditionPanel); - } - - // Create a new panel based on selection - String selectedType = (String) timeConditionTypeComboBox.getSelectedItem(); - currentConditionPanel = new JPanel(); - currentConditionPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(5, 5, 5, 5); - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.gridx = 0; - gbc.gridy = 0; - gbc.gridwidth = 4; - - // Create the appropriate condition panel - if (CONDITION_DEFAULT.equals(selectedType)) { - JLabel defaultLabel = new JLabel("Default plugin with 1-second interval (always runs last)"); - defaultLabel.setForeground(Color.WHITE); - currentConditionPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); - currentConditionPanel.add(defaultLabel); - } - else if (CONDITION_SPECIFIC_TIME.equals(selectedType)) { - currentConditionPanel.setLayout(new GridBagLayout()); - TimeConditionPanelUtil.createSingleTriggerConfigPanel(currentConditionPanel, gbc); - } - else if (CONDITION_INTERVAL.equals(selectedType)) { - currentConditionPanel.setLayout(new GridBagLayout()); - TimeConditionPanelUtil.createIntervalConfigPanel(currentConditionPanel, gbc); - } - else if (CONDITION_TIME_WINDOW.equals(selectedType)) { - currentConditionPanel.setLayout(new GridBagLayout()); - TimeConditionPanelUtil.createTimeWindowConfigPanel(currentConditionPanel, gbc); - } - else if (CONDITION_DAY_OF_WEEK.equals(selectedType)) { - currentConditionPanel.setLayout(new GridBagLayout()); - TimeConditionPanelUtil.createDayOfWeekConfigPanel(currentConditionPanel, gbc); - } - - // Add the panel - conditionConfigPanel.add(currentConditionPanel, BorderLayout.CENTER); - conditionConfigPanel.revalidate(); - conditionConfigPanel.repaint(); - } - - private JButton createButton(String text, Color color) { - JButton button = new JButton(text); - button.setFont(FontManager.getRunescapeSmallFont()); - button.setFocusPainted(false); - button.setForeground(Color.WHITE); - button.setBackground(ColorScheme.DARKER_GRAY_COLOR); - button.setBorder(new CompoundBorder( - BorderFactory.createLineBorder(color.darker(), 1), - BorderFactory.createEmptyBorder(5, 5, 5, 5))); - - button.addMouseListener(new java.awt.event.MouseAdapter() { - @Override - public void mouseEntered(java.awt.event.MouseEvent e) { - button.setBackground(color); - button.setForeground(ColorScheme.DARK_GRAY_COLOR); - } - - @Override - public void mouseExited(java.awt.event.MouseEvent e) { - button.setBackground(ColorScheme.DARKER_GRAY_COLOR); - button.setForeground(Color.WHITE); - } - }); - - return button; - } - - public void updatePluginList(List plugins) { - if (plugins == null || plugins.isEmpty()) { - log.debug("No plugins available to populate combo box"); - return; - } - - // If filters are not yet initialized, use the old method - if (primaryFilterComboBox == null || secondaryFilterComboBox == null) { - pluginComboBox.removeAllItems(); - for (String plugin : plugins) { - pluginComboBox.addItem(plugin); - } - } else { - // Use the filter system to populate the combo box - updateFilteredPluginList(); - } - } - - public void loadPlugin(PluginScheduleEntry entry) { - - if (entry == null ) { - log.warn("Attempted to load null plugin entry"); - //switch to "new schedule" tab - tabbedPane.setSelectedIndex(0); - // set tab 1 not showing - // Disable the properties tab when no plugin is selected - tabbedPane.setEnabledAt(1, false); - setEditMode(false); - return; - } - tabbedPane.setEnabledAt(1, true); - if (entry.equals(selectedPlugin)){ - log.debug("Attempted to load already selected plugin entry"); - return; - } - - this.selectedPlugin = entry; - - // Block combobox events temporarily to avoid feedback loops - ActionListener[] listeners = pluginComboBox.getActionListeners(); - for (ActionListener listener : listeners) { - pluginComboBox.removeActionListener(listener); - } - - // Update plugin selection - pluginComboBox.setSelectedItem(entry.getName()); - - // Re-add listeners - for (ActionListener listener : listeners) { - pluginComboBox.addActionListener(listener); - } - - // Set random scheduling checkbox - randomSchedulingCheckbox.setSelected(entry.isAllowRandomScheduling()); - - // Set time-based stop condition checkbox - timeBasedStopConditionCheckbox.setSelected(entry.isNeedsStopCondition()); - - // Set allow continue checkbox - allowContinueCheckbox.setSelected(entry.isAllowContinue()); - - // Set priority spinner - prioritySpinner.setValue(entry.getPriority()); - - // Set default checkbox - defaultPluginCheckbox.setSelected(entry.isDefault()); - // Update the properties panel - updatePropertiesPanel(entry); - - // Determine the time condition type and set appropriate panel - TimeCondition startCondition = null; - if (entry.getStartConditionManager() != null) { - List timeConditions = entry.getStartConditionManager().getTimeConditions(); - if (!timeConditions.isEmpty()) { - startCondition = timeConditions.get(0); - } - } - - if (startCondition == null) { - // Default to showing the default panel, as we can't determine the condition type - timeConditionTypeComboBox.setSelectedItem(CONDITION_INTERVAL); - updateConditionPanel(); - return; - } - - TimeCondition mainStartCondition = entry.getMainTimeStartCondition(); - - // Block combobox events again for condition type changes - ActionListener[] conditionListeners = timeConditionTypeComboBox.getActionListeners(); - for (ActionListener listener : conditionListeners) { - timeConditionTypeComboBox.removeActionListener(listener); - } - - // If it's a default plugin (by flag or by interval), show "Run Default" - if (startCondition instanceof SingleTriggerTimeCondition) { - timeConditionTypeComboBox.setSelectedItem(CONDITION_SPECIFIC_TIME); - updateConditionPanel(); - setupTimeConditionPanel(startCondition); - } else if (startCondition instanceof IntervalCondition) { - Optional nextTriger = startCondition.getDurationUntilNextTrigger(); - IntervalCondition interval = (IntervalCondition) startCondition; - - if (interval.getInterval().getSeconds() <= 1 && entry.isDefault()) { - timeConditionTypeComboBox.setSelectedItem(CONDITION_DEFAULT); - updateConditionPanel(); - }else{ - // Configure the panel with existing values - timeConditionTypeComboBox.setSelectedItem(CONDITION_INTERVAL); - updateConditionPanel(); - setupTimeConditionPanel(startCondition); - } - //updateConditionPanel(); - - } else if (startCondition instanceof TimeWindowCondition) { - timeConditionTypeComboBox.setSelectedItem(CONDITION_TIME_WINDOW); - updateConditionPanel(); - setupTimeConditionPanel(startCondition); - } else if (startCondition instanceof DayOfWeekCondition) { - timeConditionTypeComboBox.setSelectedItem(CONDITION_DAY_OF_WEEK); - updateConditionPanel(); - setupTimeConditionPanel(startCondition); - } - // Re-add condition type listeners - for (ActionListener listener : conditionListeners) { - timeConditionTypeComboBox.addActionListener(listener); - } - - // Update the control button to reflect the current plugin - updateControlButton(); - - - } - - /** - * Updates the properties panel to show the selected plugin's properties - */ - private void updatePropertiesPanel(PluginScheduleEntry entry) { - - if (entry == null) { - // Show the no selection panel - JPanel noSelectionPanel = (JPanel) pluginPropertiesPanel.getClientProperty("noSelectionPanel"); - pluginPropertiesPanel.removeAll(); - pluginPropertiesPanel.add(noSelectionPanel, BorderLayout.CENTER); - pluginPropertiesPanel.revalidate(); - pluginPropertiesPanel.repaint(); - return; - } - - // Get the editor panel - JPanel editorPanel = (JPanel) pluginPropertiesPanel.getClientProperty("editorPanel"); - - // Update the header with the plugin name - directly use the selectedPluginNameLabel reference - if (selectedPluginNameLabel != null) { - selectedPluginNameLabel.setText("Plugin: " + entry.getCleanName()); - } - - // Set the flag to prevent update loops - updatingValues = true; - - try { - - selectedPluginEnabledCheckbox.setSelected(entry.isEnabled()); - - // Update default checkbox first as it may impact the priority spinner - selectedPluginDefaultCheckbox.setSelected(entry.isDefault()); - - // Then update priority spinner - selectedPluginPrioritySpinner.setValue(entry.getPriority()); - - // Update other checkboxes - selectedPluginRandomCheckbox.setSelected(entry.isAllowRandomScheduling()); - selectedPluginTimeStopCheckbox.setSelected(entry.isNeedsStopCondition()); - selectedPluginAllowContinueCheckbox.setSelected(entry.isAllowContinue()); - - // Update statistics - updateStatistics(); - } finally { - // Reset the flag after all updates - updatingValues = false; - } - - // Show the editor panel - pluginPropertiesPanel.removeAll(); - pluginPropertiesPanel.add(editorPanel, BorderLayout.CENTER); - pluginPropertiesPanel.revalidate(); - pluginPropertiesPanel.repaint(); - } - - public void clearForm() { - - this.selectedPlugin = null; - if (pluginComboBox.getItemCount() > 0) { - pluginComboBox.setSelectedIndex(0); - } - - // Reset condition type to default - timeConditionTypeComboBox.setSelectedItem(CONDITION_INTERVAL); - updateConditionPanel(); - - // Reset random scheduling - randomSchedulingCheckbox.setSelected(true); - - // Update the control button - updateControlButton(); - - // Reset properties panel - updatePropertiesPanel(null); - tabbedPane.setEnabledAt(1, false); - tabbedPane.setSelectedIndex(0); - } - - public PluginScheduleEntry getPluginFromForm(PluginScheduleEntry existingPlugin) { - String pluginName = (String) pluginComboBox.getSelectedItem(); - if (pluginName == null || pluginName.isEmpty()) { - return null; - } - - // Get the selected time condition type - String selectedType = (String) timeConditionTypeComboBox.getSelectedItem(); - TimeCondition timeCondition = null; - - // Create the appropriate time condition - if (CONDITION_DEFAULT.equals(selectedType)) { - // Default plugin with 1-second interval - timeCondition = new IntervalCondition(Duration.ofSeconds(1)); - } else if (CONDITION_SPECIFIC_TIME.equals(selectedType)) { - timeCondition = TimeConditionPanelUtil.createSingleTriggerCondition(currentConditionPanel); - } else if (CONDITION_INTERVAL.equals(selectedType)) { - timeCondition = TimeConditionPanelUtil.createIntervalCondition(currentConditionPanel); - } else if (CONDITION_TIME_WINDOW.equals(selectedType)) { - timeCondition = TimeConditionPanelUtil.createTimeWindowCondition(currentConditionPanel); - } else if (CONDITION_DAY_OF_WEEK.equals(selectedType)) { - timeCondition = TimeConditionPanelUtil.createDayOfWeekCondition(currentConditionPanel); - } - - // If we couldn't create a time condition, return null - if (timeCondition == null) { - log.warn("Could not create time condition from form"); - return null; - } - - // Get other settings - boolean randomScheduling = randomSchedulingCheckbox.isSelected(); - boolean needsStopCondition = timeBasedStopConditionCheckbox.isSelected(); - boolean allowContinue = allowContinueCheckbox.isSelected(); - int priority = (Integer) prioritySpinner.getValue(); - boolean isDefault = defaultPluginCheckbox.isSelected(); - - // Create the plugin schedule entry - PluginScheduleEntry entry; - log.debug("values for PluginScheduleEntry entry {}\n priority {}\n isDefault {} \n needsStopCondition {} \n randomScheduling {}",pluginName,priority, isDefault, needsStopCondition, randomScheduling); - if (existingPlugin != null) { - log.debug("Updating existing plugin entry"); - - // Update the existing plugin with new values - existingPlugin.updatePrimaryTimeCondition(timeCondition); - existingPlugin.setAllowRandomScheduling(randomScheduling); - existingPlugin.setNeedsStopCondition(needsStopCondition); - existingPlugin.setAllowContinue(allowContinue); - existingPlugin.setPriority(priority); - existingPlugin.setDefault(isDefault); - entry = existingPlugin; - } else { - - log.debug("Creating new plugin entry"); - // Create a new plugin schedule entry - entry = new PluginScheduleEntry( - pluginName, - timeCondition, - true, // Enabled by default - randomScheduling - ); - entry.setNeedsStopCondition(needsStopCondition); - entry.setAllowContinue(allowContinue); - entry.setPriority(priority); - entry.setDefault(isDefault); - } - if (entry != null) { - randomSchedulingCheckbox.setSelected(entry.isAllowRandomScheduling()); - timeBasedStopConditionCheckbox.setSelected(entry.isNeedsStopCondition()); - allowContinueCheckbox.setSelected(entry.isAllowContinue()); - prioritySpinner.setValue(entry.getPriority()); - defaultPluginCheckbox.setSelected(entry.isDefault()); - updatePropertiesPanel(entry); - } - return entry; - } - - /** - * Updates the selected plugin with values from the properties panel - */ - private void updateSelectedPlugin() { - if (selectedPlugin == null) return; - - boolean enabled = selectedPluginEnabledCheckbox.isSelected(); - boolean randomScheduling = selectedPluginRandomCheckbox.isSelected(); - boolean needsStopCondition = selectedPluginTimeStopCheckbox.isSelected(); - boolean allowContinue = selectedPluginAllowContinueCheckbox.isSelected(); - int priority = (Integer) selectedPluginPrioritySpinner.getValue(); - boolean isDefault = selectedPluginDefaultCheckbox.isSelected(); - - // Update the plugin - selectedPlugin.setEnabled(enabled); - selectedPlugin.setAllowRandomScheduling(randomScheduling); - selectedPlugin.setNeedsStopCondition(needsStopCondition); - selectedPlugin.setAllowContinue(allowContinue); - selectedPlugin.setPriority(priority); - selectedPlugin.setDefault(isDefault); - - // Save the changes - plugin.saveScheduledPlugins(); - - // Update the control button - updateControlButton(); - - // Notify the main window to refresh the table - //plugin.refreshScheduleTable(); - } - public void refresh(){ - // Check if plugins have changed - Set currentPlugins = Microbot.getPluginManager().getPlugins().stream() - .filter(plugin -> plugin instanceof SchedulablePlugin) - .collect(Collectors.toSet()); - - if (!currentPlugins.equals(lastKnownPlugins)) { - log.info("Plugin changes detected, updating plugin list"); - lastKnownPlugins = new HashSet<>(currentPlugins); - updateFilteredPluginList(); - } - - updateControlButton(); - } - public void updateControlButton() { - boolean isRunning = selectedPlugin != null && selectedPlugin.isRunning(); - boolean isEnabled = selectedPlugin != null && selectedPlugin.isEnabled(); - boolean anyPluginRunning = plugin.isScheduledPluginRunning(); - - if (isRunning) { - // If this plugin is running, show Stop button - controlButton.setText("Stop Plugin"); - controlButton.setBackground(ColorScheme.PROGRESS_ERROR_COLOR); - } else { - // Otherwise show Run Now button - controlButton.setText("Run Now"); - controlButton.setBackground(ColorScheme.PROGRESS_COMPLETE_COLOR); - } - - // Disable the button if: - // 1. No plugin is selected, OR - // 2. Selected plugin is disabled, OR - // 3. Any plugin is running (not just the selected one) and we're showing "Run Now" - controlButton.setEnabled( - selectedPlugin != null && - isEnabled && - (!anyPluginRunning || isRunning) - ); - - // Update tooltip to explain why button might be disabled - if (selectedPlugin == null) { - controlButton.setToolTipText("No plugin selected"); - } else if (!isEnabled) { - controlButton.setToolTipText("Plugin is disabled"); - } else if (anyPluginRunning && !isRunning) { - controlButton.setToolTipText("Cannot start: Another plugin is already running"); - } else { - controlButton.setToolTipText(isRunning ? "Stop the running plugin" : "Run this plugin now"); - } - } - - private void onControlButtonClicked(ActionEvent e) { - if (selectedPlugin == null) { - return; - } - - if (selectedPlugin.isRunning()) { - // Stop the plugin - if (plugin.getCurrentPlugin()!= null && plugin.getCurrentPlugin().equals(selectedPlugin)) { - plugin.forceStopCurrentPluginScheduleEntry(true); - }else{ - plugin.forceStopCurrentPluginScheduleEntry(false); - } - } else { - // Start the plugin using the new manualStartPlugin method - String result = plugin.manualStartPlugin(selectedPlugin); - if (!result.isEmpty()) { - // Show error message if starting failed - JOptionPane.showMessageDialog( - SwingUtilities.getWindowAncestor(this), - result, - "Cannot Start Plugin immediately, update only main time start condition", - JOptionPane.WARNING_MESSAGE - ); - } - } - - // Update control button and statistics - updateControlButton(); - updateStatistics(); - } - - public void setEditMode(boolean editMode) { - updateButton.setVisible(editMode); - addButton.setVisible(!editMode); - } - - public void setAddButtonAction(ActionListener listener) { - for (ActionListener l : addButton.getActionListeners()) { - addButton.removeActionListener(l); - } - addButton.addActionListener(listener); - } - - public void setUpdateButtonAction(ActionListener listener) { - for (ActionListener l : updateButton.getActionListeners()) { - updateButton.removeActionListener(l); - } - updateButton.addActionListener(listener); - } - - public void setRemoveButtonAction(ActionListener listener) { - for (ActionListener l : removeButton.getActionListeners()) { - removeButton.removeActionListener(l); - } - removeButton.addActionListener(listener); - } - - /** - * Sets up the time condition panel with values from an existing condition - */ - private void setupTimeConditionPanel(TimeCondition condition) { - if (condition == null || currentConditionPanel == null) { - return; - } - - if (condition instanceof SingleTriggerTimeCondition) { - TimeConditionPanelUtil.setupTimeCondition(currentConditionPanel, (SingleTriggerTimeCondition) condition); - } else if (condition instanceof IntervalCondition) { - TimeConditionPanelUtil.setupTimeCondition(currentConditionPanel, (IntervalCondition) condition); - } else if (condition instanceof TimeWindowCondition) { - TimeConditionPanelUtil.setupTimeCondition(currentConditionPanel, (TimeWindowCondition) condition); - } else if (condition instanceof DayOfWeekCondition) { - TimeConditionPanelUtil.setupTimeCondition(currentConditionPanel, (DayOfWeekCondition) condition); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleTableModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleTableModel.java deleted file mode 100644 index a874aabd0ac..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleTableModel.java +++ /dev/null @@ -1,17 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.PluginScheduleEntry; - -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; - -/** - * Interface for table models that store PluginScheduleEntry instances - * and can retrieve them by row index. - */ -public interface ScheduleTableModel { - /** - * Gets the PluginScheduleEntry at the specified row index - * - * @param row The row index - * @return The PluginScheduleEntry at that row, or null if not found - */ - PluginScheduleEntry getPluginAtRow(int row); -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleTablePanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleTablePanel.java deleted file mode 100644 index a4cd3ae6c49..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/PluginScheduleEntry/ScheduleTablePanel.java +++ /dev/null @@ -1,2038 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.PluginScheduleEntry; - -import net.runelite.client.plugins.microbot.Microbot; -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.ConditionType; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LogicalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.pluginscheduler.util.SchedulerPluginUtil; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import javax.swing.table.DefaultTableCellRenderer; -import javax.swing.table.DefaultTableModel; -import javax.swing.table.JTableHeader; - -import lombok.extern.slf4j.Slf4j; - -import java.awt.*; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.event.MouseWheelEvent; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.Consumer; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.awt.font.TextAttribute; - -@Slf4j -public class ScheduleTablePanel extends JPanel implements ScheduleTableModel { - private final SchedulerPlugin schedulerPlugin; - private final JTable scheduleTable; - private final DefaultTableModel tableModel; - private Consumer selectionListener; - private boolean updatingTable = false; - - // Tooltip enhancement fields - private Timer tooltipRefreshTimer; - private Point hoverLocation; - private int hoverRow = -1; - private int hoverColumn = -1; - private int lastSelectedRow = -1; - private static final int TOOLTIP_REFRESH_INTERVAL = 1000; // 1 second refresh - - // Colors for different row states with improved visibility - private static final Color CURRENT_PLUGIN_COLOR = new Color(138, 43, 226, 80); // Purple with transparency - private static final Color NEXT_PLUGIN_COLOR = new Color(255, 140, 0, 90); // Dark orange with more opacity - private static final Color SELECTION_COLOR = new Color(0, 120, 215, 150); // Blue with transparency - private static final Color CONDITION_MET_COLOR = new Color(76, 175, 80, 45); // Darker green with lower transparency - private static final Color CONDITION_NOT_MET_COLOR = new Color(244, 67, 54, 45); // Darker red with lower transparency - private static final Color DEFAULT_PLUGIN_COLOR = new Color(0, 150, 136, 40); // Teal with transparency - - // Blend method is already defined elsewhere in the class - - // Column indices for easy reference - kept for future development but currently unused - @SuppressWarnings("unused") private static final int COL_NAME = 0; - @SuppressWarnings("unused") private static final int COL_SCHEDULE = 1; - @SuppressWarnings("unused") private static final int COL_NEXT_RUN = 2; - @SuppressWarnings("unused") private static final int COL_START_COND = 3; - @SuppressWarnings("unused") private static final int COL_STOP_COND = 4; - @SuppressWarnings("unused") private static final int COL_PRIORITY = 5; - @SuppressWarnings("unused") private static final int COL_ENABLED = 6; - @SuppressWarnings("unused") private static final int COL_RUNS = 7; - - private List rowToPluginMap = new ArrayList<>(); - - public int getRowCount() { - if (tableModel == null) { - return 0; - } - return tableModel.getRowCount(); - } - - public ScheduleTablePanel(SchedulerPlugin schedulerPlugin) { - - this.schedulerPlugin = schedulerPlugin; - - // Configure ToolTipManager for persistent tooltips - ToolTipManager.sharedInstance().setDismissDelay(Integer.MAX_VALUE); // Keep tooltips visible indefinitely - ToolTipManager.sharedInstance().setInitialDelay(300); // Show tooltips faster (300ms) - ToolTipManager.sharedInstance().setReshowDelay(0); // No delay when moving between tooltips - - setLayout(new BorderLayout()); - setBorder(BorderFactory.createTitledBorder( - BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(1, 1, 1, 1, ColorScheme.DARK_GRAY_COLOR), - BorderFactory.createEmptyBorder(5, 5, 5, 5) - ), - "Scheduled Plugins", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont() - )); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Focused table model with essential columns only - // The columns are: Plugin, Schedule, Next Run, Start Conditions, Stop Conditions, Priority, Enabled, Run Count - tableModel = new DefaultTableModel( - new Object[]{"Plugin", "Schedule", "Next Run", "Start Conditions", "Stop Conditions", "Priority", "Enabled", "Runs"}, 0) { - @Override - public Class getColumnClass(int column) { - if (column == 6) return Boolean.class; - if (column == 5) return Integer.class; - if (column == 7) return Integer.class; // Run count as Integer - return String.class; - } - @Override - public boolean isCellEditable(int row, int column) { - return column == 5 || column == 6; - } - }; - - // Update the table model listener to handle Priority column changes - tableModel.addTableModelListener(e -> { - if (updatingTable) { - return; // Skip processing if we're already updating or it's not our columns - } - if (e.getColumn() == 5 || e.getColumn() == 6) { - try { - updatingTable = true; - int firstRow = e.getFirstRow(); - int lastRow = e.getLastRow(); - - // Update all rows in the affected range - for (int row = firstRow; row <= lastRow; row++) { - if (row >= 0 && row < rowToPluginMap.size()) { - PluginScheduleEntry scheduled = rowToPluginMap.get(row); - - // Skip separator rows (null) and unavailable plugins - if (scheduled == null || !scheduled.isPluginAvailable()) { - continue; - } - - if (e.getColumn() == 5) { // Priority column - Integer priority = (Integer) tableModel.getValueAt(row, 5); - - // Check if this is a default plugin that should always have priority 0 - if (scheduled.isDefault() && priority != 0) { - tableModel.setValueAt(0, row, 5); // Reset to 0 - JOptionPane.showMessageDialog( - this, - "Default plugins must have priority 0.", - "Invalid Priority", - JOptionPane.INFORMATION_MESSAGE - ); - } else { - // For non-default plugins, update the value - //scheduled.setPriority(priority); - // Set default flag based on priority - //scheduled.setDefault(priority == 0); - // Save changes - //schedulerPlugin.saveScheduledPlugins(); - } - } - else if (e.getColumn() == 6) { // Enabled column - Boolean enabled = (Boolean) tableModel.getValueAt(row, 6); - tableModel.setValueAt(enabled, row, 6); - - Microbot.getClientThread().invokeLater(() -> { - // Update the enabled status of the plugin - scheduled.setEnabled(enabled); - }); - } - } - } - - // Save after all updates are done - schedulerPlugin.saveScheduledPlugins(); - - // Refresh the table to update visual indicators - refreshTable(); - } finally { - updatingTable = false; - } - } - }); - - // Create table with custom styling - scheduleTable = new JTable(tableModel) { - @Override - public boolean isCellEditable(int row, int column) { - // Only allow editing of available plugins (not separators or unavailable plugins) - if (row >= 0 && row < rowToPluginMap.size()) { - PluginScheduleEntry plugin = rowToPluginMap.get(row); - if (plugin == null) { - return false; // Separator rows are not editable - } - // Unavailable plugins can't be edited for priority/enabled, but can be selected for removal - if (!plugin.isPluginAvailable() && (column == 5 || column == 6)) { - return false; // Priority and Enabled columns not editable for unavailable plugins - } - } - return super.isCellEditable(row, column); - } - }; - scheduleTable.setFillsViewportHeight(true); - scheduleTable.setRowHeight(25); // Reduced row height for better density - scheduleTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - - // Custom selection model to prevent selection of separators and unavailable plugins - scheduleTable.setSelectionModel(new DefaultListSelectionModel() { - @Override - public void setSelectionInterval(int index0, int index1) { - if (isRowSelectable(index0)) { - super.setSelectionInterval(index0, index1); - } - } - - @Override - public void addSelectionInterval(int index0, int index1) { - if (isRowSelectable(index0) && isRowSelectable(index1)) { - super.addSelectionInterval(index0, index1); - } - } - - private boolean isRowSelectable(int row) { - if (row >= 0 && row < rowToPluginMap.size()) { - PluginScheduleEntry plugin = rowToPluginMap.get(row); - // Separator rows (null) are not selectable, but unavailable plugins ARE selectable for removal - return plugin != null; - } - return false; - } - }); - - scheduleTable.setShowGrid(false); - scheduleTable.setIntercellSpacing(new Dimension(0, 0)); - scheduleTable.setBackground(ColorScheme.DARKER_GRAY_COLOR); - scheduleTable.setForeground(Color.WHITE); - - // Set the custom editor for the Priority column - scheduleTable.getColumnModel().getColumn(5).setCellEditor(new PrioritySpinnerEditor()); - - // Add mouse listener to handle clicks outside the table data and tooltip refreshing - scheduleTable.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - int row = scheduleTable.rowAtPoint(e.getPoint()); - int col = scheduleTable.columnAtPoint(e.getPoint()); - // Explicitly select the row before custom logic - final int currentSelectedRow = scheduleTable.getSelectedRow(); - boolean isLastSelected = currentSelectedRow == lastSelectedRow; - lastSelectedRow = row; // Update last selected row - // To check if the mouse event is a "pressed" event, use e.getID() == MouseEvent.MOUSE_PRESSED - if (e.getID() == MouseEvent.MOUSE_PRESSED && e.getButton() == MouseEvent.BUTTON1) { - - if (!isLastSelected) { - // If the clicked row is different, select it - scheduleTable.setRowSelectionInterval(row, row); - return; - } - if (row == -1 || col == -1) { - clearSelection(); - } - - - - if (col != 6 && col != 5) { - // handle single-click toggle for selection/deselection - if (e.getClickCount() == 1) { - // if we clicked on the previously selected row and it's still selected, deselect it - if (currentSelectedRow == row) { - clearSelection(); - return; - } - } - - // keep the double-click functionality for backwards compatibility - if (e.getClickCount() == 2) { - int selectedRow = scheduleTable.getSelectedRow(); - if (selectedRow == row) { - clearSelection(); - return; - } - } - } - } - super.mousePressed(e); - } - - @Override - public void mouseExited(MouseEvent e) { - // Reset hover tracking when mouse leaves table - hoverRow = -1; - hoverColumn = -1; - if (tooltipRefreshTimer != null && tooltipRefreshTimer.isRunning()) { - tooltipRefreshTimer.stop(); - } - } - }); - - // Add mouse motion listener for hover detection and tooltip refresh - scheduleTable.addMouseMotionListener(new MouseAdapter() { - @Override - public void mouseMoved(MouseEvent e) { - int row = scheduleTable.rowAtPoint(e.getPoint()); - int col = scheduleTable.columnAtPoint(e.getPoint()); - - if (row >= 0 && col >= 0) { - // If hovering over a new cell, update tracking - if (row != hoverRow || col != hoverColumn) { - hoverRow = row; - hoverColumn = col; - hoverLocation = e.getPoint(); - - // Start/restart timer for tooltip refresh - startTooltipRefreshTimer(); - } - } else { - // Not over any cell - hoverRow = -1; - hoverColumn = -1; - if (tooltipRefreshTimer != null && tooltipRefreshTimer.isRunning()) { - tooltipRefreshTimer.stop(); - } - } - } - }); - - // Style the table header - JTableHeader header = scheduleTable.getTableHeader(); - header.setBackground(ColorScheme.DARKER_GRAY_COLOR); - header.setForeground(Color.WHITE); - header.setFont(FontManager.getRunescapeBoldFont()); - header.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, ColorScheme.LIGHT_GRAY_COLOR)); - - // Add mouse listener to header to clear selection when clicked - header.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - clearSelection(); - } - }); - - // Set column widths - scheduleTable.getColumnModel().getColumn(0).setPreferredWidth(180); // Plugin - scheduleTable.getColumnModel().getColumn(1).setPreferredWidth(140); // Schedule - scheduleTable.getColumnModel().getColumn(2).setPreferredWidth(140); // Next Run - scheduleTable.getColumnModel().getColumn(3).setPreferredWidth(130); // Start Conditions - scheduleTable.getColumnModel().getColumn(4).setPreferredWidth(130); // Stop Conditions - scheduleTable.getColumnModel().getColumn(5).setPreferredWidth(70); // Priority - scheduleTable.getColumnModel().getColumn(6).setPreferredWidth(70); // Enabled - scheduleTable.getColumnModel().getColumn(7).setPreferredWidth(60); // Run Count - - // Custom cell renderer for alternating row colors and special highlights - DefaultTableCellRenderer renderer = new DefaultTableCellRenderer() { - @Override - public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { - Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); - if (row >= 0 && row < rowToPluginMap.size()) { - PluginScheduleEntry rowPlugin = rowToPluginMap.get(row); - - // Handle separator row (null plugin) - if (rowPlugin == null) { - c.setBackground(new Color(70, 70, 70)); // Dark gray background - c.setForeground(new Color(169, 169, 169)); // Light gray text - Font originalFont = c.getFont(); - c.setFont(originalFont.deriveFont(Font.BOLD)); - setHorizontalAlignment(SwingConstants.CENTER); - return c; - } - - if (isSelected) { - // Selected row styling takes precedence - use a distinct blue color - c.setBackground(SELECTION_COLOR); - c.setForeground(Color.WHITE); - } - else if (!rowPlugin.isPluginAvailable()) { - // Unavailable plugin styling (not installed) - improved styling - c.setBackground(new Color(69, 39, 19, 80)); // Darker brown with more transparency - c.setForeground(new Color(255, 160, 122)); // Sandy brown text for better readability - // Add italic styling and slightly dimmed appearance - Font originalFont = c.getFont(); - c.setFont(originalFont.deriveFont(Font.ITALIC)); - } - else if (!rowPlugin.isEnabled()) { - // Disabled plugin styling - c.setBackground(row % 2 == 0 ? ColorScheme.DARKER_GRAY_COLOR : ColorScheme.DARK_GRAY_COLOR); - c.setForeground(Color.GRAY); // Gray text to indicate disabled - // Add strikethrough for disabled plugins - if (value instanceof String) { - Font originalFont = c.getFont(); - Map attributes = new HashMap<>(); - attributes.put(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON); - attributes.put(TextAttribute.FONT, originalFont); - c.setFont(Font.getFont(attributes)); - } - } - else if ( schedulerPlugin.getCurrentPlugin() != null && schedulerPlugin.getCurrentPlugin().equals(rowPlugin) && - rowPlugin.getName().equals(schedulerPlugin.getCurrentPlugin().getName())) { - // Currently running plugin - c.setBackground(CURRENT_PLUGIN_COLOR); - c.setForeground(Color.WHITE); - } - else if (isNextToRun(rowPlugin)) { - // Next plugin to run - use better contrast colors - c.setBackground(NEXT_PLUGIN_COLOR); - c.setForeground(Color.BLACK); - // Make text bold to stand out more - Font originalFont = c.getFont(); - c.setFont(originalFont.deriveFont(Font.BOLD)); - } - else if (rowPlugin.isDefault()) { - // Default plugin styling - c.setBackground(DEFAULT_PLUGIN_COLOR); - c.setForeground(Color.WHITE); - } - else { - // Normal alternating row colors - c.setBackground(row % 2 == 0 ? ColorScheme.DARKER_GRAY_COLOR : ColorScheme.DARK_GRAY_COLOR); - c.setForeground(Color.WHITE); - } - - // Apply the condition renderer to the condition columns - // Custom cell renderer for the condition columns - if (row >= 0 && row < rowToPluginMap.size() && !isSelected) { - PluginScheduleEntry entry = rowToPluginMap.get(row); - - // Apply background color based on condition status - if (column == 3) { // Start conditions - boolean hasStartConditions = entry.hasAnyStartConditions(); - boolean startConditionsMet = hasStartConditions && entry.getStartConditionManager().areAllConditionsMet(); - boolean isRelevant = entry.isEnabled() && !entry.isRunning(); - - if (hasStartConditions) { - // When conditions are relevant (enabled but not running), show clearer status - if (isRelevant) { - // Use darker base colors with the game's theme - Color baseColor = ColorScheme.DARKER_GRAY_COLOR; - - if (startConditionsMet) { - // For met conditions, use a subtle green tint - c.setBackground(new Color( - blend(baseColor.getRed(), 70, 0.85f), - blend(baseColor.getGreen(), 130, 0.85f), - blend(baseColor.getBlue(), 70, 0.85f) - )); - } else { - // For unmet conditions, use a subtle red tint - c.setBackground(new Color( - blend(baseColor.getRed(), 140, 0.85f), - blend(baseColor.getGreen(), 60, 0.85f), - blend(baseColor.getBlue(), 60, 0.85f) - )); - - // For blocking conditions, use an indicator rather than border - if (c instanceof JComponent) { - ((JComponent)c).setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(0, 3, 0, 0, new Color(180, 50, 50, 120)), - BorderFactory.createEmptyBorder(1, 4, 1, 4) - )); - } - } - } else { - // Almost no color when not relevant - just slightly tinted - Color baseColor = row % 2 == 0 ? ColorScheme.DARKER_GRAY_COLOR : ColorScheme.DARK_GRAY_COLOR; - - if (startConditionsMet) { - c.setBackground(new Color( - blend(baseColor.getRed(), 76, 0.95f), - blend(baseColor.getGreen(), 120, 0.95f), - blend(baseColor.getBlue(), 76, 0.95f) - )); - } else { - c.setBackground(new Color( - blend(baseColor.getRed(), 120, 0.95f), - blend(baseColor.getGreen(), 76, 0.95f), - blend(baseColor.getBlue(), 76, 0.95f) - )); - } - } - c.setForeground(Color.BLACK); - } - } else if (column == 4) { // Stop conditions - boolean hasStopConditions = entry.hasAnyStopConditions(); - boolean stopConditionsMet = hasStopConditions && entry.getStopConditionManager().areAllConditionsMet(); - boolean isRelevant = entry.isRunning(); - - if (hasStopConditions) { - // More prominent when currently running - if (isRelevant) { - // Use darker base colors with the game's theme - Color baseColor = ColorScheme.DARKER_GRAY_COLOR; - - if (stopConditionsMet) { - // For met conditions, use a noticeable but not harsh green tint - c.setBackground(new Color( - blend(baseColor.getRed(), 60, 0.85f), - blend(baseColor.getGreen(), 140, 0.85f), - blend(baseColor.getBlue(), 70, 0.85f) - )); - - // For satisfied stop conditions, use an indicator rather than full border - if (c instanceof JComponent) { - ((JComponent)c).setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(0, 0, 0, 3, new Color(50, 180, 50, 140)), - BorderFactory.createEmptyBorder(1, 4, 1, 4) - )); - } - } else { - // For unmet conditions, use a blueish tint (less alarming than red for stop) - c.setBackground(new Color( - blend(baseColor.getRed(), 70, 0.85f), - blend(baseColor.getGreen(), 70, 0.85f), - blend(baseColor.getBlue(), 140, 0.85f) - )); - } - } else { - // Almost no color when not relevant - just slightly tinted - Color baseColor = row % 2 == 0 ? ColorScheme.DARKER_GRAY_COLOR : ColorScheme.DARK_GRAY_COLOR; - - if (stopConditionsMet) { - c.setBackground(new Color( - blend(baseColor.getRed(), 76, 0.95f), - blend(baseColor.getGreen(), 120, 0.95f), - blend(baseColor.getBlue(), 76, 0.95f) - )); - } else { - c.setBackground(new Color( - blend(baseColor.getRed(), 76, 0.95f), - blend(baseColor.getGreen(), 76, 0.95f), - blend(baseColor.getBlue(), 120, 0.95f) - )); - } - } - c.setForeground(Color.BLACK); - } - } - } - - // Set tooltip based on column for better information - always fresh with dynamic data - if (column == 0) { // Plugin name column - setToolTipText(getPluginDetailsTooltip(rowPlugin)); - } else if (column == 1) { // Schedule - setToolTipText(getScheduleTooltip(rowPlugin)); - } else if (column == 2) { // Next Run - setToolTipText(getNextRunTooltip(rowPlugin)); - } else if (column == 3) { // Start Conditions - setToolTipText(getStartConditionsTooltip(rowPlugin)); - } else if (column == 4) { // Stop Conditions - setToolTipText(getStopConditionsTooltip(rowPlugin)); - } else if (column == 5) { // Priority - setToolTipText(getPriorityTooltip(rowPlugin)); - } - } - setBorder(new EmptyBorder(2, 5, 2, 5)); - return c; - } - }; - - renderer.setHorizontalAlignment(SwingConstants.LEFT); - - // Apply renderer to all columns except the boolean column - for (int i = 0; i < scheduleTable.getColumnCount(); i++) { - if (i != 6) { // Skip Enabled column which is a checkbox - scheduleTable.getColumnModel().getColumn(i).setCellRenderer(renderer); - } - } - - // Add table to scroll pane with custom styling - JScrollPane scrollPane = new JScrollPane(scheduleTable); - scrollPane.setBorder(BorderFactory.createEmptyBorder()); - scrollPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Style the scrollbar - scrollPane.getVerticalScrollBar().setPreferredSize(new Dimension(10, 0)); - scrollPane.getVerticalScrollBar().setUnitIncrement(16); - scrollPane.getVerticalScrollBar().setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Add mouse listener to the scroll pane to clear selection when clicking empty space - scrollPane.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - clearSelection(); - } - - }); - scrollPane.addMouseWheelListener(new MouseAdapter() { - @Override - public void mouseWheelMoved(MouseWheelEvent e) { - if (e.getWheelRotation() == 0 || e.getUnitsToScroll() == 0) { - return; // Ignore zero rotation events - } - // Check if the mouse location is within the table's visible rectangle - Rectangle tableRect = scheduleTable.getVisibleRect(); - Point mousePoint = SwingUtilities.convertPoint(scrollPane, e.getPoint(), scheduleTable); - if (!tableRect.contains(mousePoint)) { - // Mouse is over the table, do not clear selection - return; - } - clearSelection(); - // Handle the scroll event here - // e.getWheelRotation() gives the number of clicks (positive or negative) - // e.getUnitsToScroll() gives the number of units to scroll - } - }); - - add(scrollPane, BorderLayout.CENTER); - - // Add an improved legend panel with more information - JPanel legendPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 2)); - legendPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - legendPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); - - // Current plugin indicator - addLegendItem(legendPanel, CURRENT_PLUGIN_COLOR, "Running", "Currently running plugin"); - - // Next plugin indicator - addLegendItem(legendPanel, NEXT_PLUGIN_COLOR, "Next", "Plugin scheduled to run next"); - - // Default plugin indicator - addLegendItem(legendPanel, DEFAULT_PLUGIN_COLOR, "Default", "Default plugin (Priority 0)"); - - // Condition indicators - addLegendItem(legendPanel, CONDITION_MET_COLOR, "Condition Met", "Condition has been satisfied"); - addLegendItem(legendPanel, CONDITION_NOT_MET_COLOR, "Not Met", "Condition not yet satisfied"); - - // Information about tooltips - JLabel tooltipInfo = new JLabel("Hover over cells for detailed tooltips"); - tooltipInfo.setForeground(Color.LIGHT_GRAY); - tooltipInfo.setFont(FontManager.getRunescapeSmallFont()); - tooltipInfo.setToolTipText("Tooltips show detailed information
They stay visible and refresh automatically"); - legendPanel.add(Box.createHorizontalStrut(10)); - legendPanel.add(tooltipInfo); - - // Add the legend panel to the bottom of the main panel - add(legendPanel, BorderLayout.SOUTH); - } - - /** - * Helper method to add a legend item with consistent styling - */ - private void addLegendItem(JPanel legendPanel, Color color, String text, String tooltip) { - JPanel indicator = new JPanel(); - indicator.setBackground(color); - indicator.setPreferredSize(new Dimension(15, 15)); - indicator.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY)); - indicator.setToolTipText(tooltip); - - JLabel label = new JLabel(text); - label.setForeground(Color.WHITE); - label.setFont(FontManager.getRunescapeSmallFont()); - label.setToolTipText(tooltip); - - legendPanel.add(indicator); - legendPanel.add(label); - } - - /** - * Creates a tooltip for the priority column - */ - private String getPriorityTooltip(PluginScheduleEntry entry) { - StringBuilder tooltip = new StringBuilder("Priority Information
"); - - if (entry.isDefault()) { - tooltip.append("
This is a default plugin with priority 0."); - tooltip.append("
Default plugins are always scheduled last."); - } else { - tooltip.append("
Priority: ").append(entry.getPriority()).append(""); - tooltip.append("
Higher priority plugins run before lower priority plugins."); - } - - tooltip.append("

To change a plugin to default status, set its priority to 0."); - - return tooltip.toString() + ""; - } - - /** - * Creates a detailed tooltip for schedule information - */ - private String getScheduleTooltip(PluginScheduleEntry entry) { - StringBuilder tooltip = new StringBuilder("Schedule Details:
"); - - // Add schedule type and details - tooltip.append(entry.getIntervalDisplay()); - - if (entry.hasAnyOneTimeStartConditions()) { - tooltip.append("
One-time schedule"); - if (entry.hasTriggeredOneTimeStartConditions() && !entry.canStartTriggerAgain()) { - tooltip.append(" (completed)"); - } - } - - // Add priority information - tooltip.append("

Priority: ").append(entry.getPriority()); - if (entry.isDefault()) { - tooltip.append(" (Default plugin)"); - } - - // Add random scheduling info - tooltip.append("
Random Scheduling: ").append(entry.isAllowRandomScheduling() ? "Enabled" : "Disabled"); - - return tooltip.toString() + ""; - } - /** - * Creates a detailed tooltip for start conditions with improved condition type display - */ - private String getStartConditionsTooltip(PluginScheduleEntry entry) { - if (!entry.hasStartConditions()) { - return "No start conditions defined"; - } - - List conditions = entry.getStartConditions(); - - // Determine if start conditions are relevant - when plugin is enabled but not started - boolean conditionsAreRelevant = entry.isEnabled() && !entry.isRunning(); - - StringBuilder tooltip = new StringBuilder("Start Conditions:
"); - - // Group conditions by type - Map> grouped = groupConditionsByType(conditions); - - // Show root causes prominently if conditions are not met but are relevant - if (conditionsAreRelevant && !entry.canBeStarted()) { - tooltip.append("
"); - tooltip.append("Blocking Conditions:
"); - - // First show user-defined root causes - String userRootCauses = entry.getStartConditionManager().getUserRootCausesSummary(); - if (!userRootCauses.isEmpty()) { - tooltip.append("User-defined:
"); - tooltip.append("").append(userRootCauses).append("
"); - } - - // Then show plugin-defined root causes - String pluginRootCauses = entry.getStartConditionManager().getPluginRootCausesSummary(); - if (!pluginRootCauses.isEmpty()) { - tooltip.append("Plugin-defined:
"); - tooltip.append("").append(pluginRootCauses).append("
"); - } - tooltip.append("

"); - } - - // If there are many conditions, add a summary first - if (conditions.size() > 3) { - tooltip.append("").append(entry.getDetailedStartConditionsStatus()).append(""); - tooltip.append("

"); - } - - if (grouped.isEmpty()) { - tooltip.append("Will start as soon as enabled"); - } else { - tooltip.append(""); - for (Map.Entry> group : grouped.entrySet()) { - ConditionType type = group.getKey(); - List typeConditions = group.getValue(); - - // Add a header for this type - tooltip.append(""); - - // Add each condition - for (Condition condition : typeConditions) { - boolean isSatisfied = condition.isSatisfied(); - tooltip.append(""); - - // Status icon column - tooltip.append(""); - - // Description column - tooltip.append(""); - tooltip.append(""); - } - } - tooltip.append("
") - .append(getConditionTypeIcon(type)) - .append(" ") - .append(formatConditionTypeName(type)) - .append("
"); - // Show either relevance icon or satisfaction status - if (conditionsAreRelevant) { - tooltip.append("⚡ ") - .append(getStatusSymbol(isSatisfied)); - } else { - tooltip.append("○"); - } - tooltip.append(""); - String description = formatConditionDescription(condition); - if (isSatisfied) { - if (conditionsAreRelevant) { - tooltip.append("").append(description).append(""); - } else { - tooltip.append("").append(description).append(""); - } - } else { - if (conditionsAreRelevant) { - tooltip.append("").append(description).append(""); - } else { - tooltip.append("").append(description).append(""); - } - } - tooltip.append("
"); - } - - // Add overall status - if (conditionsAreRelevant) { - tooltip.append("
Status: ⚡ Currently relevant"); - if (entry.canBeStarted()) { - tooltip.append("
All conditions met - plugin ready to start"); - } else { - tooltip.append("
Some conditions not met - waiting to start"); - - // Add detailed explanation section if there are blocking conditions - String userExplanation = entry.getStartConditionManager().getUserBlockingExplanation(); - String pluginExplanation = entry.getStartConditionManager().getPluginBlockingExplanation(); - - if (!userExplanation.isEmpty() || !pluginExplanation.isEmpty()) { - tooltip.append("

"); - tooltip.append("Show detailed diagnostics"); - - if (!userExplanation.isEmpty()) { - tooltip.append("
"); - tooltip.append("User condition details:
"); - tooltip.append(userExplanation.replace("\n", "
")); - tooltip.append("
"); - } - - if (!pluginExplanation.isEmpty()) { - tooltip.append("
"); - tooltip.append("Plugin condition details:
"); - tooltip.append(pluginExplanation.replace("\n", "
")); - tooltip.append("
"); - } - - tooltip.append("
"); - } - } - } else { - tooltip.append("
Status: Not currently relevant"); - } - - tooltip.append(""); - return tooltip.toString(); - } - - /** - * Creates a detailed tooltip for stop conditions with improved condition type display - * Updated to be more compact with less line breaks to reduce row height - */ - private String getStopConditionsTooltip(PluginScheduleEntry entry) { - if (!entry.hasStopConditions()) { - return "No stop conditions defined"; - } - - List conditions = entry.getStopConditions(); - - // Determine if stop conditions are relevant - when plugin is currently running - boolean conditionsAreRelevant = entry.isRunning(); - - StringBuilder tooltip = new StringBuilder("Stop Conditions:
"); - - // Group conditions by type - Map> grouped = groupConditionsByType(conditions); - - // Show blocking conditions prominently if plugin is running but stop conditions not met - if (conditionsAreRelevant && !entry.allowedToBeStop()) { - // Show overall progress first - double progress = entry.getStopConditionProgress(); - if (progress > 0) { - String progressColor = progress > 80 ? "#81C784" : progress > 50 ? "#FFB74D" : "#64B5F6"; - - tooltip.append("
"); - tooltip.append("Current Progress: "); - tooltip.append("") - .append(String.format("%.1f%%", progress)) - .append("

"); - } - - // Then show what's preventing the plugin from stopping - tooltip.append("
"); - tooltip.append("Waiting For:
"); - - // First show user-defined waiting conditions - String userRootCauses = entry.getStopConditionManager().getUserRootCausesSummary(); - if (!userRootCauses.isEmpty()) { - tooltip.append("User-defined:
"); - tooltip.append("").append(userRootCauses).append("
"); - } - - // Then show plugin-defined waiting conditions - String pluginRootCauses = entry.getStopConditionManager().getPluginRootCausesSummary(); - if (!pluginRootCauses.isEmpty()) { - tooltip.append("Plugin-defined:
"); - tooltip.append("").append(pluginRootCauses).append("
"); - } - tooltip.append("

"); - } - // If there are many conditions, add a summary first - else if (conditions.size() > 3) { - tooltip.append("").append(entry.getDetailedStopConditionsStatus()).append(""); - tooltip.append("

"); - } - - // Show overall progress if relevant and available - if (conditionsAreRelevant && !entry.allowedToBeStop()) { - // Progress already shown above - } else if (conditionsAreRelevant) { - double progress = entry.getStopConditionProgress(); - if (progress > 0) { - tooltip.append("Overall Progress: "); - - String progressColor = progress > 80 ? "green" : - progress > 50 ? "orange" : "blue"; - - tooltip.append("") - .append(String.format("%.1f%%", progress)) - .append("
"); - } - tooltip.append("
"); - } - - if (grouped.isEmpty()) { - tooltip.append("Will run until manually stopped"); - } else { - tooltip.append(""); - for (Map.Entry> group : grouped.entrySet()) { - ConditionType type = group.getKey(); - List typeConditions = group.getValue(); - - // Add a header for this type - tooltip.append(""); - - // Add each condition - for (Condition condition : typeConditions) { - boolean isSatisfied = condition.isSatisfied(); - tooltip.append(""); - - // Status icon column - tooltip.append(""); - - // Description column - tooltip.append(""); - tooltip.append(""); - } - } - tooltip.append("
") - .append(getConditionTypeIcon(type)) - .append(" ") - .append(formatConditionTypeName(type)) - .append("
"); - // Show either relevance icon or satisfaction status - if (conditionsAreRelevant) { - tooltip.append("⚡ ") - .append(getStatusSymbol(isSatisfied)); - } else { - tooltip.append("○"); - } - tooltip.append(""); - String description = formatConditionDescription(condition); - if (isSatisfied) { - if (conditionsAreRelevant) { - tooltip.append("").append(description).append(""); - } else { - tooltip.append("").append(description).append(""); - } - } else { - if (conditionsAreRelevant) { - tooltip.append("").append(description).append(""); - } else { - tooltip.append("").append(description).append(""); - } - } - tooltip.append("
"); - } - - // Add overall status - if (conditionsAreRelevant) { - tooltip.append("
Status: ⚡ Currently relevant"); - if (entry.allowedToBeStop()) { - tooltip.append("
Plugin Stop conditions met - plugin can be stopped"); - } else { - tooltip.append("
Waiting for plugin stop conditions"); - - // If there's timing info available, show it - String nextTrigger = entry.getNextStopTriggerTimeString(); - if (!nextTrigger.contains("None") && !nextTrigger.isEmpty()) { - tooltip.append("
Next potential trigger: ") - .append(nextTrigger) - .append(""); - } - - // Add detailed explanation section if there are blocking conditions - String userExplanation = entry.getStopConditionManager().getUserBlockingExplanation(); - String pluginExplanation = entry.getStopConditionManager().getPluginBlockingExplanation(); - - if (!userExplanation.isEmpty() || !pluginExplanation.isEmpty()) { - tooltip.append("

"); - tooltip.append("Show detailed diagnostics"); - - if (!userExplanation.isEmpty()) { - tooltip.append("
"); - tooltip.append("User condition details:
"); - tooltip.append(userExplanation.replace("\n", "
")); - tooltip.append("
"); - } - - if (!pluginExplanation.isEmpty()) { - tooltip.append("
"); - tooltip.append("Plugin condition details:
"); - tooltip.append(pluginExplanation.replace("\n", "
")); - tooltip.append("
"); - } - - tooltip.append("
"); - } - } - } else { - tooltip.append("
Status: Not currently relevant"); - } - - tooltip.append(""); - return tooltip.toString(); - } - - /** - * Groups conditions by their type for organized display - */ - private Map> groupConditionsByType(List conditions) { - Map> groupedConditions = new HashMap<>(); - - for (Condition condition : conditions) { - ConditionType type = condition.getType(); - groupedConditions.computeIfAbsent(type, k -> new ArrayList<>()).add(condition); - } - - return groupedConditions; - } - - /** - * Returns an appropriate icon for a condition type that will reliably display in tooltips - */ - private String getConditionTypeIcon(ConditionType type) { - // Using HTML entities and standard characters instead of Unicode emojis - // as these render more reliably in Swing tooltips - switch (type) { - case TIME: - return "⏰"; // Clock icon using HTML entity - //case SKILL: - // return "📊"; // Chart icon using HTML entity - case SKILL: - return "📈"; // Chart with upward trend using HTML entity - case RESOURCE: - return "▣"; // Square icon - case LOCATION: - return "☉"; // Compass icon - case LOGICAL: - return "↻"; // Recycling/Loop icon - case NPC: - return "☺"; // Face/Person icon - default: - return "•"; // Bullet point - } - } - - /** - * Returns a user-friendly name for a condition type - */ - private String formatConditionTypeName(ConditionType type) { - switch (type) { - case TIME: return "Time Conditions"; - case SKILL: return "Skill Conditions"; - case RESOURCE: return "Resource Conditions"; - case LOCATION: return "Location Conditions"; - case LOGICAL: return "Logical Conditions"; - case NPC: return "NPC Conditions"; - default: return type.toString() + " Conditions"; - } - } - - /** - * Returns a colored status symbol for condition status that will reliably display in tooltips - */ - private String getStatusSymbol(boolean satisfied) { - return satisfied ? - "" : // HTML entity for checkmark - ""; // HTML entity for X mark - } - - /** - * Formats a condition description for better readability - * Handles special cases like nested logical conditions - */ - private String formatConditionDescription(Condition condition) { - String description = condition.getDescription(); - - // For logical conditions, use HTML formatting when available - if (condition instanceof LogicalCondition) { - LogicalCondition logicalCondition = (LogicalCondition) condition; - // Get a truncated HTML description with reasonable length limit - return logicalCondition.getHtmlDescription(80) - .replace("", "") - .replace("", ""); - } - - // For logical conditions, include more detailed info about progress - if (condition.getType() == ConditionType.LOGICAL) { - // Try to get more detailed info about logical condition's progress - int metCount = condition.getMetConditionCount(); - int totalCount = condition.getTotalConditionCount(); - - if (totalCount > 1) { - description += String.format(" (%d/%d sub-conditions met)", metCount, totalCount); - } - } - - return description; - } - /** - * Creates a detailed tooltip for next run information - */ - private String getNextRunTooltip(PluginScheduleEntry entry) { - StringBuilder tooltip = new StringBuilder("Next Run Details:
"); - - // Add next run time - Optional nextTime = entry.getCurrentStartTriggerTime(); - if (nextTime.isPresent()) { - tooltip.append("Next scheduled time: ").append(nextTime.get().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); - } else { - tooltip.append("No specific next run time"); - } - - // Add completed runs - tooltip.append("
Times run: ").append(entry.getRunCount()); - - // Add enabled status - tooltip.append("
Status: ").append(entry.isEnabled() ? "Enabled" : "Disabled"); - if (entry.isRunning()) { - tooltip.append(" (Currently running)"); - } - - return tooltip.toString() + ""; - } - - /** - * Determines if the provided plugin is the next one scheduled to run - */ - private boolean isNextToRun(PluginScheduleEntry scheduledPlugin) { - if (!scheduledPlugin.isEnabled()) { - return false; - } - - PluginScheduleEntry nextPlugin = schedulerPlugin.getNextPluginToBeScheduled(); - PluginScheduleEntry nextUpCommigPlugin = schedulerPlugin.getNextPluginToBeScheduled(); - boolean isNextUpComingPlugin = nextUpCommigPlugin != null && nextUpCommigPlugin.equals(scheduledPlugin); - boolean isNextPlugin = nextPlugin != null && nextPlugin.equals(scheduledPlugin); - if (nextPlugin!= null){ - return isNextPlugin; - }else{ - return isNextUpComingPlugin; - } -// return nextPlugin != null && nextPlugin.equals(scheduledPlugin); - } - private void detectChangesInPluginlist(){ - // Get the full combined list (available + separator + unavailable) like in refreshTable - List availablePlugins = schedulerPlugin.getScheduledPlugins(); - List unavailablePlugins = schedulerPlugin.getUnavailableScheduledPlugins(); - List sortedAvailablePlugins = SchedulerPluginUtil.sortPluginScheduleEntries(availablePlugins); - - List expectedPlugins = new ArrayList<>(); - expectedPlugins.addAll(sortedAvailablePlugins); - - // Add separator if there are unavailable plugins - if (!unavailablePlugins.isEmpty() && !sortedAvailablePlugins.isEmpty()) { - expectedPlugins.add(null); // separator - } - - expectedPlugins.addAll(unavailablePlugins); - - // Only refresh if the structure has actually changed - if (expectedPlugins.size() != rowToPluginMap.size()) { - log.info("Plugin list size changed: {} -> {}, refreshing", rowToPluginMap.size(), expectedPlugins.size()); - this.rowToPluginMap = new ArrayList<>(expectedPlugins); - return; - } - - // Check if the content order has changed - boolean hasChanged = false; - for (int i = 0; i < expectedPlugins.size(); i++) { - PluginScheduleEntry expected = expectedPlugins.get(i); - PluginScheduleEntry current = i < rowToPluginMap.size() ? rowToPluginMap.get(i) : null; - - // Handle separator rows (both null) - if (expected == null && current == null) { - continue; - } - - // Handle mismatched null vs non-null - if (expected == null || current == null) { - hasChanged = true; - break; - } - - // Check if plugins are different - if (!expected.equals(current)) { - hasChanged = true; - break; - } - } - - if (hasChanged) { - log.info("Plugin list order changed, refreshing"); - this.rowToPluginMap = new ArrayList<>(expectedPlugins); - } - } - - - public void refreshTable() { - if (this.updatingTable) { - return; // Skip if already updating - } - - this.updatingTable = true; - - try { - detectChangesInPluginlist(); - // Save current selection - PluginScheduleEntry selectedPlugin = getSelectedPlugin(); - int selectedRow = scheduleTable.getSelectedRow(); - // Get available and unavailable plugins separately - List availablePlugins = schedulerPlugin.getScheduledPlugins(); - List unavailablePlugins = schedulerPlugin.getUnavailableScheduledPlugins(); - - // Sort available plugins by next run time - List sortedAvailablePlugins = SchedulerPluginUtil.sortPluginScheduleEntries(availablePlugins); - - // Combine lists: available plugins first, then separator, then unavailable plugins - List sortedPlugins = new ArrayList<>(); - sortedPlugins.addAll(sortedAvailablePlugins); - - // Add a placeholder separator entry if there are unavailable plugins - if (!unavailablePlugins.isEmpty() && !sortedAvailablePlugins.isEmpty()) { - sortedPlugins.add(null); // null entry will be rendered as separator - } - - sortedPlugins.addAll(unavailablePlugins); - //SchedulerPluginUtil.logSortedPluginScheduleEntryList(sortedPlugins); - - - - // Create a new row map with the correct size to match the sorted plugins - List newRowMap = new ArrayList<>(sortedPlugins.size()); - // Initialize with nulls first - for (int i = 0; i < sortedPlugins.size(); i++) { - newRowMap.add(null); - } - - - // We no longer need to save the previous row map for comparison - // Track if we need to force repaint (visual changes that might not trigger repaint) - boolean needsRepaint = false; - - // Set to track plugins we've processed to avoid duplicates - Set processedPlugins = new HashSet<>(); - - // First pass: update existing rows in place if possible - for (int newIndex = 0; newIndex < sortedPlugins.size(); newIndex++) { - PluginScheduleEntry plugin = sortedPlugins.get(newIndex); - - // Skip if this plugin has already been processed (but allow null separators) - if (plugin != null && processedPlugins.contains(plugin)) { - continue; - } - - - // Same position, just update data in place - if (newIndex < tableModel.getRowCount()) { - updateRowWithPlugin(newIndex, plugin); - //tableModel.insertRow(newIndex, createRowData(plugin)); - } else { - tableModel.addRow(createRowData(plugin)); - needsRepaint = true; - } - newRowMap.set(newIndex, plugin); - - // Mark plugin as processed (if it's not a separator) - if (plugin != null) { - processedPlugins.add(plugin); - } - } - - // Remove any excess rows - while (tableModel.getRowCount() > sortedPlugins.size()) { - tableModel.removeRow(tableModel.getRowCount() - 1); - needsRepaint = true; - } - - // Update our tracking map - this.rowToPluginMap = newRowMap; - - // Restore selection if possible - if (selectedPlugin != null) { - int newSlectedRow = -1; - for (int i = 0; i < newRowMap.size(); i++) { - if (newRowMap.get(i).equals(selectedPlugin)) { - newSlectedRow = i; - break; - } - } - if (newSlectedRow != -1) { - scheduleTable.setRowSelectionInterval(newSlectedRow, newSlectedRow); - scheduleTable.scrollRectToVisible(scheduleTable.getCellRect(newSlectedRow, 0, true)); - } else { - // If the selected plugin is no longer in the list, clear selection - scheduleTable.clearSelection(); - } - - } - - // Force repaint if needed - if (needsRepaint) { - scheduleTable.repaint(); - } - - // Update tooltips after refresh - updateTooltipsAfterRefresh(newRowMap); - } finally { - this.updatingTable = false; - } - } - - /** - * Updates row data array with current plugin values - * Currently unused but kept for future use - */ - @SuppressWarnings("unused") - private void updateRowData(Object[] rowData, PluginScheduleEntry plugin) { - // Handle separator row (null plugin) - if (plugin == null) { - Object[] separatorData = createRowData(null); // Use existing separator logic - System.arraycopy(separatorData, 0, rowData, 0, Math.min(separatorData.length, rowData.length)); - return; - } - - // Get basic information - String pluginName = plugin.getCleanName(); - - if (schedulerPlugin.isRunningEntry(plugin)) { - pluginName = "â–ķ " + pluginName; - } - - // For default plugins, add a visual indicator - if (plugin.isDefault()) { - pluginName = "* " + pluginName; // Use simple asterisk instead of emoji - } - - // For unavailable plugins, add visual indicator - if (!plugin.isPluginAvailable()) { - pluginName = "❌ " + pluginName + " (Not Installed)"; - } - - // Update row data - rowData[0] = pluginName; - rowData[1] = getEnhancedScheduleDisplay(plugin); - rowData[2] = getEnhancedNextRunDisplay(plugin); - rowData[3] = getStartConditionInfo(plugin); - rowData[4] = getStopConditionInfo(plugin); - rowData[5] = plugin.getPriority(); - rowData[6] = plugin.isEnabled(); - rowData[7] = plugin.getRunCount(); - } - - /** - * Updates existing row in the table with current plugin values - */ - private void updateRowWithPlugin(int rowIndex, PluginScheduleEntry plugin) { - // Handle separator row (null plugin) - if (plugin == null) { - Object[] separatorData = createRowData(null); // Use existing separator logic - for (int col = 0; col < separatorData.length && col < tableModel.getColumnCount(); col++) { - tableModel.setValueAt(separatorData[col], rowIndex, col); - } - return; - } - - // Get basic information - String pluginName = plugin.getCleanName(); - - if (schedulerPlugin.isRunningEntry(plugin)) { - pluginName = "â–ķ " + pluginName; - } - - // For default plugins, add a visual indicator - if (plugin.isDefault()) { - pluginName = "* " + pluginName; // Use simple asterisk instead of emoji - } - - // For unavailable plugins, add visual indicator - if (!plugin.isPluginAvailable()) { - pluginName = "❌ " + pluginName + " (Not Installed)"; - } - - // Update existing row with focused columns - tableModel.setValueAt(pluginName, rowIndex, 0); - tableModel.setValueAt(getEnhancedScheduleDisplay(plugin), rowIndex, 1); - tableModel.setValueAt(getEnhancedNextRunDisplay(plugin), rowIndex, 2); - tableModel.setValueAt(getStartConditionInfo(plugin), rowIndex, 3); - tableModel.setValueAt(getStopConditionInfo(plugin), rowIndex, 4); - tableModel.setValueAt(plugin.getPriority(), rowIndex, 5); - tableModel.setValueAt(plugin.isEnabled(), rowIndex, 6); - tableModel.setValueAt(plugin.getRunCount(), rowIndex, 7); - } - - /** - * Creates a new row data array for a plugin - */ - private Object[] createRowData(PluginScheduleEntry plugin) { - // Handle separator row (null plugin) - if (plugin == null) { - return new Object[]{ - "────── Not Installed / Would be scheduled if installed ──────", - "", - "", - "", - "", - 0, // Priority column - Integer - false, // Enabled column - Boolean - 0 // Run Count column - Integer - }; - } - - // Get basic information - String pluginName = plugin.getCleanName(); - - // Check if plugin is available - boolean isAvailable = plugin.isPluginAvailable(); - - if (schedulerPlugin.isRunningEntry(plugin)) { - pluginName = "â–ķ " + pluginName; - } - - // For default plugins, add a visual indicator - if (plugin.isDefault()) { - pluginName = "* " + pluginName; // Use simple asterisk instead of emoji - } - - // For unavailable plugins, add visual indicator - if (!isAvailable) { - pluginName = "❌ " + pluginName + " (Not Installed)"; - } - - return new Object[]{ - pluginName, - getEnhancedScheduleDisplay(plugin), - getEnhancedNextRunDisplay(plugin), - getStartConditionInfo(plugin), - getStopConditionInfo(plugin), - plugin.getPriority(), - plugin.isEnabled(), - plugin.getRunCount() - }; - } - /** - * Creates a display of start condition information - * Updated to be more compact with less line breaks to reduce row height - */ - private String getStartConditionInfo(PluginScheduleEntry entry) { - int startTotal = entry.getStartConditionManager().getConditions().size(); - if (startTotal == 0) { - return "None"; - } - - int startMet = (int) entry.getStartConditionManager().getConditions().stream() - .filter(Condition::isSatisfied).count(); - - StringBuilder info = new StringBuilder(); - info.append(""); - - // Check if conditions are relevant - boolean isRelevant = entry.isEnabled() && !entry.isRunning(); - - if (isRelevant && startMet < startTotal && entry.getStartConditionManager().requiresAll()) { - // When blocking conditions exist and ALL conditions required, show blocking info - int userBlockingCount = entry.getStartConditionManager().getUserLeafBlockingConditions().size(); - int pluginBlockingCount = entry.getStartConditionManager().getPluginLeafBlockingConditions().size(); - int totalBlocking = userBlockingCount + pluginBlockingCount; - - if (totalBlocking > 0) { - info.append("") - .append(startMet).append("/").append(startTotal) - .append(" met (").append(totalBlocking).append(" blocking)"); - } else { - info.append("").append(startMet).append("/").append(startTotal).append(" met"); - } - } else if (isRelevant && !entry.canBeStarted()) { - // When ANY condition is required but none are met - info.append("") - .append(startMet).append("/").append(startTotal).append(" (Blocked)"); - } else { - // Normal display - info.append("").append(startMet).append("/").append(startTotal).append(" met"); - } - - // Add type indicator and one-time indicator if applicable more concisely - boolean startRequiresAll = entry.getStartConditionManager().requiresAll(); - info.append(" ") - .append(startRequiresAll ? "ALL" : "ANY"); - - if (entry.hasAnyOneTimeStartConditions()) { - info.append(", One-time"); - } - info.append(""); - - return info.toString() + ""; - } - - /** - * Creates a display of stop condition information - * Updated to be more compact with less line breaks to reduce row height - */ - private String getStopConditionInfo(PluginScheduleEntry entry) { - int stopTotal = entry.getStopConditionManager().getConditions().size(); - if (stopTotal == 0) { - return "None"; - } - - int stopMet = (int) entry.getStopConditionManager().getConditions().stream() - .filter(Condition::isSatisfied).count(); - - StringBuilder info = new StringBuilder(); - info.append(""); - - // Check if conditions are relevant (plugin is running) - boolean isRelevant = entry.isRunning(); - - // Add progress for running plugins in a more compact format - if (isRelevant) { - double progress = entry.getStopConditionProgress(); - - // Show progress and satisfied conditions count on same line - if (entry.allowedToBeStop()) { - // Ready to stop - info.append("") - .append(stopMet).append("/").append(stopTotal) - .append(" met (Ready ✓)"); - } else { - // Still waiting for conditions - String progressStr = progress > 0 ? String.format(" %.0f%%", progress) : ""; - info.append("") - .append(stopMet).append("/").append(stopTotal) - .append(" met").append(progressStr).append(""); - - // If there are specific blocking conditions worth mentioning, add concisely - int waitingCount = entry.getStopConditionManager().getLeafBlockingConditions().size(); - if (waitingCount > 0) { - info.append(" (") - .append(waitingCount).append(" waiting)"); - } - } - } else { - // Standard display when not relevant - info.append("").append(stopMet).append("/").append(stopTotal).append(" met"); - } - - // Add type indicator and one-time indicator more concisely - boolean stopRequiresAll = entry.getStopConditionManager().requiresAll(); - info.append(" ") - .append(stopRequiresAll ? "ALL" : "ANY"); - - if (entry.hasAnyOneTimeStopConditions()) { - info.append(", One-time"); - } - info.append(""); - - return info.toString() + ""; - } - - - - /** - * Creates an enhanced display of schedule information - */ - private String getEnhancedScheduleDisplay(PluginScheduleEntry entry) { - // Check for one-time schedule first - boolean isOneTime = entry.hasAnyOneTimeStartConditions(); - - // If it's a one-time schedule that's already triggered, show completion status - if (isOneTime && entry.hasTriggeredOneTimeStartConditions() && !entry.canStartTriggerAgain()) { - return "One-time (Completed)"; - } - - // Get the base interval display - String baseDisplay = entry.getIntervalDisplay(); - - // For one-time schedules, add an indicator - if (isOneTime) { - return "One-time: " + baseDisplay; - } - - return baseDisplay; - } - - /** - * Creates an enhanced display of the next run time, including last stop icon - */ - private String getEnhancedNextRunDisplay(PluginScheduleEntry entry) { - StringBuilder display = new StringBuilder(); - display.append(""); - - // First add the plugin's optimized display method for scheduling - String baseDisplay = entry.getNextRunDisplay(); - - // Add extra information for one-time entries that can't run again - if (entry.hasAnyOneTimeStartConditions() && !entry.canStartTriggerAgain()) { - if (entry.getRunCount() > 0) { - display.append("Completed"); - } else { - display.append("Cannot run"); - } - } else { - display.append(baseDisplay); - } - - // Add just an icon for the last stop reason (if plugin has run before and isn't currently running) - if (entry.getRunCount() > 0 && !entry.isRunning() && entry.getLastStopReasonType() != PluginScheduleEntry.StopReason.NONE) { - // Add spacing - display.append(" "); - - // Show only an icon based on stop reason - switch(entry.getLastStopReasonType()) { - case SCHEDULED_STOP: - display.append("✓"); // Green checkmark for normal stop - break; - case PLUGIN_FINISHED: - display.append("✅"); // Green check box for self-completed - break; - case MANUAL_STOP: - display.append("âđ"); // Gray square for manual stop - break; - case HARD_STOP: - display.append("⚠"); // Orange warning for timeout - break; - case INTERRUPTED: - display.append("âļ"); // Blue pause for interrupted - break; - case ERROR: - display.append("❌"); // Red X for error - break; - default: - display.append("â€Ē"); // Gray dot for unknown - break; - } - } - - return display.toString() + ""; - } - - /** - * Creates an enhanced display of condition information - * Currently unused but kept for future use - */ - @SuppressWarnings("unused") - private String getEnhancedConditionInfo(PluginScheduleEntry entry) { - StringBuilder info = new StringBuilder(); - - // Add start condition info if available - int startTotal = entry.getStartConditionManager().getConditions().size(); - if (startTotal > 0) { - int startMet = (int) entry.getStartConditionManager().getConditions().stream() - .filter(Condition::isSatisfied).count(); - - info.append("Start: ").append(startMet).append("/").append(startTotal); - - // Add type indicator - boolean startRequiresAll = entry.getStartConditionManager().requiresAll(); - info.append(startRequiresAll ? " (ALL)" : " (ANY)"); - } - - // Add stop condition info if available - int stopTotal = entry.getStopConditionManager().getConditions().size(); - if (stopTotal > 0) { - if (info.length() > 0) { - info.append(" | "); - } - - int stopMet = (int) entry.getStopConditionManager().getConditions().stream() - .filter(Condition::isSatisfied).count(); - - info.append("Stop: ").append(stopMet).append("/").append(stopTotal); - - // Add type indicator - boolean stopRequiresAll = entry.getStopConditionManager().requiresAll(); - info.append(stopRequiresAll ? " (ALL)" : " (ANY)"); - - // Add progress for running plugins - if (entry.isRunning()) { - double progress = entry.getStopConditionProgress(); - if (progress > 0) { - info.append(String.format(" (%.0f%%)", progress)); - } - } - } - - // If no conditions, show "None" - if (info.length() == 0) { - return "None"; - } - - return info.toString(); - } - - public void addSelectionListener(Consumer listener) { - this.selectionListener = listener; - scheduleTable.getSelectionModel().addListSelectionListener(e -> { - if (!e.getValueIsAdjusting()) { - int selectedRow = scheduleTable.getSelectedRow(); - if (selectedRow >= 0 && selectedRow < schedulerPlugin.getScheduledPlugins().size()) { - listener.accept(getPluginAtRow(selectedRow)); - } else { - listener.accept(null); - } - } - }); - } - - public PluginScheduleEntry getSelectedPlugin() { - int selectedRow = scheduleTable.getSelectedRow(); - if (selectedRow >= 0 && selectedRow < schedulerPlugin.getScheduledPlugins().size()) { - if (rowToPluginMap.size() > selectedRow) { - // Use the rowToPluginMap to get the plugin - return rowToPluginMap.get(selectedRow); - }else { - // Fallback to the original list if rowToPluginMap is not available - // This should not happen in normal operation - rowToPluginMap = new ArrayList<>(schedulerPlugin.getScheduledPlugins()); - return schedulerPlugin.getScheduledPlugins().get(selectedRow); - } - - //return schedulerPlugin.getScheduledPlugins().get(selectedRow); - } - return null; - } - - /** - * Clears the current table selection and notifies the selection listener - */ - public void clearSelection() { - scheduleTable.clearSelection(); - lastSelectedRow = -1; // Reset last selected row - if (selectionListener != null) { - selectionListener.accept(null); - } - } - - /** - * Clears the table selection without triggering the selection callback. - * This is useful when we want to clear selection programmatically without - * triggering a cascade of UI updates. - */ - public void clearSelectionWithoutCallback() { - scheduleTable.clearSelection(); - // Unlike clearSelection(), this method does not call selectionListener - } - - public void addAndSelect(PluginScheduleEntry pluginEntry) { - if (pluginEntry == null) return; - - for (PluginScheduleEntry entry : schedulerPlugin.getScheduledPlugins()) { - if ( pluginEntry == entry) { - // Plugin already exists, no need to add -> no duplicate - return; - } - } - schedulerPlugin.addScheduledPlugin(pluginEntry); - rowToPluginMap.add(pluginEntry); - tableModel.addRow(new Object[]{ - pluginEntry.getName(), - pluginEntry.getIntervalDisplay(), - pluginEntry.getNextRunDisplay(), - getStartConditionInfo(pluginEntry), - getStopConditionInfo(pluginEntry), - pluginEntry.getPriority(), - pluginEntry.isDefault(), - pluginEntry.isEnabled(), - pluginEntry.isAllowRandomScheduling(), - pluginEntry.isNeedsStopCondition(), - pluginEntry.getRunCount() - }); - scheduleTable.setRowSelectionInterval(getRowCount(), getRowCount()); - } - /** - * Selects the given plugin in the table - * @param plugin The plugin to select - */ - public void selectPlugin(PluginScheduleEntry plugin) { - if (plugin == null) return; - List plugins = this.rowToPluginMap; - for (int i = 0; i < tableModel.getRowCount(); i++) { - String rowName = String.valueOf(tableModel.getValueAt(i, 0)) - .replaceAll("â–ķ ", ""); // Remove play indicator if present - // First try to find the exact same object reference - - if (plugins.get(i) == plugin) { // Use reference equality - scheduleTable.setRowSelectionInterval(i, i); - // Make sure the selected row is visible - Rectangle rect = scheduleTable.getCellRect(i, 0, true); - scheduleTable.scrollRectToVisible(rect); - - // Notify listeners - if (selectionListener != null) { - selectionListener.accept(plugin); - } - return; - } - } - } - /** - * Creates a comprehensive tooltip for plugin details including last stop information - */ - private String getPluginDetailsTooltip(PluginScheduleEntry entry) { - StringBuilder tooltip = new StringBuilder("Plugin Details: ").append(entry.getCleanName()); - - // Status section - tooltip.append("

Status: "); - if (entry.isRunning()) { - tooltip.append("Currently Running"); - } else if (!entry.isEnabled()) { - tooltip.append("Disabled"); - } else if (isNextToRun(entry)) { - tooltip.append("Next to Run"); - } else { - tooltip.append("Waiting for schedule"); - } - - // Run information - tooltip.append("
Run Count: ").append(entry.getRunCount()); - - // Last stop information - if (entry.getRunCount() > 0 && !entry.isRunning()) { - tooltip.append("

Last Stop Info:"); - - // Stop reason - String stopReason = entry.getLastStopReason(); - if (stopReason != null && !stopReason.isEmpty()) { - tooltip.append("
Reason: ").append(stopReason); - } - - // Stop reason type with matching icon - tooltip.append("
Type: "); - // Use the description from the enum if available - PluginScheduleEntry.StopReason reasonType = entry.getLastStopReasonType(); - if (reasonType != null) { - switch (reasonType) { - case SCHEDULED_STOP: - tooltip.append("✓ Scheduled Stop (conditions met)"); - break; - case MANUAL_STOP: - tooltip.append("âđ Manual Stop (user initiated)"); - break; - case HARD_STOP: - tooltip.append("⚠ Hard Stop (forced after timeout)"); - break; - case ERROR: - tooltip.append("❌ Error"); - break; - case PLUGIN_FINISHED: - tooltip.append("✅ Plugin Self-reported Completion"); - break; - case INTERRUPTED: - tooltip.append("âļ Plugin Interrupted"); - break; - case NONE: - tooltip.append("â€Ē " + reasonType.getDescription()); - break; - default: - tooltip.append("â€Ē " + reasonType.getDescription()); - break; - } - } else { - tooltip.append("â€Ē Unknown"); - } - - // Success status - tooltip.append("
Success: "); - tooltip.append(entry.isLastRunSuccessful() ? - "Yes" : - "No"); - } - - // Schedule information - tooltip.append("

Schedule: ").append(entry.getIntervalDisplay()); - - // Start/Stop condition information - use the detailed tooltip methods - tooltip.append("

"); - - // Extract the content from the start and stop condition tooltips, but exclude the html tags - String startConditions = getStartConditionsTooltip(entry) - .replace("", "") - .replace("", ""); - - String stopConditions = getStopConditionsTooltip(entry) - .replace("", "") - .replace("", ""); - - tooltip.append(startConditions); - tooltip.append("

"); - tooltip.append(stopConditions); - - // Configuration - tooltip.append("

Configuration:"); - tooltip.append("
Priority: ").append(entry.getPriority()); - tooltip.append("
Default: ").append(entry.isDefault() ? "Yes" : "No"); - tooltip.append("
Random Scheduling: ").append(entry.isAllowRandomScheduling() ? "Enabled" : "Disabled"); - - return tooltip.toString() + ""; - } - - /** - * Implementation of ScheduleTableModel interface - gets the plugin at a specific row - */ - @Override - public PluginScheduleEntry getPluginAtRow(int row) { - if (row >= 0 && row < rowToPluginMap.size()) { - return rowToPluginMap.get(row); - } - if (row >= 0 && row < tableModel.getRowCount()) { - return schedulerPlugin.getScheduledPlugins().get(row); - } - return null; - } - - /** - * Helper method to blend colors with a given ratio - * @param baseValue Base color component value (0-255) - * @param targetValue Target color component value (0-255) - * @param ratio Blend ratio (0.0-1.0) where 1.0 is completely base color - * @return Blended color component value - */ - private int blend(int baseValue, int targetValue, float ratio) { - return Math.max(0, Math.min(255, Math.round(baseValue * ratio + targetValue * (1 - ratio)))); - } - - /** - * Starts or restarts the tooltip refresh timer. - * This timer periodically triggers tooltip updates while hovering over a table cell. - * The implementation ensures tooltips remain visible and are refreshed with live data. - */ - private void startTooltipRefreshTimer() { - // Stop existing timer if running - if (tooltipRefreshTimer != null && tooltipRefreshTimer.isRunning()) { - tooltipRefreshTimer.stop(); - } - - // Create new timer if needed - if (tooltipRefreshTimer == null) { - tooltipRefreshTimer = new Timer(TOOLTIP_REFRESH_INTERVAL, e -> { - if (hoverRow >= 0 && hoverColumn >= 0 && hoverRow < rowToPluginMap.size()) { - try { - // Get current plugin entry for tooltip refresh - PluginScheduleEntry currentEntry = rowToPluginMap.get(hoverRow); - - // Force tooltip to hide and then show again with fresh content - // This two-step approach ensures the tooltip content is refreshed - //ToolTipManager.sharedInstance().mouseMoved( - // new MouseEvent(scheduleTable, MouseEvent.MOUSE_EXITED, - // System.currentTimeMillis(), 0, - // hoverLocation.x, hoverLocation.y, - // 0, false)); - - // Small delay to allow the tooltip to hide before showing again - SwingUtilities.invokeLater(() -> { - // Now show fresh tooltip - //ToolTipManager.sharedInstance().mouseMoved( - // new MouseEvent(scheduleTable, MouseEvent.MOUSE_MOVED, - // System.currentTimeMillis(), 0, - // hoverLocation.x, hoverLocation.y, - // 0, false)); - - // Also trigger a cell repaint to ensure tooltip data is current - scheduleTable.repaint(scheduleTable.getCellRect(hoverRow, hoverColumn, false)); - }); - } catch (IndexOutOfBoundsException | NullPointerException ex) { - // Safety check for race conditions when table data changes while hovering - // Just skip this refresh cycle - log.debug("Skipped tooltip refresh due to data change"); - } - } - }); - tooltipRefreshTimer.setRepeats(true); - } - - // Start the timer - tooltipRefreshTimer.start(); - } - - /** - * Handles tooltip refresh when the table data changes - * This is called from refreshTable to update tooltips with fresh data - */ - private void updateTooltipsAfterRefresh(List newRowMap) { - // Handle tooltip update if a tooltip is currently showing - if (hoverRow >= 0 && hoverColumn >= 0 && tooltipRefreshTimer != null && tooltipRefreshTimer.isRunning()) { - // If the row we were hovering over still exists - if (hoverRow < newRowMap.size()) { - // Force tooltip refresh with updated data - SwingUtilities.invokeLater(() -> { - // Re-trigger tooltip on the updated data - ToolTipManager.sharedInstance().mouseMoved( - new MouseEvent(scheduleTable, MouseEvent.MOUSE_MOVED, - System.currentTimeMillis(), 0, - hoverLocation.x, hoverLocation.y, - 0, false)); - }); - } else { - // Row is gone, reset tooltip tracking - hoverRow = -1; - hoverColumn = -1; - tooltipRefreshTimer.stop(); - } - } - } - - /** - * Cleans up timer resources when the panel is no longer used - */ - @Override - public void removeNotify() { - // Stop and clean up timer when component is removed from UI - if (tooltipRefreshTimer != null && tooltipRefreshTimer.isRunning()) { - tooltipRefreshTimer.stop(); - tooltipRefreshTimer = null; - } - super.removeNotify(); - } - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerInfoPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerInfoPanel.java deleted file mode 100644 index cfc59f61755..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerInfoPanel.java +++ /dev/null @@ -1,1494 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui; - -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerPlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerState; -import net.runelite.client.plugins.microbot.pluginscheduler.api.SchedulablePlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.ui.PrePostScheduleTasksInfoPanel; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.util.UIUtils; -import net.runelite.client.plugins.microbot.util.antiban.enums.Activity; -import net.runelite.client.plugins.microbot.util.antiban.enums.ActivityIntensity; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; -import net.runelite.client.plugins.microbot.util.events.PluginPauseEvent; -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import java.util.List; -import java.util.stream.Collectors; -import lombok.extern.slf4j.Slf4j; - -import java.awt.*; -import java.time.Duration; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; - - -/** - * Displays real-time information about the scheduler status and plugins - */ -@Slf4j -public class SchedulerInfoPanel extends JPanel { - private final SchedulerPlugin plugin; - // Scheduler status components - private final JLabel statusLabel; - private final JLabel runtimeLabel; - private final JLabel currentPluginInStatusLabel; // New field for current plugin in status section - private ZonedDateTime schedulerStartTime; - - // Control buttons - private final JButton runSchedulerButton; - private final JButton stopSchedulerButton; - private final JButton loginButton; - private final JButton pauseResumePluginButton; // button for only pusing the currently running plugin (PluginScheduleEntry), by SchedulerPlugin - private final JButton pauseResumeSchedulerButton; // button for pausing the whole scheduler -> all condition progress is paused for all PluginScheduleEntry currently running managed by SchedulerPlugin by SchedulerPlugin - // Added hard reset button to reset all user condition states for all scheduled plugins-> initial settings are applied again to all start and stop conditions for the curre - private final JButton hardResetButton; - - // Combined plugin information panel - private final JPanel pluginInfoPanel; - - // Current plugin components - private JLabel currentPluginNameLabel; - private JLabel currentPluginRuntimeLabel; - private JProgressBar stopConditionProgressBar; - private JLabel stopConditionStatusLabel; - private ZonedDateTime currentPluginStartTime; - - // Next plugin components - private JLabel nextUpComingPluginNameLabel; - private JLabel nextUpComingPluginTimeLabel; - private JLabel nextUpComingPluginScheduleLabel; - - // Previous plugin components - private JLabel prevPluginNameLabel; - private JLabel prevPluginDurationLabel; - private JTextArea prevPluginStatusLabel; - private JLabel prevPluginStopTimeLabel; - - // Player status components - private final JPanel playerStatusPanel; - private final JLabel activityLabel; - private final JLabel activityIntensityLabel; - private final JLabel idleTimeLabel; - private final JLabel loginTimeLabel; - private final JLabel breakStatusLabel; - private final JLabel nextBreakLabel; - private final JLabel breakDurationLabel; - - // Pre/Post Schedule Tasks components - private final PrePostScheduleTasksInfoPanel prePostTasksInfoPanel; - - // State tracking for optimized updates - private PluginScheduleEntry lastTrackedCurrentPlugin; - private PluginScheduleEntry lastTrackedPreviousPlugin; - private PluginScheduleEntry lastTrackedNextUpComingPlugin; - - - public SchedulerInfoPanel(SchedulerPlugin plugin) { - this.plugin = plugin; - // Use a box layout instead of BorderLayout - setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); - setBorder(new EmptyBorder(4, 4, 4, 4)); // Reduced padding from 8,8,8,8 for tighter layout - setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Add panels with some vertical spacing - JPanel statusPanel = UIUtils.createInfoPanel("Scheduler Status"); - GridBagConstraints gbc = UIUtils.createGbc(0, 0); - - statusPanel.add(new JLabel("Status:"), gbc); - gbc.gridx++; - statusLabel = UIUtils.createValueLabel("Not Running"); - statusPanel.add(statusLabel, gbc); - - gbc.gridx = 0; - gbc.gridy++; - statusPanel.add(new JLabel("Runtime:"), gbc); - gbc.gridx++; - runtimeLabel = UIUtils.createValueLabel("00:00:00"); - statusPanel.add(runtimeLabel, gbc); - - // Add current plugin field to status panel - gbc.gridx = 0; - gbc.gridy++; - statusPanel.add(new JLabel("Current Plugin:"), gbc); - gbc.gridx++; - currentPluginInStatusLabel = UIUtils.createValueLabel("None"); - statusPanel.add(currentPluginInStatusLabel, gbc); - - // Create control buttons panel - gbc.gridx = 0; - gbc.gridy++; - gbc.gridwidth = 2; - // Use GridLayout with 3 rows instead of 2x2 to properly fit all 5 buttons - JPanel buttonPanel = new JPanel(new GridLayout(3, 2, 5, 5)); - buttonPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Create run scheduler button - runSchedulerButton = createCompactButton("Run Scheduler", new Color(76, 175, 80)); - runSchedulerButton.addActionListener(e -> { - plugin.startScheduler(); - updateButtonStates(); - }); - buttonPanel.add(runSchedulerButton); - - // Create stop scheduler button - stopSchedulerButton = createCompactButton("Stop Scheduler", new Color(244, 67, 54)); - stopSchedulerButton.addActionListener(e -> { - SwingUtilities.invokeLater(() -> { - plugin.stopScheduler(); - updateButtonStates(); - }); - }); - buttonPanel.add(stopSchedulerButton); - - // Create login button - loginButton = createCompactButton("Login", new Color(33, 150, 243)); // Blue - loginButton.addActionListener(e -> { - SwingUtilities.invokeLater(() -> { - plugin.toggleManualLogin(); - }); - }); - buttonPanel.add(loginButton); - - // Create pause/resume button - pauseResumePluginButton = createCompactButton("Pause Plugin", new Color(0, 188, 212)); // Cyan color - pauseResumePluginButton.setVisible(false); // Initially hidden - pauseResumePluginButton.addActionListener(e -> { - // Toggle the pause state - - - // Update button text and color based on state - if (!plugin.isCurrentPluginPaused()) { - boolean pauseSuccess = plugin.pauseRunningPlugin(); - if (pauseSuccess){ - pauseResumePluginButton.setText("Resume Plugin"); - pauseResumePluginButton.setBackground(new Color(76, 175, 80)); // Green color - } - } else { - if(plugin.isCurrentPluginPaused()){ - plugin.resumeRunningPlugin(); - pauseResumePluginButton.setText("Pause Plugin"); - pauseResumePluginButton.setBackground(new Color(0, 188, 212)); // Cyan color - } - } - // updateCurrentPluginInfo(); // Commented out - moved to status section - updateButtonStates(); - }); - buttonPanel.add(pauseResumePluginButton); - - // Create pause/resume scheduler button - pauseResumeSchedulerButton = createCompactButton("Pause Scheduler", new Color(255, 152, 0)); // Orange color - pauseResumeSchedulerButton.addActionListener(e -> { - // Toggle the pause state using our new methods - if (plugin.isPaused() ) { - // Currently paused, so resume - plugin.resumeScheduler(); - pauseResumeSchedulerButton.setText("Pause Scheduler"); - pauseResumeSchedulerButton.setBackground(new Color(255, 152, 0)); // Orange color - }else if(plugin.isOnBreak() && (plugin.getCurrentState() == SchedulerState.BREAK) || - plugin.getCurrentState() == SchedulerState.PLAYSCHEDULE_BREAK){ - // If currently on break, resume the break - plugin.resumeBreak(); - }else { - // Currently running, so pause - plugin.pauseScheduler(); - pauseResumeSchedulerButton.setText("Resume Scheduler"); - pauseResumeSchedulerButton.setBackground(new Color(76, 175, 80)); // Green color - } - - // Update UI - // updateCurrentPluginInfo(); // Commented out - moved to status section - updateButtonStates(); - }); - buttonPanel.add(pauseResumeSchedulerButton); - - // Create hard reset button - hardResetButton = createCompactButton("Hard Reset", new Color(156, 39, 176)); // Purple color - hardResetButton.setToolTipText("Reset all user condition states for all scheduled plugins"); - hardResetButton.addActionListener(e -> showHardResetConfirmation()); - buttonPanel.add(hardResetButton); - - statusPanel.add(buttonPanel, gbc); - - add(statusPanel); - add(Box.createRigidArea(new Dimension(0, 10))); // Add spacing - - // Create the player status panel - playerStatusPanel = UIUtils.createInfoPanel("Player Status"); - gbc = UIUtils.createGbc(0, 0); - - playerStatusPanel.add(new JLabel("Activity:"), gbc); - gbc.gridx++; - activityLabel = UIUtils.createValueLabel("None"); - playerStatusPanel.add(activityLabel, gbc); - - gbc.gridx = 0; - gbc.gridy++; - playerStatusPanel.add(new JLabel("Intensity:"), gbc); - gbc.gridx++; - activityIntensityLabel = UIUtils.createValueLabel("None"); - playerStatusPanel.add(activityIntensityLabel, gbc); - - gbc.gridx = 0; - gbc.gridy++; - playerStatusPanel.add(new JLabel("Idle Time:"), gbc); - gbc.gridx++; - idleTimeLabel = UIUtils.createValueLabel("0 ticks"); - playerStatusPanel.add(idleTimeLabel, gbc); - - gbc.gridx = 0; - gbc.gridy++; - playerStatusPanel.add(new JLabel("Login Duration:"), gbc); - gbc.gridx++; - loginTimeLabel = UIUtils.createValueLabel("Not logged in"); - playerStatusPanel.add(loginTimeLabel, gbc); - - gbc.gridx = 0; - gbc.gridy++; - playerStatusPanel.add(new JLabel("Break Status:"), gbc); - gbc.gridx++; - breakStatusLabel = UIUtils.createValueLabel("Not on break"); - playerStatusPanel.add(breakStatusLabel, gbc); - - gbc.gridx = 0; - gbc.gridy++; - playerStatusPanel.add(new JLabel("Next Break:"), gbc); - gbc.gridx++; - nextBreakLabel = UIUtils.createValueLabel("--:--:--"); - playerStatusPanel.add(nextBreakLabel, gbc); - - gbc.gridx = 0; - gbc.gridy++; - playerStatusPanel.add(new JLabel("Current Break:"), gbc); - gbc.gridx++; - breakDurationLabel = UIUtils.createValueLabel("00:00:00"); - playerStatusPanel.add(breakDurationLabel, gbc); - - add(playerStatusPanel, BorderLayout.CENTER); - - // Create compact plugin information panel - pluginInfoPanel = createDynamicPluginInfoPanel(); - add(pluginInfoPanel); - - // Add spacing before pre/post schedule tasks panel - add(Box.createRigidArea(new Dimension(0, 5))); - - // Create pre/post schedule tasks info panel - prePostTasksInfoPanel = new PrePostScheduleTasksInfoPanel(); - add(prePostTasksInfoPanel); - - // Add spacing after pre/post schedule tasks panel for better layout - add(Box.createRigidArea(new Dimension(0, 5))); - - // Initial refresh - refresh(); - } - - /** - * Creates a dynamic, responsive plugin info panel that adapts to content and window size - * Now shows only previous and next plugin information (current plugin moved to status section) - */ - private JPanel createDynamicPluginInfoPanel() { - // Create sections using utility methods - removed current section - JPanel prevSection = UIUtils.createAdaptiveSection("Previous"); - JPanel nextSection = UIUtils.createAdaptiveSection("Next"); - - // Add content to sections using utility methods - addPreviousPluginContentWithUtils(prevSection); - addNextPluginContentWithUtils(nextSection); - - // Create bottom panel for progress and stop reason - JPanel bottomPanel = createDynamicBottomPanelWithUtils(); - - // Create the main panel using utility - only previous and next sections - JPanel[] sections = {prevSection, nextSection}; - return UIUtils.createDynamicInfoPanel("Previous & Next Plugins", sections, bottomPanel); - } - - /** - * Adds content to the previous plugin section using utility methods - */ - private void addPreviousPluginContentWithUtils(JPanel section) { - prevPluginNameLabel = UIUtils.createAdaptiveValueLabel("None"); - prevPluginDurationLabel = UIUtils.createAdaptiveValueLabel("00:00:00"); - prevPluginStopTimeLabel = UIUtils.createAdaptiveValueLabel("--:--:--"); - - UIUtils.LabelValuePair[] rows = { - new UIUtils.LabelValuePair("Name:", prevPluginNameLabel), - new UIUtils.LabelValuePair("Duration:", prevPluginDurationLabel), - new UIUtils.LabelValuePair("Stop Time:", prevPluginStopTimeLabel) - }; - - UIUtils.addContentToSection(section, rows); - } - - /** - * Adds content to the current plugin section using utility methods - * Currently commented out since current plugin info moved to status section - */ - /* - private void addCurrentPluginContentWithUtils(JPanel section) { - currentPluginNameLabel = UIUtils.createAdaptiveValueLabel("None"); - currentPluginRuntimeLabel = UIUtils.createAdaptiveValueLabel("00:00:00"); - stopConditionStatusLabel = UIUtils.createAdaptiveValueLabel("None"); - stopConditionStatusLabel.setToolTipText("Detailed stop condition information"); - - UIUtils.LabelValuePair[] rows = { - new UIUtils.LabelValuePair("Name:", currentPluginNameLabel), - new UIUtils.LabelValuePair("Runtime:", currentPluginRuntimeLabel), - new UIUtils.LabelValuePair("Conditions:", stopConditionStatusLabel) - }; - - UIUtils.addContentToSection(section, rows); - } - */ - - /** - * Adds content to the next plugin section using utility methods - */ - private void addNextPluginContentWithUtils(JPanel section) { - nextUpComingPluginNameLabel = UIUtils.createAdaptiveValueLabel("None"); - nextUpComingPluginTimeLabel = UIUtils.createAdaptiveValueLabel("--:--"); - nextUpComingPluginScheduleLabel = UIUtils.createAdaptiveValueLabel("None"); - - UIUtils.LabelValuePair[] rows = { - new UIUtils.LabelValuePair("Name:", nextUpComingPluginNameLabel), - new UIUtils.LabelValuePair("Time:", nextUpComingPluginTimeLabel), - new UIUtils.LabelValuePair("Schedule:", nextUpComingPluginScheduleLabel) - }; - - UIUtils.addContentToSection(section, rows); - } - - /** - * Creates the dynamic bottom panel using utility methods - */ - private JPanel createDynamicBottomPanelWithUtils() { - // Create progress bar - stopConditionProgressBar = new JProgressBar(0, 100); - stopConditionProgressBar.setStringPainted(true); - stopConditionProgressBar.setString("No conditions"); - stopConditionProgressBar.setForeground(new Color(76, 175, 80)); - stopConditionProgressBar.setBackground(ColorScheme.DARKER_GRAY_COLOR); - stopConditionProgressBar.setPreferredSize(new Dimension(0, 16)); - - // Create status text area - prevPluginStatusLabel = UIUtils.createAdaptiveTextArea("None"); - - return UIUtils.createDynamicBottomPanel(stopConditionProgressBar, prevPluginStatusLabel); - } - - /** - * Helper method to create and style a compact button - * @param text Button text - * @param bgColor Background color - * @return Styled JButton - */ - private JButton createCompactButton(String text, Color bgColor) { - JButton button = new JButton(text); - button.setBackground(bgColor); - button.setForeground(Color.WHITE); - button.setFocusPainted(false); - // Make buttons more compact - button.setFont(button.getFont().deriveFont(11f)); // Smaller font - button.setMargin(new Insets(2, 4, 2, 4)); // Smaller margins - return button; - } - - /** - * Helper method to create a text area for multi-line text display - * @param text Initial text for the text area - * @return A configured JTextArea - */ - private JTextArea createMultiLineTextArea(String text) { - JTextArea textArea = new JTextArea(text); - textArea.setLineWrap(true); - textArea.setWrapStyleWord(true); - textArea.setOpaque(false); - textArea.setEditable(false); - textArea.setFocusable(false); - textArea.setBackground(ColorScheme.DARKER_GRAY_COLOR); - textArea.setForeground(Color.WHITE); - textArea.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); - textArea.setFont(FontManager.getRunescapeFont()); - return textArea; - } - - /** - * Refreshes all displayed information with selective updates based on plugin state changes - */ - public void refresh() { - // Always update scheduler status and buttons for real-time feedback - updateSchedulerStatus(); - updateButtonStates(); - - // Always update player status as it changes frequently - updatePlayerStatusInfo(); - - // Get current plugin states - PluginScheduleEntry currentPlugin = plugin.getCurrentPlugin(); - PluginScheduleEntry previousPlugin = plugin.getLastPlugin(); - PluginScheduleEntry nextUpComingPlugin = plugin.getUpComingPlugin(); - - // Update current plugin info if it changed or is running (for runtime updates) - // Note: Current plugin display moved to status section, keeping runtime-only updates - if (currentPlugin != lastTrackedCurrentPlugin) { - // updateCurrentPluginInfo(); // Commented out - moved to status section - lastTrackedCurrentPlugin = currentPlugin; - } else if (currentPlugin != null && currentPlugin.isRunning()) { - // Always update runtime for running plugins even if plugin object hasn't changed - updateCurrentPluginRuntimeOnly(); - } - - // Update previous plugin info only if it changed - if (previousPlugin != lastTrackedPreviousPlugin) { - updatePreviousPluginInfo(); - lastTrackedPreviousPlugin = previousPlugin; - } - - // Update next plugin info if it changed - if (nextUpComingPlugin != lastTrackedNextUpComingPlugin) { - updateNextUpComingPluginInfo(); - lastTrackedNextUpComingPlugin = nextUpComingPlugin; - } else if (nextUpComingPlugin != null) { - // Always update time display for next plugin since countdown changes every second - updateNextUpComingPluginTimeDisplay(nextUpComingPlugin); - } - - // Update pre/post schedule tasks info for current plugin - updatePrePostTasksInfo(); - } - - /** - * Forces an immediate update of all plugin information. - * Useful when plugin states change and immediate UI refresh is needed. - */ - public void forcePluginInfoUpdate() { - lastTrackedCurrentPlugin = null; - lastTrackedPreviousPlugin = null; - lastTrackedNextUpComingPlugin = null; - // updateCurrentPluginInfo(); // Commented out - moved to status section - updatePreviousPluginInfo(); - updateNextUpComingPluginInfo(); - } - - /** - * Updates the button states based on scheduler state - */ - private void updateButtonStates() { - SchedulerState state = plugin.getCurrentState(); - boolean isActive = plugin.getCurrentState().isSchedulerActive(); - - // Only enable run button if we're in READY or HOLD state - runSchedulerButton.setEnabled((!isActive && (state == SchedulerState.READY || state == SchedulerState.HOLD)) && !state.isPaused()); - - runSchedulerButton.setToolTipText( - !runSchedulerButton.isEnabled() ? - "Scheduler cannot be started in " + state.getDisplayName() + " state" : - "Start running the scheduler"); - - // Only enable stop button if scheduler is active - stopSchedulerButton.setEnabled(isActive); - stopSchedulerButton.setToolTipText( - isActive ? "Stop the scheduler" : "Scheduler is not running"); - - // login/logout button logic - only allow in scheduling/waiting states or manual login active - boolean isInManualLoginState = state == SchedulerState.MANUAL_LOGIN_ACTIVE; - boolean isInSchedulingState = state == SchedulerState.SCHEDULING || state == SchedulerState.WAITING_FOR_SCHEDULE; - boolean canUseManualLogin = isInManualLoginState || isInSchedulingState || - state == SchedulerState.BREAK || state == SchedulerState.PLAYSCHEDULE_BREAK; - - loginButton.setEnabled(canUseManualLogin && state != SchedulerState.WAITING_FOR_LOGIN && state != SchedulerState.LOGIN); - - // update button text and tooltip based on current state - String buttonText; - String loginTooltip; - - if (isInManualLoginState) { - buttonText = "Logout"; - loginTooltip = "logout and resume automatic break handling"; - } else if (Microbot.isLoggedIn()) { - buttonText = "Logout"; - loginTooltip = "logout manually (will switch to manual login mode)"; - } else { - buttonText = "Login"; - if (plugin.isOnBreak()) { - loginTooltip = "login manually (will interrupt break and pause break handling)"; - } else { - loginTooltip = "login manually (will pause automatic break handling)"; - } - } - - loginButton.setText(buttonText); - loginButton.setToolTipText(loginTooltip); - - - - // Update pause/resume scheduler button state - pauseResumeSchedulerButton.setEnabled(isActive || plugin.getCurrentState()== SchedulerState.SCHEDULER_PAUSED || plugin.getCurrentState() == SchedulerState.BREAK); - - boolean isSchedulerPaused = plugin.isPaused(); - - if (isSchedulerPaused || plugin.isOnBreak()) { - if (plugin.isOnBreak()){ - pauseResumeSchedulerButton.setText("Resume Break"); - pauseResumeSchedulerButton.setBackground(new Color(76, 175, 80)); // Green color - pauseResumeSchedulerButton.setToolTipText("Resume the break"); - pauseResumePluginButton.setEnabled(false); // Disable plugin pause/resume while scheduler is paused - }else{ - pauseResumeSchedulerButton.setText("Resume Scheduler"); - pauseResumeSchedulerButton.setBackground(new Color(76, 175, 80)); // Green color - pauseResumeSchedulerButton.setToolTipText("Resume the paused scheduler"); - pauseResumePluginButton.setEnabled(false); // Disable plugin pause/resume while scheduler is paused - } - } else { - pauseResumeSchedulerButton.setText("Pause Scheduler"); - pauseResumeSchedulerButton.setBackground(new Color(255, 152, 0)); // Orange color - pauseResumeSchedulerButton.setToolTipText("Pause the scheduler without stopping it"); - pauseResumePluginButton.setEnabled(true); // Enable plugin pause/resume while scheduler is running - } - if ( state.isBreaking()){ - pauseResumeSchedulerButton.setEnabled(false); - } - boolean currentRunningPluginPaused = plugin.isCurrentPluginPaused(); - - // Only show the pause button when a plugin is actively running - pauseResumePluginButton.setVisible(state == SchedulerState.RUNNING_PLUGIN || - state == SchedulerState.RUNNING_PLUGIN_PAUSED); - - // If Scheulder PLugin is not Running any Plugin at the moment -> detect changed state and we're no longer running, ensure pause for scripts and plugin is reset - - if (state == SchedulerState.RUNNING_PLUGIN && !(state == SchedulerState.RUNNING_PLUGIN_PAUSED || - state == SchedulerState.SCHEDULER_PAUSED) && PluginPauseEvent.isPaused()) { - PluginPauseEvent.setPaused(false); - pauseResumePluginButton.setText("Pause Plugin"); - pauseResumePluginButton.setBackground(new Color(255, 152, 0)); - } - if(state == SchedulerState.RUNNING_PLUGIN || - state == SchedulerState.RUNNING_PLUGIN_PAUSED){ - pauseResumePluginButton.setEnabled(true); - // Update pause/resume plugin button state - if (currentRunningPluginPaused) { - pauseResumePluginButton.setText("Resume Plugin"); - pauseResumePluginButton.setBackground(new Color(76, 175, 80)); // Green color - pauseResumePluginButton.setToolTipText("Resume the currently paused plugin"); - pauseResumeSchedulerButton.setEnabled(false); // Disable scheduler pause/resume while a plugin is paused - } else { - pauseResumePluginButton.setText("Pause Plugin"); - pauseResumePluginButton.setBackground(new Color(0, 188, 212)); // Cyan color - pauseResumePluginButton.setToolTipText("Pause the currently running plugin"); - pauseResumeSchedulerButton.setEnabled(true); // Disable scheduler pause/resume while a plugin is paused - } - }else { - // If the scheduler is paused, disable the pause/resume plugin button - pauseResumePluginButton.setEnabled(false); - pauseResumePluginButton.setToolTipText("Cannot pause/resume plugin when scheduler is not running"); - } - - // Hard reset button is always enabled if there are plugins scheduled - boolean hasScheduledPlugins = !plugin.getScheduledPlugins().isEmpty(); - hardResetButton.setEnabled(hasScheduledPlugins); - hardResetButton.setToolTipText( - hasScheduledPlugins ? - "Hard reset all user condition states for scheduled plugins" : - "No plugins scheduled to reset"); - } - - /** - * Updates the scheduler status information - */ - private void updateSchedulerStatus() { - SchedulerState state = plugin.getCurrentState(); - // Update state information if available - String stateInfo = state.getStateInformation(); - if (stateInfo != null && !stateInfo.isEmpty()) { - statusLabel.setToolTipText(stateInfo); - } else { - statusLabel.setToolTipText(null); - } - // Update state display - statusLabel.setText(state.getDisplayName()); - statusLabel.setForeground(state.getColor()); - - // Update current plugin in status section - updateCurrentPluginInStatusSection(state); - - // Update runtime if active - if (plugin.getCurrentState().isSchedulerActive()) { - if (schedulerStartTime == null) { - schedulerStartTime = ZonedDateTime.now(); - } - - Duration runtime = Duration.between(schedulerStartTime, ZonedDateTime.now()); - long totalSeconds = runtime.getSeconds(); - long hours = totalSeconds / 3600; - long minutes = (totalSeconds % 3600) / 60; - long seconds = totalSeconds % 60; - - runtimeLabel.setText(String.format("%02d:%02d:%02d", hours, minutes, seconds)); - } else { - schedulerStartTime = null; - runtimeLabel.setText("00:00:00"); - } - } - - /** - * Updates the current plugin display in the status section - */ - private void updateCurrentPluginInStatusSection(SchedulerState state) { - PluginScheduleEntry currentPlugin = plugin.getCurrentPlugin(); - - // Only show current plugin name when in specific states - if (currentPlugin != null && (state.isPluginRunning() || state.isStopping() || state.isPaused())) { - String displayName = currentPlugin.getCleanName(); - - // Add pause indicator if plugin is paused - if (currentPlugin.isPaused()) { - displayName += " (PAUSED)"; - currentPluginInStatusLabel.setForeground(new Color(255, 152, 0)); // Orange - } else { - currentPluginInStatusLabel.setForeground(Color.WHITE); - } - - currentPluginInStatusLabel.setText(displayName); - } else { - currentPluginInStatusLabel.setText("None"); - currentPluginInStatusLabel.setForeground(Color.LIGHT_GRAY); - } - } - - /** - * Updates information about the currently running plugin - * NOTE: This method is commented out because current plugin display moved to status section - */ - /* - private void updateCurrentPluginInfo() { - PluginScheduleEntry currentPlugin = plugin.getCurrentPlugin(); - - if (currentPlugin != null && currentPlugin.isRunning()) { - // Update name with pause indicator if needed - String displayName = currentPlugin.getCleanName(); - String pauseTooltip = null; - - if (PluginPauseEvent.isPaused()) { - displayName += " [PAUSED]"; - currentPluginNameLabel.setForeground(new Color(255, 152, 0)); // Orange - - // Create detailed pause tooltip - pauseTooltip = createPauseTooltipForCurrentPlugin(currentPlugin); - } else { - currentPluginNameLabel.setForeground(Color.WHITE); - - // Check if any other plugins are paused and create tooltip - pauseTooltip = createPauseTooltipForAllPlugins(); - } - - currentPluginNameLabel.setText(displayName); - currentPluginNameLabel.setToolTipText(pauseTooltip); - - // Update runtime - if (currentPluginStartTime == null) { - currentPluginStartTime = ZonedDateTime.now(); - } - - Duration runtime = Duration.between(currentPluginStartTime, ZonedDateTime.now()); - long totalSeconds = runtime.getSeconds(); - long hours = totalSeconds / 3600; - long minutes = (totalSeconds % 3600) / 60; - long seconds = totalSeconds % 60; - - currentPluginRuntimeLabel.setText(String.format("%02d:%02d:%02d", hours, minutes, seconds)); - - // Update stop condition status - if (currentPlugin.hasAnyStopConditions()) { - int total = currentPlugin.getTotalStopConditionCount(); - int satisfied = currentPlugin.getSatisfiedStopConditionCount(); - - // Updates the stop condition status text - stopConditionStatusLabel.setText(String.format("%d/%d conditions met", satisfied, total)); - stopConditionStatusLabel.setToolTipText(currentPlugin.getDetailedStopConditionsStatus()); - - // Update progress bar - double progress = currentPlugin.getStopConditionProgress(); - stopConditionProgressBar.setValue((int) progress); - stopConditionProgressBar.setString(String.format("%.1f%%", progress)); - - // Color the progress bar based on progress - if (progress > 80) { - stopConditionProgressBar.setForeground(new Color(76, 175, 80)); // Green - } else if (progress > 50) { - stopConditionProgressBar.setForeground(new Color(255, 193, 7)); // Amber - } else { - stopConditionProgressBar.setForeground(new Color(33, 150, 243)); // Blue - } - - stopConditionProgressBar.setVisible(true); - } else { - stopConditionStatusLabel.setText("None"); - stopConditionStatusLabel.setToolTipText("No stop conditions defined"); - stopConditionProgressBar.setVisible(false); - } - } else { - // No current plugin - check for paused plugins and show in tooltip - String noneText = "None"; - String noneTooltip = null; - - if (plugin.anyPluginEntryPaused()) { - List pausedPlugins = plugin.getScheduledPlugins().stream() - .filter(PluginScheduleEntry::isPaused) - .collect(Collectors.toList()); - - if (!pausedPlugins.isEmpty()) { - noneText = "None (" + pausedPlugins.size() + " paused)"; - noneTooltip = createPauseTooltipForAllPlugins(); - } - } - - // Reset all fields with pause information - currentPluginNameLabel.setText(noneText); - currentPluginNameLabel.setToolTipText(noneTooltip); - currentPluginNameLabel.setForeground(plugin.anyPluginEntryPaused() ? new Color(255, 152, 0) : Color.WHITE); - currentPluginRuntimeLabel.setText("00:00:00"); - stopConditionStatusLabel.setText("None"); - stopConditionProgressBar.setValue(0); - stopConditionProgressBar.setString("No conditions"); - currentPluginStartTime = null; - } - } - */ - - /** - * Updates information about the next scheduled plugin - */ - private void updateNextUpComingPluginInfo() { - PluginScheduleEntry nextUpComingPlugin = plugin.getUpComingPlugin(); - - if (nextUpComingPlugin != null) { - // Update name - nextUpComingPluginNameLabel.setText(nextUpComingPlugin.getCleanName()); - - // Set the next run time display (already handles various condition types) - nextUpComingPluginTimeLabel.setText(nextUpComingPlugin.getNextRunDisplay()); - - // Create an enhanced schedule description - StringBuilder scheduleDesc = new StringBuilder(nextUpComingPlugin.getIntervalDisplay()); - - // Add information about one-time schedules - if (nextUpComingPlugin.hasAnyOneTimeStartConditions()) { - if (nextUpComingPlugin.hasTriggeredOneTimeStartConditions() && !nextUpComingPlugin.canStartTriggerAgain()) { - scheduleDesc.append(" (Completed)"); - } else { - scheduleDesc.append(" (One-time)"); - } - } - - nextUpComingPluginScheduleLabel.setText(scheduleDesc.toString()); - } else { - // Reset all fields - nextUpComingPluginNameLabel.setText("None"); - nextUpComingPluginTimeLabel.setText("--:--"); - nextUpComingPluginScheduleLabel.setText("None"); - } - } - - /** - * Updates information about the player's status - */ - private void updatePlayerStatusInfo() { - // Update activity info - Activity activity = plugin.getCurrentActivity(); - if (activity != null) { - activityLabel.setText(activity.toString()); - activityLabel.setForeground(Color.WHITE); - } else { - activityLabel.setText("None"); - activityLabel.setForeground(Color.GRAY); - } - - // Update activity intensity - ActivityIntensity intensity = plugin.getCurrentIntensity(); - if (intensity != null) { - activityIntensityLabel.setText(intensity.getName()); - activityIntensityLabel.setForeground(Color.WHITE); - } else { - activityIntensityLabel.setText("None"); - activityIntensityLabel.setForeground(Color.GRAY); - } - - // Update idle time - int idleTime = plugin.getIdleTime(); - if (idleTime >= 0) { - idleTimeLabel.setText(idleTime + " ticks"); - // Change color based on idle time - if (idleTime > 100) { - idleTimeLabel.setForeground(new Color(255, 106, 0)); // Orange for long idle - } else if (idleTime > 50) { - idleTimeLabel.setForeground(new Color(255, 193, 7)); // Yellow for medium idle - } else { - idleTimeLabel.setForeground(Color.WHITE); // Normal color - } - } else { - idleTimeLabel.setText("0 ticks"); - idleTimeLabel.setForeground(Color.WHITE); - } - - // Update login duration - Duration loginDuration = Microbot.getLoginTime(); - if (loginDuration.getSeconds() > 0 && Microbot.isLoggedIn()) { - long hours = loginDuration.toHours(); - long minutes = (loginDuration.toMinutes() % 60); - long seconds = (loginDuration.getSeconds() % 60); - loginTimeLabel.setText(String.format("%02d:%02d:%02d", hours, minutes, seconds)); - loginTimeLabel.setForeground(Color.WHITE); - } else { - loginTimeLabel.setText("Not logged in"); - loginTimeLabel.setForeground(Color.GRAY); - } - - // Update break status - boolean onBreak = plugin.isOnBreak(); - if (onBreak) { - breakStatusLabel.setText("On Break"); - breakStatusLabel.setForeground(new Color(255, 193, 7)); // Amber color - } else { - breakStatusLabel.setText("Not on break"); - breakStatusLabel.setForeground(Color.WHITE); - } - - // Update next break time - Duration timeUntilBreak = plugin.getTimeUntilNextBreak(); - if (timeUntilBreak.getSeconds() > 0) { - long hours = timeUntilBreak.toHours(); - long minutes = (timeUntilBreak.toMinutes() % 60); - long seconds = (timeUntilBreak.getSeconds() % 60); - nextBreakLabel.setText(String.format("%02d:%02d:%02d", hours, minutes, seconds)); - nextBreakLabel.setForeground(Color.WHITE); - } else { - nextBreakLabel.setText("--:--:--"); - nextBreakLabel.setForeground(Color.GRAY); - } - - // Update current break duration - Duration breakDuration = plugin.getCurrentBreakDuration(); - if (breakDuration.getSeconds() > 0) { - long hours = breakDuration.toHours(); - long minutes = (breakDuration.toMinutes() % 60); - long seconds = (breakDuration.getSeconds() % 60); - breakDurationLabel.setText(String.format("%02d:%02d:%02d", hours, minutes, seconds)); - breakDurationLabel.setForeground(onBreak ? new Color(255, 193, 7) : Color.WHITE); // Amber if on break - } else { - breakDurationLabel.setText("00:00:00"); - breakDurationLabel.setForeground(Color.GRAY); - } - } - - /** - * Updates information about the previously run plugin - */ - private void updatePreviousPluginInfo() { - PluginScheduleEntry lastPlugin = plugin.getLastPlugin(); - - if (lastPlugin != null) { - // Update name - prevPluginNameLabel.setText(lastPlugin.getCleanName()); - - // Update duration if available - if (lastPlugin.getLastRunDuration() != null && !lastPlugin.getLastRunDuration().isZero()) { - Duration duration = lastPlugin.getLastRunDuration(); - long hours = duration.toHours(); - long minutes = (duration.toMinutes() % 60); - long seconds = (duration.getSeconds() % 60); - prevPluginDurationLabel.setText(String.format("%02d:%02d:%02d", hours, minutes, seconds)); - } else { - prevPluginDurationLabel.setText("Unknown"); - } - - // Update stop reason - String stopReason = lastPlugin.getLastStopReason(); - PluginScheduleEntry.StopReason stopReasonType = lastPlugin.getLastStopReasonType(); - - if (stopReason != null && !stopReason.isEmpty()) { - // Define colors for different states - Color successColor = new Color(76, 175, 80); // Green for success - Color unsuccessfulColor = new Color(255, 152, 0); // Orange for unsuccessful - Color errorColor = new Color(244, 67, 54); // Red for error - Color defaultColor = Color.WHITE; // Default color - - // Determine message color based on stop reason type and success state - Color messageColor = defaultColor; - - if (stopReasonType != null) { - switch (stopReasonType) { - case PLUGIN_FINISHED: - messageColor = lastPlugin.isLastRunSuccessful() ? successColor : unsuccessfulColor; - break; - case ERROR: - messageColor = errorColor; - break; - case INTERRUPTED: - messageColor = unsuccessfulColor; - break; - default: - messageColor = defaultColor; - break; - } - } - - // Set the text and color for the status text area - prevPluginStatusLabel.setText(stopReason); - prevPluginStatusLabel.setForeground(messageColor); - } else { - prevPluginStatusLabel.setText("Unknown"); - prevPluginStatusLabel.setForeground(Color.WHITE); - } - - // Update stop time - if (lastPlugin.getLastRunEndTime() != null) { - ZonedDateTime stopTime = lastPlugin.getLastRunEndTime(); - prevPluginStopTimeLabel.setText(stopTime.format(DateTimeFormatter.ofPattern("HH:mm:ss"))); - } else { - prevPluginStopTimeLabel.setText("Unknown"); - } - } else { - // Reset all fields - prevPluginNameLabel.setText("None"); - prevPluginDurationLabel.setText("00:00:00"); - prevPluginStatusLabel.setText("N/A"); - prevPluginStatusLabel.setForeground(Color.WHITE); - prevPluginStopTimeLabel.setText("--:--:--"); - } - } - - /** - * Shows a confirmation dialog for hard resetting all user conditions - */ - private void showHardResetConfirmation() { - String message = - "" + - "

Hard Reset All User Conditions

" + - "

This will perform a complete reset of all user conditions for all scheduled plugins.

" + - "

This will reset:

" + - "
    " + - "
  • All accumulated state tracking variables
  • " + - "
  • Maximum trigger counters
  • " + - "
  • Daily/periodic usage limits
  • " + - "
  • Historical tracking data
  • " + - "
  • Time-based condition states
  • " + - "
" + - "

Are you sure you want to continue?

" + - ""; - - int result = JOptionPane.showConfirmDialog( - SwingUtilities.getWindowAncestor(this), - message, - "Hard Reset Confirmation", - JOptionPane.YES_NO_OPTION, - JOptionPane.WARNING_MESSAGE - ); - - if (result == JOptionPane.YES_OPTION) { - hardResetAllUserConditions(); - } - } - - /** - * Performs a hard reset on all user conditions for all scheduled plugins - */ - private void hardResetAllUserConditions() { - try { - // Delegate the hard reset operation to the SchedulerPlugin - List resetPlugins = plugin.hardResetAllUserConditions(); - - // Show success message with details - String resultMessage = String.format( - "" + - "

Hard Reset Complete

" + - "

Successfully reset %d user condition states.

", - resetPlugins.size()); - - if (!resetPlugins.isEmpty()) { - resultMessage += "

Reset conditions for:

    "; - for (String pluginName : resetPlugins) { - resultMessage += "
  • " + pluginName + "
  • "; - } - resultMessage += "
"; - } - - resultMessage += ""; - - JOptionPane.showMessageDialog( - SwingUtilities.getWindowAncestor(this), - resultMessage, - "Hard Reset Complete", - JOptionPane.INFORMATION_MESSAGE - ); - - log.info("Hard reset completed for {} user condition states", resetPlugins.size()); - - } catch (Exception e) { - // Show error message - JOptionPane.showMessageDialog( - SwingUtilities.getWindowAncestor(this), - "An error occurred while resetting user conditions: " + e.getMessage(), - "Hard Reset Error", - JOptionPane.ERROR_MESSAGE - ); - - log.error("Error during hard reset of user conditions", e); - } - } - - /** - * Updates only the time display for the next plugin without full refresh. - * This is used for regular time updates when the plugin hasn't changed. - * - * @param nextUpComingPlugin The next scheduled plugin - */ - private void updateNextUpComingPluginTimeDisplay(PluginScheduleEntry nextUpComingPlugin) { - if (nextUpComingPlugin != null) { - // Only update the time display, keep other fields unchanged - nextUpComingPluginTimeLabel.setText(nextUpComingPlugin.getNextRunDisplay()); - } - } - - /** - * Updates only the runtime display for the current plugin without refreshing other info. - * This is used for regular runtime updates when the plugin hasn't changed. - */ - private void updateCurrentPluginRuntimeOnly() { - PluginScheduleEntry currentPlugin = plugin.getCurrentPlugin(); - - if (currentPlugin != null && currentPlugin.isRunning()) { - // Update runtime - if (currentPluginStartTime != null) { - Duration runtime = Duration.between(currentPluginStartTime, ZonedDateTime.now()); - long totalSeconds = runtime.getSeconds(); - long hours = totalSeconds / 3600; - long minutes = (totalSeconds % 3600) / 60; - long seconds = totalSeconds % 60; - - currentPluginRuntimeLabel.setText(String.format("%02d:%02d:%02d", hours, minutes, seconds)); - } - - // Update stop condition progress (in case conditions have progressed) - if (currentPlugin.hasAnyStopConditions()) { - double progress = currentPlugin.getStopConditionProgress(); - stopConditionProgressBar.setValue((int) progress); - stopConditionProgressBar.setString(String.format("%.1f%%", progress)); - - // Color the progress bar based on progress - if (progress > 80) { - stopConditionProgressBar.setForeground(new Color(76, 175, 80)); // Green - } else if (progress > 50) { - stopConditionProgressBar.setForeground(new Color(255, 193, 7)); // Amber - } else { - stopConditionProgressBar.setForeground(new Color(33, 150, 243)); // Blue - } - } - } - } - - /** - * Creates an alternative tabbed layout for extremely small spaces - * This method provides a fallback when the regular layout doesn't fit - * @deprecated This method is no longer used. Use UIUtils methods instead. - */ - @Deprecated - @SuppressWarnings("unused") - private JPanel createTabbedPluginInfoPanelDeprecated() { - JPanel wrapperPanel = new JPanel(new BorderLayout()); - wrapperPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(1, 1, 1, 1, ColorScheme.DARK_GRAY_COLOR), - BorderFactory.createEmptyBorder(2, 2, 2, 2) - ), - "Plugin Information", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - wrapperPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Create tabbed pane for very small spaces - JTabbedPane tabbedPane = new JTabbedPane(); - tabbedPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - tabbedPane.setForeground(Color.WHITE); - tabbedPane.setFont(FontManager.getRunescapeSmallFont()); - - // Previous Plugin Tab - JPanel prevTab = new JPanel(new GridBagLayout()); - prevTab.setBackground(ColorScheme.DARKER_GRAY_COLOR); - GridBagConstraints gbc = UIUtils.createGbc(0, 0); - - prevTab.add(new JLabel("Name:"), gbc); - gbc.gridx++; - prevPluginNameLabel = UIUtils.createCompactValueLabel("None"); - prevTab.add(prevPluginNameLabel, gbc); - - gbc.gridx = 0; gbc.gridy++; - prevTab.add(new JLabel("Duration:"), gbc); - gbc.gridx++; - prevPluginDurationLabel = UIUtils.createCompactValueLabel("00:00:00"); - prevTab.add(prevPluginDurationLabel, gbc); - - gbc.gridx = 0; gbc.gridy++; - prevTab.add(new JLabel("Stop Time:"), gbc); - gbc.gridx++; - prevPluginStopTimeLabel = UIUtils.createCompactValueLabel("--:--:--"); - prevTab.add(prevPluginStopTimeLabel, gbc); - - gbc.gridx = 0; gbc.gridy++; - gbc.gridwidth = 2; - prevPluginStatusLabel = createMultiLineTextArea("None"); - prevPluginStatusLabel.setPreferredSize(new Dimension(0, 40)); - prevTab.add(prevPluginStatusLabel, gbc); - - // Current Plugin Tab - JPanel currentTab = new JPanel(new GridBagLayout()); - currentTab.setBackground(ColorScheme.DARKER_GRAY_COLOR); - gbc = UIUtils.createGbc(0, 0); - - currentTab.add(new JLabel("Name:"), gbc); - gbc.gridx++; - currentPluginNameLabel = UIUtils.createCompactValueLabel("None"); - currentTab.add(currentPluginNameLabel, gbc); - - gbc.gridx = 0; gbc.gridy++; - currentTab.add(new JLabel("Runtime:"), gbc); - gbc.gridx++; - currentPluginRuntimeLabel = UIUtils.createCompactValueLabel("00:00:00"); - currentTab.add(currentPluginRuntimeLabel, gbc); - - gbc.gridx = 0; gbc.gridy++; - currentTab.add(new JLabel("Conditions:"), gbc); - gbc.gridx++; - stopConditionStatusLabel = UIUtils.createCompactValueLabel("None"); - currentTab.add(stopConditionStatusLabel, gbc); - - gbc.gridx = 0; gbc.gridy++; - gbc.gridwidth = 2; - stopConditionProgressBar = new JProgressBar(0, 100); - stopConditionProgressBar.setStringPainted(true); - stopConditionProgressBar.setString("No conditions"); - stopConditionProgressBar.setForeground(new Color(76, 175, 80)); - stopConditionProgressBar.setBackground(ColorScheme.DARKER_GRAY_COLOR); - stopConditionProgressBar.setPreferredSize(new Dimension(0, 8)); - currentTab.add(stopConditionProgressBar, gbc); - - // Next Plugin Tab - JPanel nextTab = new JPanel(new GridBagLayout()); - nextTab.setBackground(ColorScheme.DARKER_GRAY_COLOR); - gbc = UIUtils.createGbc(0, 0); - - nextTab.add(new JLabel("Name:"), gbc); - gbc.gridx++; - nextUpComingPluginNameLabel = UIUtils.createCompactValueLabel("None"); - nextTab.add(nextUpComingPluginNameLabel, gbc); - - gbc.gridx = 0; gbc.gridy++; - nextTab.add(new JLabel("Time:"), gbc); - gbc.gridx++; - nextUpComingPluginTimeLabel = UIUtils.createCompactValueLabel("--:--"); - nextTab.add(nextUpComingPluginTimeLabel, gbc); - - gbc.gridx = 0; gbc.gridy++; - nextTab.add(new JLabel("Type:"), gbc); - gbc.gridx++; - nextUpComingPluginScheduleLabel = UIUtils.createCompactValueLabel("None"); - nextTab.add(nextUpComingPluginScheduleLabel, gbc); - - tabbedPane.addTab("Prev", prevTab); - tabbedPane.addTab("Current", currentTab); - tabbedPane.addTab("Next", nextTab); - - wrapperPanel.add(tabbedPane, BorderLayout.CENTER); - - return wrapperPanel; - } - - /** - * Creates a structured plugin info panel using BoxLayout for better vertical control - * This is an alternative to the FlowLayout approach if height issues persist - * @deprecated This method is no longer used. Use UIUtils methods instead. - */ - @Deprecated - @SuppressWarnings("unused") - private JPanel createStructuredPluginInfoPanelDeprecated() { - JPanel wrapperPanel = new JPanel(new BorderLayout()); - wrapperPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(1, 1, 1, 1, ColorScheme.DARK_GRAY_COLOR), - BorderFactory.createEmptyBorder(5, 5, 5, 5) - ), - "Plugin Information", - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - wrapperPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Create main content panel with BoxLayout for better vertical control - JPanel contentPanel = new JPanel(); - contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); - contentPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Create sections panel using a more structured approach - JPanel sectionsPanel = new JPanel(new BorderLayout()); - sectionsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Create individual section panels with fixed heights - JPanel prevSection = createStructuredSection("Previous"); - JPanel currentSection = createStructuredSection("Current"); - JPanel nextSection = createStructuredSection("Next"); - - // Add content to sections - addPreviousPluginContent(prevSection); - addCurrentPluginContent(currentSection); - addNextPluginContent(nextSection); - - // Use a horizontal layout with equal weights - sectionsPanel.add(prevSection, BorderLayout.WEST); - sectionsPanel.add(currentSection, BorderLayout.CENTER); - sectionsPanel.add(nextSection, BorderLayout.EAST); - - contentPanel.add(sectionsPanel); - contentPanel.add(Box.createRigidArea(new Dimension(0, 5))); - - // Add progress panel - JPanel progressPanel = createProgressPanel(); - contentPanel.add(progressPanel); - contentPanel.add(Box.createRigidArea(new Dimension(0, 5))); - - // Add stop reason panel - JPanel stopReasonPanel = createStopReasonPanel(); - contentPanel.add(stopReasonPanel); - - // Wrap in scroll pane - JScrollPane scrollPane = new JScrollPane(contentPanel); - scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); - scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); - scrollPane.setBorder(null); - scrollPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - scrollPane.getViewport().setBackground(ColorScheme.DARKER_GRAY_COLOR); - - wrapperPanel.add(scrollPane, BorderLayout.CENTER); - return wrapperPanel; - } - - /** - * Creates a structured section with fixed height and proper spacing - */ - private JPanel createStructuredSection(String title) { - JPanel panel = new JPanel(); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - panel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(ColorScheme.DARK_GRAY_COLOR), - BorderFactory.createEmptyBorder(8, 6, 8, 6) - ), - title, - TitledBorder.CENTER, - TitledBorder.TOP, - FontManager.getRunescapeSmallFont(), - Color.LIGHT_GRAY - )); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - panel.setPreferredSize(new Dimension(140, 180)); // Fixed height - panel.setMinimumSize(new Dimension(120, 160)); - panel.setMaximumSize(new Dimension(160, 200)); - return panel; - } - - /** - * Adds content to the previous plugin section - */ - private void addPreviousPluginContent(JPanel section) { - section.add(UIUtils.createLabelValueRow("Name:", prevPluginNameLabel = UIUtils.createCompactValueLabel("None"))); - section.add(Box.createRigidArea(new Dimension(0, 5))); - section.add(UIUtils.createLabelValueRow("Duration:", prevPluginDurationLabel = UIUtils.createCompactValueLabel("00:00:00"))); - section.add(Box.createRigidArea(new Dimension(0, 5))); - section.add(UIUtils.createLabelValueRow("Stop Time:", prevPluginStopTimeLabel = UIUtils.createCompactValueLabel("--:--:--"))); - section.add(Box.createVerticalGlue()); - } - - /** - * Adds content to the current plugin section - */ - private void addCurrentPluginContent(JPanel section) { - section.add(UIUtils.createLabelValueRow("Name:", currentPluginNameLabel = UIUtils.createCompactValueLabel("None"))); - section.add(Box.createRigidArea(new Dimension(0, 5))); - section.add(UIUtils.createLabelValueRow("Runtime:", currentPluginRuntimeLabel = UIUtils.createCompactValueLabel("00:00:00"))); - section.add(Box.createRigidArea(new Dimension(0, 5))); - section.add(UIUtils.createLabelValueRow("Conditions:", stopConditionStatusLabel = UIUtils.createCompactValueLabel("None"))); - section.add(Box.createVerticalGlue()); - } - - /** - * Adds content to the next plugin section - */ - private void addNextPluginContent(JPanel section) { - section.add(UIUtils.createLabelValueRow("Name:", nextUpComingPluginNameLabel = UIUtils.createCompactValueLabel("None"))); - section.add(Box.createRigidArea(new Dimension(0, 5))); - section.add(UIUtils.createLabelValueRow("Time:", nextUpComingPluginTimeLabel = UIUtils.createCompactValueLabel("--:--"))); - section.add(Box.createRigidArea(new Dimension(0, 5))); - section.add(UIUtils.createLabelValueRow("Type:", nextUpComingPluginScheduleLabel = UIUtils.createCompactValueLabel("None"))); - section.add(Box.createVerticalGlue()); - } - - /** - * Creates the progress panel - */ - private JPanel createProgressPanel() { - JPanel panel = new JPanel(new BorderLayout()); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - panel.setBorder(new EmptyBorder(2, 0, 2, 0)); - panel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 20)); - - JLabel label = new JLabel("Progress:"); - label.setForeground(Color.WHITE); - label.setFont(FontManager.getRunescapeSmallFont()); - - stopConditionProgressBar = new JProgressBar(0, 100); - stopConditionProgressBar.setStringPainted(true); - stopConditionProgressBar.setString("No conditions"); - stopConditionProgressBar.setForeground(new Color(76, 175, 80)); - stopConditionProgressBar.setBackground(ColorScheme.DARKER_GRAY_COLOR); - stopConditionProgressBar.setPreferredSize(new Dimension(0, 12)); - - panel.add(label, BorderLayout.WEST); - panel.add(stopConditionProgressBar, BorderLayout.CENTER); - - return panel; - } - - /** - * Creates the stop reason panel - */ - private JPanel createStopReasonPanel() { - JPanel panel = new JPanel(new BorderLayout()); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - panel.setBorder(new EmptyBorder(2, 0, 0, 0)); - panel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 50)); - - JLabel label = new JLabel("Stop Reason:"); - label.setForeground(Color.WHITE); - label.setFont(FontManager.getRunescapeSmallFont()); - - prevPluginStatusLabel = createMultiLineTextArea("None"); - prevPluginStatusLabel.setPreferredSize(new Dimension(0, 40)); - - panel.add(label, BorderLayout.WEST); - panel.add(prevPluginStatusLabel, BorderLayout.CENTER); - - return panel; - } - - /** - * Creates a detailed tooltip for the current plugin pause status - */ - private String createPauseTooltipForCurrentPlugin(PluginScheduleEntry currentPlugin) { - StringBuilder tooltip = new StringBuilder(""); - tooltip.append("Current Plugin Paused:
"); - tooltip.append("Name: ").append(currentPlugin.getName()).append("
"); - tooltip.append("Priority: ").append(currentPlugin.getPriority()).append("
"); - tooltip.append("Enabled: ").append(currentPlugin.isEnabled() ? "Yes" : "No").append("
"); - tooltip.append("Running: ").append(currentPlugin.isRunning() ? "Yes" : "No").append("
"); - - if (currentPlugin.getLastRunStartTime() != null) { - Duration runtime = Duration.between(currentPlugin.getLastRunStartTime(), ZonedDateTime.now()); - tooltip.append("Runtime: ").append(formatDurationForTooltip(runtime)).append("
"); - } - - // Check for other paused plugins - List pausedPlugins = plugin.getScheduledPlugins().stream() - .filter(p -> p.isPaused() && !p.equals(currentPlugin)) - .collect(Collectors.toList()); - - if (!pausedPlugins.isEmpty()) { - tooltip.append("
Other Paused Plugins:
"); - for (PluginScheduleEntry pausedPlugin : pausedPlugins) { - tooltip.append("â€Ē ").append(pausedPlugin.getName()) - .append(" (Priority: ").append(pausedPlugin.getPriority()).append(")
"); - } - } - - tooltip.append(""); - return tooltip.toString(); - } - - /** - * Creates a tooltip showing all paused plugins when no plugin is currently running paused - */ - private String createPauseTooltipForAllPlugins() { - List pausedPlugins = plugin.getScheduledPlugins().stream() - .filter(PluginScheduleEntry::isPaused) - .collect(Collectors.toList()); - - if (pausedPlugins.isEmpty()) { - return null; // No tooltip needed - } - - StringBuilder tooltip = new StringBuilder(""); - - if (plugin.isPaused()) { - tooltip.append("Scheduler Paused
"); - } - - if (plugin.anyPluginEntryPaused()) { - tooltip.append("Paused Plugins (").append(pausedPlugins.size()).append("):
"); - for (PluginScheduleEntry pausedPlugin : pausedPlugins) { - tooltip.append("â€Ē ").append(pausedPlugin.getName()) - .append(" (Priority: ").append(pausedPlugin.getPriority()) - .append(", Enabled: ").append(pausedPlugin.isEnabled() ? "Yes" : "No") - .append(")
"); - } - } - - tooltip.append(""); - return tooltip.toString(); - } - - /** - * Formats duration for tooltip display - */ - private String formatDurationForTooltip(Duration duration) { - if (duration.isZero() || duration.isNegative()) { - return "00:00:00"; - } - - long hours = duration.toHours(); - long minutes = duration.toMinutesPart(); - long seconds = duration.toSecondsPart(); - - return String.format("%02d:%02d:%02d", hours, minutes, seconds); - } - - /** - * Updates the pre/post schedule tasks information panel - */ - private void updatePrePostTasksInfo() { - PluginScheduleEntry currentPlugin = plugin.getCurrentPlugin(); - - if (currentPlugin != null) { - // Get the schedulable plugin interface - net.runelite.client.plugins.Plugin pluginInstance = currentPlugin.getPlugin(); - - if (pluginInstance instanceof SchedulablePlugin) { - SchedulablePlugin schedulablePlugin = (SchedulablePlugin) pluginInstance; - - // Update the pre/post tasks panel with the current plugin - prePostTasksInfoPanel.updatePlugin(schedulablePlugin); - } else { - // Plugin doesn't implement SchedulablePlugin, clear the panel - prePostTasksInfoPanel.clear(); - } - } else { - // No current plugin, clear the panel - prePostTasksInfoPanel.clear(); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerPanel.java deleted file mode 100644 index 1c7992021d7..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerPanel.java +++ /dev/null @@ -1,792 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui; -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerPlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerState; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.util.events.PluginPauseEvent; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; -import net.runelite.client.ui.PluginPanel; - -import javax.swing.*; -import javax.swing.border.CompoundBorder; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.time.Duration; -import java.time.ZonedDateTime; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -public class SchedulerPanel extends PluginPanel { - private final SchedulerPlugin plugin; - - // Current plugin section - private final JLabel currentPluginLabel; - private final JLabel runtimeLabel; - - // Previous plugin section - private final JLabel prevPluginNameLabel; - private final JLabel prevPluginDurationLabel; - private final JLabel prevPluginStopReasonLabel; - private final JLabel prevPluginStopTimeLabel; - - // Next plugin section - private final JLabel nextUpComingPluginNameLabel; - private final JLabel nextUpComingPluginTimeLabel; - private final JLabel nextUpComingPluginScheduleLabel; - - // Scheduler status section - private final JLabel schedulerStatusLabel; - // Control buttons - private final JButton configButton; - private final JButton runButton; - private final JButton stopButton; - private final JButton pauseSchedulerButton; - private final JButton pauseResumePluginButton; - private final JButton antibanButton; - - // State tracking for optimized updates - private PluginScheduleEntry lastTrackedCurrentPlugin; - private PluginScheduleEntry lastTrackedNextUpComingPlugin; - private SchedulerState lastTrackedState; - - - public SchedulerPanel(SchedulerPlugin plugin) { - super(false); - this.plugin = plugin; - - setBorder(new EmptyBorder(8, 8, 8, 8)); - setBackground(ColorScheme.DARK_GRAY_COLOR); - setLayout(new BorderLayout()); - - // Create main panel - JPanel mainPanel = new JPanel(); - mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); - mainPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); - - // Current plugin info panel - JPanel infoPanel = createInfoPanel("Current Plugin"); - - JLabel pluginLabel = new JLabel("Plugin:"); - pluginLabel.setForeground(Color.WHITE); - pluginLabel.setFont(FontManager.getRunescapeFont()); - infoPanel.add(pluginLabel, createGbc(0, 0)); - - currentPluginLabel = createValueLabel("None"); - infoPanel.add(currentPluginLabel, createGbc(1, 0)); - - JLabel runtimeTitleLabel = new JLabel("Runtime:"); - runtimeTitleLabel.setForeground(Color.WHITE); - runtimeTitleLabel.setFont(FontManager.getRunescapeFont()); - infoPanel.add(runtimeTitleLabel, createGbc(0, 1)); - - runtimeLabel = createValueLabel("00:00:00"); - infoPanel.add(runtimeLabel, createGbc(1, 1)); - - // Previous plugin info panel - JPanel prevPluginPanel = createInfoPanel("Previous Plugin"); - JLabel prevPluginTitleLabel = new JLabel("Plugin:"); - prevPluginTitleLabel.setForeground(Color.WHITE); - prevPluginTitleLabel.setFont(FontManager.getRunescapeFont()); - prevPluginPanel.add(prevPluginTitleLabel, createGbc(0, 0)); - - prevPluginNameLabel = createValueLabel("None"); - prevPluginPanel.add(prevPluginNameLabel, createGbc(1, 0)); - - JLabel prevDurationLabel = new JLabel("Duration:"); - prevDurationLabel.setForeground(Color.WHITE); - prevDurationLabel.setFont(FontManager.getRunescapeFont()); - prevPluginPanel.add(prevDurationLabel, createGbc(0, 1)); - - prevPluginDurationLabel = createValueLabel("00:00:00"); - prevPluginPanel.add(prevPluginDurationLabel, createGbc(1, 1)); - - JLabel prevStopReasonLabel = new JLabel("Stop Reason:"); - prevStopReasonLabel.setForeground(Color.WHITE); - prevStopReasonLabel.setFont(FontManager.getRunescapeFont()); - prevPluginPanel.add(prevStopReasonLabel, createGbc(0, 2)); - - prevPluginStopReasonLabel = createValueLabel("None"); - prevPluginPanel.add(prevPluginStopReasonLabel, createGbc(1, 2)); - - JLabel prevStopTimeLabel = new JLabel("Stop Time:"); - prevStopTimeLabel.setForeground(Color.WHITE); - prevStopTimeLabel.setFont(FontManager.getRunescapeFont()); - prevPluginPanel.add(prevStopTimeLabel, createGbc(0, 3)); - - prevPluginStopTimeLabel = createValueLabel("--:--"); - prevPluginPanel.add(prevPluginStopTimeLabel, createGbc(1, 3)); - - // Next plugin info panel - JPanel nextUpComingPluginPanel = createInfoPanel("Next Scheduled Plugin"); - JLabel nextUpComingPluginTitleLabel = new JLabel("Plugin:"); - nextUpComingPluginTitleLabel.setForeground(Color.WHITE); - nextUpComingPluginTitleLabel.setFont(FontManager.getRunescapeFont()); - nextUpComingPluginPanel.add(nextUpComingPluginTitleLabel, createGbc(0, 0)); - - nextUpComingPluginNameLabel = createValueLabel("None"); - nextUpComingPluginPanel.add(nextUpComingPluginNameLabel, createGbc(1, 0)); - - JLabel nextRunLabel = new JLabel("Next Run:"); - nextRunLabel.setForeground(Color.WHITE); - nextRunLabel.setFont(FontManager.getRunescapeFont()); - nextUpComingPluginPanel.add(nextRunLabel, createGbc(0, 1)); - - nextUpComingPluginTimeLabel = createValueLabel("--:--"); - nextUpComingPluginPanel.add(nextUpComingPluginTimeLabel, createGbc(1, 1)); - - JLabel scheduleLabel = new JLabel("Schedule:"); - scheduleLabel.setForeground(Color.WHITE); - scheduleLabel.setFont(FontManager.getRunescapeFont()); - nextUpComingPluginPanel.add(scheduleLabel, createGbc(0, 2)); - - nextUpComingPluginScheduleLabel = createValueLabel("None"); - nextUpComingPluginPanel.add(nextUpComingPluginScheduleLabel, createGbc(1, 2)); - - // Scheduler status panel - JPanel statusPanel = createInfoPanel("Scheduler Status"); - JLabel statusLabel = new JLabel("Status:"); - statusLabel.setForeground(Color.WHITE); - statusLabel.setFont(FontManager.getRunescapeFont()); - statusPanel.add(statusLabel, createGbc(0, 0)); - - schedulerStatusLabel = createValueLabel("Inactive"); - schedulerStatusLabel.setForeground(Color.YELLOW); - statusPanel.add(schedulerStatusLabel, createGbc(1, 0)); - - // Button panel - vertical layout (one button per row) - JPanel buttonPanel = new JPanel(new GridLayout(6, 1, 0, 5)); // Changed to 6 rows for all buttons - buttonPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); - - // Add config button - JButton configButton = createButton("Open Scheduler"); - configButton.addActionListener(this::onOpenConfigButtonClicked); - this.configButton = configButton; - - // Control buttons - Color greenColor = new Color(76, 175, 80); - JButton runButton = createButton("Run Scheduler", greenColor); - runButton.addActionListener(e -> { - plugin.startScheduler(); - refresh(); - }); - this.runButton = runButton; - - Color redColor = new Color(244, 67, 54); - JButton stopButton = createButton("Stop Scheduler", redColor); - stopButton.addActionListener(e -> { - plugin.stopScheduler(); - refresh(); - }); - this.stopButton = stopButton; - - // Add Antiban button - uses a distinct purple color - Color purpleColor = new Color(156, 39, 176); - JButton antibanButton = createButton("Antiban Settings", purpleColor); - antibanButton.addActionListener(this::onAntibanButtonClicked); - antibanButton.setToolTipText("Open Antiban settings in a separate window"); - this.antibanButton = antibanButton; - - - // Add pause/resume button - uses orange color - Color orangeColor = new Color(255, 152, 0); - JButton pauseSchedulerButton = createButton("Pause Scheduler", orangeColor); - pauseSchedulerButton.addActionListener(e -> { - if (plugin.isPaused()) { - plugin.resumeScheduler(); - pauseSchedulerButton.setText("Pause Scheduler"); - pauseSchedulerButton.setBackground(orangeColor); - } else { - plugin.pauseScheduler(); - pauseSchedulerButton.setText("Resume Scheduler"); - pauseSchedulerButton.setBackground(greenColor); - } - refresh(); - }); - pauseSchedulerButton.setToolTipText("Pause or resume the scheduler without stopping it"); - this.pauseSchedulerButton = pauseSchedulerButton; - - // Add pause/resume button for the currently running plugin - use cyan color - Color cyanColor = new Color(0, 188, 212); // Material design cyan color - JButton pauseResumePluginButton = createButton("Pause Plugin", cyanColor); - pauseResumePluginButton.addActionListener(e -> { - // Toggle the pause state - boolean newPauseState = !PluginPauseEvent.isPaused(); - PluginPauseEvent.setPaused(newPauseState); - - // Update button text and color based on state - if (newPauseState) { - plugin.pauseRunningPlugin(); - pauseResumePluginButton.setText("Resume Plugin"); - pauseResumePluginButton.setBackground(greenColor); // Change to green for resume - } else { - plugin.resumeRunningPlugin(); - pauseResumePluginButton.setText("Pause Plugin"); - pauseResumePluginButton.setBackground(cyanColor); // Change back to cyan for pause - } - refresh(); - }); - pauseResumePluginButton.setToolTipText("Pause or resume the currently running plugin"); - this.pauseResumePluginButton = pauseResumePluginButton; - - buttonPanel.add(configButton); - buttonPanel.add(runButton); - buttonPanel.add(stopButton); - buttonPanel.add(pauseSchedulerButton); - buttonPanel.add(pauseResumePluginButton); - buttonPanel.add(antibanButton); - - - // Add components to main panel - mainPanel.add(infoPanel); - mainPanel.add(Box.createRigidArea(new Dimension(0, 8))); - mainPanel.add(prevPluginPanel); - mainPanel.add(Box.createRigidArea(new Dimension(0, 8))); - mainPanel.add(nextUpComingPluginPanel); - mainPanel.add(Box.createRigidArea(new Dimension(0, 8))); - mainPanel.add(statusPanel); - mainPanel.add(Box.createRigidArea(new Dimension(0, 8))); - mainPanel.add(buttonPanel); - mainPanel.add(Box.createRigidArea(new Dimension(0, 8))); - - // Wrap main panel in scroll pane for better fit in different sidebar sizes - JScrollPane scrollPane = new JScrollPane(mainPanel); - scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - scrollPane.setBorder(BorderFactory.createEmptyBorder()); - scrollPane.setBackground(ColorScheme.DARK_GRAY_COLOR); - scrollPane.getViewport().setBackground(ColorScheme.DARK_GRAY_COLOR); - - // Set the preferred width to maintain proper sidebar display - scrollPane.setPreferredSize(new Dimension(200, 400)); - - add(scrollPane, BorderLayout.CENTER); // Changed from NORTH to CENTER for proper filling - refresh(); - } - - private GridBagConstraints createGbc(int x, int y) { - GridBagConstraints gbc = new GridBagConstraints(); - gbc.gridx = x; - gbc.gridy = y; - gbc.gridwidth = 1; - gbc.gridheight = 1; - gbc.insets = new Insets(5, 5, 5, 5); - - gbc.anchor = (x == 0) ? GridBagConstraints.WEST : GridBagConstraints.EAST; - gbc.fill = (x == 0) ? GridBagConstraints.BOTH - : GridBagConstraints.HORIZONTAL; - - gbc.weightx = (x == 0) ? 0.1 : 1.0; - gbc.weighty = 1.0; - return gbc; - } - - private JPanel createInfoPanel(String title) { - JPanel panel = new JPanel(new GridBagLayout()); - - panel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(1, 1, 1, 1, ColorScheme.MEDIUM_GRAY_COLOR), - BorderFactory.createEmptyBorder(5, 5, 5, 5) - ), - title, - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - return panel; - } - - private JLabel createValueLabel(String text) { - JLabel label = new JLabel(text); - label.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - label.setFont(FontManager.getRunescapeFont()); - return label; - } - - private JButton createButton(String text) { - return createButton(text, ColorScheme.BRAND_ORANGE); - } - - private JButton createButton(String text, Color backgroundColor) { - JButton button = new JButton(text); - button.setFont(FontManager.getRunescapeSmallFont()); - button.setFocusPainted(false); - button.setForeground(Color.WHITE); - button.setBackground(backgroundColor); - button.setBorder(new CompoundBorder( - BorderFactory.createLineBorder(backgroundColor.darker(), 1), - BorderFactory.createEmptyBorder(5, 15, 5, 15) - )); - - // Add hover effect that maintains the button's color theme - button.addMouseListener(new java.awt.event.MouseAdapter() { - public void mouseEntered(java.awt.event.MouseEvent evt) { - button.setBackground(backgroundColor.brighter()); - } - - public void mouseExited(java.awt.event.MouseEvent evt) { - button.setBackground(backgroundColor); - } - }); - - return button; - } - - public void refresh() { - // Get current state information - SchedulerState currentState = plugin.getCurrentState(); - PluginScheduleEntry currentPlugin = plugin.getCurrentPlugin(); - PluginScheduleEntry nextUpComingPlugin = plugin.getUpComingPlugin(); - - // Update current plugin info if it changed - if (currentPlugin != lastTrackedCurrentPlugin) { - updatePluginInfo(); - lastTrackedCurrentPlugin = currentPlugin; - } else if (currentPlugin != null && currentPlugin.isRunning()) { - // Update only the runtime display without full refresh when plugin is running - updateCurrentPluginRuntimeOnly(); - } - - // Update next plugin info if it changed - if (nextUpComingPlugin != lastTrackedNextUpComingPlugin) { - updateNextPluginInfo(); - lastTrackedNextUpComingPlugin = nextUpComingPlugin; - } else if (nextUpComingPlugin != null && nextUpComingPlugin.isEnabled()) { - // Update only the time display without full refresh for real-time countdown - updateNextPluginTimeDisplayOnly(nextUpComingPlugin); - } - - // Update scheduler status and buttons if state changed - if (currentState != lastTrackedState) { - updateButtonStates(); - - // Update scheduler status with pause information - String statusText = currentState.getDisplayName(); - String statusTooltip = currentState.getDescription(); - - // Add pause indicator to status if any plugins are paused - if (plugin.anyPluginEntryPaused()) { - List pausedPlugins = plugin.getScheduledPlugins().stream() - .filter(PluginScheduleEntry::isPaused) - .collect(Collectors.toList()); - - if (!pausedPlugins.isEmpty()) { - statusText += " (" + pausedPlugins.size() + " paused)"; - statusTooltip = createAllPausedPluginsTooltip(); - } - } - - schedulerStatusLabel.setText(statusText); - schedulerStatusLabel.setForeground(currentState.getColor()); - schedulerStatusLabel.setToolTipText(statusTooltip); - - lastTrackedState = currentState; - } - } - - /** - * Updates button states based on plugin initialization status - */ - private void updateButtonStates() { - - - SchedulerState state = plugin.getCurrentState(); - boolean schedulerActive = plugin.getCurrentState().isSchedulerActive(); - boolean pluginRunning = plugin.getCurrentState().isPluginRunning(); - configButton.setEnabled(state != SchedulerState.UNINITIALIZED || state != SchedulerState.ERROR || state != SchedulerState.INITIALIZING); - - // Only enable run button if we're in READY or HOLD state - runButton.setEnabled(!schedulerActive && (state != SchedulerState.UNINITIALIZED || state != SchedulerState.ERROR || state != SchedulerState.INITIALIZING)); - - // Only enable stop button in certain states - stopButton.setEnabled(schedulerActive); - - // Update pause scheduler button state - pauseSchedulerButton.setEnabled(schedulerActive|| state == SchedulerState.SCHEDULER_PAUSED); - if (plugin.isPaused()) { - pauseSchedulerButton.setText("Resume Scheduler"); - pauseSchedulerButton.setBackground(new Color(76, 175, 80)); // Green color - pauseSchedulerButton.setToolTipText("Resume the paused scheduler"); - } else { - pauseSchedulerButton.setText("Pause Scheduler"); - pauseSchedulerButton.setBackground(new Color(255, 152, 0)); // Orange color - pauseSchedulerButton.setToolTipText("Pause the scheduler without stopping it"); - } - - // Update pause plugin button state - only visible and enabled when a plugin is running - boolean pluginCanBePaused = state == SchedulerState.RUNNING_PLUGIN || - state == SchedulerState.RUNNING_PLUGIN_PAUSED; - pauseResumePluginButton.setVisible(pluginCanBePaused); - pauseResumePluginButton.setEnabled(pluginCanBePaused); - - // Update button text and color based on plugin pause state - if (PluginPauseEvent.isPaused()) { - pauseResumePluginButton.setText("Resume Plugin"); - pauseResumePluginButton.setBackground(new Color(76, 175, 80)); // Green color - pauseResumePluginButton.setToolTipText("Resume the paused plugin"); - } else { - pauseResumePluginButton.setText("Pause Plugin"); - pauseResumePluginButton.setBackground(new Color(0, 188, 212)); // Cyan color - pauseResumePluginButton.setToolTipText("Pause the currently running plugin"); - } - - // Only enable antiban button when no plugin is running - antibanButton.setEnabled(!pluginRunning); - - - - // Add tooltips - if (state == SchedulerState.UNINITIALIZED || state == SchedulerState.ERROR || state == SchedulerState.INITIALIZING) { - configButton.setToolTipText("Plugin not initialized yet"); - runButton.setToolTipText("Plugin not initialized yet"); - stopButton.setToolTipText("Plugin not initialized yet"); - pauseSchedulerButton.setToolTipText("Plugin not initialized yet"); - } else { - configButton.setToolTipText("Open scheduler configuration"); - runButton.setToolTipText(!runButton.isEnabled() ? - "Cannot start scheduler in " + state.getDisplayName() + " state" : - "Start the scheduler"); - stopButton.setToolTipText(!stopButton.isEnabled() ? - "Cannot stop scheduler: not running" : - "Stop the scheduler"); - } - - - } - - void updatePluginInfo() { - PluginScheduleEntry currentPlugin = plugin.getCurrentPlugin(); - - if (currentPlugin != null) { - // Get start time for runtime calculation - ZonedDateTime startTimeZdt = currentPlugin.getLastRunStartTime(); - String pluginName = currentPlugin.getCleanName(); - - // Add pause indicator if current plugin is paused - if (currentPlugin.isRunning() && PluginPauseEvent.isPaused()) { - pluginName += " [PAUSED]"; - currentPluginLabel.setForeground(new Color(255, 152, 0)); // Orange - } else { - currentPluginLabel.setForeground(Color.WHITE); - } - - // Add the stop reason indicator to the plugin name if available - if (currentPlugin.getLastStopReasonType() != null && - currentPlugin.getLastStopReasonType() != PluginScheduleEntry.StopReason.NONE) { - String stopReason = formatStopReason(currentPlugin.getLastStopReasonType()); - pluginName += " (" + stopReason + ")"; - } - - currentPluginLabel.setText(pluginName); - - // Create and set tooltip with pause information - String pauseTooltip = createPauseTooltipForCurrentPlugin(currentPlugin); - currentPluginLabel.setToolTipText(pauseTooltip); - - // Show runtime - either current or last run duration - if (currentPlugin.isRunning()) { - // Calculate and display current runtime for active plugin - if (startTimeZdt != null) { - long startTimeMillis = startTimeZdt.toInstant().toEpochMilli(); - long runtimeMillis = System.currentTimeMillis() - startTimeMillis; - runtimeLabel.setText(formatDuration(runtimeMillis)); - } else { - runtimeLabel.setText("Running"); - } - } else if (currentPlugin.getLastRunDuration() != null && !currentPlugin.getLastRunDuration().isZero()) { - // Show the stored last run duration for completed plugins - runtimeLabel.setText(formatDuration(currentPlugin.getLastRunDuration().toMillis())); - } else { - runtimeLabel.setText("Not started"); - } - - // Update previous plugin information if it has run at least once - updatePreviousPluginInfo(currentPlugin); - } else { - // No current plugin - check for paused plugins and show in tooltip - String noneText = "None"; - String noneTooltip = null; - - if (plugin.anyPluginEntryPaused()) { - List pausedPlugins = plugin.getScheduledPlugins().stream() - .filter(PluginScheduleEntry::isPaused) - .collect(Collectors.toList()); - - if (!pausedPlugins.isEmpty()) { - noneText = "None (" + pausedPlugins.size() + " paused)"; - noneTooltip = createAllPausedPluginsTooltip(); - } - } - - currentPluginLabel.setText(noneText); - currentPluginLabel.setToolTipText(noneTooltip); - currentPluginLabel.setForeground(plugin.anyPluginEntryPaused() ? new Color(255, 152, 0) : Color.WHITE); - runtimeLabel.setText("00:00:00"); - - // Clear previous plugin info when there's no current plugin - prevPluginNameLabel.setText("None"); - prevPluginDurationLabel.setText("00:00:00"); - prevPluginStopReasonLabel.setText("None"); - prevPluginStopTimeLabel.setText("--:--"); - } - } - - /** - * Updates information about the previously run plugin - */ - private void updatePreviousPluginInfo(PluginScheduleEntry plugin) { - if (plugin == null) return; - - // Only show previous plugin info if the plugin has been run at least once - if (plugin.getLastRunEndTime() != null && plugin.getLastRunDuration() != null) { - // Set name - prevPluginNameLabel.setText(plugin.getCleanName()); - - // Set duration - long durationMillis = plugin.getLastRunDuration().toMillis(); - prevPluginDurationLabel.setText(formatDuration(durationMillis)); - - // Set stop reason - String stopReason = "None"; - if (plugin.getLastStopReasonType() != null && - plugin.getLastStopReasonType() != PluginScheduleEntry.StopReason.NONE) { - stopReason = formatStopReason(plugin.getLastStopReasonType()); - } - prevPluginStopReasonLabel.setText(stopReason); - - // Set stop time - ZonedDateTime endTime = plugin.getLastRunEndTime(); - if (endTime != null) { - prevPluginStopTimeLabel.setText( - endTime.format(PluginScheduleEntry.TIME_FORMATTER) - ); - } else { - prevPluginStopTimeLabel.setText("--:--"); - } - } - } - - /** - * Formats a duration in milliseconds as HH:MM:SS - */ - private String formatDuration(long durationMillis) { - long hours = TimeUnit.MILLISECONDS.toHours(durationMillis); - long minutes = TimeUnit.MILLISECONDS.toMinutes(durationMillis) % 60; - long seconds = TimeUnit.MILLISECONDS.toSeconds(durationMillis) % 60; - return String.format("%02d:%02d:%02d", hours, minutes, seconds); - } - - /** - * Returns a formatted stop reason - */ - private String formatStopReason(PluginScheduleEntry.StopReason stopReason) { - // Use the description from the enum if available - if (stopReason != null) { - switch (stopReason) { - case MANUAL_STOP: - return "Stopped"; - case PLUGIN_FINISHED: - return "Completed"; - case ERROR: - return "Error"; - case SCHEDULED_STOP: - return "Timed out"; - case INTERRUPTED: - return "Interrupted"; - case HARD_STOP: - return "Force stopped"; - default: - return stopReason.getDescription(); - } - } - return ""; - } - - void updateNextPluginInfo() { - PluginScheduleEntry nextUpComingPlugin = plugin.getUpComingPlugin(); - - if (nextUpComingPlugin != null) { - // Set the plugin name - nextUpComingPluginNameLabel.setText(nextUpComingPlugin.getCleanName()); - - // Set the next run time display (already handles various condition types) - nextUpComingPluginTimeLabel.setText(nextUpComingPlugin.getNextRunDisplay()); - - // Create an enhanced schedule description - StringBuilder scheduleDesc = new StringBuilder(nextUpComingPlugin.getIntervalDisplay()); - - // Add information about one-time schedules - if (nextUpComingPlugin.hasAnyOneTimeStartConditions()) { - if (nextUpComingPlugin.hasTriggeredOneTimeStartConditions() && !nextUpComingPlugin.canStartTriggerAgain()) { - scheduleDesc.append(" (Completed)"); - } else { - scheduleDesc.append(" (One-time)"); - } - } - - // Add condition status information if available - if (nextUpComingPlugin.hasAnyStartConditions()) { - int total = nextUpComingPlugin.getStartConditionManager().getConditions().size(); - long satisfied = nextUpComingPlugin.getStartConditionManager().getConditions().stream() - .filter(condition -> condition.isSatisfied()) - .count(); - - if (total > 1) { - scheduleDesc.append(String.format(" [%d/%d conditions met]", satisfied, total)); - } - } - - // Set the updated schedule description - nextUpComingPluginScheduleLabel.setText(scheduleDesc.toString()); - } else { - // No next plugin scheduled - nextUpComingPluginNameLabel.setText("None"); - nextUpComingPluginTimeLabel.setText("--:--"); - nextUpComingPluginScheduleLabel.setText("None"); - } - } - - /** - * Updates only the runtime display for the current plugin without refreshing other info. - * This is used for regular runtime updates when the plugin hasn't changed. - */ - private void updateCurrentPluginRuntimeOnly() { - PluginScheduleEntry currentPlugin = plugin.getCurrentPlugin(); - if (currentPlugin != null && currentPlugin.isRunning()) { - // Calculate and display current runtime for active plugin - ZonedDateTime startTimeZdt = currentPlugin.getLastRunStartTime(); - if (startTimeZdt != null) { - long startTimeMillis = startTimeZdt.toInstant().toEpochMilli(); - long runtimeMillis = System.currentTimeMillis() - startTimeMillis; - runtimeLabel.setText(formatDuration(runtimeMillis)); - } else { - runtimeLabel.setText("Running"); - } - } - } - - /** - * Updates only the time display for the next plugin without full refresh. - * This is used for regular time updates when the plugin hasn't changed. - * - * @param nextUpComingPlugin The next scheduled plugin - */ - private void updateNextPluginTimeDisplayOnly(PluginScheduleEntry nextUpComingPlugin) { - if (nextUpComingPlugin != null && nextUpComingPlugin.isEnabled()) { - // Update only the time display - this calls getCurrentStartTriggerTime() for real-time accuracy - nextUpComingPluginTimeLabel.setText(nextUpComingPlugin.getNextRunDisplay()); - } - } - - private void onOpenConfigButtonClicked(ActionEvent e) { - plugin.openSchedulerWindow(); - } - - private void onAntibanButtonClicked(ActionEvent e) { - plugin.openAntibanSettings(); - } - - /** - * Creates a tooltip showing pause status for the current plugin - * @param currentPlugin The current plugin (can be null) - * @return HTML formatted tooltip string - */ - private String createPauseTooltipForCurrentPlugin(PluginScheduleEntry currentPlugin) { - StringBuilder tooltip = new StringBuilder(""); - - if (currentPlugin == null) { - tooltip.append("No current plugin"); - tooltip.append(""); - return tooltip.toString(); - } - - // Current plugin info - tooltip.append("Current Plugin: ").append(currentPlugin.getName()).append("
"); - tooltip.append("Priority: ").append(currentPlugin.getPriority()).append("
"); - tooltip.append("Enabled: ").append(currentPlugin.isEnabled() ? "Yes" : "No").append("
"); - tooltip.append("Running: ").append(currentPlugin.isRunning() ? "Yes" : "No").append("
"); - - // Pause status - if (currentPlugin.isPaused()) { - tooltip.append("Status: PAUSED
"); - } else if (currentPlugin.isRunning()) { - tooltip.append("Status: RUNNING
"); - } else { - tooltip.append("Status: STOPPED
"); - } - - // Runtime info if available - if (currentPlugin.isRunning() && currentPlugin.getLastRunStartTime() != null) { - Duration runtime = Duration.between(currentPlugin.getLastRunStartTime(), ZonedDateTime.now()); - tooltip.append("Runtime: ").append(formatDurationForTooltip(runtime)).append("
"); - } - - // Add pause info for all plugins if any are paused - if (plugin.anyPluginEntryPaused()) { - tooltip.append("
Other Paused Plugins:
"); - List pausedPlugins = plugin.getScheduledPlugins().stream() - .filter(p -> p.isPaused() && !p.equals(currentPlugin)) - .collect(Collectors.toList()); - - if (pausedPlugins.isEmpty()) { - tooltip.append("None"); - } else { - for (PluginScheduleEntry pausedPlugin : pausedPlugins) { - tooltip.append("â€Ē ").append(pausedPlugin.getName()) - .append(" (Priority: ").append(pausedPlugin.getPriority()).append(")
"); - } - } - } - - tooltip.append(""); - return tooltip.toString(); - } - - /** - * Creates a tooltip showing all paused plugins - * @return HTML formatted tooltip string - */ - private String createAllPausedPluginsTooltip() { - StringBuilder tooltip = new StringBuilder("Paused Plugins:
"); - - List pausedPlugins = plugin.getScheduledPlugins().stream() - .filter(PluginScheduleEntry::isPaused) - .collect(Collectors.toList()); - - if (pausedPlugins.isEmpty()) { - tooltip.append("No plugins are currently paused"); - } else { - for (PluginScheduleEntry pausedPlugin : pausedPlugins) { - tooltip.append("â€Ē ").append(pausedPlugin.getName()).append("
"); - tooltip.append(" Priority: ").append(pausedPlugin.getPriority()).append("
"); - tooltip.append(" Enabled: ").append(pausedPlugin.isEnabled() ? "Yes" : "No").append("
"); - tooltip.append(" Running: ").append(pausedPlugin.isRunning() ? "Yes" : "No").append("
"); - - if (pausedPlugin.isRunning() && pausedPlugin.getLastRunStartTime() != null) { - Duration runtime = Duration.between(pausedPlugin.getLastRunStartTime(), ZonedDateTime.now()); - tooltip.append(" Runtime: ").append(formatDurationForTooltip(runtime)).append("
"); - } - tooltip.append("
"); - } - } - - tooltip.append(""); - return tooltip.toString(); - } - - /** - * Formats a duration for tooltip display - * @param duration The duration to format - * @return Formatted duration string (HH:MM:SS) - */ - private String formatDurationForTooltip(Duration duration) { - long hours = duration.toHours(); - long minutes = duration.toMinutesPart(); - long seconds = duration.toSecondsPart(); - return String.format("%02d:%02d:%02d", hours, minutes, seconds); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerWindow.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerWindow.java deleted file mode 100644 index 76ca99e5817..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/SchedulerWindow.java +++ /dev/null @@ -1,999 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui; - -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerPlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerState; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.logical.LogicalCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ui.ConditionConfigPanel; -import net.runelite.client.plugins.microbot.pluginscheduler.condition.ui.callback.ConditionUpdateCallback; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.PluginScheduleEntry.ScheduleFormPanel; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.PluginScheduleEntry.ScheduleTablePanel; -import net.runelite.client.plugins.microbot.pluginscheduler.ui.util.SchedulerUIUtils; - -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.CompoundBorder; -import javax.swing.border.EmptyBorder; - -import java.awt.*; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.io.File; -import java.util.concurrent.CompletableFuture; - -import lombok.extern.slf4j.Slf4j; - -/** - * A graphical user interface for the Plugin Scheduler system. - *

- * This window provides a comprehensive interface for managing scheduled plugins including: - *

    - *
  • Viewing and managing the list of scheduled plugins
  • - *
  • Adding new plugins to the schedule
  • - *
  • Configuring plugin run parameters and stop conditions
  • - *
  • Monitoring scheduler status and currently running plugins
  • - *
- *

- * The UI is organized into tabbed sections: - *

    - *
  • Schedule Tab - Contains a table of scheduled plugins and a form for adding/editing entries
  • - *
  • Stop Conditions Tab - Allows configuration of complex stop conditions for plugins
  • - *
- *

- * An information panel on the right side displays real-time information about the scheduler state. - * - * @see SchedulerPlugin - * @see ScheduleTablePanel - * @see ScheduleFormPanel - * @see ConditionConfigPanel - * @see SchedulerInfoPanel - */ - -@Slf4j -public class SchedulerWindow extends JFrame implements ConditionUpdateCallback { - private final SchedulerPlugin plugin; - private JTabbedPane tabbedPane; - private final ScheduleTablePanel tablePanel; - private final ScheduleFormPanel formPanel; - private final ConditionConfigPanel stopConditionPanel; - private final ConditionConfigPanel startConditionPanel; - private final SchedulerInfoPanel infoPanel; - // Timer for refreshing the info panel - private Timer refreshTimer; - // Last used file for saving/loading conditions - private File lastSaveFile; - - public SchedulerWindow(SchedulerPlugin plugin) { - super("Plugin Scheduler"); - this.plugin = plugin; - - // Increase width to accommodate the info panel - setSize(1050, 600); - setLocationRelativeTo(null); - setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); - - // Create main components - tablePanel = new ScheduleTablePanel(plugin); - formPanel = new ScheduleFormPanel(plugin); - stopConditionPanel = new ConditionConfigPanel(true); - startConditionPanel = new ConditionConfigPanel(false); - infoPanel = new SchedulerInfoPanel(plugin); - - // Set up form panel actions - formPanel.setAddButtonAction(e -> onAddPlugin()); - formPanel.setUpdateButtonAction(e -> onUpdatePlugin()); - formPanel.setRemoveButtonAction(e -> onRemovePlugin()); - formPanel.setEditMode(false); - - // Set up form panel to clear table selection when ComboBox changes - formPanel.setSelectionChangeListener(() -> { - if (tablePanel.getSelectedPlugin() == null) { - return; - } - tablePanel.clearSelection(); - }); - - // Set up condition panels with the callback - stopConditionPanel.setConditionUpdateCallback(this); - startConditionPanel.setConditionUpdateCallback(this); - - - - // Create main content area using a better layout - JPanel mainContent = createMainContentPanel(); - // Add main content to the center of the window - add(mainContent, BorderLayout.CENTER); - - // Add tab change listener to sync selection - tabbedPane.addChangeListener(e -> { - PluginScheduleEntry selected = tablePanel.getSelectedPlugin(); - int tabIndex = tabbedPane.getSelectedIndex(); - - // When switching to either conditions tab, ensure the condition panel shows the currently selected plugin - if (tabIndex == 1 || tabIndex == 2) { // Start Conditions or Stop Conditions tab - if (selected == null) { - PluginScheduleEntry currentPlugin = plugin.getCurrentPlugin(); - if (currentPlugin != null) { - tablePanel.selectPlugin(currentPlugin); - selected = currentPlugin; - } else { - log.warn("No plugin selected for editing conditions and no current plugin found."); - - PluginScheduleEntry nextPlugin = plugin.getNextScheduledPluginEntry(false,null).orElse(selected); - if (nextPlugin == null) { - log.warn("No plugin selected for editing conditions and no next scheduled plugin found."); - }else{ - tablePanel.selectPlugin(nextPlugin); - log.warn("No plugin selected for editing conditions, taking next scheduled plugin: " + nextPlugin.getCleanName()); - } - selected = nextPlugin; - } - } - - if (tabIndex == 1) { // Start Conditions tab - startConditionPanel.setSelectScheduledPlugin(selected); - } else if (tabIndex == 2) { // Stop Conditions tab - stopConditionPanel.setSelectScheduledPlugin(selected); - } - - } - if (tabIndex == 0) { - formPanel.loadPlugin(selected); - - formPanel.setEditMode(selected != null); - - - - } - }); - - // Add table selection listener - //tablePanel.addSelectionListener(this::onPluginSelected); - // Modify the existing table selection listener to update ComboBox - tablePanel.addSelectionListener(pluginEntry -> { - onPluginSelected(pluginEntry); - // Synchronize form panel ComboBox with table selection - }); - // Create refresh timer to update info panel - refreshTimer = new Timer(1000, e -> infoPanel.refresh()); - - // Start timer when window is opened, stop when closed - addWindowListener(new WindowAdapter() { - @Override - public void windowOpened(WindowEvent e) { - refreshTimer.start(); - } - - @Override - public void windowClosing(WindowEvent e) { - refreshTimer.stop(); - } - }); - - // Style all comboboxes in the UI - styleAllComboBoxes(this); - - // Initialize with data - refresh(); - } - - /** - * Implementation of ConditionUpdateCallback interface. - * Called when conditions are updated in the UI and need to be saved. - */ - @Override - public void onConditionsUpdated( LogicalCondition logicalCondition, - PluginScheduleEntry plugin, - boolean isStopCondition) { - // Save to default configuration - onConditionsUpdated(logicalCondition, plugin, isStopCondition, null); - } - - /** - * Implementation of ConditionUpdateCallback interface. - * Called when conditions are updated and need to be saved to a specific file. - */ - @Override - public void onConditionsUpdated(LogicalCondition logicalCondition, PluginScheduleEntry pluginEntry, - boolean isStopCondition, File saveFile) { - if (pluginEntry == null) { - log.warn("Cannot save conditions: No plugin selected"); - return; - } - - - - try { - - // Update the plugin's condition manager with the new logical condition - /*if (isStopCondition) { - // For stop conditions - - this.plugin.saveConditionsToPlugin( - pluginEntry, - pluginEntry.getStopConditions(), - null, // No changes to start conditions - requireAll, - true, // Stop on conditions met - saveFile - ); - } else { - // For start conditions - this.plugin.saveConditionsToPlugin( - pluginEntry, - pluginEntry.getStopConditions(), // Keep existing stop conditions - pluginEntry.getStartConditions(), // Update start conditions - requireAll, - true, // Stop on conditions met - saveFile - ); - }*/ - - // Remember this file for future operations - if (saveFile != null) { - this.lastSaveFile = saveFile; - } - PluginScheduleEntry selected = tablePanel.getSelectedPlugin(); - if (selected != null) { - // Check if we're waiting to start a plugin - if ( isStopCondition && plugin.getCurrentState() == SchedulerState.WAITING_FOR_STOP_CONDITION && - plugin.getCurrentPlugin() == selected && - !selected.getStopConditionManager().getConditions().isEmpty()) { - - // Conditions added for the plugin we're waiting to start - continue starting - int result = JOptionPane.showConfirmDialog( - this, - "Stop conditions have been added. Would you like to start the plugin now?", - "Start Plugin", - JOptionPane.YES_NO_OPTION - ); - - if (result == JOptionPane.YES_OPTION) { - plugin.continuePendingStart(selected); - } else { - // User decided not to start - reset state - plugin.resetPendingStart(); - } - } - } - // Refresh UI elements - tablePanel.refreshTable(); - infoPanel.refresh(); - plugin.saveScheduledPlugins(); - log.info("Successfully saved {} conditions for plugin: {}", - isStopCondition ? "stop" : "start", - pluginEntry.getCleanName()); - } catch (Exception e) { - log.error("Error saving conditions for plugin: " + pluginEntry.getCleanName(), e); - JOptionPane.showMessageDialog( - this, - "Error saving conditions: " + e.getMessage(), - "Save Error", - JOptionPane.ERROR_MESSAGE - ); - } - } - - /** - * Implementation of ConditionUpdateCallback interface. - * Called when conditions are reset in the UI. - */ - @Override - public void onConditionsReset(PluginScheduleEntry pluginEntry, boolean isStopCondition) { - if (pluginEntry == null) { - log.warn("Cannot reset conditions: No plugin selected"); - return; - } - - log.info("Resetting {} conditions for plugin: {}", - isStopCondition ? "stop" : "start", - pluginEntry.getCleanName()); - - try { - // Clear conditions from the plugin's condition manager - if (isStopCondition) { - pluginEntry.getStopConditionManager().clearUserConditions(); - } else { - pluginEntry.getStartConditionManager().clearUserConditions(); - } - - // Save changes to config - this.plugin.saveScheduledPlugins(); - - // Refresh UI - tablePanel.refreshTable(); - infoPanel.refresh(); - - // If this was the start conditions tab, refresh also the start condition panel - if (!isStopCondition) { - CompletableFuture.runAsync(() -> { - try { - Thread.sleep(100); // Small delay to ensure changes are processed - SwingUtilities.invokeLater(() -> startConditionPanel.refreshConditions()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }); - } - // If this was the stop conditions tab, refresh also the stop condition panel - else { - CompletableFuture.runAsync(() -> { - try { - Thread.sleep(100); // Small delay to ensure changes are processed - SwingUtilities.invokeLater(() -> stopConditionPanel.refreshConditions()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }); - } - - log.info("Successfully reset {} conditions for plugin: {}", - isStopCondition ? "stop" : "start", - pluginEntry.getCleanName()); - } catch (Exception e) { - log.error("Error resetting conditions for plugin: " + pluginEntry.getCleanName(), e); - JOptionPane.showMessageDialog( - this, - "Error resetting conditions: " + e.getMessage(), - "Reset Error", - JOptionPane.ERROR_MESSAGE - ); - } - } - - /** - * Shows a dialog to choose a file for saving or loading conditions - * - * @param save True for save dialog, false for open dialog - * @return The selected file, or null if canceled - */ - public File showFileChooser(boolean save) { - JFileChooser fileChooser = new JFileChooser(); - fileChooser.setDialogTitle(save ? "Save Scheduler Plan" : "Load Scheduler Plan"); - fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); - - // Set initial directory to last used if available - if (lastSaveFile != null && lastSaveFile.getParentFile() != null && lastSaveFile.getParentFile().exists()) { - fileChooser.setCurrentDirectory(lastSaveFile.getParentFile()); - } - - // Add file extension filter - fileChooser.setFileFilter(new javax.swing.filechooser.FileFilter() { - @Override - public boolean accept(File f) { - return f.isDirectory() || f.getName().toLowerCase().endsWith(".json"); - } - - @Override - public String getDescription() { - return "JSON Files (*.json)"; - } - }); - - int result = save ? fileChooser.showSaveDialog(this) : fileChooser.showOpenDialog(this); - - if (result == JFileChooser.APPROVE_OPTION) { - File selectedFile = fileChooser.getSelectedFile(); - - // Add .json extension if missing for save dialogs - if (save && !selectedFile.getName().toLowerCase().endsWith(".json")) { - selectedFile = new File(selectedFile.getAbsolutePath() + ".json"); - } - - return selectedFile; - } - - return null; - } - - /** - * Saves the currently loaded scheduler plan to a file - */ - public void saveSchedulerPlanToFile() { - File saveFile = showFileChooser(true); - if (saveFile == null) { - return; - } - - // Check if file already exists - if (saveFile.exists()) { - int option = JOptionPane.showConfirmDialog( - this, - "File already exists. Overwrite?", - "File Exists", - JOptionPane.YES_NO_OPTION - ); - - if (option != JOptionPane.YES_OPTION) { - return; - } - } - - // Save the plan - boolean success = plugin.savePluginScheduleEntriesToFile(saveFile); - - if (success) { - lastSaveFile = saveFile; - JOptionPane.showMessageDialog( - this, - "Scheduler plan saved successfully!", - "Save Complete", - JOptionPane.INFORMATION_MESSAGE - ); - } else { - JOptionPane.showMessageDialog( - this, - "Failed to save scheduler plan. See log for details.", - "Save Failed", - JOptionPane.ERROR_MESSAGE - ); - } - } - - /** - * Loads a scheduler plan from a file - */ - public void loadSchedulerPlanFromFile() { - File loadFile = showFileChooser(false); - if (loadFile == null) { - return; - } - - // Confirm before loading - int option = JOptionPane.showConfirmDialog( - this, - "Loading will replace the current scheduler plan. Continue?", - "Load Scheduler Plan", - JOptionPane.YES_NO_OPTION - ); - - if (option != JOptionPane.YES_OPTION) { - return; - } - - // Load the plan - boolean success = plugin.loadPluginScheduleEntriesFromFile(loadFile); - - if (success) { - lastSaveFile = loadFile; - refresh(); - JOptionPane.showMessageDialog( - this, - "Scheduler plan loaded successfully!", - "Load Complete", - JOptionPane.INFORMATION_MESSAGE - ); - } else { - JOptionPane.showMessageDialog( - this, - "Failed to load scheduler plan. See log for details.", - "Load Failed", - JOptionPane.ERROR_MESSAGE - ); - } - } - - /** - * Recursively applies styling to all JComboBox components found in the container hierarchy. - * - * @param container The parent container to start searching from - */ - - private void styleAllComboBoxes(Container container) { - for (Component component : container.getComponents()) { - if (component instanceof JComboBox) { - SchedulerUIUtils.styleComboBox((JComboBox) component); - } - if (component instanceof Container) { - styleAllComboBoxes((Container) component); - } - } - } - - /** - * Creates the main content panel with tabbed interface and information sidebar. - * - * @return JPanel with the configured layout - */ - private JPanel createMainContentPanel() { - JPanel mainContent = new JPanel(new BorderLayout()); - mainContent.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Create a more flexible tabbed pane structure - tabbedPane = new JTabbedPane(); - tabbedPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - tabbedPane.setForeground(Color.WHITE); - - // Schedule tab - Split into table (top) and form (bottom) with adjustable divider - JPanel scheduleTab = new JPanel(new BorderLayout()); - scheduleTab.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Create a split pane for the table and form - use VERTICAL layout for better table visibility - JSplitPane scheduleSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); - scheduleSplitPane.setTopComponent(tablePanel); - scheduleSplitPane.setBottomComponent(formPanel); - - // Calculate the preferred size for showing 3 rows in the table - // Row height (30px) * 3 rows + header height (~25px) + legend panel (~30px) + borders/margins (~15px) - int preferredTableHeight = (30 * 3) + 25 + 30 + 15; // ~160px for 3 rows - - // Calculate the maximum size for showing 8 rows in the table - int maxTableHeight = (30 * 8) + 25 + 30 + 15; // ~310px for 8 rows - - // Set minimum size for the form panel to ensure it doesn't get too small - formPanel.setMinimumSize(new Dimension(0, 140)); - - // Set minimum size for the table panel to ensure at least 3 rows are visible - tablePanel.setMinimumSize(new Dimension(0, preferredTableHeight)); - - // Set preferred size for the table panel - tablePanel.setPreferredSize(new Dimension(0, preferredTableHeight)); - - // Set maximum size for the table panel to prevent expanding beyond 8 rows - tablePanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, maxTableHeight)); - - scheduleSplitPane.setResizeWeight(0.7); // Give 70% space to the table on top - scheduleSplitPane.setDividerLocation(preferredTableHeight); // Set initial divider position - scheduleSplitPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - scheduleTab.add(scheduleSplitPane, BorderLayout.CENTER); - - // Start Conditions tab - JPanel startConditionsTab = new JPanel(new BorderLayout()); - startConditionsTab.setBackground(ColorScheme.DARKER_GRAY_COLOR); - startConditionsTab.add(startConditionPanel, BorderLayout.CENTER); - - // Stop Conditions tab - JPanel stopConditionsTab = new JPanel(new BorderLayout()); - stopConditionsTab.setBackground(ColorScheme.DARKER_GRAY_COLOR); - stopConditionsTab.add(stopConditionPanel, BorderLayout.CENTER); - - // Add tabs to tabbed pane - tabbedPane.addTab("Schedule", scheduleTab); - tabbedPane.addTab("Start Conditions", startConditionsTab); - tabbedPane.addTab("Stop Conditions", stopConditionsTab); - - // Create a split pane for the tabs and info panel - JSplitPane mainSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); - mainSplitPane.setLeftComponent(tabbedPane); - - // Configure the info panel for proper scaling - configureInfoPanelForScrollPane(); - - // Wrap info panel in a scroll pane with proper configuration - JScrollPane infoScrollPane = createInfoScrollPane(); - - // Set up the split pane configuration - mainSplitPane.setRightComponent(infoScrollPane); - mainSplitPane.setResizeWeight(0.75); // Favor the main content (75% left, 25% right) - mainSplitPane.setDividerLocation(800); - mainSplitPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Add a single resize listener for proper scaling - mainSplitPane.addComponentListener(new ComponentAdapter() { - @Override - public void componentResized(ComponentEvent e) { - updateInfoPanelSize(mainSplitPane, infoScrollPane); - } - }); - - // Listen for divider location changes - mainSplitPane.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, evt -> { - SwingUtilities.invokeLater(() -> updateInfoPanelSize(mainSplitPane, infoScrollPane)); - }); - - // Add a top control panel for file operations with better visibility - JPanel controlPanel = new JPanel(); - controlPanel.setLayout(new BorderLayout()); - controlPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); - controlPanel.setBorder(new EmptyBorder(5, 8, 5, 8)); - - // Create button panel with FlowLayout for better spacing - JPanel fileButtonsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0)); - fileButtonsPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); - - // Create load button with improved styling - JButton loadButton = createStyledButton("Load Plan from File...", Color.BLUE, "load.png"); - loadButton.setToolTipText("Load a saved scheduler plan from a file"); - loadButton.addActionListener(e -> loadSchedulerPlanFromFile()); - - // Create save button with improved styling - JButton saveButton = createStyledButton("Save Plan to File...", ColorScheme.PROGRESS_COMPLETE_COLOR, "save.png"); - saveButton.setToolTipText("Save current scheduler plan to a file"); - saveButton.addActionListener(e -> saveSchedulerPlanToFile()); - - // Add buttons to the panel - fileButtonsPanel.add(loadButton); - fileButtonsPanel.add(saveButton); - - // Add the buttons panel to the control panel - controlPanel.add(fileButtonsPanel, BorderLayout.WEST); - - // Add optional heading/title if desired - JLabel controlPanelTitle = new JLabel("Scheduler Controls"); - controlPanelTitle.setForeground(Color.WHITE); - controlPanelTitle.setFont(FontManager.getRunescapeBoldFont().deriveFont(14f)); - controlPanel.add(controlPanelTitle, BorderLayout.EAST); - - // Add control panel to the top - mainContent.add(controlPanel, BorderLayout.NORTH); - - // Add main split pane to the center - mainContent.add(mainSplitPane, BorderLayout.CENTER); - - // Schedule initial size update after the window is displayed - SwingUtilities.invokeLater(() -> { - updateInfoPanelSize(mainSplitPane, infoScrollPane); - }); - - return mainContent; - } - - /** - * Creates a consistently styled button with icon and hover effects - */ - private JButton createStyledButton(String text, Color color, String iconName) { - JButton button = new JButton(text); - button.setFont(FontManager.getRunescapeSmallFont()); - button.setForeground(Color.WHITE); - button.setBackground(ColorScheme.DARKER_GRAY_COLOR); - button.setFocusPainted(false); - button.setBorder(new CompoundBorder( - BorderFactory.createLineBorder(color.darker(), 1), - BorderFactory.createEmptyBorder(5, 10, 5, 10))); - - // Add hover effect - button.addMouseListener(new java.awt.event.MouseAdapter() { - public void mouseEntered(java.awt.event.MouseEvent evt) { - button.setBackground(ColorScheme.DARK_GRAY_COLOR); - button.setBorder(new CompoundBorder( - BorderFactory.createLineBorder(color, 1), - BorderFactory.createEmptyBorder(5, 10, 5, 10))); - } - - public void mouseExited(java.awt.event.MouseEvent evt) { - button.setBackground(ColorScheme.DARKER_GRAY_COLOR); - button.setBorder(new CompoundBorder( - BorderFactory.createLineBorder(color.darker(), 1), - BorderFactory.createEmptyBorder(5, 10, 5, 10))); - } - }); - - return button; - } - - /** - * Refreshes all UI components with the latest data from the plugin. - * Updates the table, form state, condition panel, and information panel. - */ - public void refresh() { - tablePanel.refreshTable(); - if (formPanel != null) { - formPanel.refresh(); - } - if (stopConditionPanel != null) { - stopConditionPanel.refreshConditions(); - } - if (startConditionPanel != null) { - startConditionPanel.refreshConditions(); - } - - // Refresh info panel - if (infoPanel != null) { - infoPanel.refresh(); - } - } - /** - * Handles plugin selection events from the table. - * Updates the form panel and condition panel with the selected plugin's data. - * - * @param plugin The selected plugin or null if no selection - */ - private void onPluginSelected(PluginScheduleEntry plugin) { - if (tablePanel.getRowCount() == 0) { - log.info("No plugins in table."); - return; - } - - PluginScheduleEntry selected = tablePanel.getSelectedPlugin(); - - if (selected == null) { - formPanel.clearForm(); - formPanel.setEditMode(false); - // Update both condition panels when no plugin is selected - startConditionPanel.setSelectScheduledPlugin(null); - stopConditionPanel.setSelectScheduledPlugin(null); - - if (plugin == null) { - return; - } - - // Handle case where we have a plugin reference but nothing is selected - log.info("No plugin selected for editing. Plugin: {}", plugin.getCleanName()); - // Find if this is the next scheduled plugin - PluginScheduleEntry nextPlugin = this.plugin.getNextScheduledPluginEntry(false,null).orElse(selected); - if (nextPlugin == null) { - log.warn("No plugin selected for editing conditions and no next scheduled plugin found."); - } else { - tablePanel.selectPlugin(nextPlugin); - log.info("No plugin selected for editing conditions, selecting next scheduled plugin: {}", nextPlugin.getCleanName()); - } - selected = nextPlugin; - } - - // Update form panel with selection - formPanel.loadPlugin(selected); - formPanel.setEditMode(true); - - // Update condition panels based on the current tab - int currentTabIndex = tabbedPane.getSelectedIndex(); - if (currentTabIndex == 1) { // Start Conditions tab - startConditionPanel.setSelectScheduledPlugin(selected); - } else if (currentTabIndex == 2) { // Stop Conditions tab - stopConditionPanel.setSelectScheduledPlugin(selected); - } - - // Always update control button when selection changes - formPanel.updateControlButton(); - } - - /** - * Processes the addition of a new plugin from the form data. - * Checks for stop conditions and prompts user if none are configured. - */ - private void onAddPlugin() { - PluginScheduleEntry scheduledPlugin = formPanel.getPluginFromForm(null); - if (scheduledPlugin == null) return; - - // Check if the plugin has stop conditions - if (scheduledPlugin.getStopConditionManager().getUserConditions().isEmpty()) { - // Check if this plugin needs time-based stop conditions (from checkbox) - if (scheduledPlugin.isNeedsStopCondition()) { - int result = JOptionPane.showConfirmDialog(this, - "No stop conditions are set. The plugin will run until manually stopped.\n" + - "Would you like to configure stop conditions now?", - "No Stop Conditions", - JOptionPane.YES_NO_CANCEL_OPTION); - - if (result == JOptionPane.YES_OPTION) { - // Add the plugin first (disabled by default) so we can set conditions on it - scheduledPlugin.setEnabled(false); - plugin.addScheduledPlugin(scheduledPlugin); - plugin.saveScheduledPlugins(); - refresh(); - log.info("Plugin added without conditions: row count" + tablePanel.getRowCount()); - // Select the newly added plugin - tablePanel.selectPlugin(scheduledPlugin); - // Switch to stop conditions tab - tabbedPane.setSelectedIndex(2); - return; - } else if (result == JOptionPane.CANCEL_OPTION) { - scheduledPlugin.setEnabled(false); // Set to disabled by default - return; // Cancel the operation - } - // If NO, continue with adding plugin without conditions - } - } - - // Add the plugin (disabled by default for safety) - scheduledPlugin.setEnabled(false); - plugin.addScheduledPlugin(scheduledPlugin); - plugin.saveScheduledPlugins(); - refresh(); - - // Select the newly added plugin - tablePanel.selectPlugin(scheduledPlugin); - - // Show a hint about enabling the plugin - JOptionPane.showMessageDialog(this, - "Plugin added successfully (currently disabled).\n" + - "Enable it in the Properties tab when you're ready to schedule it.", - "Plugin Added", - JOptionPane.INFORMATION_MESSAGE); - - // Switch to the Properties tab in the form panel - if (formPanel.getComponent(0) instanceof JTabbedPane) { - ((JTabbedPane)formPanel.getComponent(0)).setSelectedIndex(1); - } - } - - /** - * Updates an existing plugin with data from the form. - * Preserves the plugin's identity while updating its configuration. - */ - private void onUpdatePlugin() { - PluginScheduleEntry selectedPlugin = tablePanel.getSelectedPlugin(); - if (selectedPlugin == null) { - return; - } - - try { - // Apply form values to the selected plugin - formPanel.getPluginFromForm(selectedPlugin); - - // Update the UI - plugin.saveScheduledPlugins(); - tablePanel.refreshTable(); - - // Clear edit mode and selection to encourage users to review the changes - formPanel.loadPlugin(selectedPlugin); - formPanel.setEditMode(false); - tablePanel.clearSelection(); - - JOptionPane.showMessageDialog(this, - "Plugin schedule updated successfully!", - "Update Success", - JOptionPane.INFORMATION_MESSAGE); - - } catch (Exception e) { - log.error("Error updating plugin: {}", e.getMessage(), e); - JOptionPane.showMessageDialog( - this, - "Error updating plugin: " + e.getMessage(), - "Update Error", - JOptionPane.ERROR_MESSAGE - ); - } - } - - /** - * Removes the currently selected plugin from the schedule. - */ - private void onRemovePlugin() { - PluginScheduleEntry selectedPlugin = tablePanel.getSelectedPlugin(); - if (selectedPlugin == null) { - return; - } - - // Confirm deletion - int result = JOptionPane.showConfirmDialog(this, - "Are you sure you want to remove '" + selectedPlugin.getCleanName() + "' from the schedule?", - "Confirm Removal", - JOptionPane.YES_NO_OPTION, - JOptionPane.WARNING_MESSAGE); - - if (result != JOptionPane.YES_OPTION) { - return; - } - - // Stop the plugin if it's running - if (plugin.getCurrentPlugin()!=null && - plugin.getCurrentPlugin().equals(selectedPlugin) && - selectedPlugin.isRunning()) { - - plugin.forceStopCurrentPluginScheduleEntry(false); - } - - // Remove from schedule - plugin.removeScheduledPlugin(selectedPlugin); - plugin.saveScheduledPlugins(); - - // Update UI - tablePanel.refreshTable(); - formPanel.clearForm(); - startConditionPanel.setSelectScheduledPlugin(null); - stopConditionPanel.setSelectScheduledPlugin(null); - formPanel.setEditMode(false); - JOptionPane.showMessageDialog(this, - "Plugin removed from schedule.", - "Plugin Removed", - JOptionPane.INFORMATION_MESSAGE); - } - /** - * Cleans up resources when window is closed. - * Stops the refresh timer before disposing the window. - */ - @Override - public void dispose() { - if (refreshTimer != null) { - refreshTimer.stop(); - } - super.dispose(); - } - /** - * Programmatically selects a plugin in the table. - * - * @param plugin The plugin entry to select - */ - public void selectPlugin(PluginScheduleEntry plugin) { - if (tablePanel != null) { - tablePanel.selectPlugin(plugin); - } - } - /** - * Switches the UI to display the stop conditions tab. - */ - public void switchToStopConditionsTab() { - if (tabbedPane != null) { - tabbedPane.setSelectedIndex(2); // Switch to stop conditions tab (now at index 2) - } - } - - /** - * Configures the info panel for optimal display within a scroll pane. - * This ensures the panel layout is properly prepared for dynamic resizing. - */ - private void configureInfoPanelForScrollPane() { - // Set a minimum width for the info panel to ensure readability - int minInfoPanelWidth = 240; - infoPanel.setMinimumSize(new Dimension(minInfoPanelWidth, 0)); - - // Allow the info panel to expand vertically but control horizontal size - infoPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); - - // Set an initial preferred size that will be adjusted by resize listeners - infoPanel.setPreferredSize(new Dimension(minInfoPanelWidth, 600)); - } - - /** - * Creates and configures the scroll pane for the info panel with optimal settings. - * - * @return A properly configured JScrollPane containing the info panel - */ - private JScrollPane createInfoScrollPane() { - JScrollPane scrollPane = new JScrollPane(infoPanel); - - // Configure scroll policies - scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - - // Style the scroll pane to match the application theme - scrollPane.setBorder(BorderFactory.createEmptyBorder()); - scrollPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - scrollPane.getViewport().setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Set initial size constraints - int minScrollPaneWidth = 250; - scrollPane.setMinimumSize(new Dimension(minScrollPaneWidth, 0)); - scrollPane.setPreferredSize(new Dimension(minScrollPaneWidth, 600)); - - return scrollPane; - } - - /** - * Updates the info panel size based on the current split pane configuration. - * This method ensures the info panel properly fits within the allocated space. - * - * @param splitPane The main split pane containing the info panel - * @param scrollPane The scroll pane wrapping the info panel - */ - private void updateInfoPanelSize(JSplitPane splitPane, JScrollPane scrollPane) { - if (splitPane == null || scrollPane == null) { - return; - } - - // Get the actual width allocated to the right component - Component rightComponent = splitPane.getRightComponent(); - if (rightComponent == null) { - return; - } - - int availableWidth = rightComponent.getWidth(); - if (availableWidth <= 0) { - // If width is not yet available, use the divider location to estimate - int dividerLocation = splitPane.getDividerLocation(); - int totalWidth = splitPane.getWidth(); - availableWidth = totalWidth - dividerLocation - splitPane.getDividerSize(); - } - - if (availableWidth > 0) { - // Account for potential scrollbar width and margins - int scrollBarWidth = 20; // Conservative estimate including margins - int contentWidth = Math.max(240, availableWidth - scrollBarWidth); // Minimum width of 240px - - // Update the info panel size - Dimension newSize = new Dimension(contentWidth, infoPanel.getPreferredSize().height); - infoPanel.setPreferredSize(newSize); - infoPanel.setMaximumSize(new Dimension(contentWidth, Integer.MAX_VALUE)); - - // Revalidate to apply the changes - infoPanel.revalidate(); - scrollPane.revalidate(); - - log.debug("Updated info panel size to {}px wide (available: {}px)", contentWidth, availableWidth); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DatePickerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DatePickerPanel.java deleted file mode 100644 index 4f23d0c3613..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DatePickerPanel.java +++ /dev/null @@ -1,254 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.components; - -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.event.PopupMenuEvent; -import javax.swing.event.PopupMenuListener; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.time.DayOfWeek; -import java.time.LocalDate; -import java.time.YearMonth; -import java.time.format.DateTimeFormatter; -import java.util.function.Consumer; - -/** - * A custom date picker component that shows a calendar popup for date selection - */ -public class DatePickerPanel extends JPanel { - private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - private JTextField dateField; - private LocalDate selectedDate; - private JPopupMenu calendarPopup; - private JPanel calendarPanel; - private JLabel monthYearLabel; - private YearMonth currentYearMonth; - private Consumer dateChangeListener; - - public DatePickerPanel() { - this(LocalDate.now()); - } - - public DatePickerPanel(LocalDate initialDate) { - this.selectedDate = initialDate; - this.currentYearMonth = YearMonth.from(initialDate); - setLayout(new BorderLayout(5, 0)); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setBorder(new EmptyBorder(0, 0, 0, 0)); - - initComponents(); - } - - private void initComponents() { - // Date text field with formatted date - dateField = new JTextField(selectedDate.format(dateFormatter), 10); - dateField.setForeground(Color.WHITE); - dateField.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - dateField.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - BorderFactory.createEmptyBorder(2, 5, 2, 5))); - - // Calendar button with ImageIcon - JButton calendarButton = new JButton(); - calendarButton.setFocusPainted(false); - calendarButton.setForeground(Color.WHITE); - calendarButton.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - calendarButton.setPreferredSize(new Dimension(30, dateField.getPreferredSize().height)); - - - try { - ImageIcon icon = new ImageIcon(getClass().getResource("/net/runelite/client/plugins/microbot/pluginscheduler/"+"calendar-icon.png")); - // Scale the icon to fit the button - Image img = icon.getImage().getScaledInstance(16, 16, Image.SCALE_SMOOTH); - calendarButton.setIcon(new ImageIcon(img)); - } catch (Exception e) { - // Fallback to simple text if icon can't be loaded - calendarButton.setText("▾"); - } - - // Initialize calendar popup - createCalendarPopup(); - - // Show calendar on button click - calendarButton.addActionListener(e -> { - Point location = dateField.getLocationOnScreen(); - calendarPopup.show(dateField, 0, dateField.getHeight()); - calendarPopup.setLocation(location.x, location.y + dateField.getHeight()); - }); - - // Update date when text field changes - dateField.addActionListener(e -> { - try { - LocalDate newDate = LocalDate.parse(dateField.getText(), dateFormatter); - setSelectedDate(newDate); - } catch (Exception ex) { - // If parsing fails, revert to current selection - dateField.setText(selectedDate.format(dateFormatter)); - } - }); - - // Add components to panel - add(dateField, BorderLayout.CENTER); - add(calendarButton, BorderLayout.EAST); - } - - private void createCalendarPopup() { - calendarPopup = new JPopupMenu(); - calendarPopup.setBorder(BorderFactory.createLineBorder(ColorScheme.DARK_GRAY_COLOR)); - - JPanel contentPanel = new JPanel(new BorderLayout()); - contentPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - contentPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); - - // Month navigation panel - JPanel navigationPanel = new JPanel(new BorderLayout()); - navigationPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JButton prevButton = new JButton("<"); - prevButton.setFocusPainted(false); - prevButton.setForeground(Color.WHITE); - prevButton.setBackground(ColorScheme.BRAND_ORANGE); - prevButton.addActionListener(e -> { - currentYearMonth = currentYearMonth.minusMonths(1); - updateCalendar(); - }); - - JButton nextButton = new JButton(">"); - nextButton.setFocusPainted(false); - nextButton.setForeground(Color.WHITE); - nextButton.setBackground(ColorScheme.BRAND_ORANGE); - nextButton.addActionListener(e -> { - currentYearMonth = currentYearMonth.plusMonths(1); - updateCalendar(); - }); - - monthYearLabel = new JLabel("", SwingConstants.CENTER); - monthYearLabel.setForeground(Color.WHITE); - monthYearLabel.setFont(FontManager.getRunescapeBoldFont()); - - navigationPanel.add(prevButton, BorderLayout.WEST); - navigationPanel.add(monthYearLabel, BorderLayout.CENTER); - navigationPanel.add(nextButton, BorderLayout.EAST); - - // Calendar panel (will be populated in updateCalendar()) - calendarPanel = new JPanel(new GridLayout(7, 7, 2, 2)); - calendarPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - contentPanel.add(navigationPanel, BorderLayout.NORTH); - contentPanel.add(calendarPanel, BorderLayout.CENTER); - - calendarPopup.add(contentPanel); - - // Update calendar when shown - calendarPopup.addPopupMenuListener(new PopupMenuListener() { - @Override - public void popupMenuWillBecomeVisible(PopupMenuEvent e) { - currentYearMonth = YearMonth.from(selectedDate); - updateCalendar(); - } - - @Override - public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {} - - @Override - public void popupMenuCanceled(PopupMenuEvent e) {} - }); - } - - private void updateCalendar() { - calendarPanel.removeAll(); - - // Update month/year label - monthYearLabel.setText(currentYearMonth.format(DateTimeFormatter.ofPattern("MMMM yyyy"))); - - // Day of week headers - String[] daysOfWeek = {"Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"}; - for (String day : daysOfWeek) { - JLabel label = new JLabel(day, SwingConstants.CENTER); - label.setForeground(Color.LIGHT_GRAY); - calendarPanel.add(label); - } - - // Get the first day of the month and adjust for Monday-based week - LocalDate firstOfMonth = currentYearMonth.atDay(1); - int dayOfWeekValue = firstOfMonth.getDayOfWeek().getValue(); // 1 for Monday, 7 for Sunday - - // Add empty cells before the first day of the month - for (int i = 1; i < dayOfWeekValue; i++) { - calendarPanel.add(new JLabel()); - } - - // Add day buttons - for (int day = 1; day <= currentYearMonth.lengthOfMonth(); day++) { - final int dayValue = day; - final LocalDate date = currentYearMonth.atDay(day); - - JButton dayButton = new JButton(String.valueOf(day)); - dayButton.setFocusPainted(false); - dayButton.setMargin(new Insets(2, 2, 2, 2)); - - // Highlight today - if (date.equals(LocalDate.now())) { - dayButton.setBackground(new Color(70, 130, 180)); // Steel blue - dayButton.setForeground(Color.WHITE); - } - // Highlight selected date - else if (date.equals(selectedDate)) { - dayButton.setBackground(ColorScheme.BRAND_ORANGE); - dayButton.setForeground(Color.WHITE); - } - // Weekend days - else if (date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY) { - dayButton.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - dayButton.setForeground(Color.LIGHT_GRAY); - } - // Regular days - else { - dayButton.setBackground(ColorScheme.DARK_GRAY_COLOR); - dayButton.setForeground(Color.WHITE); - } - - // Select this date and close popup when clicked - dayButton.addActionListener(e -> { - setSelectedDate(date); - calendarPopup.setVisible(false); - }); - - calendarPanel.add(dayButton); - } - - calendarPanel.revalidate(); - calendarPanel.repaint(); - } - - public LocalDate getSelectedDate() { - return selectedDate; - } - - public void setSelectedDate(LocalDate date) { - this.selectedDate = date; - dateField.setText(date.format(dateFormatter)); - - if (dateChangeListener != null) { - dateChangeListener.accept(date); - } - } - - public void setDateChangeListener(Consumer listener) { - this.dateChangeListener = listener; - } - - public void setEditable(boolean editable) { - dateField.setEditable(editable); - } - - public JTextField getTextField() { - return dateField; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DateRangePanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DateRangePanel.java deleted file mode 100644 index edf637f9be3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DateRangePanel.java +++ /dev/null @@ -1,160 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.components; - -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import java.awt.*; -import java.time.LocalDate; -import java.util.function.BiConsumer; - -/** - * A panel for selecting a date range with start and end dates - */ -public class DateRangePanel extends JPanel { - private final DatePickerPanel startDatePicker; - private final DatePickerPanel endDatePicker; - private BiConsumer rangeChangeListener; - - public DateRangePanel() { - this(LocalDate.now(), LocalDate.now().plusMonths(1)); - } - - public DateRangePanel(LocalDate startDate, LocalDate endDate) { - setLayout(new GridBagLayout()); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setBorder(new EmptyBorder(5, 5, 5, 5)); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 0; - gbc.anchor = GridBagConstraints.WEST; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.insets = new Insets(2, 2, 2, 2); - - JLabel startLabel = new JLabel("Start Date:"); - startLabel.setForeground(Color.WHITE); - startLabel.setFont(FontManager.getRunescapeSmallFont()); - add(startLabel, gbc); - - gbc.gridx = 1; - startDatePicker = new DatePickerPanel(startDate); - add(startDatePicker, gbc); - - gbc.gridx = 0; - gbc.gridy = 1; - JLabel endLabel = new JLabel("End Date:"); - endLabel.setForeground(Color.WHITE); - endLabel.setFont(FontManager.getRunescapeSmallFont()); - add(endLabel, gbc); - - gbc.gridx = 1; - endDatePicker = new DatePickerPanel(endDate); - add(endDatePicker, gbc); - - // Add common presets panel - gbc.gridx = 0; - gbc.gridy = 2; - gbc.gridwidth = 2; - gbc.insets = new Insets(10, 2, 2, 2); - - JPanel presetsPanel = createPresetsPanel(); - add(presetsPanel, gbc); - - // Set up change listeners - startDatePicker.setDateChangeListener(d -> { - // Ensure end date is not before start date - if (endDatePicker.getSelectedDate().isBefore(d)) { - endDatePicker.setSelectedDate(d); - } - notifyRangeChanged(); - }); - - endDatePicker.setDateChangeListener(d -> { - // Ensure start date is not after end date - if (startDatePicker.getSelectedDate().isAfter(d)) { - startDatePicker.setSelectedDate(d); - } - notifyRangeChanged(); - }); - } - - private JPanel createPresetsPanel() { - JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel presetsLabel = new JLabel("Quick Presets:"); - presetsLabel.setForeground(Color.WHITE); - panel.add(presetsLabel); - - // Common date range presets - LocalDate today = LocalDate.now(); - - addPresetButton(panel, "Today", today, today); - addPresetButton(panel, "This Week", today, today.plusDays(7 - today.getDayOfWeek().getValue())); - addPresetButton(panel, "This Month", today, today.withDayOfMonth(today.lengthOfMonth())); - addPresetButton(panel, "Next 7 Days", today, today.plusDays(7)); - addPresetButton(panel, "Next 30 Days", today, today.plusDays(30)); - addPresetButton(panel, "Next 90 Days", today, today.plusDays(90)); - - // Add unlimited option - addPresetButton(panel, "Unlimited", - net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition.UNLIMITED_START_DATE, - net.runelite.client.plugins.microbot.pluginscheduler.condition.time.TimeWindowCondition.UNLIMITED_END_DATE); - - return panel; - } - - private void addPresetButton(JPanel panel, String label, LocalDate start, LocalDate end) { - JButton button = new JButton(label); - button.setFocusPainted(false); - button.setBackground(ColorScheme.DARK_GRAY_COLOR); - button.setForeground(Color.WHITE); - button.setFont(FontManager.getRunescapeSmallFont()); - button.addActionListener(e -> { - startDatePicker.setSelectedDate(start); - endDatePicker.setSelectedDate(end); - }); - panel.add(button); - } - - private void notifyRangeChanged() { - if (rangeChangeListener != null) { - rangeChangeListener.accept(getStartDate(), getEndDate()); - } - } - - public LocalDate getStartDate() { - return startDatePicker.getSelectedDate(); - } - - public LocalDate getEndDate() { - return endDatePicker.getSelectedDate(); - } - - public void setStartDate(LocalDate date) { - startDatePicker.setSelectedDate(date); - } - - public void setEndDate(LocalDate date) { - endDatePicker.setSelectedDate(date); - } - - public void setRangeChangeListener(BiConsumer listener) { - this.rangeChangeListener = listener; - } - - public void setEditable(boolean editable) { - startDatePicker.setEditable(editable); - endDatePicker.setEditable(editable); - } - - public DatePickerPanel getStartDatePicker() { - return startDatePicker; - } - - public DatePickerPanel getEndDatePicker() { - return endDatePicker; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DateTimePickerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DateTimePickerPanel.java deleted file mode 100644 index 0d2502d384d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/DateTimePickerPanel.java +++ /dev/null @@ -1,88 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.components; - -import net.runelite.client.ui.ColorScheme; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import java.awt.*; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.util.function.Consumer; - -/** - * A combined date and time picker panel - */ -public class DateTimePickerPanel extends JPanel { - private final DatePickerPanel datePicker; - private final TimePickerPanel timePicker; - private Consumer dateTimeChangeListener; - - public DateTimePickerPanel() { - this(LocalDate.now(), LocalTime.now()); - } - - public DateTimePickerPanel(LocalDate date, LocalTime time) { - setLayout(new BorderLayout(10, 0)); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setBorder(new EmptyBorder(0, 0, 0, 0)); - - datePicker = new DatePickerPanel(date); - timePicker = new TimePickerPanel(time); - - JPanel container = new JPanel(new BorderLayout(5, 0)); - container.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - container.add(datePicker, BorderLayout.CENTER); - container.add(timePicker, BorderLayout.EAST); - - add(container, BorderLayout.CENTER); - - // Set up change listeners - datePicker.setDateChangeListener(d -> notifyDateTimeChanged()); - timePicker.setTimeChangeListener(t -> notifyDateTimeChanged()); - } - - private void notifyDateTimeChanged() { - if (dateTimeChangeListener != null) { - dateTimeChangeListener.accept(getDateTime()); - } - } - - public LocalDateTime getDateTime() { - return LocalDateTime.of(datePicker.getSelectedDate(), timePicker.getSelectedTime()); - } - - public void setDateTime(LocalDateTime dateTime) { - datePicker.setSelectedDate(dateTime.toLocalDate()); - timePicker.setSelectedTime(dateTime.toLocalTime()); - } - - public void setDateTimeChangeListener(Consumer listener) { - this.dateTimeChangeListener = listener; - } - - public void setEditable(boolean editable) { - datePicker.setEditable(editable); - timePicker.setEditable(editable); - } - - public DatePickerPanel getDatePicker() { - return datePicker; - } - - public TimePickerPanel getTimePicker() { - return timePicker; - } - - public void setDate(LocalDate date) { - datePicker.setSelectedDate(date); - notifyDateTimeChanged(); - } - - public void setTime(Integer hour, Integer minute) { - LocalTime time = LocalTime.of(hour, minute); - timePicker.setSelectedTime(time); - notifyDateTimeChanged(); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/InitialDelayPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/InitialDelayPanel.java deleted file mode 100644 index ffc926d40d7..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/InitialDelayPanel.java +++ /dev/null @@ -1,156 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.components; - -import net.runelite.client.ui.ColorScheme; - -import javax.swing.*; -import java.awt.*; - -/** - * A reusable panel component for configuring an initial delay with hours, minutes, and seconds spinners. - */ -public class InitialDelayPanel extends JPanel { - private final JCheckBox initialDelayCheckBox; - private final JSpinner hoursSpinner; - private final JSpinner minutesSpinner; - private final JSpinner secondsSpinner; - - /** - * Creates a new InitialDelayPanel with default values. - */ - public InitialDelayPanel() { - setLayout(new FlowLayout(FlowLayout.LEFT)); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - - initialDelayCheckBox = new JCheckBox("Initial Delay"); - initialDelayCheckBox.setBackground(ColorScheme.DARKER_GRAY_COLOR); - initialDelayCheckBox.setForeground(Color.WHITE); - add(initialDelayCheckBox); - - // Hours spinner - SpinnerNumberModel hoursModel = new SpinnerNumberModel(0, 0, 23, 1); - hoursSpinner = new JSpinner(hoursModel); - hoursSpinner.setPreferredSize(new Dimension(60, hoursSpinner.getPreferredSize().height)); - hoursSpinner.setEnabled(false); - add(hoursSpinner); - - JLabel hoursLabel = new JLabel("hr"); - hoursLabel.setForeground(Color.WHITE); - add(hoursLabel); - - // Minutes spinner - SpinnerNumberModel minutesModel = new SpinnerNumberModel(0, 0, 59, 1); - minutesSpinner = new JSpinner(minutesModel); - minutesSpinner.setPreferredSize(new Dimension(60, minutesSpinner.getPreferredSize().height)); - minutesSpinner.setEnabled(false); - add(minutesSpinner); - - JLabel minutesLabel = new JLabel("min"); - minutesLabel.setForeground(Color.WHITE); - add(minutesLabel); - - // Seconds spinner - SpinnerNumberModel secondsModel = new SpinnerNumberModel(0, 0, 59, 1); - secondsSpinner = new JSpinner(secondsModel); - secondsSpinner.setPreferredSize(new Dimension(60, secondsSpinner.getPreferredSize().height)); - secondsSpinner.setEnabled(false); - add(secondsSpinner); - - JLabel secondsLabel = new JLabel("sec"); - secondsLabel.setForeground(Color.WHITE); - add(secondsLabel); - - // Enable/disable spinners based on checkbox - initialDelayCheckBox.addActionListener(e -> { - boolean selected = initialDelayCheckBox.isSelected(); - hoursSpinner.setEnabled(selected); - minutesSpinner.setEnabled(selected); - secondsSpinner.setEnabled(selected); - }); - - // Add overflow logic for spinners - addSpinnerOverflowLogic(); - } - - private void addSpinnerOverflowLogic() { - // Seconds overflow - secondsSpinner.addChangeListener(e -> { - int seconds = (int) secondsSpinner.getValue(); - if (seconds > 59) { - secondsSpinner.setValue(0); - minutesSpinner.setValue((int) minutesSpinner.getValue() + 1); - } - }); - - // Minutes overflow - minutesSpinner.addChangeListener(e -> { - int minutes = (int) minutesSpinner.getValue(); - if (minutes > 59) { - minutesSpinner.setValue(0); - hoursSpinner.setValue((int) hoursSpinner.getValue() + 1); - } - }); - } - - /** - * Gets whether the initial delay is enabled. - * - * @return True if enabled, false otherwise. - */ - public boolean isInitialDelayEnabled() { - return initialDelayCheckBox.isSelected(); - } - - /** - * Gets the configured hours. - * - * @return The hours value. - */ - public int getHours() { - return (int) hoursSpinner.getValue(); - } - - /** - * Gets the configured minutes. - * - * @return The minutes value. - */ - public int getMinutes() { - return (int) minutesSpinner.getValue(); - } - - /** - * Gets the configured seconds. - * - * @return The seconds value. - */ - public int getSeconds() { - return (int) secondsSpinner.getValue(); - } - - /** - * Gets the hours spinner component. - * - * @return The hours spinner. - */ - public JSpinner getHoursSpinner() { - return hoursSpinner; - } - - /** - * Gets the minutes spinner component. - * - * @return The minutes spinner. - */ - public JSpinner getMinutesSpinner() { - return minutesSpinner; - } - - /** - * Gets the seconds spinner component. - * - * @return The seconds spinner. - */ - public JSpinner getSecondsSpinner() { - return secondsSpinner; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/IntervalPickerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/IntervalPickerPanel.java deleted file mode 100644 index ffd625a8a1b..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/IntervalPickerPanel.java +++ /dev/null @@ -1,583 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.components; - -import net.runelite.client.plugins.microbot.pluginscheduler.condition.time.IntervalCondition; -import net.runelite.client.ui.ColorScheme; -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import java.awt.*; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - -/** - * A reusable panel component for configuring time intervals with optional randomization. - * Allows setting fixed intervals or randomized intervals with min/max values. - */ -public class IntervalPickerPanel extends JPanel { - private JRadioButton fixedRadioButton; - private JRadioButton randomizedRadioButton; - private JPanel fixedPanel; - private JPanel randomizedPanel; - private JSpinner hoursSpinner; - private JSpinner minutesSpinner; - private JSpinner minHoursSpinner; - private JSpinner minMinutesSpinner; - private JSpinner maxHoursSpinner; - private JSpinner maxMinutesSpinner; - private JPanel presetPanel; - private JPanel randomPresetPanel; - private JComboBox fixedPresetComboBox; - private JComboBox randomPresetComboBox; - private List> changeListeners = new ArrayList<>(); - - /** - * Creates a new IntervalPickerPanel with default values - */ - public IntervalPickerPanel() { - this(true); - } - - /** - * Creates a new IntervalPickerPanel - * - * @param includePresets Whether to include preset buttons - */ - public IntervalPickerPanel(boolean includePresets) { - setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setBorder(new EmptyBorder(5, 5, 5, 5)); - - initComponents(includePresets); - } - - private void initComponents(boolean includePresets) { - // Interval type selection (fixed vs randomized) - JPanel typePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - typePanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - ButtonGroup typeGroup = new ButtonGroup(); - - fixedRadioButton = new JRadioButton("Fixed Interval"); - fixedRadioButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - fixedRadioButton.setForeground(Color.WHITE); - fixedRadioButton.setSelected(true); - - randomizedRadioButton = new JRadioButton("Random Interval"); - randomizedRadioButton.setBackground(ColorScheme.DARKER_GRAY_COLOR); - randomizedRadioButton.setForeground(Color.WHITE); - - typeGroup.add(fixedRadioButton); - typeGroup.add(randomizedRadioButton); - - typePanel.add(fixedRadioButton); - typePanel.add(randomizedRadioButton); - - add(typePanel); - add(Box.createVerticalStrut(5)); - - // Fixed interval panel - fixedPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - fixedPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel hoursLabel = new JLabel("Hours:"); - hoursLabel.setForeground(Color.WHITE); - - SpinnerNumberModel hoursModel = new SpinnerNumberModel(0, 0, 24, 1); - hoursSpinner = new JSpinner(hoursModel); - hoursSpinner.setPreferredSize(new Dimension(60, hoursSpinner.getPreferredSize().height)); - - JLabel minutesLabel = new JLabel("Minutes:"); - minutesLabel.setForeground(Color.WHITE); - - SpinnerNumberModel minutesModel = new SpinnerNumberModel(30, 0, 59, 1); - minutesSpinner = new JSpinner(minutesModel); - minutesSpinner.setPreferredSize(new Dimension(60, minutesSpinner.getPreferredSize().height)); - - fixedPanel.add(hoursLabel); - fixedPanel.add(hoursSpinner); - fixedPanel.add(minutesLabel); - fixedPanel.add(minutesSpinner); - - add(fixedPanel); - - // Randomized interval panel - randomizedPanel = new JPanel(); - randomizedPanel.setLayout(new BoxLayout(randomizedPanel, BoxLayout.Y_AXIS)); - randomizedPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Min interval - JPanel minPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - minPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel minLabel = new JLabel("Min Interval - Hours:"); - minLabel.setForeground(Color.WHITE); - - SpinnerNumberModel minHoursModel = new SpinnerNumberModel(0, 0, 24, 1); - minHoursSpinner = new JSpinner(minHoursModel); - minHoursSpinner.setPreferredSize(new Dimension(60, minHoursSpinner.getPreferredSize().height)); - - JLabel minMinutesLabel = new JLabel("Minutes:"); - minMinutesLabel.setForeground(Color.WHITE); - - SpinnerNumberModel minMinutesModel = new SpinnerNumberModel(30, 0, 59, 1); - minMinutesSpinner = new JSpinner(minMinutesModel); - minMinutesSpinner.setPreferredSize(new Dimension(60, minMinutesSpinner.getPreferredSize().height)); - - minPanel.add(minLabel); - minPanel.add(minHoursSpinner); - minPanel.add(minMinutesLabel); - minPanel.add(minMinutesSpinner); - - // Max interval - JPanel maxPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - maxPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel maxLabel = new JLabel("Max Interval - Hours:"); - maxLabel.setForeground(Color.WHITE); - - SpinnerNumberModel maxHoursModel = new SpinnerNumberModel(1, 0, 24, 1); - maxHoursSpinner = new JSpinner(maxHoursModel); - maxHoursSpinner.setPreferredSize(new Dimension(60, maxHoursSpinner.getPreferredSize().height)); - - JLabel maxMinutesLabel = new JLabel("Minutes:"); - maxMinutesLabel.setForeground(Color.WHITE); - - SpinnerNumberModel maxMinutesModel = new SpinnerNumberModel(0, 0, 59, 1); - maxMinutesSpinner = new JSpinner(maxMinutesModel); - maxMinutesSpinner.setPreferredSize(new Dimension(60, maxMinutesSpinner.getPreferredSize().height)); - - maxPanel.add(maxLabel); - maxPanel.add(maxHoursSpinner); - maxPanel.add(maxMinutesLabel); - maxPanel.add(maxMinutesSpinner); - - randomizedPanel.add(minPanel); - randomizedPanel.add(maxPanel); - randomizedPanel.setVisible(false); - - add(randomizedPanel); - - // Add presets if requested - if (includePresets) { - add(Box.createVerticalStrut(5)); - - // Fixed presets - presetPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - presetPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel presetsLabel = new JLabel("Presets:"); - presetsLabel.setForeground(Color.WHITE); - presetPanel.add(presetsLabel); - - String[][] presets = { - {"Select a preset...", "0", "0"}, - {"15m", "0", "15"}, - {"30m", "0", "30"}, - {"45m", "0", "45"}, - {"1h", "1", "0"}, - {"1h30m", "1", "30"}, - {"2h", "2", "0"}, - {"2h30m", "2", "30"}, - {"3h", "3", "0"}, - {"3h30m", "3", "30"}, - {"4h", "4", "0"}, - {"4h30m", "4", "30"}, - {"6h", "6", "0"} - }; - - fixedPresetComboBox = new JComboBox<>(); - for (String[] preset : presets) { - fixedPresetComboBox.addItem(preset[0]); - } - fixedPresetComboBox.setBackground(ColorScheme.DARK_GRAY_COLOR); - fixedPresetComboBox.setForeground(Color.WHITE); - fixedPresetComboBox.setPreferredSize(new Dimension(150, fixedPresetComboBox.getPreferredSize().height)); - fixedPresetComboBox.addActionListener(e -> { - int selectedIndex = fixedPresetComboBox.getSelectedIndex(); - if (selectedIndex > 0) { // Skip the "Select a preset..." option - fixedRadioButton.setSelected(true); - updatePanelVisibility(); - - // Apply preset values - String[] preset = presets[selectedIndex]; - hoursSpinner.setValue(Integer.parseInt(preset[1])); - minutesSpinner.setValue(Integer.parseInt(preset[2])); - - // Also set reasonable min/max values - setMinMaxFromFixed(); - - // Notify listeners - notifyChangeListeners(); - } - }); - presetPanel.add(fixedPresetComboBox); - - add(presetPanel); - - // Random presets - randomPresetPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - randomPresetPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel randomPresetsLabel = new JLabel("Random Presets:"); - randomPresetsLabel.setForeground(Color.WHITE); - randomPresetPanel.add(randomPresetsLabel); - - String[][] randomPresets = { - {"Select a preset...", "0", "0", "0", "0"}, - {"1-5m", "0", "1", "0", "5"}, - {"5-10m", "0", "5", "0", "10"}, - {"10-15m", "0", "10", "0", "15"}, - {"15-30m", "0", "15", "0", "30"}, - {"30-60m", "0", "30", "1", "0"}, - {"45m-1h15m", "0", "45", "1", "15"}, - {"1-2h", "1", "0", "2", "0"}, - {"1-3h", "1", "0", "3", "0"}, - {"2-3h", "2", "0", "3", "0"}, - {"2-4h", "2", "0", "4", "0"}, - }; - - randomPresetComboBox = new JComboBox<>(); - for (String[] preset : randomPresets) { - randomPresetComboBox.addItem(preset[0]); - } - randomPresetComboBox.setBackground(ColorScheme.DARK_GRAY_COLOR); - randomPresetComboBox.setForeground(Color.WHITE); - randomPresetComboBox.setPreferredSize(new Dimension(150, randomPresetComboBox.getPreferredSize().height)); - randomPresetComboBox.addActionListener(e -> { - int selectedIndex = randomPresetComboBox.getSelectedIndex(); - if (selectedIndex > 0) { // Skip the "Select a preset..." option - randomizedRadioButton.setSelected(true); - updatePanelVisibility(); - - // Apply preset values to min - String[] preset = randomPresets[selectedIndex]; - minHoursSpinner.setValue(Integer.parseInt(preset[1])); - minMinutesSpinner.setValue(Integer.parseInt(preset[2])); - - // Apply preset values to max - maxHoursSpinner.setValue(Integer.parseInt(preset[3])); - maxMinutesSpinner.setValue(Integer.parseInt(preset[4])); - - // Set fixed to average - setFixedFromMinMax(); - - // Notify listeners - notifyChangeListeners(); - } - }); - randomPresetPanel.add(randomPresetComboBox); - - randomPresetPanel.setVisible(false); - add(randomPresetPanel); - } - - // Add toggle listeners - fixedRadioButton.addActionListener(e -> { - updatePanelVisibility(); - notifyChangeListeners(); - }); - - randomizedRadioButton.addActionListener(e -> { - updatePanelVisibility(); - - // Initialize min/max values if switching to random - if (randomizedRadioButton.isSelected()) { - setMinMaxFromFixed(); - } else { - setFixedFromMinMax(); - } - - notifyChangeListeners(); - }); - - // Add spinner change listeners - hoursSpinner.addChangeListener(e -> { - if (fixedRadioButton.isSelected()) { - setMinMaxFromFixed(); - } - notifyChangeListeners(); - }); - - minutesSpinner.addChangeListener(e -> { - if (fixedRadioButton.isSelected()) { - setMinMaxFromFixed(); - } - notifyChangeListeners(); - }); - - // Add min/max validation - minHoursSpinner.addChangeListener(e -> { - validateMinMaxInterval(true); - if (randomizedRadioButton.isSelected()) { - setFixedFromMinMax(); - } - notifyChangeListeners(); - }); - - minMinutesSpinner.addChangeListener(e -> { - validateMinMaxInterval(true); - if (randomizedRadioButton.isSelected()) { - setFixedFromMinMax(); - } - notifyChangeListeners(); - }); - - maxHoursSpinner.addChangeListener(e -> { - validateMinMaxInterval(false); - if (randomizedRadioButton.isSelected()) { - setFixedFromMinMax(); - } - notifyChangeListeners(); - }); - - maxMinutesSpinner.addChangeListener(e -> { - validateMinMaxInterval(false); - if (randomizedRadioButton.isSelected()) { - setFixedFromMinMax(); - } - notifyChangeListeners(); - }); - } - - /** - * Updates panel visibility based on the selected radio button - */ - private void updatePanelVisibility() { - boolean useFixed = fixedRadioButton.isSelected(); - fixedPanel.setVisible(useFixed); - randomizedPanel.setVisible(!useFixed); - - if (presetPanel != null && randomPresetPanel != null) { - presetPanel.setVisible(useFixed); - randomPresetPanel.setVisible(!useFixed); - - // Reset combo boxes to first item when switching modes - if (useFixed && fixedPresetComboBox != null) { - fixedPresetComboBox.setSelectedIndex(0); - } else if (!useFixed && randomPresetComboBox != null) { - randomPresetComboBox.setSelectedIndex(0); - } - } - - revalidate(); - repaint(); - } - - /** - * Validates that min <= max for the interval and adjusts if needed - * - * @param isMinUpdated Whether the min value was updated (to determine which value to adjust) - */ - private void validateMinMaxInterval(boolean isMinUpdated) { - int minHours = (Integer) minHoursSpinner.getValue(); - int minMinutes = (Integer) minMinutesSpinner.getValue(); - int maxHours = (Integer) maxHoursSpinner.getValue(); - int maxMinutes = (Integer) maxMinutesSpinner.getValue(); - - int minTotalMinutes = minHours * 60 + minMinutes; - int maxTotalMinutes = maxHours * 60 + maxMinutes; - - if (isMinUpdated) { - // If min was updated and exceeds max, adjust max - if (minTotalMinutes > maxTotalMinutes) { - maxHoursSpinner.setValue(minHours); - maxMinutesSpinner.setValue(minMinutes); - } - } else { - // If max was updated and is less than min, adjust min - if (maxTotalMinutes < minTotalMinutes) { - minHoursSpinner.setValue(maxHours); - minMinutesSpinner.setValue(maxMinutes); - } - } - } - - /** - * Sets min/max values based on the fixed value (with some variation) - */ - private void setMinMaxFromFixed() { - int hours = (Integer) hoursSpinner.getValue(); - int minutes = (Integer) minutesSpinner.getValue(); - int totalMinutes = hours * 60 + minutes; - - // Set min to ~70% of fixed value - int minTotalMinutes = Math.max(1, (int)(totalMinutes * 0.7)); - minHoursSpinner.setValue(minTotalMinutes / 60); - minMinutesSpinner.setValue(minTotalMinutes % 60); - - // Set max to ~130% of fixed value - int maxTotalMinutes = (int)(totalMinutes * 1.3); - maxHoursSpinner.setValue(maxTotalMinutes / 60); - maxMinutesSpinner.setValue(maxTotalMinutes % 60); - } - - /** - * Sets the fixed value based on the average of min and max - */ - private void setFixedFromMinMax() { - int minHours = (Integer) minHoursSpinner.getValue(); - int minMinutes = (Integer) minMinutesSpinner.getValue(); - int maxHours = (Integer) maxHoursSpinner.getValue(); - int maxMinutes = (Integer) maxMinutesSpinner.getValue(); - - int minTotalMinutes = minHours * 60 + minMinutes; - int maxTotalMinutes = maxHours * 60 + maxMinutes; - int avgTotalMinutes = (minTotalMinutes + maxTotalMinutes) / 2; - - hoursSpinner.setValue(avgTotalMinutes / 60); - minutesSpinner.setValue(avgTotalMinutes % 60); - } - - /** - * Creates an IntervalCondition based on the current settings - * - * @return The configured IntervalCondition - */ - public IntervalCondition createIntervalCondition() { - boolean useFixed = fixedRadioButton.isSelected(); - - if (useFixed) { - // Get fixed duration - int hours = (Integer) hoursSpinner.getValue(); - int minutes = (Integer) minutesSpinner.getValue(); - Duration duration = Duration.ofHours(hours).plusMinutes(minutes); - - return new IntervalCondition(duration); - } else { - // Get min/max durations for randomized interval - int minHours = (Integer) minHoursSpinner.getValue(); - int minMinutes = (Integer) minMinutesSpinner.getValue(); - int maxHours = (Integer) maxHoursSpinner.getValue(); - int maxMinutes = (Integer) maxMinutesSpinner.getValue(); - - Duration minDuration = Duration.ofHours(minHours).plusMinutes(minMinutes); - Duration maxDuration = Duration.ofHours(maxHours).plusMinutes(maxMinutes); - - return IntervalCondition.createRandomized(minDuration, maxDuration); - } - } - - /** - * Configures this panel based on an existing IntervalCondition - * - * @param condition The interval condition to use for configuration - */ - public void setIntervalCondition(IntervalCondition condition) { - if (condition == null) { - return; - } - - // Check if this is a randomized min-max interval or a fixed interval - boolean isRandomized = condition.isRandomize(); - - if (isRandomized) { - // Set to randomized mode - randomizedRadioButton.setSelected(true); - - // Get duration values for min interval - long minTotalMinutes = condition.getMinInterval().toMinutes(); - long minHours = minTotalMinutes / 60; - long minMinutes = minTotalMinutes % 60; - - // Get duration values for max interval - long maxTotalMinutes = condition.getMaxInterval().toMinutes(); - long maxHours = maxTotalMinutes / 60; - long maxMinutes = maxTotalMinutes % 60; - - // Set values on spinners - minHoursSpinner.setValue((int)minHours); - minMinutesSpinner.setValue((int)minMinutes); - maxHoursSpinner.setValue((int)maxHours); - maxMinutesSpinner.setValue((int)maxMinutes); - - // Also update the fixed spinner with the average value - long avgTotalMinutes = (minTotalMinutes + maxTotalMinutes) / 2; - hoursSpinner.setValue((int)(avgTotalMinutes / 60)); - minutesSpinner.setValue((int)(avgTotalMinutes % 60)); - } else { - // Use fixed mode - fixedRadioButton.setSelected(true); - - // Get duration values from the base interval - long totalMinutes = condition.getInterval().toMinutes(); - long hours = totalMinutes / 60; - long minutes = totalMinutes % 60; - - // Set values on fixed spinners - hoursSpinner.setValue((int)hours); - minutesSpinner.setValue((int)minutes); - - // Set min/max values based on fixed value - setMinMaxFromFixed(); - } - - // Update panel visibility - updatePanelVisibility(); - } - - /** - * Adds a change listener that will be notified when the interval configuration changes - * - * @param listener A consumer that receives the updated IntervalCondition - */ - public void addChangeListener(Consumer listener) { - changeListeners.add(listener); - } - - /** - * Notifies all change listeners with the current interval condition - */ - private void notifyChangeListeners() { - IntervalCondition condition = createIntervalCondition(); - for (Consumer listener : changeListeners) { - listener.accept(condition); - } - } - - /** - * Gets the current fixed hours value - */ - public int getFixedHours() { - return (Integer) hoursSpinner.getValue(); - } - - /** - * Gets the current fixed minutes value - */ - public int getFixedMinutes() { - return (Integer) minutesSpinner.getValue(); - } - - /** - * Gets whether randomized interval mode is selected - */ - public boolean isRandomized() { - return randomizedRadioButton.isSelected(); - } - - /** - * Sets the enabled state of all components - */ - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - fixedRadioButton.setEnabled(enabled); - randomizedRadioButton.setEnabled(enabled); - hoursSpinner.setEnabled(enabled && fixedRadioButton.isSelected()); - minutesSpinner.setEnabled(enabled && fixedRadioButton.isSelected()); - minHoursSpinner.setEnabled(enabled && randomizedRadioButton.isSelected()); - minMinutesSpinner.setEnabled(enabled && randomizedRadioButton.isSelected()); - maxHoursSpinner.setEnabled(enabled && randomizedRadioButton.isSelected()); - maxMinutesSpinner.setEnabled(enabled && randomizedRadioButton.isSelected()); - - // Update preset components if they exist - if (fixedPresetComboBox != null) { - fixedPresetComboBox.setEnabled(enabled); - } - - if (randomPresetComboBox != null) { - randomPresetComboBox.setEnabled(enabled); - } - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/SingleDateTimePickerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/SingleDateTimePickerPanel.java deleted file mode 100644 index 29e68a739a1..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/SingleDateTimePickerPanel.java +++ /dev/null @@ -1,136 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.components; - -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import java.awt.*; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.util.function.Consumer; - -/** - * A panel for selecting a single date and time with presets - */ -public class SingleDateTimePickerPanel extends JPanel { - private final DateTimePickerPanel dateTimePicker; - private Consumer dateTimeChangeListener; - - public SingleDateTimePickerPanel() { - this(LocalDateTime.now().plusHours(1)); // Default to one hour from now - } - - public SingleDateTimePickerPanel(LocalDateTime initialDateTime) { - setLayout(new BorderLayout()); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setBorder(new EmptyBorder(5, 5, 5, 5)); - - dateTimePicker = new DateTimePickerPanel(initialDateTime.toLocalDate(), initialDateTime.toLocalTime()); - - // Create a main panel with title - JPanel mainPanel = new JPanel(new BorderLayout(0, 10)); - mainPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel titleLabel = new JLabel("Select Date and Time:"); - titleLabel.setForeground(Color.WHITE); - titleLabel.setFont(FontManager.getRunescapeSmallFont().deriveFont(Font.BOLD)); - - mainPanel.add(titleLabel, BorderLayout.NORTH); - mainPanel.add(dateTimePicker, BorderLayout.CENTER); - - // Add presets panel - JPanel presetsPanel = createPresetsPanel(); - mainPanel.add(presetsPanel, BorderLayout.SOUTH); - - add(mainPanel, BorderLayout.CENTER); - - // Set up change listener - dateTimePicker.setDateTimeChangeListener(this::notifyDateTimeChanged); - } - - private JPanel createPresetsPanel() { - JPanel panel = new JPanel(); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - panel.setBorder(new EmptyBorder(10, 0, 0, 0)); - - JLabel presetsLabel = new JLabel("Quick Presets:"); - presetsLabel.setForeground(Color.WHITE); - presetsLabel.setAlignmentX(Component.LEFT_ALIGNMENT); - panel.add(presetsLabel); - - // Flow panel for buttons - JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 5)); - buttonsPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - buttonsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - - LocalDateTime now = LocalDateTime.now(); - - // Common time presets - addPresetButton(buttonsPanel, "In 1 hour", now.plusHours(1)); - addPresetButton(buttonsPanel, "In 3 hours", now.plusHours(3)); - addPresetButton(buttonsPanel, "Tomorrow", now.plusDays(1).withHour(9).withMinute(0)); - addPresetButton(buttonsPanel, "This evening", now.withHour(19).withMinute(0)); - addPresetButton(buttonsPanel, "Next week", now.plusWeeks(1)); - addPresetButton(buttonsPanel, "Next month", now.plusMonths(1)); - - panel.add(buttonsPanel); - return panel; - } - - private void addPresetButton(JPanel panel, String label, LocalDateTime dateTime) { - JButton button = new JButton(label); - button.setFocusPainted(false); - button.setBackground(ColorScheme.DARK_GRAY_COLOR); - button.setForeground(Color.WHITE); - button.setFont(FontManager.getRunescapeSmallFont()); - button.addActionListener(e -> setDateTime(dateTime)); - panel.add(button); - } - - private void notifyDateTimeChanged(LocalDateTime dateTime) { - if (dateTimeChangeListener != null) { - dateTimeChangeListener.accept(dateTime); - } - } - - public LocalDateTime getDateTime() { - return dateTimePicker.getDateTime(); - } - - public void setDateTime(LocalDateTime dateTime) { - if (dateTime == null) { - // Create a default time (1 hour from now) - LocalDateTime defaultDateTime = LocalDateTime.now().plusHours(1); - try { - // Try using individual setters if available - dateTimePicker.setDate(LocalDate.now()); - dateTimePicker.setTime( - Integer.valueOf(defaultDateTime.getHour()), - Integer.valueOf(defaultDateTime.getMinute()) - ); - } catch (Exception e) { - // Fall back to the combined setter if individual ones aren't available - dateTimePicker.setDateTime(defaultDateTime); - } - } else { - try { - // Try using individual setters if available - dateTimePicker.setDate(dateTime.toLocalDate()); - dateTimePicker.setTime( - Integer.valueOf(dateTime.getHour()), - Integer.valueOf(dateTime.getMinute()) - ); - } catch (Exception e) { - // Fall back to the combined setter - dateTimePicker.setDateTime(dateTime); - } - } - } - - public void setDateTimeChangeListener(Consumer listener) { - this.dateTimeChangeListener = listener; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/TimePickerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/TimePickerPanel.java deleted file mode 100644 index ed871675375..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/TimePickerPanel.java +++ /dev/null @@ -1,184 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.components; - -import net.runelite.client.ui.ColorScheme; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import java.awt.*; - -import java.time.LocalTime; -import java.time.format.DateTimeFormatter; -import java.util.function.Consumer; -import java.awt.event.FocusEvent; -import java.awt.event.FocusAdapter; -/** - * A custom time picker component with hours and minutes selection - */ -public class TimePickerPanel extends JPanel { - private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm"); - private final JTextField timeField; // Changed from JFormattedTextField - private LocalTime selectedTime; - private Consumer timeChangeListener; - - public TimePickerPanel() { - this(LocalTime.of(9, 0)); // Default to 9:00 - } - - public TimePickerPanel(LocalTime initialTime) { - this.selectedTime = initialTime; - setLayout(new BorderLayout(5, 0)); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setBorder(new EmptyBorder(0, 0, 0, 0)); - - // Create a regular text field instead of formatted - timeField = new JTextField(selectedTime.format(timeFormatter)); - timeField.setForeground(Color.WHITE); - timeField.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - timeField.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - BorderFactory.createEmptyBorder(2, 5, 2, 5))); - - // Button to show time picker popup - JButton timeButton = new JButton("🕒"); - timeButton.setFocusPainted(false); - timeButton.setForeground(Color.WHITE); - timeButton.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - timeButton.setPreferredSize(new Dimension(30, timeField.getPreferredSize().height)); - timeButton.addActionListener(e -> showTimePickerPopup()); - - add(timeField, BorderLayout.CENTER); - add(timeButton, BorderLayout.EAST); - - // Update time when text changes - timeField.addActionListener(e -> { - try { - String text = timeField.getText(); - LocalTime parsedTime = LocalTime.parse(text, timeFormatter); - setSelectedTime(parsedTime); - } catch (Exception ex) { - // Reset to current value if parsing fails - timeField.setText(selectedTime.format(timeFormatter)); - } - }); - - // Add a focus listener to validate when the field loses focus - timeField.addFocusListener(new FocusAdapter() { - @Override - public void focusLost(FocusEvent e) { - try { - String text = timeField.getText(); - LocalTime parsedTime = LocalTime.parse(text, timeFormatter); - setSelectedTime(parsedTime); - } catch (Exception ex) { - // Reset to current value if parsing fails - timeField.setText(selectedTime.format(timeFormatter)); - } - } - }); - } - - private void showTimePickerPopup() { - JPopupMenu popup = new JPopupMenu(); - popup.setBorder(BorderFactory.createLineBorder(ColorScheme.DARK_GRAY_COLOR)); - - JPanel contentPanel = new JPanel(); - contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); - contentPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); - - // Hour and minute spinners - JPanel timeControls = new JPanel(new FlowLayout()); - timeControls.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Hour spinner (0-23) - SpinnerNumberModel hourModel = new SpinnerNumberModel(selectedTime.getHour(), 0, 23, 1); - JSpinner hourSpinner = new JSpinner(hourModel); - JComponent hourEditor = new JSpinner.NumberEditor(hourSpinner, "00"); - hourSpinner.setEditor(hourEditor); - hourSpinner.setPreferredSize(new Dimension(60, 30)); - - // Minute spinner (0-59) - SpinnerNumberModel minuteModel = new SpinnerNumberModel(selectedTime.getMinute(), 0, 59, 1); - JSpinner minuteSpinner = new JSpinner(minuteModel); - JComponent minuteEditor = new JSpinner.NumberEditor(minuteSpinner, "00"); - minuteSpinner.setEditor(minuteEditor); - minuteSpinner.setPreferredSize(new Dimension(60, 30)); - - JLabel colonLabel = new JLabel(":"); - colonLabel.setForeground(Color.WHITE); - - timeControls.add(hourSpinner); - timeControls.add(colonLabel); - timeControls.add(minuteSpinner); - - // Common time presets - JPanel presets = new JPanel(new GridLayout(2, 3, 5, 5)); - presets.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - addTimePresetButton(presets, "00:00", popup); - addTimePresetButton(presets, "06:00", popup); - addTimePresetButton(presets, "09:00", popup); - addTimePresetButton(presets, "12:00", popup); - addTimePresetButton(presets, "18:00", popup); - addTimePresetButton(presets, "22:00", popup); - - // Apply button - JButton applyButton = new JButton("Apply"); - applyButton.setFocusPainted(false); - applyButton.setBackground(ColorScheme.PROGRESS_COMPLETE_COLOR); - applyButton.setForeground(Color.WHITE); - applyButton.addActionListener(e -> { - int hour = (Integer) hourSpinner.getValue(); - int minute = (Integer) minuteSpinner.getValue(); - setSelectedTime(LocalTime.of(hour, minute)); - popup.setVisible(false); - }); - - contentPanel.add(timeControls); - contentPanel.add(Box.createVerticalStrut(10)); - contentPanel.add(new JLabel("Presets:")); - contentPanel.add(presets); - contentPanel.add(Box.createVerticalStrut(10)); - contentPanel.add(applyButton); - - popup.add(contentPanel); - popup.show(this, 0, this.getHeight()); - } - - private void addTimePresetButton(JPanel panel, String timeText, JPopupMenu popup) { - JButton button = new JButton(timeText); - button.setFocusPainted(false); - button.setBackground(ColorScheme.DARK_GRAY_COLOR); - button.setForeground(Color.WHITE); - button.addActionListener(e -> { - setSelectedTime(LocalTime.parse(timeText)); - popup.setVisible(false); - }); - panel.add(button); - } - - public LocalTime getSelectedTime() { - return selectedTime; - } - - public void setSelectedTime(LocalTime time) { - this.selectedTime = time; - timeField.setText(time.format(timeFormatter)); - - if (timeChangeListener != null) { - timeChangeListener.accept(time); - } - } - - public void setTimeChangeListener(Consumer listener) { - this.timeChangeListener = listener; - } - - public void setEditable(boolean editable) { - timeField.setEditable(editable); - } - - public JTextField getTextField() { - return timeField; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/TimeRangePanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/TimeRangePanel.java deleted file mode 100644 index f2a5d232d8b..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/components/TimeRangePanel.java +++ /dev/null @@ -1,184 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.components; - -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import java.awt.*; -import java.time.LocalTime; -import java.util.function.BiConsumer; - -/** - * A panel for selecting a time range with start and end times - */ -public class TimeRangePanel extends JPanel { - private final TimePickerPanel startTimePicker; - private final TimePickerPanel endTimePicker; - private BiConsumer rangeChangeListener; - - public TimeRangePanel() { - this(LocalTime.of(9, 0), LocalTime.of(17, 0)); - } - - public TimeRangePanel(LocalTime startTime, LocalTime endTime) { - setLayout(new GridBagLayout()); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setBorder(new EmptyBorder(5, 5, 5, 5)); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 0; - gbc.anchor = GridBagConstraints.WEST; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.insets = new Insets(2, 2, 2, 2); - - JLabel startLabel = new JLabel("Start Time:"); - startLabel.setForeground(Color.WHITE); - startLabel.setFont(FontManager.getRunescapeSmallFont()); - add(startLabel, gbc); - - gbc.gridx = 1; - startTimePicker = new TimePickerPanel(startTime); - add(startTimePicker, gbc); - - gbc.gridx = 0; - gbc.gridy = 1; - JLabel endLabel = new JLabel("End Time:"); - endLabel.setForeground(Color.WHITE); - endLabel.setFont(FontManager.getRunescapeSmallFont()); - add(endLabel, gbc); - - gbc.gridx = 1; - endTimePicker = new TimePickerPanel(endTime); - add(endTimePicker, gbc); - - // Add common presets panel - gbc.gridx = 0; - gbc.gridy = 2; - gbc.gridwidth = 2; - gbc.insets = new Insets(10, 2, 2, 2); - - JPanel presetsPanel = createPresetsPanel(); - add(presetsPanel, gbc); - - // Add specialized time window presets - gbc.gridy = 3; - JPanel specialPresetsPanel = createSpecializedPresetsPanel(); - add(specialPresetsPanel, gbc); - - // Set up change listeners - startTimePicker.setTimeChangeListener(t -> notifyRangeChanged()); - endTimePicker.setTimeChangeListener(t -> notifyRangeChanged()); - } - - private JPanel createPresetsPanel() { - JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel presetsLabel = new JLabel("Quick Presets:"); - presetsLabel.setForeground(Color.WHITE); - panel.add(presetsLabel); - - // Common time range presets - addPresetButton(panel, "Morning (6-12)", LocalTime.of(6, 0), LocalTime.of(12, 0)); - addPresetButton(panel, "Afternoon (12-18)", LocalTime.of(12, 0), LocalTime.of(18, 0)); - addPresetButton(panel, "Evening (18-22)", LocalTime.of(18, 0), LocalTime.of(22, 0)); - addPresetButton(panel, "Night (22-6)", LocalTime.of(22, 0), LocalTime.of(6, 0)); - addPresetButton(panel, "Business Hours", LocalTime.of(9, 0), LocalTime.of(17, 0)); - - return panel; - } - - private JPanel createSpecializedPresetsPanel() { - JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - JLabel specialPresetsLabel = new JLabel("Special Presets:"); - specialPresetsLabel.setForeground(Color.WHITE); - panel.add(specialPresetsLabel); - - // Special presets for all day, start of day, and end of day - addSpecialPresetButton(panel, "All Day", LocalTime.of(0, 0), LocalTime.of(23, 59)); - addSpecialPresetButton(panel, "From Start of Day", LocalTime.of(0, 0), endTimePicker.getSelectedTime()); - addSpecialPresetButton(panel, "Until End of Day", startTimePicker.getSelectedTime(), LocalTime.of(23, 59)); - - return panel; - } - - private void addPresetButton(JPanel panel, String label, LocalTime start, LocalTime end) { - JButton button = new JButton(label); - button.setFocusPainted(false); - button.setBackground(ColorScheme.DARK_GRAY_COLOR); - button.setForeground(Color.WHITE); - button.setFont(FontManager.getRunescapeSmallFont()); - button.addActionListener(e -> { - startTimePicker.setSelectedTime(start); - endTimePicker.setSelectedTime(end); - }); - panel.add(button); - } - - private void addSpecialPresetButton(JPanel panel, String label, LocalTime start, LocalTime end) { - JButton button = new JButton(label); - button.setFocusPainted(false); - button.setBackground(ColorScheme.LIGHT_GRAY_COLOR); - button.setForeground(Color.BLACK); - button.setFont(FontManager.getRunescapeSmallFont()); - button.addActionListener(e -> { - if (label.equals("From Start of Day")) { - // Only update start time, keep current end time - startTimePicker.setSelectedTime(start); - // No need to update endTimePicker as we want to keep the current end time - } else if (label.equals("Until End of Day")) { - // Only update end time, keep current start time - endTimePicker.setSelectedTime(end); - // No need to update startTimePicker as we want to keep the current start time - } else { - // Update both times - startTimePicker.setSelectedTime(start); - endTimePicker.setSelectedTime(end); - } - }); - panel.add(button); - } - - private void notifyRangeChanged() { - if (rangeChangeListener != null) { - rangeChangeListener.accept(getStartTime(), getEndTime()); - } - } - - public LocalTime getStartTime() { - return startTimePicker.getSelectedTime(); - } - - public LocalTime getEndTime() { - return endTimePicker.getSelectedTime(); - } - - public void setStartTime(LocalTime time) { - startTimePicker.setSelectedTime(time); - } - - public void setEndTime(LocalTime time) { - endTimePicker.setSelectedTime(time); - } - - public void setRangeChangeListener(BiConsumer listener) { - this.rangeChangeListener = listener; - } - - public void setEditable(boolean editable) { - startTimePicker.setEditable(editable); - endTimePicker.setEditable(editable); - } - - public TimePickerPanel getStartTimePicker() { - return startTimePicker; - } - - public TimePickerPanel getEndTimePicker() { - return endTimePicker; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/layout/DynamicFlowLayout.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/layout/DynamicFlowLayout.java deleted file mode 100644 index 8fcf38cd307..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/layout/DynamicFlowLayout.java +++ /dev/null @@ -1,300 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.layout; - -import java.awt.*; -import javax.swing.JComponent; - -/** - * A custom FlowLayout that provides better control over wrapping, spacing, and dynamic sizing. - * This layout automatically adjusts component arrangements based on available space and content. - * - * Features: - * - Intelligent wrapping when space is limited - * - Dynamic spacing based on container size - * - Better minimum size calculations - * - Content-aware layout decisions - * - * @author Vox - */ -public class DynamicFlowLayout extends FlowLayout { - - private int minRowHeight = 120; - private int preferredRowHeight = 140; - private boolean adaptiveSpacing = true; - - /** - * Creates a new DynamicFlowLayout with default settings - */ - public DynamicFlowLayout() { - super(FlowLayout.CENTER, 8, 5); - } - - /** - * Creates a new DynamicFlowLayout with specified alignment and spacing - * - * @param align the alignment value - * @param hgap the horizontal gap between components - * @param vgap the vertical gap between components - */ - public DynamicFlowLayout(int align, int hgap, int vgap) { - super(align, hgap, vgap); - } - - /** - * Sets the minimum row height for components - * - * @param minRowHeight minimum height in pixels - */ - public void setMinRowHeight(int minRowHeight) { - this.minRowHeight = minRowHeight; - } - - /** - * Sets the preferred row height for components - * - * @param preferredRowHeight preferred height in pixels - */ - public void setPreferredRowHeight(int preferredRowHeight) { - this.preferredRowHeight = preferredRowHeight; - } - - /** - * Enables or disables adaptive spacing based on container size - * - * @param adaptiveSpacing true to enable adaptive spacing - */ - public void setAdaptiveSpacing(boolean adaptiveSpacing) { - this.adaptiveSpacing = adaptiveSpacing; - } - - @Override - public Dimension preferredLayoutSize(Container target) { - synchronized (target.getTreeLock()) { - Dimension dim = new Dimension(0, 0); - int nmembers = target.getComponentCount(); - boolean firstVisibleComponent = true; - - int maxWidth = 0; - int currentRowWidth = 0; - int currentRowHeight = 0; - int totalHeight = 0; - - // Calculate container width for wrapping decisions - int containerWidth = target.getWidth(); - if (containerWidth <= 0) { - containerWidth = Integer.MAX_VALUE; // No wrapping if width unknown - } - - for (int i = 0; i < nmembers; i++) { - Component m = target.getComponent(i); - if (m.isVisible()) { - Dimension d = m.getPreferredSize(); - - // Check if we need to wrap to next row - int neededWidth = currentRowWidth + (firstVisibleComponent ? 0 : getHgap()) + d.width; - - if (!firstVisibleComponent && neededWidth > containerWidth - getHgap() * 2) { - // Wrap to next row - maxWidth = Math.max(maxWidth, currentRowWidth); - totalHeight += currentRowHeight + getVgap(); - currentRowWidth = d.width; - currentRowHeight = d.height; - } else { - // Add to current row - if (!firstVisibleComponent) { - currentRowWidth += getHgap(); - } - currentRowWidth += d.width; - currentRowHeight = Math.max(currentRowHeight, d.height); - firstVisibleComponent = false; - } - } - } - - // Add the last row - if (currentRowWidth > 0) { - maxWidth = Math.max(maxWidth, currentRowWidth); - totalHeight += currentRowHeight; - } - - Insets insets = target.getInsets(); - dim.width = maxWidth + insets.left + insets.right + getHgap() * 2; - dim.height = Math.max(totalHeight + insets.top + insets.bottom + getVgap() * 2, - preferredRowHeight); - - return dim; - } - } - - @Override - public Dimension minimumLayoutSize(Container target) { - synchronized (target.getTreeLock()) { - Dimension dim = new Dimension(0, 0); - int nmembers = target.getComponentCount(); - - if (nmembers > 0) { - // Find the widest component for minimum width - int maxComponentWidth = 0; - for (int i = 0; i < nmembers; i++) { - Component m = target.getComponent(i); - if (m.isVisible()) { - Dimension d = m.getMinimumSize(); - maxComponentWidth = Math.max(maxComponentWidth, d.width); - } - } - - Insets insets = target.getInsets(); - dim.width = maxComponentWidth + insets.left + insets.right + getHgap() * 2; - dim.height = minRowHeight + insets.top + insets.bottom + getVgap() * 2; - } - - return dim; - } - } - - @Override - public void layoutContainer(Container target) { - synchronized (target.getTreeLock()) { - Insets insets = target.getInsets(); - int maxwidth = target.getWidth() - (insets.left + insets.right + getHgap() * 2); - int nmembers = target.getComponentCount(); - int x = 0, y = insets.top + getVgap(); - int rowh = 0, start = 0; - - boolean ltr = target.getComponentOrientation().isLeftToRight(); - - // Adaptive spacing based on container width - int effectiveHgap = getHgap(); - if (adaptiveSpacing && maxwidth > 0) { - int totalComponentWidth = 0; - int visibleComponents = 0; - - for (int i = 0; i < nmembers; i++) { - Component m = target.getComponent(i); - if (m.isVisible()) { - totalComponentWidth += m.getPreferredSize().width; - visibleComponents++; - } - } - - if (visibleComponents > 1) { - int availableSpaceForGaps = maxwidth - totalComponentWidth; - int optimalGap = Math.max(5, availableSpaceForGaps / (visibleComponents + 1)); - effectiveHgap = Math.min(effectiveHgap, optimalGap); - } - } - - for (int i = 0; i < nmembers; i++) { - Component m = target.getComponent(i); - if (m.isVisible()) { - Dimension d = m.getPreferredSize(); - m.setSize(d.width, d.height); - - if ((x == 0) || ((x + d.width) <= maxwidth)) { - if (x > 0) { - x += effectiveHgap; - } - x += d.width; - rowh = Math.max(rowh, d.height); - } else { - rowh = moveComponents(target, insets.left + effectiveHgap, y, - maxwidth - x, rowh, start, i, ltr); - x = d.width; - y += getVgap() + rowh; - rowh = d.height; - start = i; - } - } - } - - moveComponents(target, insets.left + effectiveHgap, y, maxwidth - x, - rowh, start, nmembers, ltr); - } - } - - /** - * Centers the elements in the specified row, if there is any slack. - * - * @param target the component which needs to be moved - * @param x the x coordinate - * @param y the y coordinate - * @param width the width dimensions - * @param height the height dimensions - * @param rowStart the beginning of the row - * @param rowEnd the the ending of the row - * @param useBaseline Whether or not to align on baseline. - * @param ascent Ascent for the components. This is only valid if useBaseline is true. - * @param descent Ascent for the components. This is only valid if useBaseline is true. - * @return actual row height - */ - private int moveComponents(Container target, int x, int y, int width, int height, - int rowStart, int rowEnd, boolean ltr) { - switch (getAlignment()) { - case LEFT: - x += ltr ? 0 : width; - break; - case CENTER: - x += width / 2; - break; - case RIGHT: - x += ltr ? width : 0; - break; - case LEADING: - break; - case TRAILING: - x += width; - break; - } - - int maxAscent = 0; - int maxDescent = 0; - boolean useBaseline = getAlignOnBaseline(); - int[] ascent = null; - int[] descent = null; - - if (useBaseline) { - ascent = new int[rowEnd - rowStart]; - descent = new int[rowEnd - rowStart]; - for (int i = rowStart; i < rowEnd; i++) { - Component m = target.getComponent(i); - if (m.isVisible()) { - if (m instanceof JComponent) { - JComponent jc = (JComponent) m; - int baseline = jc.getBaseline(m.getWidth(), m.getHeight()); - if (baseline >= 0) { - ascent[i - rowStart] = baseline; - descent[i - rowStart] = m.getHeight() - baseline; - } else { - ascent[i - rowStart] = -1; - } - } else { - ascent[i - rowStart] = -1; - } - if (ascent[i - rowStart] >= 0) { - maxAscent = Math.max(maxAscent, ascent[i - rowStart]); - maxDescent = Math.max(maxDescent, descent[i - rowStart]); - } - } - } - height = Math.max(maxAscent + maxDescent, height); - } - - for (int i = rowStart; i < rowEnd; i++) { - Component m = target.getComponent(i); - if (m.isVisible()) { - int cy; - if (useBaseline && ascent != null && ascent[i - rowStart] >= 0) { - cy = y + maxAscent - ascent[i - rowStart]; - } else { - cy = y + (height - m.getHeight()) / 2; - } - if (ltr) { - m.setLocation(x, cy); - } else { - m.setLocation(target.getWidth() - x - m.getWidth(), cy); - } - x += m.getWidth() + getHgap(); - } - } - return height; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/util/SchedulerUIUtils.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/util/SchedulerUIUtils.java deleted file mode 100644 index c5706b81500..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/util/SchedulerUIUtils.java +++ /dev/null @@ -1,155 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.util; - -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerConfig; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.util.security.LoginManager; -import net.runelite.client.ui.ColorScheme; - -import javax.swing.*; -import javax.swing.border.CompoundBorder; -import javax.swing.border.EmptyBorder; -import javax.swing.border.LineBorder; -import javax.swing.plaf.basic.BasicComboBoxRenderer; -import javax.swing.plaf.basic.BasicComboBoxUI; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.util.function.Consumer; - -import lombok.extern.slf4j.Slf4j; - -/** - * Utility class for consistent UI styling - */ -@Slf4j -public class SchedulerUIUtils { - /** - * Shows a dialog when user attempts to log into a members world without membership. - * The dialog runs on a separate thread to avoid blocking the client thread. - * - * @param currentPlugin The current plugin being run, can be null - * @param config The scheduler configuration - * @param resultCallback A callback that receives the dialog result: - * - true if the user chose to switch to free worlds - * - false if the user chose not to switch or the dialog timed out - */ - public static void showNonMemberWorldDialog(PluginScheduleEntry currentPlugin, SchedulerConfig config, Consumer resultCallback) { - Microbot.getClientThread().runOnSeperateThread(() -> { - // Create dialog with timeout - final JOptionPane optionPane = new JOptionPane( - "You do not have membership and tried to log into a members world.\n" + - "Would you like to switch to free worlds for this login?", - JOptionPane.QUESTION_MESSAGE, - JOptionPane.YES_NO_OPTION); - - final JDialog dialog = optionPane.createDialog("Membership Required"); - - // 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("Membership Required (Timeout: " + remainingSeconds + "s)"); - } else { - dialog.setTitle("Membership Required (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; - - if (result == JOptionPane.YES_OPTION) { - // User wants to switch to free worlds - if (Microbot.getConfigManager() != null) { - Microbot.getConfigManager().setConfiguration("AutoLoginConfig", "World", - LoginManager.getRandomWorld(false)); - Microbot.getConfigManager().setConfiguration("PluginScheduler", "worldType", 0); - } - // Notify caller that user chose to switch to free worlds - resultCallback.accept(true); - } else { - // User chose not to switch to free worlds or dialog timed out - log.info("Login to member world canceled"); - // Notify caller that user chose not to switch or timed out - resultCallback.accept(false); - } - return null; - }); - } - - /** - * Applies consistent styling to a JComboBox - */ - public static void styleComboBox(JComboBox comboBox) { - // Set colors - comboBox.setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - comboBox.setForeground(Color.WHITE); - comboBox.setFont(comboBox.getFont().deriveFont(Font.PLAIN)); - - // Add a visible border to make it stand out - comboBox.setBorder(new CompoundBorder( - new LineBorder(ColorScheme.MEDIUM_GRAY_COLOR), - new EmptyBorder(2, 4, 2, 0) - )); - - // Custom renderer for dropdown items - comboBox.setRenderer(new BasicComboBoxRenderer() { - @Override - public Component getListCellRendererComponent(JList list, Object value, - int index, boolean isSelected, boolean cellHasFocus) { - super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - - if (isSelected) { - setBackground(ColorScheme.BRAND_ORANGE); - setForeground(Color.WHITE); - } else { - setBackground(ColorScheme.DARKER_GRAY_COLOR.brighter()); - setForeground(Color.WHITE); - } - - // Add some padding - setBorder(new EmptyBorder(2, 5, 2, 5)); - return this; - } - }); - - // Use a custom UI to improve the arrow button appearance - comboBox.setUI(new BasicComboBoxUI() { - @Override - protected JButton createArrowButton() { - JButton button = super.createArrowButton(); - button.setBackground(ColorScheme.MEDIUM_GRAY_COLOR); - button.setBorder(BorderFactory.createEmptyBorder()); - return button; - } - }); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/util/UIUtils.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/util/UIUtils.java deleted file mode 100644 index d25e0aaaddd..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/ui/util/UIUtils.java +++ /dev/null @@ -1,515 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.ui.util; - -import net.runelite.client.plugins.microbot.pluginscheduler.ui.layout.DynamicFlowLayout; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; - -import javax.swing.*; -import javax.swing.border.EmptyBorder; -import javax.swing.border.TitledBorder; -import java.awt.*; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; - -/** - * Utility class for creating dynamic, responsive UI panels and components. - * Provides static methods for creating adaptive layouts and components that - * automatically adjust to content length and window size. - * - * Features: - * - Dynamic section creation with adaptive sizing - * - Text-aware component sizing - * - Responsive layouts with automatic wrapping - * - Consistent styling across components - * - * @author Vox - */ -public class UIUtils { - - // Default dimensions - further reduced heights for more compact layout - private static final Dimension DEFAULT_SECTION_MIN_SIZE = new Dimension(100, 60); - private static final Dimension DEFAULT_SECTION_PREF_SIZE = new Dimension(120, 70); - private static final int DEFAULT_HORIZONTAL_GAP = 6; - - /** - * Creates a dynamic plugin information panel that adapts to content and window size - * - * @param title the title for the panel - * @param sections array of section panels to include - * @param bottomPanel optional bottom panel (can be null) - * @return configured wrapper panel with scrolling and dynamic sizing - */ - public static JPanel createDynamicInfoPanel(String title, JPanel[] sections, JPanel bottomPanel) { - JPanel wrapperPanel = new JPanel(new BorderLayout()); - wrapperPanel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(1, 1, 1, 1, ColorScheme.DARK_GRAY_COLOR), - BorderFactory.createEmptyBorder(2, 2, 2, 2) // Reduced from 5,5,5,5 for tighter spacing - ), - title, - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - wrapperPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Create main content panel with dynamic layout - JPanel contentPanel = new JPanel(); - DynamicFlowLayout layout = new DynamicFlowLayout(); - layout.setMinRowHeight(60); // Further reduced from 80 to match smaller sections - layout.setPreferredRowHeight(70); // Further reduced from 90 to match smaller sections - layout.setAdaptiveSpacing(true); - contentPanel.setLayout(layout); - contentPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Add sections to content panel - if (sections != null) { - for (JPanel section : sections) { - if (section != null) { - contentPanel.add(section); - } - } - } - - // Create main container with proper layout - JPanel mainContainer = new JPanel(new BorderLayout()); - mainContainer.setBackground(ColorScheme.DARKER_GRAY_COLOR); - mainContainer.add(contentPanel, BorderLayout.CENTER); - - if (bottomPanel != null) { - mainContainer.add(bottomPanel, BorderLayout.SOUTH); - } - - // Wrap in scroll pane with both scrollbars as needed - JScrollPane scrollPane = new JScrollPane(mainContainer); - scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); - scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - scrollPane.setBorder(null); - scrollPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); - scrollPane.getViewport().setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Add component listener for dynamic resizing - wrapperPanel.addComponentListener(new ComponentAdapter() { - @Override - public void componentResized(ComponentEvent e) { - SwingUtilities.invokeLater(() -> { - adjustSectionSizes(contentPanel); - contentPanel.revalidate(); - contentPanel.repaint(); - }); - } - }); - - wrapperPanel.add(scrollPane, BorderLayout.CENTER); - return wrapperPanel; - } - - /** - * Creates an adaptive section panel that resizes based on content length - * - * @param title the title for the section - * @return configured section panel - */ - public static JPanel createAdaptiveSection(String title) { - return createAdaptiveSection(title, DEFAULT_SECTION_MIN_SIZE, DEFAULT_SECTION_PREF_SIZE); - } - - /** - * Creates an adaptive section panel with custom dimensions - * - * @param title the title for the section - * @param minSize minimum size for the section - * @param prefSize preferred size for the section - * @return configured section panel - */ - public static JPanel createAdaptiveSection(String title, Dimension minSize, Dimension prefSize) { - JPanel panel = new JPanel(new GridBagLayout()); - panel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(ColorScheme.DARK_GRAY_COLOR), - BorderFactory.createEmptyBorder(2, 2, 2, 2) // Further reduced padding from 4,4,4,4 - ), - title, - TitledBorder.CENTER, - TitledBorder.TOP, - FontManager.getRunescapeSmallFont(), - Color.LIGHT_GRAY - )); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - // Set sizing constraints - allow the section to size to its content - panel.setMinimumSize(minSize); - // Don't enforce preferred size, let it size naturally to content - - return panel; - } - - /** - * Creates an adaptive label that adjusts its size based on content - * - * @param text the label text - * @return configured label - */ - public static JLabel createAdaptiveLabel(String text) { - JLabel label = new JLabel(text); - label.setForeground(Color.WHITE); - label.setFont(FontManager.getRunescapeSmallFont()); - return label; - } - - /** - * Creates an adaptive value label with HTML support for text wrapping - * - * @param text the label text - * @return configured label with HTML support - */ - public static JLabel createAdaptiveValueLabel(String text) { - JLabel label = new JLabel("" + text + ""); - label.setForeground(Color.WHITE); - label.setFont(FontManager.getRunescapeSmallFont()); - label.setHorizontalAlignment(SwingConstants.RIGHT); - label.setVerticalAlignment(SwingConstants.TOP); - return label; - } - - /** - * Creates an adaptive text area for multi-line content - * - * @param text initial text content - * @return configured text area - */ - public static JTextArea createAdaptiveTextArea(String text) { - JTextArea textArea = new JTextArea(text); - textArea.setLineWrap(true); - textArea.setWrapStyleWord(true); - textArea.setOpaque(false); - textArea.setEditable(false); - textArea.setFocusable(false); - textArea.setBackground(ColorScheme.DARKER_GRAY_COLOR); - textArea.setForeground(Color.WHITE); - textArea.setFont(FontManager.getRunescapeSmallFont()); - textArea.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); // Reduced from 2,2,2,2 - textArea.setRows(1); // Reduced from 2 rows to 1 for more compact display - return textArea; - } - - /** - * Creates a compact value label with smaller font - * - * @param text the initial text for the label - * @return configured compact value label - */ - public static JLabel createCompactValueLabel(String text) { - JLabel label = new JLabel(text); - label.setForeground(Color.WHITE); - label.setHorizontalAlignment(SwingConstants.RIGHT); - label.setFont(FontManager.getRunescapeSmallFont()); - return label; - } - - /** - * Creates a standard value label with regular font - * - * @param text the initial text for the label - * @return configured value label - */ - public static JLabel createValueLabel(String text) { - JLabel label = new JLabel(text); - label.setForeground(Color.WHITE); - label.setHorizontalAlignment(SwingConstants.RIGHT); - label.setFont(FontManager.getRunescapeFont()); - return label; - } - - /** - * Creates an info panel with titled border for status displays - * - * @param title the title for the panel border - * @return configured info panel with GridBagLayout - */ - public static JPanel createInfoPanel(String title) { - JPanel panel = new JPanel(new GridBagLayout()); - panel.setBorder(BorderFactory.createTitledBorder( - BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(1, 1, 1, 1, ColorScheme.DARK_GRAY_COLOR), - BorderFactory.createEmptyBorder(5, 5, 5, 5) - ), - title, - TitledBorder.DEFAULT_JUSTIFICATION, - TitledBorder.DEFAULT_POSITION, - FontManager.getRunescapeBoldFont(), - Color.WHITE - )); - panel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - return panel; - } - - /** - * Creates GridBagConstraints for label-value layouts - * - * @param x grid x position (0 for labels, 1 for values) - * @param y grid y position - * @return configured GridBagConstraints - */ - public static GridBagConstraints createGbc(int x, int y) { - GridBagConstraints gbc = new GridBagConstraints(); - gbc.gridx = x; - gbc.gridy = y; - gbc.gridwidth = 1; - gbc.gridheight = 1; - gbc.weightx = (x == 0) ? 0.0 : 1.0; // Labels (x=0) don't expand, values (x=1) do - gbc.weighty = 0.0; // Changed to 0.0 to prevent vertical compression - gbc.anchor = (x == 0) ? GridBagConstraints.WEST : GridBagConstraints.EAST; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.insets = new Insets(8, 4, 8, 4); // Increased vertical spacing - return gbc; - } - - /** - * Creates a label-value row with proper alignment - * - * @param labelText the text for the label - * @param valueLabel the value component - * @return configured row panel - */ - public static JPanel createLabelValueRow(String labelText, JLabel valueLabel) { - JPanel row = new JPanel(new BorderLayout()); - row.setBackground(ColorScheme.DARKER_GRAY_COLOR); - row.setMaximumSize(new Dimension(Integer.MAX_VALUE, 20)); - - JLabel label = new JLabel(labelText); - label.setForeground(Color.WHITE); - label.setFont(FontManager.getRunescapeSmallFont()); - - row.add(label, BorderLayout.WEST); - row.add(valueLabel, BorderLayout.EAST); - - return row; - } - - /** - * Adds content to a section using standard GridBagLayout configuration - * - * @param section the section panel to add content to - * @param rows array of LabelValuePair objects representing the rows - */ - public static void addContentToSection(JPanel section, LabelValuePair[] rows) { - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(1, 1, 1, 1); // Further reduced from 2,2,2,2 for tighter spacing - gbc.anchor = GridBagConstraints.WEST; - - for (int i = 0; i < rows.length; i++) { - LabelValuePair row = rows[i]; - - // Label column - gbc.gridx = 0; - gbc.gridy = i; - gbc.weightx = 0.3; - gbc.fill = GridBagConstraints.NONE; - section.add(createAdaptiveLabel(row.getLabel()), gbc); - - // Value column - gbc.gridx = 1; - gbc.weightx = 0.7; - gbc.fill = GridBagConstraints.HORIZONTAL; - section.add(row.getValueComponent(), gbc); - } - - // No vertical glue - let the section size naturally to its content - } - - /** - * Creates a dynamic bottom panel for progress bars and status information - * - * @param progressBar the progress bar component (can be null) - * @param statusTextArea the status text area component (can be null) - * @return configured bottom panel - */ - public static JPanel createDynamicBottomPanel(JProgressBar progressBar, JTextArea statusTextArea) { - JPanel bottomPanel = new JPanel(); - bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.Y_AXIS)); - bottomPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - bottomPanel.setBorder(new EmptyBorder(0, 0, 0, 0)); // Removed all padding for tighter spacing - - if (progressBar != null) { - // Progress panel - JPanel progressPanel = new JPanel(new BorderLayout()); - progressPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - progressPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 25)); - - JLabel progressLabel = new JLabel("Progress:"); - progressLabel.setForeground(Color.WHITE); - progressLabel.setFont(FontManager.getRunescapeSmallFont()); - - progressPanel.add(progressLabel, BorderLayout.WEST); - progressPanel.add(Box.createHorizontalStrut(5), BorderLayout.CENTER); - progressPanel.add(progressBar, BorderLayout.CENTER); - - bottomPanel.add(progressPanel); - bottomPanel.add(Box.createVerticalStrut(1)); // Further reduced spacing from 2 to 1 - } - - if (statusTextArea != null) { - // Status panel - JPanel statusPanel = new JPanel(new BorderLayout()); - statusPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); - statusPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 60)); - - JLabel statusLabel = new JLabel("Status:"); - statusLabel.setForeground(Color.WHITE); - statusLabel.setFont(FontManager.getRunescapeSmallFont()); - - statusPanel.add(statusLabel, BorderLayout.NORTH); - statusPanel.add(Box.createVerticalStrut(3), BorderLayout.CENTER); - statusPanel.add(statusTextArea, BorderLayout.CENTER); - - bottomPanel.add(statusPanel); - } - - return bottomPanel; - } - - /** - * Dynamically adjusts section sizes based on content and available space - * - * @param contentPanel the panel containing the sections - */ - public static void adjustSectionSizes(JPanel contentPanel) { - if (contentPanel.getComponentCount() == 0) return; - - int availableWidth = contentPanel.getWidth(); - if (availableWidth <= 0) return; - - int minSectionWidth = 140; - int componentCount = contentPanel.getComponentCount(); - int totalMinWidth = minSectionWidth * componentCount + (DEFAULT_HORIZONTAL_GAP * (componentCount + 1)); - - if (availableWidth >= totalMinWidth) { - // Enough space for all sections horizontally - int sectionWidth = Math.max(minSectionWidth, (availableWidth - (DEFAULT_HORIZONTAL_GAP * (componentCount + 1))) / componentCount); - - for (int i = 0; i < componentCount; i++) { - Component comp = contentPanel.getComponent(i); - if (comp instanceof JPanel) { - JPanel section = (JPanel) comp; - // Calculate content-based height - int contentHeight = calculateOptimalSectionHeight(section); - Dimension sectionSize = new Dimension(sectionWidth, contentHeight); - section.setPreferredSize(sectionSize); - - // Adjust content based on component's content - adjustSectionContentSize(section); - } - } - } else { - // Not enough space, use minimum widths and let layout handle wrapping - for (int i = 0; i < componentCount; i++) { - Component comp = contentPanel.getComponent(i); - if (comp instanceof JPanel) { - JPanel section = (JPanel) comp; - // Calculate content-based height - int contentHeight = calculateOptimalSectionHeight(section); - Dimension sectionSize = new Dimension(minSectionWidth, contentHeight); - section.setPreferredSize(sectionSize); - - adjustSectionContentSize(section); - } - } - } - } - - /** - * Adjusts individual section size based on its content length - * - * @param section the section panel to adjust - */ - private static void adjustSectionContentSize(JPanel section) { - // Find labels in the section and check their text length - Component[] components = section.getComponents(); - int maxTextLength = 0; - - for (Component comp : components) { - if (comp instanceof JLabel) { - JLabel label = (JLabel) comp; - String text = label.getText(); - if (text != null) { - // Remove HTML tags for length calculation - String plainText = text.replaceAll("<[^>]*>", ""); - maxTextLength = Math.max(maxTextLength, plainText.length()); - } - } - } - - Dimension currentSize = section.getPreferredSize(); - int newWidth = currentSize.width; - - // Adjust width based on longest text length - if (maxTextLength > 25) { - newWidth = Math.max(currentSize.width, 200); - } else if (maxTextLength > 20) { - newWidth = Math.max(currentSize.width, 180); - } else if (maxTextLength > 15) { - newWidth = Math.max(currentSize.width, 160); - } else { - newWidth = Math.max(140, currentSize.width); - } - - section.setPreferredSize(new Dimension(newWidth, currentSize.height)); - } - - /** - * Calculates the optimal height for a section based on its content - * - * @param section the section panel to calculate height for - * @return optimal height in pixels - */ - private static int calculateOptimalSectionHeight(JPanel section) { - // Count components and calculate based on content - Component[] components = section.getComponents(); - int rows = 0; - int textAreaRows = 0; - - for (Component comp : components) { - if (comp instanceof JLabel) { - rows++; - } else if (comp instanceof JTextArea) { - JTextArea textArea = (JTextArea) comp; - textAreaRows += Math.max(1, textArea.getRows()); - } - } - - // Base height for the titled border and padding - int baseHeight = 25; // Reduced title bar space from 30 - - // Height per row of content (reduced for tighter layout) - int rowHeight = 16; // Further reduced from 18 for even tighter spacing - - // Calculate content height - int contentHeight = baseHeight + (rows * rowHeight) + (textAreaRows * rowHeight); - - // Minimum height to ensure usability, maximum to prevent excessive height - return Math.max(60, Math.min(contentHeight, 120)); - } - - /** - * Simple data class to hold label-value pairs for section content - */ - public static class LabelValuePair { - private final String label; - private final JComponent valueComponent; - - public LabelValuePair(String label, JComponent valueComponent) { - this.label = label; - this.valueComponent = valueComponent; - } - - public String getLabel() { - return label; - } - - public JComponent getValueComponent() { - return valueComponent; - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/util/PluginFilterUtil.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/util/PluginFilterUtil.java deleted file mode 100644 index 5d1e30edae5..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/util/PluginFilterUtil.java +++ /dev/null @@ -1,531 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.util; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.plugins.Plugin; -import net.runelite.client.plugins.PluginDescriptor; -import net.runelite.client.plugins.microbot.pluginscheduler.api.SchedulablePlugin; - -import java.lang.reflect.Field; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** - * Utility class for filtering and categorizing plugins based on their descriptors and metadata. - * Dynamically extracts creator information and tags without hardcoding. - */ -@Slf4j -public class PluginFilterUtil { - - // Primary filter categories - public static final String FILTER_ALL = "Show All"; - public static final String FILTER_INTERNAL = "Internal Plugins"; - public static final String FILTER_EXTERNAL = "External Plugins"; - public static final String FILTER_BY_CREATOR = "By Creator"; - public static final String FILTER_BY_TAGS = "By Tags"; - - // Pattern to extract creator prefixes from plugin names - private static final Pattern CREATOR_PATTERN = Pattern.compile("\\[([^\\]]+)\\]"); - - // OSRS Skill and Activity Groups - private static final Map> TAG_GROUPS = createTagGroups(); - - // Cache for creator constants to avoid repeated reflection - private static Set creatorConstantsCache = null; - - /** - * Creates the tag groups map using Java 8 compatible syntax - */ - private static Map> createTagGroups() { - Map> groups = new HashMap<>(); - - // Combat Skills - groups.put("Combat skills", new HashSet<>(Arrays.asList("combat","combat training", "attack", "strength", "defence", "hitpoints", "prayer", "magic", "ranged"))); - - // Gathering Skills - groups.put("Gathering Skills", new HashSet<>(Arrays.asList("farming", "fishing", "hunter","mininig", "woodcutting"))); - - // Artisan Skills - groups.put("Artisan Skills", new HashSet<>(Arrays.asList("cooking", "crafting", "fletching", "herblore", "runecrafting","runecraft", "smithing"))); - - // Support Skills - groups.put("Utility Skills", new HashSet<>(Arrays.asList("agility","construction", "firemaking", "thieving", "slayer"))); - - // PvM/Bossing - groups.put("PvM/Bossing", new HashSet<>(Arrays.asList("pvm", "boss", "bossing", "raids", "tob", "cox", "cm", "chambers", "theatre", - "zulrah", "vorkath", "hydra", "cerberus", "kraken", "thermonuclear", "barrows", - "jad", "inferno", "gauntlet", "corrupted gauntlet", "nightmare", "phosani", "nex", "bandos", - "armadyl", "zamorak", "saradomin", "dagannoth", "kalphite", "kq", "kbd", "chaos fanatic", - "archaeologist", "spider", "bear", "scorpia", "vetion", "callisto", - "venenatis", "rev", "revenant killer"))); - - // Money Making - groups.put("Money Making", new HashSet<>(Arrays.asList("money making", "mm", "gp per hour", "gold", "profit", "farming ", "merching", - "flipping", "moneymaking"))); - - // Minigames - groups.put("Minigames", new HashSet<>(Arrays.asList("minigame", "minigames", "wintertodt", "tempoross", "zalcano", "gotr", "guardians rift", - "tithe", "tithe farm", "mahogany homes", "pest control", "barbarian assault", - "castle wars", "fight caves", "duel arena", "last man standing", "lms"))); - - // Questing & Achievement - groups.put("Quests & Achievement", new HashSet<>(Arrays.asList("quest", "questing", "achievement", "diary", "clue", "clues", "treasure", - "trails", "casket", "scroll", "beginner", "easy", "medium", "hard", "elite", "master"))); - - // Utility & QoL - groups.put("Utility", new HashSet<>(Arrays.asList("utility", "qol", "quality", "life", "helper", "calculator", "timer", "notification", - "overlay", "highlight", "marker", "tracker", "counter", "solver", "automation"))); - - // PvP - groups.put("PvP", new HashSet<>(Arrays.asList("pvp", "player versus player", "pk", "pking", "bounty hunter", "edge", "edgeville", "anti-pk"))); - - // Bank & Trading - groups.put("Banking & Trading", new HashSet<>(Arrays.asList("bank", "banking", "ge", "grand", "exchange", "trade", "trading", - "merchant", "merch", "flip", "flipping", "sorter", "organization","muling"))); - - // Transportation - groups.put("Transportation", new HashSet<>(Arrays.asList("teleport", "transport", "fairy", "ring", "spirit", "tree", "home", "tab", - "house", "poh", "portal"))); - - return groups; - } - - /** - * Gets all available primary filter categories - */ - public static List getPrimaryFilterCategories() { - return Arrays.asList(FILTER_ALL, FILTER_INTERNAL, FILTER_EXTERNAL, FILTER_BY_CREATOR, FILTER_BY_TAGS); - } - - /** - * Gets secondary filter options based on the primary filter selection - */ - public static List getSecondaryFilterOptions(String primaryFilter, List plugins) { - List options = new ArrayList<>(); - - switch (primaryFilter) { - case FILTER_BY_CREATOR: - options = getAvailableCreators(plugins); - break; - case FILTER_BY_TAGS: - options = getAvailableTagGroups(plugins); - break; - case FILTER_INTERNAL: - case FILTER_EXTERNAL: - options.add("All"); - break; - case FILTER_ALL: - default: - options.add("All Plugins"); - break; - } - - return options.stream().sorted().collect(Collectors.toList()); - } - - /** - * Filters plugins based on the selected primary and secondary filters - */ - public static List filterPlugins(List plugins, String primaryFilter, String secondaryFilter) { - return plugins.stream() - .filter(plugin -> plugin instanceof SchedulablePlugin) - .filter(plugin -> matchesFilter(plugin, primaryFilter, secondaryFilter)) - .collect(Collectors.toList()); - } - - /** - * Checks if a plugin matches the given filters - */ - private static boolean matchesFilter(Plugin plugin, String primaryFilter, String secondaryFilter) { - if (FILTER_ALL.equals(primaryFilter) || secondaryFilter == null || "All Plugins".equals(secondaryFilter)) { - return true; - } - - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - if (descriptor == null) { - return false; - } - - switch (primaryFilter) { - case FILTER_BY_CREATOR: - return matchesCreator(descriptor, secondaryFilter); - case FILTER_BY_TAGS: - return matchesTagGroup(descriptor, secondaryFilter); - case FILTER_INTERNAL: - return !descriptor.isExternal(); - case FILTER_EXTERNAL: - return descriptor.isExternal(); - default: - return true; - } - } - - /** - * Checks if a plugin matches the selected creator by extracting creator from plugin name - */ - private static boolean matchesCreator(PluginDescriptor descriptor, String selectedCreator) { - String pluginCreator = extractCreatorFromName(descriptor.name()); - return pluginCreator.equals(selectedCreator); - } - - /** - * Checks if a plugin matches the selected tag group - */ - private static boolean matchesTagGroup(PluginDescriptor descriptor, String selectedTagGroup) { - String[] tags = descriptor.tags(); - Set groupTags = TAG_GROUPS.get(selectedTagGroup); - - if (groupTags == null) { - return false; - } - - return Arrays.stream(tags) - .filter(tag -> !isCreatorTag(tag)) // Exclude creator tags - .anyMatch(tag -> matchesAnyGroupTag(tag.toLowerCase(), groupTags)); - } - - /** - * Checks if a tag matches any of the group tags, including partial matches for multi-word phrases - */ - private static boolean matchesAnyGroupTag(String tag, Set groupTags) { - // Direct match - if (groupTags.contains(tag)) { - return true; - } - - // Check if tag contains any of the group tag words - for (String groupTag : groupTags) { - if (tag.contains(groupTag) || groupTag.contains(tag)) { - return true; - } - } - - // Split tag on spaces and check each word - String[] tagWords = tag.split("\\s+"); - for (String word : tagWords) { - if (groupTags.contains(word.trim())) { - return true; - } - } - - return false; - } - - /** - * Checks if a tag is likely a creator tag by checking against known creator constants - */ - private static boolean isCreatorTag(String tag) { - if (tag == null) { - return false; - } - - Set creators = getCreatorConstants(); - // Check against variable names (case-sensitive) - if (creators.contains(tag)) { - return true; - } - - // Check against variations (case-insensitive) for short tags - if (tag.length() <= 6) { - return creators.contains(tag.toUpperCase()) || creators.contains(tag.toLowerCase()); - } - - return false; - } - - /** - * Gets all available creators from the plugin list by analyzing plugin names and PluginDescriptor constants - */ - private static List getAvailableCreators(List plugins) { - Set creators = new HashSet<>(); - - // Extract creators from plugin names - for (Plugin plugin : plugins) { - if (!(plugin instanceof SchedulablePlugin)) { - continue; - } - - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - if (descriptor != null) { - String creator = extractCreatorFromName(descriptor.name()); - if (!"Unknown".equals(creator)) { // Filter out "Unknown" - creators.add(creator); - } - } - } - - return creators.stream() - .filter(creator -> !"Unknown".equals(creator)) - .sorted() - .collect(Collectors.toList()); - } - - /** - * Gets available tag groups that have matching plugins - */ - private static List getAvailableTagGroups(List plugins) { - Set availableGroups = new HashSet<>(); - - for (Plugin plugin : plugins) { - if (!(plugin instanceof SchedulablePlugin)) { - continue; - } - - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - if (descriptor != null) { - String[] tags = descriptor.tags(); - - // Check which tag groups this plugin belongs to - for (Map.Entry> groupEntry : TAG_GROUPS.entrySet()) { - String groupName = groupEntry.getKey(); - Set groupTags = groupEntry.getValue(); - - boolean hasMatchingTag = Arrays.stream(tags) - .filter(tag -> !isCreatorTag(tag)) // Exclude creator tags - .anyMatch(tag -> groupTags.contains(tag.toLowerCase())); - - if (hasMatchingTag) { - availableGroups.add(groupName); - } - } - } - } - - return new ArrayList<>(availableGroups); - } - - /** - * Gets creator constants with caching - */ - private static Set getCreatorConstants() { - if (creatorConstantsCache == null) { - creatorConstantsCache = extractCreatorConstantsFromPluginDescriptor(); - } - return creatorConstantsCache; - } - - /** - * Dynamically extracts creator constants from PluginDescriptor class using reflection - */ - private static Set extractCreatorConstantsFromPluginDescriptor() { - Set creators = new HashSet<>(); - - try { - Field[] fields = PluginDescriptor.class.getDeclaredFields(); - for (Field field : fields) { - if (field.getType() == String.class && - java.lang.reflect.Modifier.isStatic(field.getModifiers()) && - java.lang.reflect.Modifier.isFinal(field.getModifiers())) { - - try { - String value = (String) field.get(null); - if (value != null && value.contains("[") && value.contains("]")) { - // Use the field name as the creator identifier - String fieldName = field.getName(); - if (!fieldName.isEmpty()) { - creators.add(fieldName); - } - } - } catch (IllegalAccessException e) { - log.debug("Could not access field {}: {}", field.getName(), e.getMessage()); - } - } - } - } catch (Exception e) { - log.warn("Error extracting creator constants from PluginDescriptor: {}", e.getMessage()); - } - - return creators; - } - - /** - * Extracts creator name from PluginDescriptor constant value - */ - private static String extractCreatorFromValue(String value) { - // Pattern to match content between > and < in HTML format - Pattern pattern = Pattern.compile(">([^<]+)<"); - Matcher matcher = pattern.matcher(value); - - if (matcher.find()) { - String extracted = matcher.group(1); - // Handle special characters and emoji - if (extracted.length() <= 4) { // Most creator codes are short - return extracted; - } - } - - // Fallback: try to extract from brackets - Matcher bracketMatcher = CREATOR_PATTERN.matcher(value); - if (bracketMatcher.find()) { - return bracketMatcher.group(1); - } - - return ""; - } - - /** - * Extracts creator from plugin name using various patterns - */ - private static String extractCreatorFromName(String pluginName) { - if (pluginName == null) { - return "Unknown"; - } - - // Try to find the variable name from PluginDescriptor constants - String variableName = extractCreatorVariableName(pluginName); - if (variableName != null && !variableName.equals("Unknown")) { - return variableName; - } - - // Fallback: First try to extract from [Creator] pattern - Matcher matcher = CREATOR_PATTERN.matcher(pluginName); - if (matcher.find()) { - String creator = cleanHtmlTags(matcher.group(1)); - return creator.isEmpty() ? "Unknown" : creator; - } - - // Check if plugin name starts with HTML format and extract creator - if (pluginName.startsWith("")) { - String extracted = extractCreatorFromValue(pluginName); - if (!extracted.isEmpty()) { - return cleanHtmlTags(extracted); - } - } - - return "Unknown"; - } - - /** - * Extracts the variable name from PluginDescriptor by matching the plugin name against constant values - */ - private static String extractCreatorVariableName(String pluginName) { - try { - Field[] fields = PluginDescriptor.class.getDeclaredFields(); - for (Field field : fields) { - if (field.getType() == String.class && - java.lang.reflect.Modifier.isStatic(field.getModifiers()) && - java.lang.reflect.Modifier.isFinal(field.getModifiers())) { - - try { - String value = (String) field.get(null); - if (value != null && pluginName.startsWith(value)) { - // Found matching constant - return field name - return field.getName(); - } - } catch (IllegalAccessException e) { - log.debug("Could not access field {}: {}", field.getName(), e.getMessage()); - } - } - } - } catch (Exception e) { - log.warn("Error extracting creator variable name: {}", e.getMessage()); - } - - return "Unknown"; - } - - /** - * Removes HTML tags and cleans up text - */ - private static String cleanHtmlTags(String text) { - if (text == null) { - return ""; - } - - // Remove HTML tags - String cleaned = text.replaceAll("<[^>]*>", "").trim(); - - // Remove common HTML entities - cleaned = cleaned.replace(" ", " ") - .replace("<", "<") - .replace(">", ">") - .replace("&", "&") - .replace(""", "\""); - - return cleaned.trim(); - } - - /** - * Gets the creator name for a specific plugin - */ - public static String getPluginCreator(Plugin plugin) { - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - if (descriptor == null) { - return "Unknown"; - } - - String creator = extractCreatorFromName(descriptor.name()); - return "Unknown".equals(creator) ? "Unknown" : creator; - } - - /** - * Gets the tags for a specific plugin, excluding creator tags - */ - public static List getPluginTags(Plugin plugin) { - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - if (descriptor == null) { - return Collections.emptyList(); - } - - return Arrays.stream(descriptor.tags()) - .filter(tag -> !isCreatorTag(tag)) // Exclude creator tags - .collect(Collectors.toList()); - } - - /** - * Gets a clean display name for a plugin without HTML formatting - */ - public static String getPluginDisplayName(Plugin plugin) { - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - if (descriptor == null) { - return plugin.getName(); - } - - String name = descriptor.name(); - // Remove HTML tags and clean up the name - return cleanHtmlTags(name); - } - - /** - * Gets a formatted display name for a plugin including creator prefix in a clean format - */ - public static String getPluginFormattedDisplayName(Plugin plugin) { - String creator = getPluginCreator(plugin); - String cleanName = getPluginDisplayName(plugin); - - if ("Unknown".equals(creator)) { - return cleanName; - } - - return String.format("[%s] %s", creator, cleanName); - } - - /** - * Gets which tag group a plugin belongs to (if any) - */ - public static String getPluginTagGroup(Plugin plugin) { - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - if (descriptor == null) { - return "Other"; - } - - String[] tags = descriptor.tags(); - - // Check which tag group this plugin belongs to - for (Map.Entry> groupEntry : TAG_GROUPS.entrySet()) { - String groupName = groupEntry.getKey(); - Set groupTags = groupEntry.getValue(); - - boolean hasMatchingTag = Arrays.stream(tags) - .filter(tag -> !isCreatorTag(tag)) // Exclude creator tags - .anyMatch(tag -> groupTags.contains(tag.toLowerCase())); - - if (hasMatchingTag) { - return groupName; - } - } - - return "Other"; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/util/SchedulerPluginUtil.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/util/SchedulerPluginUtil.java deleted file mode 100644 index dd6571e498d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/pluginscheduler/util/SchedulerPluginUtil.java +++ /dev/null @@ -1,1223 +0,0 @@ -package net.runelite.client.plugins.microbot.pluginscheduler.util; -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.time.Duration; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import javax.swing.SwingUtilities; - -import org.slf4j.event.Level; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.client.config.ConfigManager; -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.accountselector.AutoLoginPlugin; -import net.runelite.client.plugins.microbot.breakhandler.BreakHandlerConfig; -import net.runelite.client.plugins.microbot.breakhandler.BreakHandlerPlugin; -import net.runelite.client.plugins.microbot.breakhandler.BreakHandlerScript; -import java.time.format.DateTimeFormatter; - -import net.runelite.client.plugins.microbot.pluginscheduler.SchedulerPlugin; -import net.runelite.client.plugins.microbot.pluginscheduler.api.SchedulablePlugin; -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.condition.logical.PredicateCondition; -import net.runelite.client.plugins.microbot.pluginscheduler.model.PluginScheduleEntry; -import net.runelite.client.plugins.microbot.util.antiban.AntibanPlugin; -import net.runelite.client.plugins.microbot.util.antiban.Rs2Antiban; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -@Slf4j -public class SchedulerPluginUtil{ - /** - * Format a duration for display - * - * @param duration The duration to format - * @return Formatted string representation (e.g., "1h 30m 15s" or "45s") - */ - public static String formatDuration(Duration duration) { - long hours = duration.toHours(); - long minutes = duration.toMinutes() % 60; - long seconds = duration.getSeconds() % 60; - - if (hours > 0) { - return String.format("%dh %dm %ds", hours, minutes, seconds); - } else if (minutes > 0) { - return String.format("%dm %ds", minutes, seconds); - } else { - return String.format("%ds", seconds); - } - } - - /** - * Checks if a specific plugin is enabled - * - * @param pluginClass The class of the plugin to check - * @return True if the plugin is enabled, false otherwise - */ - public static boolean isPluginEnabled(Class pluginClass) { - return Microbot.isPluginEnabled(pluginClass); - } - - public static boolean isBreakHandlerEnabled() { - return isPluginEnabled(BreakHandlerPlugin.class); - } - - public static boolean isAntibanEnabled() { - return isPluginEnabled(AntibanPlugin.class); - } - - public static boolean isAutoLoginEnabled() { - return isPluginEnabled(AutoLoginPlugin.class); - } - - /** - * Enables a specific plugin - * - * @param pluginClass The class of the plugin to enable - * @return true if plugin was enabled successfully, false otherwise - */ - public static boolean enablePlugin(Class pluginClass) { - if (isPluginEnabled(pluginClass)) { - log.info("Plugin {} already enabled", pluginClass.getSimpleName()); - return true; // Already enabled - } - - Microbot.getClientThread().runOnSeperateThread(() -> { - Plugin plugin = Microbot.getPlugin(pluginClass.getName()); - log.info("Plugin {} suggested to be enabled", pluginClass.getSimpleName()); - if (plugin == null) { - log.error("Failed to find plugin {}", pluginClass.getSimpleName()); - return false; - } - log.info("Plugin {} starting", pluginClass.getSimpleName()); - Microbot.startPlugin(plugin); - return true; - }); - - log.info("Plugin {} wait", pluginClass.getSimpleName()); - sleepUntil(() -> isPluginEnabled(pluginClass), 500); - if (!isPluginEnabled(pluginClass)) { - log.error("Failed to enable plugin {}", pluginClass.getSimpleName()); - return false; - } - - log.info("Plugin {} enabled", pluginClass.getSimpleName()); - return true; - } - - /** - * Disables a specific plugin - * - * @param pluginClass The class of the plugin to disable - * @return true if plugin was disabled successfully, false otherwise - */ - public static boolean disablePlugin(Class pluginClass) { - if (!isPluginEnabled(pluginClass)) { - log.info("Plugin {} already disabled", pluginClass.getSimpleName()); - return true; // Already disabled - } - - log.info("disablePlugin {} - are we on client thread->; {}", - pluginClass.getSimpleName(), Microbot.getClient().isClientThread()); - - Microbot.getClientThread().runOnSeperateThread(() -> { - Plugin plugin = Microbot.getPlugin(pluginClass.getName()); - if (plugin == null) { - log.error("Failed to find plugin {}", pluginClass.getSimpleName()); - return false; - } - log.info("Plugin {} stopping", pluginClass.getSimpleName()); - Microbot.stopPlugin(plugin); - return true; - }); - - if (isPluginEnabled(pluginClass)) { - SwingUtilities.invokeLater(() -> { - disablePlugin(pluginClass); - }); - return false; - } - - log.info("Plugin {} disabled", pluginClass.getSimpleName()); - return true; - } - - /** - * Checks 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 - */ - public static boolean isAllSameTimestamp(List plugins) { - if (plugins == null || plugins.size() <= 1) { - return true; // Empty or single-element list has same timestamps by definition - } - - // Get first plugin's trigger time as reference - Optional firstTime = plugins.get(0).getCurrentStartTriggerTime(); - if (firstTime.isEmpty()) { - // If first plugin has no timestamp, check if all others also have no timestamp - return plugins.stream().allMatch(p -> p.getCurrentStartTriggerTime().isEmpty()); - } - - // Compare each plugin's trigger time with the first one - ZonedDateTime reference = firstTime.get().truncatedTo(ChronoUnit.MILLIS); - return plugins.stream() - .allMatch(p -> { - Optional time = p.getCurrentStartTriggerTime(); - return time.isPresent() && - time.get().truncatedTo(ChronoUnit.MILLIS).equals(reference); - }); - } - - /** - * Selects a plugin using weighted random selection. - * Plugins with lower run counts have higher probability of being selected. - * - * @param plugins List of plugins to select from - * @return The selected plugin - */ - public static PluginScheduleEntry selectPluginWeighted(List plugins) { - // Return the only plugin if there's just one - if (plugins.size() == 1) { - return plugins.get(0); - } - - // Calculate weights - plugins with lower run counts get higher weights - // Find the maximum run count - int maxRuns = plugins.stream() - .mapToInt(PluginScheduleEntry::getRunCount) - .max() - .orElse(0); - - // Add 1 to avoid division by zero and to ensure all plugins have some chance - maxRuns = maxRuns + 1; - - // Calculate weights as (maxRuns + 1) - runCount for each plugin - // This gives higher weight to plugins that have run less often - double[] weights = new double[plugins.size()]; - double totalWeight = 0; - - for (int i = 0; i < plugins.size(); i++) { - // Weight = (maxRuns + 1) - plugin's run count - weights[i] = maxRuns - plugins.get(i).getRunCount() + 1; - totalWeight += weights[i]; - } - - // Generate random value between 0 and totalWeight - double randomValue = Math.random() * totalWeight; - - // Select plugin based on random value and weights - double weightSum = 0; - for (int i = 0; i < plugins.size(); i++) { - weightSum += weights[i]; - if (randomValue < weightSum) { - // Log the selection for debugging - log.debug("Selected plugin '{}' with weight {}/{} (run count: {})", - plugins.get(i).getCleanName(), - weights[i], - totalWeight, - plugins.get(i).getRunCount()); - return plugins.get(i); - } - } - - // Fallback to the last plugin (shouldn't normally happen) - return plugins.get(plugins.size() - 1); - } - - /** - * Sort a group of randomizable plugins using a weighted approach based on run - * counts. - * Plugins with fewer runs get sorted ahead of plugins with more runs, following - * the weighting system used in the old selectPluginWeighted method. - * - * @param plugins A list of randomizable plugins with the same priority and - * default status - * @return A list sorted by weighted run count - */ - public static List applyWeightedSorting(List plugins) { - if (plugins.size() <= 1) { - return new ArrayList<>(plugins); - } - - // Similar to the old selectPluginWeighted, but we're sorting instead of - // selecting one - - // First, find the maximum run count - int maxRuns = plugins.stream() - .mapToInt(PluginScheduleEntry::getRunCount) - .max() - .orElse(0); - - // Add 1 to avoid division by zero and ensure all have a chance - maxRuns = maxRuns + 1; - - // Calculate weights for each plugin - final Map weights = new HashMap<>(); - double totalWeight = 0; - - for (PluginScheduleEntry plugin : plugins) { - // Weight = (maxRuns + 1) - plugin's run count - double weight = maxRuns - plugin.getRunCount() + 1; - weights.put(plugin, weight); - totalWeight += weight; - } - - // Create weighted comparison - Comparator weightedComparator = (p1, p2) -> { - // Higher weight (fewer runs) should come first - double weight1 = weights.getOrDefault(p1, 0.0); - double weight2 = weights.getOrDefault(p2, 0.0); - - if (Double.compare(weight1, weight2) != 0) { - // Higher weight first - return Double.compare(weight2, weight1); - } - - // If weights are equal, use name and identity for stable sorting - int nameCompare = p1.getName().compareTo(p2.getName()); - if (nameCompare != 0) { - return nameCompare; - } - - return Integer.compare(System.identityHashCode(p1), System.identityHashCode(p2)); - }; - - // Sort plugins based on weight - List sortedPlugins = new ArrayList<>(plugins); - sortedPlugins.sort(weightedComparator); - - if (log.isDebugEnabled()) { - for (int i = 0; i < sortedPlugins.size(); i++) { - PluginScheduleEntry plugin = sortedPlugins.get(i); - double weight = weights.get(plugin); - double weightPercentage = (weight / totalWeight) * 100.0; - log.debug("Weighted sorting position {}: '{}' with weight {:.2f}/{:.2f} ({:.2f}%) (run count: {})", - i, plugin.getCleanName(), weight, totalWeight, weightPercentage, plugin.getRunCount()); - } - } - - return sortedPlugins; - } - - /** - * Helper method to enable the BreakHandler plugin - */ - public static boolean enableBreakHandler() { - return enablePlugin(BreakHandlerPlugin.class); - } - - /** - * Helper method to disable the BreakHandler plugin - */ - public static boolean disableBreakHandler() { - if (isPluginEnabled(BreakHandlerPlugin.class)) { - BreakHandlerScript.setLockState(false); // Ensure we unlock before disabling - } - return disablePlugin(BreakHandlerPlugin.class); - } - - /** - * Helper method to enable the AutoLogin plugin - */ - public static boolean enableAutoLogin() { - return enablePlugin(AutoLoginPlugin.class); - } - - /** - * Helper method to enable the AutoLogin plugin with configuration - * - * @param randomWorld Whether to use a random world - * @param worldNumber The world number to use if not random - * @return true if plugin was enabled successfully, false otherwise - */ - public static boolean enableAutoLogin(boolean randomWorld, int worldNumber) { - ConfigManager configManager = Microbot.getConfigManager(); - if(configManager != null) { - configManager.setConfiguration("AutoLoginConfig", "RandomWorld", randomWorld); - configManager.setConfiguration("AutoLoginConfig", "World", worldNumber); - } - - return enableAutoLogin(); - } - - /** - * Helper method to disable the AutoLogin plugin - */ - public static boolean disableAutoLogin() { - return disablePlugin(AutoLoginPlugin.class); - } - - /** - * Helper method to enable the Antiban plugin - */ - public static boolean enableAntiban() { - return enablePlugin(AntibanPlugin.class); - } - - /** - * Helper method to disable the Antiban plugin - */ - public static boolean disableAntiban() { - return disablePlugin(AntibanPlugin.class); - } - - /** - * Checks if the bot is currently on a break - * - * @return true if on break, false otherwise - */ - public static boolean isOnBreak() { - // Check if BreakHandler is enabled and on a break - return isBreakHandlerEnabled() && BreakHandlerScript.isBreakActive(); - } - - /** - * Forces the bot to take a break immediately if BreakHandler is enabled - * - * @return true if break was initiated, false otherwise - */ - public static boolean forceBreak() { - if (!isBreakHandlerEnabled()) { - log.warn("Cannot force break: BreakHandler plugin not enabled"); - return false; - } - - // Set the breakNow config to true to trigger a break - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "breakNow", true); - Microbot.getConfigManager().setConfiguration(BreakHandlerConfig.configGroup, "logout", true); - log.info("Break requested via forceBreak()"); - return true; - } - - /** - * Attempts to take a micro break if conditions are favorable - * - * @param setState A callback function to update the scheduler state - * @return true if a micro break was taken, false otherwise - */ - public static boolean takeMicroBreak(Runnable setState) { - if (!isAntibanEnabled()) { - log.warn("Cannot take micro break: Antiban plugin not enabled"); - return false; - } - if (Rs2Player.isFullHealth()) { - if (Rs2Antiban.takeMicroBreakByChance() || BreakHandlerScript.isBreakActive()) { - if (setState != null) { - setState.run(); - } - return true; - } - } - return false; - } - - /** - * Locks the break handler to prevent breaks from occurring - * - * @return true if the break handler was successfully locked, false otherwise - */ - public static boolean lockBreakHandler() { - // Check if BreakHandler is enabled and not already locked - if (isBreakHandlerEnabled() && !BreakHandlerScript.isBreakActive() && !BreakHandlerScript.isLockState()) { - BreakHandlerScript.setLockState(true); - return true; // Successfully locked - } - return false; // Failed to lock - } - - /** - * Unlocks the break handler to allow breaks to occur - */ - public static void unlockBreakHandler() { - // Check if BreakHandler is enabled and not already unlocked - if (isBreakHandlerEnabled() && BreakHandlerScript.isLockState()) { - BreakHandlerScript.setLockState(false); - } - } - - /** - * Logs out the player if they are currently logged in - */ - public static void logout() { - if (Microbot.getClient().getGameState() == net.runelite.api.GameState.LOGGED_IN) { - if (isAutoLoginEnabled()) { - boolean successfulDisabled = disableAutoLogin(); - if (!successfulDisabled) { - Microbot.getClientThread().invokeLater(() -> { - logout(); - }); - return; - } - } - - Rs2Player.logout(); - } - } - - - - - - /** - * Sorts a list of plugins according to a consistent order, with weighted - * selection for randomizable plugins: - * 1. Enabled plugins first - * 2. Running status (running plugins first) - * 3. Due-to-run status (due plugins first) - prioritizes actionable plugins - * 4. Next run time (earliest first) - within due/not-due groups - * 5. Priority (highest first) - within due/runtime groups - * 6. Non-default status (non-default first) - * 7. Prefer non-randomizable plugins (for ties in timing) - * 8. For randomizable plugins with equal criteria: weighted by run count - * 9. Finally by name and object identity for stable ordering - * - * @param plugins The list of plugins to sort - * @param applyWeightedSelection Whether to apply weighted selection for - * randomizable plugins - * @return A sorted copy of the input list - */ - public static List sortPluginScheduleEntries(List plugins, - boolean applyWeightedSelection) { - if (plugins == null || plugins.isEmpty()) { - return new ArrayList<>(); - } - - List sortedPlugins = new ArrayList<>(plugins); - - // First, sort by all the stable criteria - sortedPlugins.sort((p1, p2) -> { - // First sort by enabled status (enabled plugins first) - if (p1.isEnabled() != p2.isEnabled()) { - return p1.isEnabled() ? -1 : 1; - } - - // For running plugins, prioritize current running plugin at the top - boolean p1IsRunning = p1.isRunning(); - boolean p2IsRunning = p2.isRunning(); - - if (p1IsRunning != p2IsRunning) { - return p1IsRunning ? -1 : 1; - } - - // Sort by due-to-run status first (due plugins first) - boolean p1IsDue = p1.isDueToRun(); - boolean p2IsDue = p2.isDueToRun(); - - if (p1IsDue != p2IsDue) { - return p1IsDue ? -1 : 1; - } - - // Then sort by next run time (earliest first) - within due/not-due groups - Optional time1 = p1.getCurrentStartTriggerTime(); - Optional time2 = p2.getCurrentStartTriggerTime(); - - if (time1.isPresent() && time2.isPresent()) { - ZonedDateTime t1 = time1.get().truncatedTo(ChronoUnit.SECONDS); - ZonedDateTime t2 = time2.get().truncatedTo(ChronoUnit.SECONDS); - int timeCompare = t1.compareTo(t2); - float timeDifference = Duration.between(t1, t2).toMillis(); - int priorityCompare = Integer.compare(p2.getPriority(), p1.getPriority()); - if (timeCompare != 0 && priorityCompare == 0) { - log.debug("Comparing times: {}() vs {}() -> result: {} ({} ms difference)", - t1.format(DateTimeFormatter.ISO_ZONED_DATE_TIME), - t2.format(DateTimeFormatter.ISO_ZONED_DATE_TIME), - timeCompare - , timeDifference); - return timeCompare; - } - } else if (time1.isPresent()) { - return -1; // p1 has time, p2 doesn't - } else if (time2.isPresent()) { - return 1; // p2 has time, p1 doesn't - } - - // Then sort by priority within due/runtime groups (highest first) - int priorityCompare = Integer.compare(p2.getPriority(), p1.getPriority()); - if (priorityCompare != 0) { - return priorityCompare; - } - - // Prefer non-default plugins - if (p1.isDefault() != p2.isDefault()) { - return p1.isDefault() ? 1 : -1; - } - - // Prefer non-randomizable plugins for deterministic behavior - if (p1.isAllowRandomScheduling() != p2.isAllowRandomScheduling()) { - return p1.isAllowRandomScheduling() ? 1 : -1; - } - - // As final tiebreakers use plugin name and object identity - int nameCompare = p1.getName().compareTo(p2.getName()); - if (nameCompare != 0) { - return nameCompare; - } - - // Last resort: use object identity hash code for stable ordering - return Integer.compare(System.identityHashCode(p1), System.identityHashCode(p2)); - }); - - // If we're not applying weighted selection, we're done - if (!applyWeightedSelection) { - return sortedPlugins; - } - - // Now we need to look for groups of randomizable plugins at the same priority, - // default status, and similar timing for weighted selection - List result = new ArrayList<>(); - List randomizableGroup = new ArrayList<>(); - Integer currentPriority = null; - boolean currentDefault = false; - ZonedDateTime currentTimeGroup = null; - final Duration TIME_GROUP_WINDOW = Duration.ofMinutes(5); // Group plugins within 5 minutes - - // Iterate through sorted plugins to find groups with the same priority, - // default status, and similar timing - for (int i = 0; i < sortedPlugins.size(); i++) { - PluginScheduleEntry current = sortedPlugins.get(i); - - // Skip non-randomizable plugins (they're already properly sorted by time) - if (!current.isAllowRandomScheduling()) { - // If we had a randomizable group, process it before adding this - // non-randomizable plugin - if (!randomizableGroup.isEmpty()) { - result.addAll(SchedulerPluginUtil.applyWeightedSorting(randomizableGroup)); - randomizableGroup.clear(); - } - - result.add(current); - continue; - } - - // Get the trigger time for timing group comparison - Optional triggerTime = current.getCurrentStartTriggerTime(); - ZonedDateTime currentTime = triggerTime.map(t -> t.truncatedTo(ChronoUnit.MINUTES)).orElse(null); - - // Check if this is part of an existing group (same priority, default status, and timing) - boolean sameGroup = currentPriority != null - && current.getPriority() == currentPriority - && current.isDefault() == currentDefault; - - // Add timing group check - plugins should be in same time window for randomization - if (sameGroup && currentTimeGroup != null && currentTime != null) { - Duration timeDifference = Duration.between(currentTimeGroup, currentTime).abs(); - sameGroup = timeDifference.compareTo(TIME_GROUP_WINDOW) <= 0; - } else if (sameGroup) { - // If one has time and other doesn't, they're not in the same group - sameGroup = (currentTimeGroup == null && currentTime == null); - } - - if (sameGroup) { - // Same group, add to current batch of randomizable plugins - randomizableGroup.add(current); - } else { - // New group - process previous group if it exists - if (!randomizableGroup.isEmpty()) { - result.addAll(SchedulerPluginUtil.applyWeightedSorting(randomizableGroup)); - randomizableGroup.clear(); - } - - // Start new group - randomizableGroup.add(current); - currentPriority = current.getPriority(); - currentDefault = current.isDefault(); - currentTimeGroup = currentTime; - } - } - - // Process any remaining group - if (!randomizableGroup.isEmpty()) { - result.addAll(SchedulerPluginUtil.applyWeightedSorting(randomizableGroup)); - } - - return result; - } - - - - /** - * Overloaded method that calls sortPluginScheduleEntries without weighted - * selection by default - */ - public static List sortPluginScheduleEntries(List plugins) { - return sortPluginScheduleEntries(plugins, false); - } - - - - public static Optional getScheduleInterval(PluginScheduleEntry plugin) { - if (plugin.hasAnyStartConditions()) { - Optional nextTrigger = plugin.getCurrentStartTriggerTime(); - if (nextTrigger.isPresent()) { - ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); - return Optional.of(Duration.between(now, nextTrigger.get())); - } - } - return Optional.empty(); - } - - - - /** - * Formats a reason message for better readability by splitting it into multiple lines - * if it's too long or contains natural break points. - * - * @param reason The original reason message - * @return A formatted reason message with appropriate line breaks - */ - public static String formatReasonMessage(String reason) { - if (reason == null || reason.isEmpty()) { - return ""; - } - - // Maximum line length before seeking a break point - final int MAX_LINE_LENGTH = 80; - - // If the reason is already short, return it as is - if (reason.length() <= MAX_LINE_LENGTH) { - return reason; - } - - StringBuilder formatted = new StringBuilder(); - - // First check if the message already contains a colon - // If so, we'll format differently - if (reason.contains(":")) { - String[] parts = reason.split(":", 2); - formatted.append(parts[0].trim()).append(":"); - - // Process the part after the colon - String afterColon = parts[1].trim(); - - // If what comes after the colon is still long, format it further - if (afterColon.length() > MAX_LINE_LENGTH) { - formatted.append("\n "); // Indent the continuation - formatted.append(formatLongText(afterColon, MAX_LINE_LENGTH)); - } else { - formatted.append("\n ").append(afterColon); - } - } else { - // No colon in the message, format as regular long text - formatted.append(formatLongText(reason, MAX_LINE_LENGTH)); - } - - return formatted.toString(); - } - - /** - * Helper method to format a long text by inserting line breaks at natural points - * - * @param text The text to format - * @param maxLineLength The maximum length for each line - * @return Formatted text with line breaks - */ - private static String formatLongText(String text, int maxLineLength) { - if (text == null || text.isEmpty() || text.length() <= maxLineLength) { - return text; - } - - StringBuilder result = new StringBuilder(); - int currentPosition = 0; - - while (currentPosition < text.length()) { - int endPosition = Math.min(currentPosition + maxLineLength, text.length()); - - // If we're not at the end of the text, look for a natural break point - if (endPosition < text.length()) { - // Look for natural break points: period, comma, space, etc. - int breakPoint = findBreakPoint(text, currentPosition, endPosition); - if (breakPoint > currentPosition) { - endPosition = breakPoint; - } - } - - // Add the current segment - if (result.length() > 0) { - result.append("\n "); // Indent continuation lines - } - result.append(text.substring(currentPosition, endPosition).trim()); - - // Move to next segment - currentPosition = endPosition; - } - - return result.toString(); - } - - /** - * Finds a suitable break point in text between start and end positions - * - * @param text The text to analyze - * @param start Start position to search from - * @param end End position to search to - * @return Position of a good break point, or end if none found - */ - private static int findBreakPoint(String text, int start, int end) { - // Search backward from the end position for a good break point - for (int i = end; i > start; i--) { - char c = text.charAt(i); - - // Good break points in priority order - if (c == '.' || c == '!' || c == '?') { - return i + 1; // Break after sentence-ending punctuation - } else if (c == ';' || c == ':') { - return i + 1; // Break after semicolons or colons - } else if (c == ',') { - return i + 1; // Break after commas - } else if (Character.isWhitespace(c)) { - return i + 1; // Break after whitespace - } - } - - // If no good break point found, just use the end position - return end; - } - - /** - * Logs detailed information about the sorted plugin list for debugging table ordering. - * This shows the order plugins appear in the schedule table and explains the sorting criteria. - * - * @param sortedPlugins The sorted list of plugins as they appear in the table - */ - public static void logPluginScheduleEntryList(List sortedPlugins) { - StringBuilder tableOrderLog = new StringBuilder(); - tableOrderLog.append("\n=== SCHEDULE TABLE ORDERING DEBUG ===\n"); - tableOrderLog.append("Plugins are sorted by priority order:\n"); - tableOrderLog.append("1. Enabled status (enabled first)\n"); - tableOrderLog.append("2. Running status (running first)\n"); - tableOrderLog.append("3. Due-to-run status (due first)\n"); - tableOrderLog.append("4. Next run time (earliest first)\n"); - tableOrderLog.append("5. Priority level (highest first)\n"); - tableOrderLog.append("6. Default status (non-default first)\n"); - tableOrderLog.append("7. Random scheduling (non-random first)\n"); - tableOrderLog.append("8. Plugin name (alphabetical)\n"); - tableOrderLog.append("9. Object identity (stable ordering)\n\n"); - tableOrderLog.append("Total plugins: ").append(sortedPlugins.size()).append("\n\n"); - - for (int i = 0; i < sortedPlugins.size(); i++) { - PluginScheduleEntry plugin = sortedPlugins.get(i); - tableOrderLog.append(String.format("Row %d: %s\n", i, plugin.getCleanName())); - - // Priority information - tableOrderLog.append(String.format(" Priority: %d %s\n", - plugin.getPriority(), - plugin.isDefault() ? "(DEFAULT - always priority 0)" : "")); - - // Status information - tableOrderLog.append(String.format(" Status: %s%s%s\n", - plugin.isEnabled() ? "ENABLED" : "DISABLED", - plugin.isRunning() ? " | RUNNING" : "", - plugin.isStopInitiated() ? " | STOPPING" : "")); - - // Next schedule time - Optional nextTime = plugin.getCurrentStartTriggerTime(); - if (nextTime.isPresent()) { - tableOrderLog.append(String.format(" Next Run: %s\n", - nextTime.get().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))); - } else { - tableOrderLog.append(" Next Run: No trigger time available\n"); - } - - // Condition information - boolean hasStartConditions = plugin.hasAnyStartConditions(); - boolean hasStopConditions = plugin.hasAnyStopConditions(); - tableOrderLog.append(String.format(" Conditions: Start=%s, Stop=%s\n", - hasStartConditions ? String.format("%d conditions", plugin.getStartConditionManager().getConditions().size()) : "None", - hasStopConditions ? String.format("%d conditions", plugin.getStopConditionManager().getConditions().size()) : "None")); - - // Start condition readiness - if (hasStartConditions) { - boolean canStart = plugin.canBeStarted(); - boolean isDue = plugin.isDueToRun(); - tableOrderLog.append(String.format(" Start Ready: %s | Due to Run: %s\n", - canStart ? "YES" : "NO", - isDue ? "YES" : "NO")); - } - - // Plugin properties - tableOrderLog.append(String.format(" Properties: RandomScheduling=%s, AllowContinue=%s, RunCount=%d\n", - plugin.isAllowRandomScheduling() ? "YES" : "NO", - plugin.isAllowContinue() ? "YES" : "NO", - plugin.getRunCount())); - - // One-time schedule information - if (plugin.hasAnyOneTimeStartConditions()) { - boolean hasTriggered = plugin.hasTriggeredOneTimeStartConditions(); - boolean canTriggerAgain = plugin.canStartTriggerAgain(); - tableOrderLog.append(String.format(" One-Time: HasTriggered=%s, CanTriggerAgain=%s\n", - hasTriggered ? "YES" : "NO", - canTriggerAgain ? "YES" : "NO")); - } - - // Last run information - if (plugin.getRunCount() > 0) { - tableOrderLog.append(String.format(" Last Run: %s (%s)\n", - plugin.getLastStopReasonType().getDescription(), - plugin.isLastRunSuccessful() ? "SUCCESS" : "FAILED")); - } - - tableOrderLog.append("\n"); - } - - - - log.info(tableOrderLog.toString()); - } - - - /** - * Detects if any enabled SchedulablePlugin has locked LockConditions or unsatisfied PredicateConditions. - * This prevents the break handler from taking breaks during critical plugin operations. - * - * @return true if any schedulable plugin has locked conditions or unsatisfied predicate conditions, false otherwise - */ - public static boolean hasLockedSchedulablePlugins() { - try { - // Get all enabled plugins from the plugin manager - return Microbot.getPluginManager().getPlugins().stream() - .filter(plugin -> Microbot.getPluginManager().isPluginEnabled(plugin)) - .filter(plugin -> plugin instanceof SchedulablePlugin) - .map(plugin -> (SchedulablePlugin) plugin) - .anyMatch(schedulablePlugin -> { - try { - // Get the stop condition from the schedulable plugin - LogicalCondition stopCondition = schedulablePlugin.getStopCondition(); - if (stopCondition != null) { - // Find all LockConditions in the logical condition structure using the utility method - List lockConditions = stopCondition.findAllLockConditions(); - // Check if any LockCondition is currently locked - boolean hasLockedConditions = lockConditions.stream().anyMatch(LockCondition::isLocked); - - // Find all PredicateConditions in the logical condition structure - List> predicateConditions = stopCondition.findAllPredicateConditions(); - // Check if any PredicateCondition is not satisfied - boolean hasUnsatisfiedPredicates = predicateConditions.stream() - .anyMatch(predicateCondition -> !predicateCondition.isSatisfied()); - - return hasLockedConditions || hasUnsatisfiedPredicates; - } - return false; - } catch (Exception e) { - log.error("Error checking stop conditions for schedulable plugin - " + e.getMessage()); - return false; - } - }); - } catch (Exception e) { - log.error("Error checking schedulable plugins for lock conditions: " + e.getMessage()); - return false; - } - } - - /** - * 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 - */ - public static Optional getTimeUntilUpComingScheduledPlugin() { - try { - // Get the SchedulerPlugin instance - SchedulerPlugin schedulerPlugin = (SchedulerPlugin) Microbot.getPlugin(SchedulerPlugin.class.getName()); - - // Check if scheduler plugin exists and is running - if (schedulerPlugin == null) { - Microbot.log("SchedulerPlugin is not loaded, cannot determine next plugin time", Level.DEBUG); - return Optional.empty(); - } - - // Check if the scheduler is in an active state - if (!schedulerPlugin.getCurrentState().isSchedulerActive()) { - Microbot.log("SchedulerPlugin is not in active state: " + schedulerPlugin.getCurrentState(), Level.DEBUG); - return Optional.empty(); - } - - // Get the upcoming plugin - PluginScheduleEntry upcomingPlugin = schedulerPlugin.getUpComingPlugin(); - if (upcomingPlugin == null) { - Microbot.log("No upcoming plugin found in scheduler", Level.DEBUG); - return Optional.empty(); - } - - // Get the time until the next run for this plugin - Optional timeUntilRun = upcomingPlugin.getTimeUntilNextRun(); - if (!timeUntilRun.isPresent()) { - Microbot.log("Cannot determine time until next run for plugin: " + upcomingPlugin.getCleanName(), Level.DEBUG); - return Optional.empty(); - } - - Duration duration = timeUntilRun.get(); - - // Log the result for debugging - Microbot.log("Next plugin '" + upcomingPlugin.getCleanName() + "' scheduled in: " + - formatDuration(duration), Level.DEBUG); - - return Optional.of(duration); - - } 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 - */ - public static Optional getUpComingScheduledPluginInfo() { - try { - SchedulerPlugin schedulerPlugin = (SchedulerPlugin) Microbot.getPlugin(SchedulerPlugin.class.getName()); - - if (schedulerPlugin == null || !schedulerPlugin.getCurrentState().isSchedulerActive()) { - return Optional.empty(); - } - - PluginScheduleEntry upcomingPlugin = schedulerPlugin.getUpComingPlugin(); - if (upcomingPlugin == null) { - return Optional.empty(); - } - - Optional timeUntilRun = upcomingPlugin.getTimeUntilNextRun(); - if (!timeUntilRun.isPresent()) { - return Optional.of("Next plugin: " + upcomingPlugin.getCleanName() + " (time unknown)"); - } - - String formattedTime = formatDuration(timeUntilRun.get()); - return Optional.of("Next plugin: " + upcomingPlugin.getCleanName() + " in " + formattedTime); - - } 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 - */ - public static Optional getNextUpComingPluginScheduleEntry() { - try { - SchedulerPlugin schedulerPlugin = (SchedulerPlugin) Microbot.getPlugin(SchedulerPlugin.class.getName()); - - if (schedulerPlugin == null || !schedulerPlugin.getCurrentState().isSchedulerActive()) { - return Optional.empty(); - } - - PluginScheduleEntry upcomingPlugin = schedulerPlugin.getUpComingPlugin(); - return Optional.ofNullable(upcomingPlugin); - - } catch (Exception e) { - Microbot.log("Error getting next scheduled plugin entry: " + e.getMessage(), Level.ERROR); - return Optional.empty(); - } - } - - /** - * Gets the estimated time until the next scheduled plugin will be ready to run. - * This method uses the enhanced estimation system to provide more accurate - * predictions by considering both current plugin stop conditions and upcoming - * plugin start conditions. - * - * @return Optional containing the estimated duration until the next plugin runs, - * or empty if no plugin is upcoming or time cannot be determined - */ - public static Optional getEstimatedTimeUntilNextScheduledPlugin() { - try { - // Get the SchedulerPlugin instance - SchedulerPlugin schedulerPlugin = (SchedulerPlugin) Microbot.getPlugin(SchedulerPlugin.class.getName()); - - // Check if scheduler plugin exists and is running - if (schedulerPlugin == null) { - Microbot.log("SchedulerPlugin is not loaded, cannot determine estimated next plugin time", Level.DEBUG); - return Optional.empty(); - } - - // Check if the scheduler is in an active state - if (!schedulerPlugin.getCurrentState().isSchedulerActive()) { - Microbot.log("SchedulerPlugin is not in active state: " + schedulerPlugin.getCurrentState(), Level.DEBUG); - return Optional.empty(); - } - - // Get the estimated schedule time using the new system - Optional estimatedTime = schedulerPlugin.getUpComingEstimatedScheduleTime(); - - if (estimatedTime.isPresent()) { - Duration duration = estimatedTime.get(); - - // Log the result for debugging - Microbot.log("Next plugin estimated to be scheduled in: " + - formatDuration(duration), Level.DEBUG); - - return Optional.of(duration); - } else { - Microbot.log("Cannot estimate time until next scheduled plugin", Level.DEBUG); - return Optional.empty(); - } - - } catch (Exception e) { - Microbot.log("Error getting estimated time until next scheduled plugin: " + e.getMessage(), Level.ERROR); - return Optional.empty(); - } - } - - /** - * Gets enhanced information about the next scheduled plugin using the estimation system. - * This method provides more accurate predictions by considering both current plugin - * stop conditions and upcoming plugin start conditions. - * - * @return Optional containing a formatted string with plugin name and estimated time until run, - * or empty if no plugin is upcoming - */ - public static Optional getNextScheduledPluginInfoWithEstimation() { - try { - SchedulerPlugin schedulerPlugin = (SchedulerPlugin) Microbot.getPlugin(SchedulerPlugin.class.getName()); - - if (schedulerPlugin == null || !schedulerPlugin.getCurrentState().isSchedulerActive()) { - return Optional.empty(); - } - - PluginScheduleEntry upcomingPlugin = schedulerPlugin.getUpComingPlugin(); - if (upcomingPlugin == null) { - return Optional.empty(); - } - - // Use the enhanced estimation system - Optional estimatedTime = schedulerPlugin.getUpComingEstimatedScheduleTime(); - if (!estimatedTime.isPresent()) { - return Optional.of("Next plugin: " + upcomingPlugin.getCleanName() + " (estimation unavailable)"); - } - - String formattedTime = formatDuration(estimatedTime.get()); - return Optional.of("Next plugin: " + upcomingPlugin.getCleanName() + " estimated in " + formattedTime); - - } catch (Exception e) { - Microbot.log("Error getting next scheduled plugin info with estimation: " + e.getMessage(), Level.ERROR); - return Optional.empty(); - } - } - - /** - * Gets enhanced information about the next scheduled plugin within a time window. - * This method provides predictions for plugins that will be ready within the specified timeframe. - * - * @param timeWindow The time window to look ahead for upcoming plugins - * @return Optional containing a formatted string with plugin name and estimated time until run, - * or empty if no plugin is upcoming within the window - */ - public static Optional getNextScheduledPluginInfoWithinTimeWindow(Duration timeWindow) { - try { - SchedulerPlugin schedulerPlugin = (SchedulerPlugin) Microbot.getPlugin(SchedulerPlugin.class.getName()); - - if (schedulerPlugin == null || !schedulerPlugin.getCurrentState().isSchedulerActive()) { - return Optional.empty(); - } - - // Get plugin within the time window - PluginScheduleEntry upcomingPlugin = schedulerPlugin.getUpComingPluginWithinTime(timeWindow); - if (upcomingPlugin == null) { - return Optional.empty(); - } - - // Use the enhanced estimation system for the time window - Optional estimatedTime = schedulerPlugin.getUpComingEstimatedScheduleTimeWithinTime(timeWindow); - if (!estimatedTime.isPresent()) { - return Optional.of("Next plugin within " + formatDuration(timeWindow) + ": " + - upcomingPlugin.getCleanName() + " (estimation unavailable)"); - } - - String formattedTime = formatDuration(estimatedTime.get()); - return Optional.of("Next plugin within " + formatDuration(timeWindow) + ": " + - upcomingPlugin.getCleanName() + " estimated in " + formattedTime); - - } catch (Exception e) { - Microbot.log("Error getting next scheduled plugin info within time window: " + e.getMessage(), Level.ERROR); - return Optional.empty(); - } - } - - public static void disableAllRunningNonEessentialPlugin() { - - - // Check if client is at login screen - List conditionProviders = new ArrayList<>(); - if (Microbot.getPluginManager() == null || Microbot.getClient() == null) { - return; - - } else { - // Find all plugins implementing ConditionProvider - conditionProviders = Microbot.getPluginManager().getPlugins().stream() - .filter(plugin -> plugin instanceof SchedulablePlugin) - .collect(Collectors.toList()); - - // Filter out essential plugins and disable non-essential enabled plugins - List enabledList = conditionProviders.stream() - .filter(plugin -> Microbot.getPluginManager().isPluginEnabled(plugin)) - .filter(plugin -> { - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - return descriptor != null && !descriptor.enabledByDefault(); - }) - .collect(Collectors.toList()); - - // Helper predicate to identify Microbot plugins - Predicate isMicrobotPlugin = plugin -> - plugin.getClass().getPackage().getName().toLowerCase().contains("microbot"); - - // Helper predicate to identify external plugins from Microbot Hub - Predicate isMicrobotExternalPlugin = plugin -> { - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - return descriptor != null && descriptor.isExternal() && - plugin.getClass().getPackage().getName().toLowerCase().contains("microbot"); - }; - - // Disable all non-essential plugins that are currently enabled, but exclude Microbot-related plugins - List allEnabledPlugins = Microbot.getPluginManager().getPlugins().stream() - .filter(plugin -> Microbot.getPluginManager().isPluginEnabled(plugin) - && !plugin.getClass().getName().equals("net.runelite.client.plugins.microbot.pluginscheduler.SchedulerPlugin")) - .collect(Collectors.toList()); - - for (Plugin plugin : allEnabledPlugins) { - PluginDescriptor descriptor = plugin.getClass().getAnnotation(PluginDescriptor.class); - - // Skip if it's not a Microbot plugin (internal or external from Microbot Hub) - if (!(isMicrobotPlugin.test(plugin) || isMicrobotExternalPlugin.test(plugin))) { - continue; - } - - // Only disable non-essential, Microbot - if (descriptor != null && !descriptor.enabledByDefault()) { - try { - Microbot.stopPlugin(plugin); - log.debug("Disabled non-essential Microbot plugin: {}", plugin.getName()); - } catch (Exception e) { - log.warn("Failed to disable plugin {}: {}", plugin.getName(), e.getMessage()); - } - } - } - } - } - - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java index fbb224c0c46..b672fdc954f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/pathfinder/PathfinderConfig.java @@ -13,7 +13,6 @@ import net.runelite.client.plugins.microbot.globval.enums.InterfaceTab; import net.runelite.client.plugins.microbot.shortestpath.*; import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.cache.Rs2SpiritTreeCache; import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; import net.runelite.client.plugins.microbot.util.magic.Rs2Magic; @@ -23,7 +22,6 @@ import net.runelite.client.plugins.microbot.util.poh.PohTeleports; import net.runelite.client.plugins.microbot.util.tabs.Rs2Tab; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; -import net.runelite.client.plugins.microbot.util.cache.Rs2SkillCache; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; @@ -260,9 +258,6 @@ private void refreshTransports(WorldPoint target) { usableTeleports.clear(); // Check spirit tree farming states for farmable spirit trees - Rs2SpiritTreeCache.getInstance().update(); - //Rs2SpiritTreeCache.logAllTreeStates(); - for (Map.Entry> entry : createMergedList().entrySet()) { WorldPoint point = entry.getKey(); Set usableTransports = new HashSet<>(entry.getValue().size()); @@ -477,8 +472,6 @@ private boolean useTransport(Transport transport) { log.debug("Transport ( O: {} D: {} ) requires quests {}", transport.getOrigin(), transport.getDestination(), transport.getQuests()); return false; } - // Check Spirit Tree specific requirements (farming state for farmable trees) - if (transport.getType() == TransportType.SPIRIT_TREE) return isSpiritTreeUsable(transport); // If the transport has varbit requirements & the varbits do not match if (!varbitChecks(transport)) { @@ -543,13 +536,7 @@ private boolean hasRequiredLevels(Transport transport) { Skill[] skills = Skill.values(); return IntStream.range(0, requiredLevels.length) .filter(i -> requiredLevels[i] > 0) - .allMatch(i -> { - if (Microbot.isRs2CacheEnabled()) { - return Rs2SkillCache.getBoostedSkillLevel(skills[i]) >= requiredLevels[i]; - } else { - return Microbot.getClient().getBoostedSkillLevel(skills[i]) >= requiredLevels[i]; - } - }); + .allMatch(i -> Microbot.getClient().getBoostedSkillLevel(skills[i]) >= requiredLevels[i]); } /** @@ -560,13 +547,7 @@ private boolean hasRequiredLevels(Restriction restriction) { Skill[] skills = Skill.values(); return IntStream.range(0, requiredLevels.length) .filter(i -> requiredLevels[i] > 0) - .allMatch(i -> { - if (Microbot.isRs2CacheEnabled()) { - return Rs2SkillCache.getBoostedSkillLevel(skills[i]) >= requiredLevels[i]; - } else { - return Microbot.getClient().getBoostedSkillLevel(skills[i]) >= requiredLevels[i]; - } - }); + .allMatch(i -> Microbot.getClient().getBoostedSkillLevel(skills[i]) >= requiredLevels[i]); } private void updateActionBasedOnQuestState(Transport transport) { @@ -732,19 +713,6 @@ private boolean hasChronicleCharges() { return charges != null && Integer.parseInt(charges) > 0; } - /** - * Check if a spirit tree transport is usable - * This method integrates with the farming system to determine if farmable spirit trees - * are planted and healthy enough for transportation - * - * @param transport The spirit tree transport to check - * @return true if the spirit tree is available for travel - */ - private boolean isSpiritTreeUsable(Transport transport) { - // Use the Rs2SpiritTreeCache directly for better performance and consistency - return Rs2SpiritTreeCache.isSpiritTreeTransportAvailable(transport); - } - @Deprecated(since = "1.6.2 - Add Restrictions to restrictions.tsv", forRemoval = true) public void setRestrictedTiles(Restriction... restrictions) { this.customRestrictions = List.of(restrictions); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/bank/Rs2Bank.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/bank/Rs2Bank.java index ee9d864877e..82492fa44de 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/bank/Rs2Bank.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/bank/Rs2Bank.java @@ -1,12 +1,10 @@ package net.runelite.client.plugins.microbot.util.bank; import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; import lombok.extern.slf4j.Slf4j; import net.runelite.api.*; import net.runelite.api.coords.WorldArea; import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.ItemContainerChanged; import net.runelite.api.gameval.InterfaceID; import net.runelite.api.gameval.ItemID; import net.runelite.api.gameval.VarbitID; @@ -21,7 +19,6 @@ import net.runelite.client.plugins.microbot.shortestpath.pathfinder.Pathfinder; import net.runelite.client.plugins.microbot.util.antiban.Rs2AntibanSettings; import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; -import net.runelite.client.plugins.microbot.util.cache.serialization.CacheSerializationManager; import net.runelite.client.plugins.microbot.util.coords.Rs2WorldPoint; import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; @@ -58,7 +55,6 @@ import static net.runelite.api.widgets.ComponentID.BANK_INVENTORY_ITEM_CONTAINER; import static net.runelite.api.widgets.ComponentID.BANK_ITEM_CONTAINER; -import static net.runelite.client.plugins.microbot.Microbot.updateItemContainer; import static net.runelite.client.plugins.microbot.util.Global.*; import static net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject.hoverOverObject; import static net.runelite.client.plugins.microbot.util.npc.Rs2Npc.hoverOverActor; @@ -2143,358 +2139,6 @@ public static boolean walkToBankAndUseBank(BankLocation bankLocation, boolean to return Rs2Bank.openBank(); } - /** - * Updates the bank items in memory based on the provided event. - * Thread-safe method called from the client thread via event handler. - * - * @param e The event containing the latest bank items. - */ - public static void updateLocalBank(ItemContainerChanged e) { - synchronized (lock) { - List list = updateItemContainer(InventoryID.BANK.getId(), e); - if (list != null) { - // Update the centralized bank data (Rs2BankData.set() is already synchronized) - updateCache(list); - } else { - log.debug("Bank data update skipped - no items received"); - } - } - } - - - /** - * Updates the cached bank data with the latest bank items and saves to config. - * - * @param items The current bank items - */ - private static void updateCache(List items) { - if (items != null) { - // save the current bank items before updating - if ( !rsProfileKey.get().isEmpty() && !rsProfileKey.get().equals(Microbot.getConfigManager().getRSProfileKey())){ - saveCacheToConfig(rsProfileKey.get()); - } - rs2BankData.set(items); - if (rsProfileKey.get().isEmpty() || !rsProfileKey.get().equals(Microbot.getConfigManager().getRSProfileKey())) { - rsProfileKey.set(Microbot.getConfigManager().getRSProfileKey()); - } - saveCacheToConfig(rsProfileKey.get()); - validLoadedCache.set(true); - } - } - public static void loadInitialCacheFromCurrentConfig() { - rsProfileKey.set(Microbot.getConfigManager().getRSProfileKey()); - loadCacheFromConfig(rsProfileKey.get()); - } - /** - * Loads the initial bank state from config. Should be called when a player logs in. - * Thread-safe method that synchronizes config loading. - */ - public static void loadCacheFromConfig(String newRsProfileKey) { - synchronized (lock) { - if (!validLoadedCache.get()) { - Player localPlayer = Microbot.getClient().getLocalPlayer(); - if (localPlayer != null && localPlayer.getName() != null) { - loadCache(newRsProfileKey); - log.debug("-load bank cache, bank items size: {}", rs2BankData.size()); - validLoadedCache.set(LoginManager.isLoggedIn()); - } - } - } - } - - /** - * Sets the initial state as unknown. Called when logging out or changing profiles. - * Thread-safe method that synchronizes state clearing. - */ - public static void setUnknownInitialCacheState() { - synchronized (lock) { - if (validLoadedCache.get() && !rsProfileKey.get().isEmpty() && Microbot.getConfigManager() != null && rsProfileKey.get().equals(Microbot.getConfigManager().getRSProfileKey())) { - saveCacheToConfig(rsProfileKey.get()); - } - markCacheStale(); - rsProfileKey.set(""); - } - } - - /** - * Handles profile changes by saving current cache and invalidating for the new profile. - * This ensures cache state is properly maintained across profile switches. - * - * @param newProfileKey the new profile key - * @param oldProfileKey the previous profile key (can be null) - */ - public static void handleProfileChange(String newProfileKey, String oldProfileKey) { - synchronized (lock) { - log.debug("Handling bank cache profile change from '{}' to '{}'", oldProfileKey, newProfileKey); - - // Save current cache state if valid - if (oldProfileKey != null && !oldProfileKey.isEmpty() && isCacheDataValid()) { - log.debug("Saving bank cache for previous profile: {}", oldProfileKey); - saveCacheToConfig(oldProfileKey); - } - - // Mark cache as stale for profile change - markCacheStale(); - - // Update profile key - rsProfileKey.set(newProfileKey != null ? newProfileKey : ""); - - // Load cache for new profile if available - if (newProfileKey != null && !newProfileKey.isEmpty()) { - loadCacheFromConfig(newProfileKey); - } - } - } - - /** - * Loads bank state from config, handling profile changes. - * Similar to QuestBank.loadState(). - */ - private static void loadCache(String newRsProfileKey ) { - // Only re-load from config if loading from a new profile - if (newRsProfileKey != null && !newRsProfileKey.equals(rsProfileKey.get())) { - // If we've hopped between profiles, save current state first - if (!rsProfileKey.get().isEmpty() && validLoadedCache.get()) { - saveCacheToConfig(rsProfileKey.get()); - } - - loadCacheFromConfigInternal(newRsProfileKey); - } - } - - /** - * Loads bank data from RuneLite config system. - * Updated to use character-specific caching. - */ - private static void loadCacheFromConfigInternal(String rsProfileKey) { - if (rsProfileKey == null || Microbot.getConfigManager() == null) { - log.warn("Cannot load bank data, rsProfileKey or config manager is null"); - return; - } - - // get current player name for character-specific loading - String playerName = getCurrentPlayerName(); - if (playerName == null) { - log.warn("Cannot load bank data - no player name available"); - return; - } - - Rs2Bank.rsProfileKey.set(rsProfileKey); - worldType = RuneScapeProfileType.getCurrent(Microbot.getClient()); - log.debug("Loading bank data for profile: {}, player: {}, world type: {}", rsProfileKey, playerName, worldType); - - // use character-specific key - String characterSpecificKey = CacheSerializationManager.createCharacterSpecificKey(BANK_KEY, playerName); - String json = Microbot.getConfigManager().getConfiguration(CONFIG_GROUP, rsProfileKey, characterSpecificKey); - - try { - if (json != null && !json.isEmpty()) { - int[] data = gson.fromJson(json, int[].class); - log.debug("Loaded {} bank items from config for player {}", data.length, playerName); - rs2BankData.setIdQuantityAndSlot(data); - log.debug("finished loading bank data for player {}, size: {}", playerName, rs2BankData.size()); - - // Load cached items if no live bank data - if (rs2BankData.getBankItems().isEmpty()) { - // Cache is already loaded via setIdQuantityAndSlot - log.debug("Loaded {} cached bank items from config for player {}", rs2BankData.size(), playerName); - } - log.debug("build data should now be valid for player {}, size: {}", playerName, rs2BankData.size()); - } else { - rs2BankData.setEmpty(); - log.debug("No cached bank data found in config for player {}", playerName); - } - } catch (JsonSyntaxException err) { - log.warn("Failed to parse cached bank data from config for player {}, resetting cache", playerName, err); - rs2BankData.setEmpty(); - saveCacheToConfig(Rs2Bank.rsProfileKey.get()); - } - } - - /** - * Saves the current bank state to RuneLite config system. - * Updated to use character-specific caching. - */ - public static void saveCacheToConfig(String newRsProfileKey) { - if (newRsProfileKey == null || Microbot.getConfigManager() == null) { - return; - } - - // get current player name for character-specific saving - String playerName = getCurrentPlayerName(); - if (playerName == null) { - log.warn("Cannot save bank data - no player name available"); - return; - } - - try { - // use character-specific key - String characterSpecificKey = CacheSerializationManager.createCharacterSpecificKey(BANK_KEY, playerName); - String json = gson.toJson(rs2BankData.getIdQuantityAndSlot()); - Microbot.getConfigManager().setConfiguration(CONFIG_GROUP, newRsProfileKey, characterSpecificKey, json); - log.debug("Saved {} bank items to config cache for player {}", rs2BankData.size(), playerName); - } catch (Exception e) { - log.error("Failed to save bank data to config for player {}", playerName, e); - } - } - - /** - * Clears the bank cache state. Called when logging out. - * Thread-safe method that synchronizes cache clearing. - */ - public static void emptyCacheState() { - synchronized (lock) { - rsProfileKey.set(""); - worldType = null; - rs2BankData.setEmpty(); - validLoadedCache.set(false); - // Rs2BankData handles its own cache states when emptied - log.debug("Emptied bank state and cache"); - } - } - - - /** - * Checks if we have cached bank data available. - * - * @return true if cached bank data is available, false otherwise - */ - public static boolean hasCachedBankData() { - return !rs2BankData.isEmpty(); - } - - /** - * Checks if the bank cache data is VALID (Profile-level validation). - * - * VALID = Rs2Bank profile state is consistent and trustworthy - * - validLoadedCache flag is true (Rs2Bank has processed cache data) - * - rsProfileKey matches current RuneLite profile (no profile switches) - * - ConfigManager is available for reading/writing cache - * - No stale cache from previous sessions or different characters - * - This is Rs2Bank's validation layer ON TOP OF Rs2BankData states - * - * NOTE: This does NOT check if cache is loaded or built - only profile consistency - * Use isCacheLoaded() to check complete cache readiness - * - * @return true if cache data is valid and current, false if stale or needs rebuild - */ - public static boolean isCacheDataValid() { - return validLoadedCache.get() - && !rsProfileKey.get().isEmpty() - && Microbot.getConfigManager() != null - && rsProfileKey.get().equals(Microbot.getConfigManager().getRSProfileKey()); - } - - /** - * Checks if the bank cache is COMPLETE AND READY for script usage. - * - * This is the MASTER CHECK that combines all validation layers: - * - * 1. VALID (Profile-level): Rs2Bank profile state is consistent - * - No profile switches, config manager available, flags consistent - * - * 2. LOADED (Data-level): Raw cache data exists from config - * - idQuantityAndSlot array populated with [id, quantity, slot] triplets - * - * 3. BUILT (Object-level): Rs2ItemModel objects are ready for use - * - rebuildBankItemsList() executed successfully on client thread - * - Items have proper names, properties, and are script-accessible - * - * Scripts should ONLY use bank data when this returns true. - * This prevents NPE, stale data, and incomplete cache issues. - * - * @return true if ALL cache layers are ready (valid + loaded + built), false otherwise - */ - public static boolean isCacheLoaded() { - return isCacheDataValid() && rs2BankData.isCacheReady(); - } - - /** - * Marks the cache as "stale" requiring rebuild on invalid cache data. - * This is called when cache data becomes inconsistent or profile changes. - */ - public static void markCacheStale() { - synchronized (lock) { - log.debug("Marking bank cache as stale - needs rebuild"); - rs2BankData.markForRebuild(); - validLoadedCache.set(false); - } - } - - /** - * Invalidates the bank cache, optionally saving current state first. - * Similar to Rs2CacheManager invalidation pattern. - * - * @param saveBeforeInvalidating if true, saves current cache state before invalidating - */ - public static void invalidateCache(boolean saveBeforeInvalidating) { - synchronized (lock) { - if (saveBeforeInvalidating && isCacheDataValid()) { - log.debug("Saving bank cache before invalidation"); - saveCacheToConfig(rsProfileKey.get()); - } - log.debug("Invalidating bank cache"); - rs2BankData.setEmpty(); - markCacheStale(); - } - } - - /** - * Forces a cache rebuild by marking it as stale and clearing data. - * This should be called when profile switches or data becomes inconsistent. - */ - public static void forceCacheRebuild() { - synchronized (lock) { - log.debug("Forcing bank cache rebuild due to inconsistent state"); - invalidateCache(true); - } - } - - /** - * Gets comprehensive cache state information for debugging. - * Includes both Rs2Bank and Rs2BankData states. - * - * @return formatted string with complete cache state details - */ - public static String getDetailedCacheState() { - return String.format("Rs2Bank[profileValid=%s, profileKey='%s'] + %s", - isCacheDataValid(), - rsProfileKey.get(), - rs2BankData.getCacheStateInfo()); - } - - /** - * Checks if the bank cache data is LOADED (Stage 1: Raw data from config). - * - * STAGE 1 LOADED = Raw integers available but NOT usable yet - * - idQuantityAndSlot array contains [id, quantity, slot] triplets - * - Data restored from RuneLite config on login/profile switch - * - Items are still just numbers - NO Rs2ItemModel objects yet - * - Client thread processing NOT required for this stage - * - Does NOT mean scripts can use the data yet - * - * @return true if raw cache data is loaded from config, false otherwise - */ - public static boolean isCacheDataLoaded() { - return rs2BankData.isCacheLoaded(); - } - - /** - * Checks if the bank cache is BUILT (Stage 2: Usable objects ready). - * - * STAGE 2 BUILT = Rs2ItemModel objects ready for script usage - * - rebuildBankItemsList() has completed successfully - * - Raw data converted to full Rs2ItemModel objects with names/properties - * - ItemManager validation completed on client thread - * - Scripts can immediately use hasItem(), count(), findBankItem(), etc. - * - No rebuild delays or client thread waiting required - * - * @return true if bankItems list is fully built and ready, false otherwise - */ - public static boolean isCacheDataBuilt() { - return rs2BankData.isCacheBuilt(); - } - /** * Handle bank pin boolean. * diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/CacheMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/CacheMode.java deleted file mode 100644 index 1afe1dcaaf0..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/CacheMode.java +++ /dev/null @@ -1,41 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -/** - * Enumeration defining different cache invalidation modes. - * Controls how and when cache entries are invalidated. - * - * @author Vox - * @version 1.0 - */ -public enum CacheMode { - /** - * Automatic invalidation mode. - * Cache entries are automatically invalidated based on TTL (Time-To-Live) - * and global invalidation intervals. This is the default behavior for - * most caches that need periodic refresh. - */ - AUTOMATIC_INVALIDATION, - - /** - * Event-driven invalidation mode. - * Cache entries are only invalidated when specific events occur. - * No automatic timeout-based invalidation is performed. - * - * This mode is ideal for entity caches (NPCs, Objects, Ground Items) - * where data should persist until: - * - GameState changes - * - Entity despawn events - * - Manual invalidation - */ - EVENT_DRIVEN_ONLY, - - /** - * Manual invalidation mode. - * Cache entries are never automatically invalidated. - * Invalidation must be triggered manually by calling invalidation methods. - * - * This mode provides maximum control over cache lifecycle - * and is suitable for data that rarely changes. - */ - MANUAL_ONLY -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/MemorySizeCalculator.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/MemorySizeCalculator.java deleted file mode 100644 index 9e3e733b5a7..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/MemorySizeCalculator.java +++ /dev/null @@ -1,514 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Quest; -import net.runelite.api.QuestState; -import net.runelite.api.Skill; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.util.cache.model.*; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; - -import java.lang.reflect.Array; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; - -/** - * Utility class for calculating approximate memory sizes of objects in cache. - * Provides both precise calculations using Java Instrumentation (when available) - * and estimates using reflection for fallback. - * - * For maximum accuracy, enable Java Instrumentation by adding JVM argument: - * -javaagent:microbot-agent.jar - */ -@Slf4j -public class MemorySizeCalculator { - - // Configuration flags - private static final boolean USE_INSTRUMENTATION_WHEN_AVAILABLE = true; - private static final boolean LOG_MEASUREMENT_METHOD = false; // Set to true for debugging - - // Object header overhead on 64-bit JVM with compressed OOPs - private static final int OBJECT_HEADER_SIZE = 12; - private static final int REFERENCE_SIZE = 4; // Compressed OOPs - private static final int ARRAY_HEADER_SIZE = 16; - - // Primitive type sizes - private static final int BOOLEAN_SIZE = 1; - private static final int BYTE_SIZE = 1; - private static final int CHAR_SIZE = 2; - private static final int SHORT_SIZE = 2; - private static final int INT_SIZE = 4; - private static final int FLOAT_SIZE = 4; - private static final int LONG_SIZE = 8; - private static final int DOUBLE_SIZE = 8; - - // Common object sizes (cached for performance) - private static final Map, Integer> KNOWN_SIZES = new ConcurrentHashMap<>(); - - static { - // Primitive wrapper sizes - KNOWN_SIZES.put(Boolean.class, OBJECT_HEADER_SIZE + BOOLEAN_SIZE); - KNOWN_SIZES.put(Byte.class, OBJECT_HEADER_SIZE + BYTE_SIZE); - KNOWN_SIZES.put(Character.class, OBJECT_HEADER_SIZE + CHAR_SIZE); - KNOWN_SIZES.put(Short.class, OBJECT_HEADER_SIZE + SHORT_SIZE); - KNOWN_SIZES.put(Integer.class, OBJECT_HEADER_SIZE + INT_SIZE); - KNOWN_SIZES.put(Float.class, OBJECT_HEADER_SIZE + FLOAT_SIZE); - KNOWN_SIZES.put(Long.class, OBJECT_HEADER_SIZE + LONG_SIZE); - KNOWN_SIZES.put(Double.class, OBJECT_HEADER_SIZE + DOUBLE_SIZE); - - // Common RuneLite types - KNOWN_SIZES.put(WorldPoint.class, OBJECT_HEADER_SIZE + (3 * INT_SIZE)); // x, y, plane - KNOWN_SIZES.put(Skill.class, OBJECT_HEADER_SIZE + INT_SIZE); // enum ordinal - KNOWN_SIZES.put(Quest.class, OBJECT_HEADER_SIZE + INT_SIZE); // enum ordinal - KNOWN_SIZES.put(QuestState.class, OBJECT_HEADER_SIZE + INT_SIZE); // enum ordinal - - // AtomicLong used in cache statistics - KNOWN_SIZES.put(AtomicLong.class, OBJECT_HEADER_SIZE + LONG_SIZE); - } - - /** - * Calculates the approximate memory size of a cache key. - * - * @param key The cache key - * @return Estimated memory size in bytes - */ - public static long calculateKeySize(Object key) { - if (key == null) return 0; - - Class keyClass = key.getClass(); - - // Check known sizes first - Integer knownSize = KNOWN_SIZES.get(keyClass); - if (knownSize != null) { - return knownSize; - } - - // Handle common key types - if (key instanceof String) { - String str = (String) key; - return OBJECT_HEADER_SIZE + INT_SIZE + // String object (hash field) - ARRAY_HEADER_SIZE + (str.length() * CHAR_SIZE); // char array - } - - if (key instanceof Integer) { - return OBJECT_HEADER_SIZE + INT_SIZE; - } - - if (key instanceof Long) { - return OBJECT_HEADER_SIZE + LONG_SIZE; - } - - // For unknown types, use reflection-based calculation - return calculateObjectSizeReflection(key, new HashSet<>()); - } - - /** - * Calculates the approximate memory size of a cache value. - * Optimized for known Microbot cache value types. - * - * @param value The cache value - * @return Estimated memory size in bytes - */ - public static long calculateValueSize(Object value) { - if (value == null) return 0; - - Class valueClass = value.getClass(); - - // Check known sizes first - Integer knownSize = KNOWN_SIZES.get(valueClass); - if (knownSize != null) { - return knownSize; - } - - // Handle known Microbot cache value types - if (value instanceof Rs2NpcModel) { - return calculateNpcModelSize((Rs2NpcModel) value); - } - - if (value instanceof Rs2ObjectModel) { - return calculateObjectModelSize((Rs2ObjectModel) value); - } - - if (value instanceof Rs2GroundItemModel) { - return calculateGroundItemModelSize((Rs2GroundItemModel) value); - } - - if (value instanceof VarbitData) { - return calculateVarbitDataSize((VarbitData) value); - } - - if (value instanceof SkillData) { - return calculateSkillDataSize((SkillData) value); - } - - if (value instanceof SpiritTreeData) { - return calculateSpiritTreeDataSize((SpiritTreeData) value); - } - - if (value instanceof QuestState) { - return OBJECT_HEADER_SIZE + INT_SIZE; // Enum ordinal - } - - if (value instanceof String) { - String str = (String) value; - return OBJECT_HEADER_SIZE + INT_SIZE + - ARRAY_HEADER_SIZE + (str.length() * CHAR_SIZE); - } - - // For collections - if (value instanceof Collection) { - return calculateCollectionSize((Collection) value); - } - - if (value instanceof Map) { - return calculateMapSize((Map) value); - } - - // For unknown types, use reflection-based calculation - return calculateObjectSizeReflection(value, new HashSet<>()); - } - - /** - * Calculates memory size for Rs2NpcModel objects. - */ - private static long calculateNpcModelSize(Rs2NpcModel npcModel) { - long size = OBJECT_HEADER_SIZE; // Base object - - // Primitive fields in Rs2NpcModel (estimated from typical NPC model) - size += INT_SIZE * 6; // id, index, combatLevel, hitpoints, maxHitpoints, interacting - size += BOOLEAN_SIZE * 4; // animating, moving, inCombat, dead - size += LONG_SIZE * 2; // spawnTick, lastUpdateTick - - // Reference fields - size += REFERENCE_SIZE * 8; // worldPoint, animation, graphic, overhead, etc. - - // WorldPoint - size += OBJECT_HEADER_SIZE + (3 * INT_SIZE); - - // String name (average NPC name ~10 characters) - size += OBJECT_HEADER_SIZE + INT_SIZE + ARRAY_HEADER_SIZE + (10 * CHAR_SIZE); - - return size; - } - - /** - * Calculates memory size for Rs2ObjectModel objects. - */ - private static long calculateObjectModelSize(Rs2ObjectModel objectModel) { - long size = OBJECT_HEADER_SIZE; // Base object - - // Primitive fields - size += INT_SIZE * 4; // id, orientation, type, flags - size += LONG_SIZE * 2; // spawnTick, lastUpdateTick - - // Reference fields - size += REFERENCE_SIZE * 4; // worldPoint, actions, etc. - - // WorldPoint - size += OBJECT_HEADER_SIZE + (3 * INT_SIZE); - - // Actions array (average 5 actions, 8 chars each) - size += ARRAY_HEADER_SIZE + (5 * REFERENCE_SIZE); - size += 5 * (OBJECT_HEADER_SIZE + INT_SIZE + ARRAY_HEADER_SIZE + (8 * CHAR_SIZE)); - - return size; - } - - /** - * Calculates memory size for Rs2GroundItemModel objects. - */ - private static long calculateGroundItemModelSize(Rs2GroundItemModel groundItem) { - long size = OBJECT_HEADER_SIZE; // Base object - - // Primitive fields - size += INT_SIZE * 3; // id, quantity, visibleTicks - size += LONG_SIZE * 2; // spawnTick, despawnTick - - // Reference fields - size += REFERENCE_SIZE * 3; // worldPoint, name, etc. - - // WorldPoint - size += OBJECT_HEADER_SIZE + (3 * INT_SIZE); - - // Item name (average item name ~15 characters) - size += OBJECT_HEADER_SIZE + INT_SIZE + ARRAY_HEADER_SIZE + (15 * CHAR_SIZE); - - return size; - } - - /** - * Calculates memory size for VarbitData objects. - */ - private static long calculateVarbitDataSize(VarbitData varbitData) { - long size = OBJECT_HEADER_SIZE; // Base object - - // Primitive fields - size += INT_SIZE * 2; // varbit, value - size += LONG_SIZE * 2; // lastUpdateTick, cacheTimestamp - - // Reference fields - size += REFERENCE_SIZE * 2; // worldPoint, metadata - - // WorldPoint (if present) - size += OBJECT_HEADER_SIZE + (3 * INT_SIZE); - - // Metadata map (small, average 2 entries) - size += calculateMapSize(2, String.class, Object.class); - - return size; - } - - /** - * Calculates memory size for SkillData objects. - */ - private static long calculateSkillDataSize(SkillData skillData) { - long size = OBJECT_HEADER_SIZE; // Base object - - // Primitive fields - size += INT_SIZE * 3; // level, experience, boostedLevel - size += LONG_SIZE * 2; // lastUpdateTick, cacheTimestamp - - // Reference fields - size += REFERENCE_SIZE * 2; // skill enum, metadata - - // Skill enum - size += OBJECT_HEADER_SIZE + INT_SIZE; - - return size; - } - - /** - * Calculates memory size for SpiritTreeData objects. - */ - private static long calculateSpiritTreeDataSize(SpiritTreeData spiritTreeData) { - long size = OBJECT_HEADER_SIZE; // Base object - - // Primitive fields - size += INT_SIZE * 3; // patchIndex, state, level - size += LONG_SIZE * 3; // plantedTick, harvestTick, lastUpdateTick - - // Reference fields - size += REFERENCE_SIZE * 4; // location, treeType, metadata, etc. - - // WorldPoint - size += OBJECT_HEADER_SIZE + (3 * INT_SIZE); - - // String treeType (average ~12 characters) - size += OBJECT_HEADER_SIZE + INT_SIZE + ARRAY_HEADER_SIZE + (12 * CHAR_SIZE); - - return size; - } - - /** - * Calculates memory size for collections. - */ - private static long calculateCollectionSize(Collection collection) { - if (collection.isEmpty()) { - return OBJECT_HEADER_SIZE + INT_SIZE; // Empty collection overhead - } - - long size = OBJECT_HEADER_SIZE; // Collection object - - if (collection instanceof ArrayList) { - size += REFERENCE_SIZE + INT_SIZE * 2; // elementData array ref, size, modCount - size += ARRAY_HEADER_SIZE + (collection.size() * REFERENCE_SIZE); // Array overhead - } else if (collection instanceof HashSet) { - size += REFERENCE_SIZE * 3 + INT_SIZE * 3; // HashMap backing, size, threshold, modCount - size += calculateMapSize(collection.size(), Object.class, Object.class); - } else { - // Generic collection estimate - size += INT_SIZE + (collection.size() * REFERENCE_SIZE); - } - - // Add estimated content size (sample first few elements) - Iterator iter = collection.iterator(); - long avgElementSize = 0; - int sampleCount = Math.min(3, collection.size()); - - for (int i = 0; i < sampleCount && iter.hasNext(); i++) { - avgElementSize += calculateValueSize(iter.next()); - } - - if (sampleCount > 0) { - avgElementSize /= sampleCount; - size += avgElementSize * collection.size(); - } - - return size; - } - - /** - * Calculates memory size for maps. - */ - private static long calculateMapSize(Map map) { - if (map.isEmpty()) { - return OBJECT_HEADER_SIZE + INT_SIZE * 3; // Empty map overhead - } - - long size = OBJECT_HEADER_SIZE; // Map object - - if (map instanceof HashMap || map instanceof ConcurrentHashMap) { - size += REFERENCE_SIZE + INT_SIZE * 3; // table array ref, size, threshold, modCount - size += ARRAY_HEADER_SIZE + (map.size() * REFERENCE_SIZE * 2); // Bucket array + entries - size += map.size() * (OBJECT_HEADER_SIZE + INT_SIZE + REFERENCE_SIZE * 3); // Node objects - } else { - // Generic map estimate - size += INT_SIZE + (map.size() * REFERENCE_SIZE * 2); - } - - // Add estimated content size (sample first few entries) - Iterator> iter = map.entrySet().iterator(); - long avgKeySize = 0, avgValueSize = 0; - int sampleCount = Math.min(3, map.size()); - - for (int i = 0; i < sampleCount && iter.hasNext(); i++) { - Map.Entry entry = iter.next(); - avgKeySize += calculateKeySize(entry.getKey()); - avgValueSize += calculateValueSize(entry.getValue()); - } - - if (sampleCount > 0) { - avgKeySize /= sampleCount; - avgValueSize /= sampleCount; - size += (avgKeySize + avgValueSize) * map.size(); - } - - return size; - } - - /** - * Calculates memory size for a map with estimated entry count and types. - */ - private static long calculateMapSize(int entryCount, Class keyType, Class valueType) { - if (entryCount == 0) { - return OBJECT_HEADER_SIZE + INT_SIZE * 3; - } - - long size = OBJECT_HEADER_SIZE; // Map object - size += REFERENCE_SIZE + INT_SIZE * 3; // HashMap structure - size += ARRAY_HEADER_SIZE + (entryCount * REFERENCE_SIZE * 2); // Bucket array - size += entryCount * (OBJECT_HEADER_SIZE + INT_SIZE + REFERENCE_SIZE * 3); // Node objects - - // Add estimated key/value sizes - long keySize = KNOWN_SIZES.getOrDefault(keyType, 24); // Default estimate - long valueSize = KNOWN_SIZES.getOrDefault(valueType, 32); // Default estimate - - size += (keySize + valueSize) * entryCount; - - return size; - } - - /** - * Reflection-based object size calculation for unknown types. - * Uses visited set to handle circular references. - */ - private static long calculateObjectSizeReflection(Object obj, Set visited) { - if (obj == null || visited.contains(obj)) { - return 0; - } - - visited.add(obj); - - try { - Class clazz = obj.getClass(); - long size = OBJECT_HEADER_SIZE; - - // Handle arrays - if (clazz.isArray()) { - int length = Array.getLength(obj); - size += ARRAY_HEADER_SIZE; - - if (clazz.getComponentType().isPrimitive()) { - size += getPrimitiveArraySize(clazz.getComponentType(), length); - } else { - size += length * REFERENCE_SIZE; - // Sample array elements for content size - for (int i = 0; i < Math.min(3, length); i++) { - Object element = Array.get(obj, i); - size += calculateObjectSizeReflection(element, visited) / Math.min(3, length) * length; - } - } - return size; - } - - // Handle regular objects - while (clazz != null) { - for (Field field : clazz.getDeclaredFields()) { - if (Modifier.isStatic(field.getModifiers())) { - continue; - } - - Class fieldType = field.getType(); - - if (fieldType.isPrimitive()) { - size += getPrimitiveSize(fieldType); - } else { - size += REFERENCE_SIZE; - - try { - field.setAccessible(true); - Object fieldValue = field.get(obj); - - // Only recurse for small objects to avoid deep recursion - if (fieldValue != null && visited.size() < 10) { - size += calculateObjectSizeReflection(fieldValue, visited); - } - } catch (Exception e) { - // Field access failed, add conservative estimate - size += 32; - } - } - } - clazz = clazz.getSuperclass(); - } - - return size; - - } catch (Exception e) { - log.warn("Error calculating object size for {}: {}", obj.getClass().getSimpleName(), e.getMessage()); - return 64; // Conservative fallback estimate - } finally { - visited.remove(obj); - } - } - - /** - * Gets the size of a primitive type. - */ - private static int getPrimitiveSize(Class primitiveType) { - if (primitiveType == boolean.class) return BOOLEAN_SIZE; - if (primitiveType == byte.class) return BYTE_SIZE; - if (primitiveType == char.class) return CHAR_SIZE; - if (primitiveType == short.class) return SHORT_SIZE; - if (primitiveType == int.class) return INT_SIZE; - if (primitiveType == float.class) return FLOAT_SIZE; - if (primitiveType == long.class) return LONG_SIZE; - if (primitiveType == double.class) return DOUBLE_SIZE; - return 0; - } - - /** - * Gets the size of a primitive array. - */ - private static long getPrimitiveArraySize(Class componentType, int length) { - return (long) getPrimitiveSize(componentType) * length; - } - - /** - * Formats memory size in human-readable format. - */ - public static String formatMemorySize(long bytes) { - if (bytes < 1024) { - return bytes + " B"; - } else if (bytes < 1024 * 1024) { - return String.format("%.1f KB", bytes / 1024.0); - } else { - return String.format("%.1f MB", bytes / (1024.0 * 1024.0)); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2Cache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2Cache.java deleted file mode 100644 index 3e90c16ed43..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2Cache.java +++ /dev/null @@ -1,1356 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Client; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.strategy.*; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Abstract base cache implementation following game cache framework guidelines. - * - * This abstract class provides common cache functionality and is designed to be extended - * by specific cache implementations like Rs2NpcCache, Rs2SkillCache, etc. - * - * Key improvements: - * - Composition over inheritance via strategy pattern - * - Pluggable invalidation, query, and wrapper strategies - * - Thread-safe reads with minimal locks - * - Unified interface for all cache types - * - Configurable eviction policies - * - Event-driven invalidation support - * - * @param The type of keys used in the cache - * @param The type of values stored in the cache - */ -@Slf4j -public abstract class Rs2Cache implements AutoCloseable, CacheOperations { - - // ============================================ - // UTC Timestamp Constants - // ============================================ - - /** UTC timestamp formatter for cache logging */ - private static final DateTimeFormatter UTC_TIMESTAMP_FORMATTER = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS 'UTC'") - .withZone(ZoneOffset.UTC); - - /** - * Gets the current UTC timestamp in milliseconds since epoch. - * - * @return Current UTC timestamp in milliseconds - */ - private static long getCurrentUtcTimestamp() { - return Instant.now().toEpochMilli(); - } - - /** - * Gets the current UTC time as ZonedDateTime. - * - * @return Current UTC time as ZonedDateTime - */ - public static ZonedDateTime getCurrentUtcTime() { - return ZonedDateTime.now(ZoneOffset.UTC); - } - - /** - * Formats a timestamp (in milliseconds since epoch) to a human-readable UTC string. - * - * @param timestampMillis The timestamp in milliseconds since epoch - * @return Formatted UTC timestamp string - */ - public static String formatUtcTimestamp(long timestampMillis) { - return UTC_TIMESTAMP_FORMATTER.format(Instant.ofEpochMilli(timestampMillis)); - } - - /** - * Formats a ZonedDateTime to a human-readable UTC string. - * - * @param zonedDateTime The ZonedDateTime to format - * @return Formatted UTC timestamp string - */ - public static String formatUtcTimestamp(ZonedDateTime zonedDateTime) { - return UTC_TIMESTAMP_FORMATTER.format(zonedDateTime.withZoneSameInstant(ZoneOffset.UTC)); - } - - // ============================================ - // POH Region Constants - // ============================================ - - /** POH region ID */ - private static final int POH_REGION_RIMMINGTON_1 = 7513; - private static final int POH_REGION_RIMMINGTON_2 = 7514; - private static final int POH_REGION_UNKNOWN__1 = 8025; - private static final int POH_REGION_ADVERTISEMENT_2 = 8026; - private static final int POH_REGION_ADVERTISEMENT_3 = 7769; - private static final int POH_REGION_ADVERTISEMENT_4 = 7770; - - /** All POH region IDs for easy checking (immutable list) */ - private static final List POH_REGIONS = Collections.unmodifiableList(Arrays.asList( - POH_REGION_RIMMINGTON_1, - POH_REGION_RIMMINGTON_2, - POH_REGION_UNKNOWN__1, - POH_REGION_ADVERTISEMENT_2, - POH_REGION_ADVERTISEMENT_3, - POH_REGION_ADVERTISEMENT_4 - )); - - - - private final String cacheName; - - @Getter - private final CacheMode cacheMode; - - // Core cache storage - private final ConcurrentHashMap cache; // Object to support wrapped values - private final ConcurrentHashMap cacheTimestamps; - private final AtomicLong lastGlobalInvalidation; - private final AtomicBoolean isShutdown; - private final AtomicLong cacheHits; - private final AtomicLong cacheMisses; - private final AtomicLong totalInvalidations; - - // Cache configuration - private final long ttlMillis; - private volatile boolean enableCustomTTLInvalidation = false; - private final long creationTime; - - // Strategy composition - following framework guidelines - private final List> updateStrategies; - private final List> queryStrategies; - @SuppressWarnings("rawtypes") - private volatile ValueWrapper valueWrapper; // Optional value wrapping - - // Periodic cleanup system - private final ScheduledExecutorService cleanupExecutor; - private ScheduledFuture cleanupTask; - private final long cleanupIntervalMs; - - // ============================================ - // Region Change Detection - Unified Support - // ============================================ - - // Track current regions to detect changes across all cache types - private static volatile int[] lastKnownRegions = null; - private static volatile int lastRegionCheckTick = -1; - - // Thread-safe flag to prevent multiple region checks per tick - private static final AtomicBoolean regionCheckInProgress = new AtomicBoolean(false); - - /** - * Checks for region changes and clears cache if regions have changed. - * This handles the issue where RuneLite doesn't fire despawn events on region changes. - * Optimized to only check once per game tick across all cache instances. - */ - public static boolean checkAndHandleRegionChange(CacheOperations cache) { - return checkAndHandleRegionChange(cache, false,false); - } - - /** - * Checks for region changes and clears cache if regions have changed. - * - * @param cache The cache to potentially clear on region change - * @param force Whether to force a region check regardless of tick optimization - * @return true if region changed and cache was cleared, false otherwise - */ - public static boolean checkAndHandleRegionChange(CacheOperations cache, boolean force, boolean withInvalidation) { - // Get current game tick from client - Client client = Microbot.getClient(); - if (client == null) { - return false; - } - - int currentGameTick = client.getTickCount(); - - // Only check once per game tick to avoid redundant checks during burst events - if (!force && lastRegionCheckTick == currentGameTick) { - return false; - } - - // Prevent multiple concurrent region checks - if (!regionCheckInProgress.compareAndSet(false, true)) { - return false; - } - - try { - // Skip if cache is empty and we're not forcing - if (!force && cache.size() == 0) { - lastRegionCheckTick = currentGameTick; - return false; - } - - @SuppressWarnings("deprecation") - int[] currentRegions = client.getMapRegions(); - if (currentRegions == null) { - lastRegionCheckTick = currentGameTick; - return false; - } - - // Check if regions have changed - if (lastKnownRegions == null || !Arrays.equals(lastKnownRegions, currentRegions)) { - if (lastKnownRegions != null) { - log.debug("Region change detected for cache {} - clearing cache. Old regions: {}, New regions: {}", - cache.getCacheName(), Arrays.toString(lastKnownRegions), Arrays.toString(currentRegions)); - if(withInvalidation) cache.invalidateAll(); - lastKnownRegions = currentRegions.clone(); - lastRegionCheckTick = currentGameTick; - return true; - } else { - // First time initialization - lastKnownRegions = currentRegions.clone(); - } - } - - // Mark that we've checked regions this tick - lastRegionCheckTick = currentGameTick; - return false; - - } finally { - regionCheckInProgress.set(false); - } - } - - - - /** - * Checks if player is currently in a Player-Owned House (POH). - * Uses instance detection, portal presence, and region-based detection for reliable POH detection. - * - * @return true if player is in POH, false otherwise - */ - public static boolean isInPOH() { - try { - // Check if player is in instance and portal object exists - boolean instanceAndPortal = Rs2Player.IsInInstance() && Rs2GameObject.getTileObject(4525) != null; - // Check if current region is standard POH region - int[] currentRegions = getCurrentRegions(); - boolean inStandardPoh = currentRegions != null && - Arrays.stream(currentRegions).anyMatch(region -> POH_REGIONS.contains(region)); - // Return true if any detection method confirms POH - return inStandardPoh; - - } catch (Exception e) { - log.warn("Error checking POH status: {}", e.getMessage()); - return false; - } - } - - /** - * Gets the current player's regions if available. - * - * @return Array of current regions, or null if not available - */ - public static int[] getCurrentRegions() { - try { - Client client = Microbot.getClient(); - if (client == null) { - return null; - } - - @SuppressWarnings("deprecation") - int[] regions = client.getMapRegions(); - return regions != null ? regions.clone() : null; - - } catch (Exception e) { - log.warn("Error getting current regions: {}", e.getMessage()); - return null; - } - } - - // ============================================ - // Serialization Support - // ============================================ - - private String configKey; - private boolean persistenceEnabled = false; - - /** - * Enables persistence for this cache with the specified config key. - * The cache will be automatically saved and loaded from RuneLite profile configuration. - * - * @param configKey The config key to use for persistence - * @return This cache for method chaining - */ - public Rs2Cache withPersistence(String configKey) { - this.configKey = configKey; - this.persistenceEnabled = true; - log.debug("Enabled persistence for cache {} with config key: {}", cacheName, configKey); - return this; - } - - /** - * Gets the config key for this cache. - * - * @return The config key, or null if persistence is not enabled - */ - public String getConfigKey() { - return configKey; - } - - /** - * Checks if persistence is enabled for this cache. - * - * @return true if persistence is enabled - */ - public boolean isPersistenceEnabled() { - return persistenceEnabled; - } - // ============================================ - // custom invalidation Support - // ============================================ - protected void setEnableCustomTTLInvalidation() { - this.enableCustomTTLInvalidation = true; - log.debug("Enabled custom TTL invalidation for cache {}", cacheName); - } - protected boolean isEnableCustomTTLInvalidation() { - return enableCustomTTLInvalidation; - } - - - /** - * Constructor for cache with default configuration. - * - * @param cacheName The name of this cache for logging and debugging - */ - public Rs2Cache(String cacheName) { - this(cacheName, CacheMode.AUTOMATIC_INVALIDATION, 30_000L); - } - - /** - * Constructor for cache with specific mode. - * - * @param cacheName The name of this cache for logging and debugging - * @param cacheMode The cache invalidation mode - */ - public Rs2Cache(String cacheName, CacheMode cacheMode) { - this(cacheName, cacheMode, 30_000L); - } - - - /** - * Constructor for cache with full configuration. - * - * @param cacheName The name of this cache for logging and debugging - * @param cacheMode The cache invalidation mode - * @param ttlMillis Time-to-live for individual cache entries in milliseconds - */ - public Rs2Cache(String cacheName, CacheMode cacheMode, long ttlMillis) { - this.cacheName = cacheName; - this.cacheMode = cacheMode; - this.ttlMillis = ttlMillis; - this.creationTime = getCurrentUtcTimestamp(); - - // Initialize cleanup system - this.cleanupIntervalMs = Math.min(ttlMillis / 4, 30000); // Quarter of TTL or max 30 seconds - this.cleanupExecutor = Executors.newSingleThreadScheduledExecutor(r -> { - Thread thread = new Thread(r, "Rs2Cache-Cleanup-" + cacheName); - thread.setDaemon(true); - return thread; - }); - - this.cache = new ConcurrentHashMap<>(); - this.cacheTimestamps = new ConcurrentHashMap<>(); - this.lastGlobalInvalidation = new AtomicLong(getCurrentUtcTimestamp()); - this.isShutdown = new AtomicBoolean(false); - this.cacheHits = new AtomicLong(0); - this.cacheMisses = new AtomicLong(0); - this.totalInvalidations = new AtomicLong(0); - - // Initialize strategy collections - thread-safe - this.updateStrategies = new CopyOnWriteArrayList<>(); - this.queryStrategies = new CopyOnWriteArrayList<>(); - this.valueWrapper = null; - - // Start periodic cleanup task for all cache modes - startPeriodicCleanup(); - - log.debug("Created unified cache: {} with mode: {}, TTL: {}ms, Global invalidation: {}ms, Cleanup interval: {}ms", - cacheName, cacheMode, ttlMillis, cleanupIntervalMs); - } - - // ============================================ - // Strategy Management - Composition Pattern - // ============================================ - - /** - * Adds an invalidation strategy to this cache. - * Follows framework guideline: "Pluggable invalidation strategies" - * - * @param strategy The invalidation strategy to add - * @return This cache for method chaining - */ - public Rs2Cache withUpdateStrategy(CacheUpdateStrategy strategy) { - updateStrategies.add(strategy); - strategy.onAttach(this); - log.debug("Added invalidation strategy {} to cache {}", strategy.getClass().getSimpleName(), cacheName); - return this; - } - - /** - * Adds a query strategy to this cache. - * Enables specialized queries without inheritance. - * - * @param strategy The query strategy to add - * @return This cache for method chaining - */ - public Rs2Cache withQueries(QueryStrategy strategy) { - queryStrategies.add(strategy); - log.debug("Added query strategy {} to cache {}", strategy.getClass().getSimpleName(), cacheName); - return this; - } - - /** - * Sets a value wrapper strategy for this cache. - * Enables entity tracking, metadata, etc. without inheritance. - * - * @param wrapper The value wrapper to use - * @return This cache for method chaining - */ - @SuppressWarnings("rawtypes") - public Rs2Cache withWrapper(ValueWrapper wrapper) { - this.valueWrapper = wrapper; - log.debug("Added value wrapper {} to cache {}", wrapper.getClass().getSimpleName(), cacheName); - return this; - } - - // ============================================ - // Core Cache Operations - Thread-Safe - // ============================================ - - /** - * Gets a value from the cache if it exists and is not expired. - * Thread-safe reads with minimal locks following framework guidelines. - * - * @param key The key to retrieve - * @return The cached value or null if not found or expired - */ - @Override - @SuppressWarnings("unchecked") - public V get(K key) { - if (isShutdown.get()) { - return null; - } - - // No longer need checkGlobalInvalidation() - handled by periodic cleanup - - Object cachedValue = cache.get(key); - Long timestamp = cacheTimestamps.get(key); - - // Check if value exists and is not expired (respect cache mode) - if (cachedValue != null && timestamp != null && !isExpired(key)) { - cacheHits.incrementAndGet(); - log.trace("Cache hit for key {} in cache {}", key, cacheName); - - // Unwrap value if wrapper is present - if (valueWrapper != null) { - return (V) valueWrapper.unwrap(cachedValue); - } else { - return (V) cachedValue; - } - } - - cacheMisses.incrementAndGet(); - log.trace("Cache miss for key {} in cache {}", key, cacheName); - return null; - } - - /** - * Gets a raw cached value without expiration checking or additional operations. - * This method is specifically designed for update strategies during scene synchronization - * to avoid triggering recursive cache operations like scene scans. - * - * @param key The key to retrieve - * @return The raw cached value or null if not present - */ - @Override - public V getRawValue(K key) { - return getRawCachedValue(key); - } - - /** - * Retrieves a value from the cache. If not present or expired, loads it using the provided supplier. - * - * @param key The key to retrieve - * @param valueLoader Supplier function to load the value if not cached or expired - * @return The cached or newly loaded value - */ - public V get(K key, Supplier valueLoader) { - if (isShutdown.get()) { - log.warn("Cache \"{}\" is shut down, loading value directly", cacheName); - return valueLoader.get(); - } - - V cachedValue = get(key); - if (cachedValue != null) { - return cachedValue; - } - - // Load new value - try { - V newValue = valueLoader.get(); - if (newValue != null) { - put(key, newValue); - log.trace("Loaded new value for key {} in cache {}", key, cacheName); - } - return newValue; - } catch (Exception e) { - log.error("Error loading value for key {} in cache {}: {}", key, cacheName, e.getMessage(), e); - return null; - } - } - - /** - * Puts a value into the cache with current timestamp. - * - * @param key The key to store - * @param value The value to store - */ - @Override - public void put(K key, V value) { - - if (isShutdown.get() || value == null) { - return; - } - - // Wrap value if wrapper is present - Object valueToStore = value; - if (valueWrapper != null) { - @SuppressWarnings("unchecked") - Object wrapped = valueWrapper.wrap(value, key); - valueToStore = wrapped; - } - - cache.put(key, valueToStore); - cacheTimestamps.put(key, getCurrentUtcTimestamp()); - - log.trace("Put value for key {} in cache {}", key, cacheName); - } - - /** - * Removes a specific key from the cache. - * - * @param key The key to remove - */ - @Override - public void remove(K key) { - cache.remove(key); - cacheTimestamps.remove(key); - log.trace("Removed key {} from cache {}", key, cacheName); - } - - /** - * Invalidates all cached data in a thread-safe manner. - * Uses synchronization to ensure atomicity of cache and timestamp clearing. - */ - @Override - public synchronized void invalidateAll() { - int sizeBefore = cache.size(); - cache.clear(); - cacheTimestamps.clear(); - lastGlobalInvalidation.set(getCurrentUtcTimestamp()); - totalInvalidations.incrementAndGet(); - log.debug("Invalidated all {} entries in cache {}", sizeBefore, cacheName); - } - - /** - * Gets the cache timestamp for a specific key. - * - * @param key The key to get the timestamp for - * @return The timestamp when the key was cached, or -1 if not found - */ - public long getCacheTimestamp(K key) { - Long timestamp = cacheTimestamps.get(key); - return timestamp != null ? timestamp : 0L; - } - - // ============================================ - // Query Strategy Support - // ============================================ - - /** - * Executes a query using registered query strategies. - * - * @param criteria The query criteria - * @return Stream of matching values - */ - public synchronized Stream query(QueryCriteria criteria) { - for (QueryStrategy strategy : queryStrategies) { - for (Class supportedType : strategy.getSupportedQueryTypes()) { - if (supportedType.isInstance(criteria)) { - return strategy.executeQuery(this, criteria); - } - } - } - - log.warn("No query strategy found for criteria type: {} in cache {}", - criteria.getClass().getSimpleName(), cacheName); - return Stream.empty(); - } - - /** - * Gets all non-expired values from the cache. - * - * @return Collection of cached values - */ - @SuppressWarnings("unchecked") - public synchronized Collection values() { - if (isShutdown.get()) { - return Collections.emptyList(); - } - - // No longer need checkGlobalInvalidation() - handled by periodic cleanup - - return cache.entrySet().stream() - .filter(entry -> { - Long timestamp = cacheTimestamps.get(entry.getKey()); - return timestamp != null && !isExpired(entry.getKey()); - }) - .map(entry -> { - if (valueWrapper != null) { - return (V) valueWrapper.unwrap(entry.getValue()); - } else { - return (V) entry.getValue(); - } - }) - .collect(Collectors.toList()); - - } - - // ============================================ - // Stream Support for Specialized Caches - // ============================================ - - /** - * Gets all values as a stream for specialized cache implementations. - * Each specialized cache can use this to implement its own domain-specific methods. - * - * @return Stream of all cached values - */ - - public synchronized Stream stream() { - if (isShutdown.get()) { - return Stream.empty(); - } - // No longer need checkGlobalInvalidation() - handled by periodic cleanup - - // Defensive copy for strong consistency, but less efficient: - List> entries = new ArrayList<>(cache.entrySet()); - return entries.stream() - .filter(entry -> { - Long timestamp = cacheTimestamps.get(entry.getKey()); - return timestamp != null && !isExpired(entry.getKey()); - }) - .map(entry -> { - if (valueWrapper != null) { - @SuppressWarnings("unchecked") - V unwrapped = (V) valueWrapper.unwrap(entry.getValue()); - return unwrapped; - } else { - @SuppressWarnings("unchecked") - V value = (V) entry.getValue(); - return value; - } - }) - .filter(Objects::nonNull); - } - - /** - * Returns statistics as a formatted string for legacy compatibility. - */ - public String getStatisticsString() { - CacheStatistics stats = getStatistics(); - return String.format( - "Cache: %s | Size: %d | Hits: %d | Misses: %d | Hit Rate: %.2f%% | Mode: %s | Invalidations: %d | Memory: %s", - stats.cacheName, - stats.currentSize, - stats.cacheHits, - stats.cacheMisses, - stats.getHitRate() * 100, - stats.cacheMode.toString(), - stats.totalInvalidations, - stats.getFormattedMemorySize() - ); - } - - // ============================================ - // Event Handling for Strategies - // ============================================ - - /** - * Handles an event by delegating to all registered invalidation strategies. - * - * @param event The event to handle - */ - public void handleEvent(Object event) { - for (CacheUpdateStrategy strategy : updateStrategies) { - for (Class eventType : strategy.getHandledEventTypes()) { - if (eventType.isInstance(event)) { - try { - strategy.handleEvent(event, this); - } catch (Exception e) { - log.error("Error handling event {} in strategy {} for cache {}: {}", - event.getClass().getSimpleName(), - strategy.getClass().getSimpleName(), - cacheName, e.getMessage(), e); - } - break; - } - } - } - } - - // ============================================ - // CacheOperations Interface Implementation - // ============================================ - - @Override - public boolean containsKey(K key) { - return cache.containsKey(key) && !isExpired(key); - } - - @Override - public int size() { - return cache.size(); - } - - @Override - public String getCacheName() { - return cacheName; - } - - @Override - public Stream keyStream() { - return entryStream().map(Map.Entry::getKey); - } - - @Override - public Stream valueStream() { - return entryStream().map(Map.Entry::getValue); - } - - // ============================================ - // Print Functions for Cache Information - // ============================================ - - /** - * Returns a detailed formatted string containing all cache information. - * Includes cache metadata, statistics, configuration, and stored data. - * - * @return Detailed multi-line string representation of the cache - */ - public String printDetailedCacheInfo() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") - .withZone(ZoneId.systemDefault()); - - sb.append("=".repeat(80)).append("\n"); - sb.append(" DETAILED CACHE INFORMATION\n"); - sb.append("=".repeat(80)).append("\n"); - - // Cache metadata - sb.append(String.format("Cache Name: %s\n", cacheName)); - sb.append(String.format("Cache Mode: %s\n", cacheMode)); - sb.append(String.format("Created: %s\n", formatter.format(Instant.ofEpochMilli(creationTime)))); - sb.append(String.format("Uptime: %d ms\n", getCurrentUtcTimestamp() - creationTime)); - sb.append(String.format("Is Shutdown: %s\n", isShutdown.get())); - sb.append("\n"); - - // Configuration - sb.append("CONFIGURATION:\n"); - sb.append(String.format("TTL (milliseconds): %d\n", ttlMillis)); - sb.append(String.format("Persistence Enabled: %s\n", persistenceEnabled)); - if (persistenceEnabled) { - sb.append(String.format("Config Key: %s\n", configKey)); - } - sb.append("\n"); - - // Statistics - CacheStatistics stats = getStatistics(); - sb.append("STATISTICS:\n"); - sb.append(String.format("Current Size: %d entries\n", stats.currentSize)); - sb.append(String.format("Cache Hits: %d\n", stats.cacheHits)); - sb.append(String.format("Cache Misses: %d\n", stats.cacheMisses)); - sb.append(String.format("Hit Rate: %.2f%%\n", stats.getHitRate() * 100)); - sb.append(String.format("Total Invalidations: %d\n", stats.totalInvalidations)); - sb.append(String.format("Last Global Invalidation: %s\n", - formatter.format(Instant.ofEpochMilli(lastGlobalInvalidation.get())))); - sb.append("\n"); - - // Strategies - sb.append("STRATEGIES:\n"); - sb.append(String.format("Update Strategies: %d\n", updateStrategies.size())); - for (CacheUpdateStrategy strategy : updateStrategies) { - sb.append(String.format(" - %s\n", strategy.getClass().getSimpleName())); - } - sb.append(String.format("Query Strategies: %d\n", queryStrategies.size())); - for (QueryStrategy strategy : queryStrategies) { - sb.append(String.format(" - %s\n", strategy.getClass().getSimpleName())); - } - sb.append(String.format("Value Wrapper: %s\n", - valueWrapper != null ? valueWrapper.getClass().getSimpleName() : "None")); - sb.append("\n"); - - // Cache entries - sb.append("-".repeat(80)).append("\n"); - sb.append(" CACHE ENTRIES\n"); - sb.append("-".repeat(80)).append("\n"); - - sb.append(String.format("%-20s %-30s %-19s\n", "KEY", "VALUE", "TIMESTAMP")); - sb.append("-".repeat(80)).append("\n"); - - cache.entrySet().stream() - .sorted((e1, e2) -> { - Long t1 = cacheTimestamps.get(e1.getKey()); - Long t2 = cacheTimestamps.get(e2.getKey()); - if (t1 == null || t2 == null) return 0; - return Long.compare(t2, t1); // Most recent first - }) - .forEach(entry -> { - String key = String.valueOf(entry.getKey()); - String value = String.valueOf(entry.getValue()); - Long timestamp = cacheTimestamps.get(entry.getKey()); - String timestampStr = timestamp != null ? - formatter.format(Instant.ofEpochMilli(timestamp)) : "Unknown"; - - // Truncate long values - if (key.length() > 20) key = key.substring(0, 17) + "..."; - if (value.length() > 30) value = value.substring(0, 27) + "..."; - - sb.append(String.format("%-20s %-30s %-19s\n", key, value, timestampStr)); - }); - - if (cache.isEmpty()) { - sb.append("No entries in cache\n"); - } - - sb.append("-".repeat(80)).append("\n"); - sb.append(String.format("Generated at: %s\n", formatter.format(Instant.now()))); - sb.append("=".repeat(80)); - - return sb.toString(); - } - - /** - * Returns a summary formatted string containing essential cache information. - * Compact view showing key metrics and status. - * - * @return Summary multi-line string representation of the cache - */ - public String printCacheSummary() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss"); - - sb.append("┌─ CACHE SUMMARY: ").append(cacheName).append(" ") - .append("─".repeat(Math.max(1, 45 - cacheName.length()))).append("┐\n"); - - CacheStatistics stats = getStatistics(); - - // Summary statistics - sb.append(String.format("│ Entries: %-3d │ Mode: %-12s │ Status: %-8s │\n", - stats.currentSize, cacheMode, isShutdown.get() ? "Shutdown" : "Active")); - - sb.append(String.format("│ Hits: %-6d │ Misses: %-6d │ Hit Rate: %5.1f%% │\n", - stats.cacheHits, stats.cacheMisses, stats.getHitRate() * 100)); - - // Configuration summary - sb.append(String.format("│ TTL: %-7d ms │ Invalidations: %-8d │\n", - ttlMillis, stats.totalInvalidations)); - - // Strategy summary - sb.append(String.format("│ Strategies: %-2d │ Persistence: %-8s │\n", - updateStrategies.size() + queryStrategies.size(), - persistenceEnabled ? "Enabled" : "Disabled")); - - // Uptime - long uptimeMs = getCurrentUtcTimestamp() - creationTime; - String uptimeStr; - if (uptimeMs < 60000) { - uptimeStr = String.format("%ds", uptimeMs / 1000); - } else if (uptimeMs < 3600000) { - uptimeStr = String.format("%dm %ds", uptimeMs / 60000, (uptimeMs % 60000) / 1000); - } else { - uptimeStr = String.format("%dh %dm", uptimeMs / 3600000, (uptimeMs % 3600000) / 60000); - } - - sb.append(String.format("│ Uptime: %-16s │ Generated: %-8s │\n", - uptimeStr, formatter.format(Instant.now()))); - - sb.append("└").append("─".repeat(63)).append("┘"); - - return sb.toString(); - } - - // ============================================ - // Entry Access for Serialization - // ============================================ - - /** - * Gets all cache entries for serialization purposes. - * Returns a stream of key-value entries that are not expired. - * - * @return Stream of cache entries suitable for serialization - */ - public Stream> entryStream() { - if (isShutdown.get()) { - return Stream.empty(); - } - - // No longer need checkGlobalInvalidation() - handled by periodic cleanup - - return cache.entrySet().stream() - .filter(entry -> { - Long timestamp = cacheTimestamps.get(entry.getKey()); - return timestamp != null && !isExpired(entry.getKey()); - }) - .map(entry -> { - V value; - if (valueWrapper != null) { - value = (V) valueWrapper.unwrap(entry.getValue()); - } else { - value = (V) entry.getValue(); - } - return new AbstractMap.SimpleEntry<>(entry.getKey(), value); - }); - } - - /** - * Gets all cache entries as a map for serialization purposes. - * Only returns non-expired entries. - * - * @return Map of all non-expired cache entries - */ - public Map getEntriesForSerialization() { - Map result = new ConcurrentHashMap<>(); - - if (isShutdown.get()) { - return result; - } - - // No longer need checkGlobalInvalidation() - handled by periodic cleanup - - for (Map.Entry entry : cache.entrySet()) { - K key = entry.getKey(); - Long timestamp = cacheTimestamps.get(key); - - // Only include non-expired entries - if (timestamp != null && !isExpired(key)) { - Object cachedValue = entry.getValue(); - - // Unwrap value if wrapper is present - V value; - if (valueWrapper != null) { - @SuppressWarnings("unchecked") - V unwrappedValue = (V) valueWrapper.unwrap(cachedValue); - value = unwrappedValue; - } else { - @SuppressWarnings("unchecked") - V castedValue = (V) cachedValue; - value = castedValue; - } - - result.put(key, value); - } - } - - return result; - } - - /** - * Gets all cache entries as a map for serialization. - * - * @return Map of all non-expired cache entries - */ - public Map entryMap() { - return entryStream().collect(Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue, - (existing, replacement) -> replacement, // Handle duplicates by keeping the latest - ConcurrentHashMap::new - )); - } - - // ============================================ - // Private Helper Methods - // ============================================ - - /** - * Checks if a cache entry is expired. - * This method can be overridden by subclasses to implement custom expiration logic. - * - * @param key The cache key to check for expiration - * @return true if the entry should be considered expired - */ - protected boolean isExpired(K key) { - Long timestamp = cacheTimestamps.get(key); - if (timestamp == null) { - return true; - } - - // EVENT_DRIVEN_ONLY mode: entries never expire by time (unless custom logic overrides) - if (cacheMode == CacheMode.EVENT_DRIVEN_ONLY && !enableCustomTTLInvalidation) { - return false; - } - - // MANUAL_ONLY mode: entries never expire automatically - if (cacheMode == CacheMode.MANUAL_ONLY) { - return false; - } - - // AUTOMATIC_INVALIDATION mode: check TTL and remove if expired - long currentTime = getCurrentUtcTimestamp(); - if (currentTime - timestamp > ttlMillis) { - // Item has expired - remove it immediately from cache - Object removedValue = cache.remove(key); - cacheTimestamps.remove(key); - if (removedValue != null) { - log.debug("Removed expired entry during TTL check: key={} in cache {}", key, cacheName); - } - return true; - } - - return false; - } - - /** - * Protected method to get raw cached value without TTL validation. - * Used by subclasses for custom expiration logic to avoid recursion. - * - * @param key The cache key - * @return The raw cached value or null if not present - */ - @SuppressWarnings("unchecked") - protected V getRawCachedValue(K key) { - Object cachedValue = cache.get(key); - if (cachedValue == null) { - return null; - } - - // Unwrap value if wrapper is present - if (valueWrapper != null) { - return (V) valueWrapper.unwrap(cachedValue); - } else { - return (V) cachedValue; - } - } - - /** - * Public method for strategies to access raw cached values without triggering - * additional cache operations (like scene scans). This prevents recursive scanning. - * - * @param key The cache key - * @return The raw cached value or null if not present - */ - public V getRawCachedValueForStrategy(K key) { - return getRawCachedValue(key); - } - - /** - * Starts the periodic cleanup task for all cache modes. - * AUTOMATIC_INVALIDATION: Removes expired entries based on TTL - * EVENT_DRIVEN_ONLY: Can be overridden by subclasses for custom cleanup (e.g., despawn checking) - * MANUAL_ONLY: Generally no cleanup, but available for override - */ - protected void startPeriodicCleanup() { - if (cleanupExecutor.isShutdown()) { - return; - } - - cleanupTask = cleanupExecutor.scheduleWithFixedDelay(() -> { - try { - performPeriodicCleanup(); - } catch (Exception e) { - log.warn("Error during periodic cleanup for cache {}: {}", cacheName, e.getMessage()); - } - }, cleanupIntervalMs, cleanupIntervalMs, TimeUnit.MILLISECONDS); - - log.debug("Started periodic cleanup for cache {} every {}ms", cacheName, cleanupIntervalMs); - } - - /** - * Performs the actual periodic cleanup. - * Can be overridden by subclasses to implement custom cleanup logic. - * Default implementation removes expired entries for AUTOMATIC_INVALIDATION mode. - */ - protected void performPeriodicCleanup() { - if (isShutdown.get()) { - return; - } - - if (cacheMode == CacheMode.AUTOMATIC_INVALIDATION) { - performTtlCleanup(); - } - // For other modes, subclasses can override this method for custom cleanup - } - - /** - * Removes expired entries based on TTL for AUTOMATIC_INVALIDATION mode. - * This replaces the global invalidation approach with per-entry checking. - */ - private void performTtlCleanup() { - if (cache.isEmpty()) { - return; - } - - long currentTime = getCurrentUtcTimestamp(); - List expiredKeys = new ArrayList<>(); - - // Collect expired keys - for (Map.Entry entry : cache.entrySet()) { - K key = entry.getKey(); - Long timestamp = cacheTimestamps.get(key); - - if (timestamp == null || (currentTime - timestamp) > ttlMillis) { - expiredKeys.add(key); - } - } - - // Remove expired entries - int removedCount = 0; - for (K key : expiredKeys) { - if (cache.remove(key) != null) { - cacheTimestamps.remove(key); - removedCount++; - } - } - - if (removedCount > 0) { - totalInvalidations.addAndGet(removedCount); - log.debug("Removed {} expired entries from cache {}", removedCount, cacheName); - } - } - - /** - * Calculates the estimated memory size of this cache in bytes. - * Includes keys, values, timestamps, and internal data structures. - * - * @return Estimated memory usage in bytes - */ - public long getEstimatedMemorySize() { - if (cache.isEmpty()) { - return getEmptyCacheMemorySize(); - } - - long totalSize = 0; - - // Base cache object overhead - totalSize += getBaseCacheMemorySize(); - - // Calculate size of stored entries - long keySize = 0; - long valueSize = 0; - long timestampSize = 0; - - // Sample a few entries to estimate average sizes - int sampleSize = Math.min(5, cache.size()); - int sampledEntries = 0; - - for (Map.Entry entry : cache.entrySet()) { - if (sampledEntries >= sampleSize) break; - - keySize += MemorySizeCalculator.calculateKeySize(entry.getKey()); - valueSize += MemorySizeCalculator.calculateValueSize(entry.getValue()); - timestampSize += Long.BYTES; // Long timestamp - - sampledEntries++; - } - - if (sampledEntries > 0) { - // Calculate average sizes and multiply by total entry count - long avgKeySize = keySize / sampledEntries; - long avgValueSize = valueSize / sampledEntries; - long avgTimestampSize = timestampSize / sampledEntries; - - totalSize += (avgKeySize + avgValueSize + avgTimestampSize) * cache.size(); - - // Add ConcurrentHashMap overhead per entry (Node objects, buckets) - totalSize += cache.size() * 64; // Estimated overhead per map entry - } - - return totalSize; - } - - /** - * Gets the base memory size of an empty cache. - */ - private long getEmptyCacheMemorySize() { - long size = 0; - - // Object header + all instance fields - size += 12; // Object header (64-bit JVM with compressed OOPs) - size += 4 * 8; // 8 reference fields (String, CacheMode, 2 ConcurrentHashMaps, 4 AtomicLong, AtomicBoolean) - size += 8 * 4; // 4 long fields - size += 4 * 2; // 2 int fields (if any) - size += 4 * 2; // 2 CopyOnWriteArrayList references - size += 4; // ValueWrapper reference - - // Empty ConcurrentHashMap overhead (x2 for cache and timestamps) - size += 2 * (12 + 4 + 4*3 + 16 + 16*4); // Object + fields + empty bucket array - - // AtomicLong objects (6 total) - size += 6 * (12 + 8); // Object header + long value - - // AtomicBoolean object - size += 12 + 1; // Object header + boolean value - - // CopyOnWriteArrayList objects (2 total) - size += 2 * (12 + 4*3 + 16); // Object + fields + empty array - - // String objects (cacheName) - if (cacheName != null) { - size += 12 + 4 + 16 + (cacheName.length() * 2); // String + char array - } - - return size; - } - - /** - * Gets the base memory size of cache infrastructure. - */ - private long getBaseCacheMemorySize() { - long size = getEmptyCacheMemorySize(); - - // Add strategy collections overhead - size += updateStrategies.size() * 4; // Reference per strategy - size += queryStrategies.size() * 4; // Reference per strategy - - // Add minimal strategy object overhead (strategies are typically small) - size += (updateStrategies.size() + queryStrategies.size()) * 32; // Estimated per strategy - - return size; - } - - /** - * Returns memory usage information as a formatted string. - */ - public String getMemoryUsageString() { - long memoryBytes = getEstimatedMemorySize(); - return String.format("Memory: %s (%d bytes)", - MemorySizeCalculator.formatMemorySize(memoryBytes), memoryBytes); - } - - /** - * Gets cache statistics for monitoring, including memory usage. - */ - public CacheStatistics getStatistics() { - return new CacheStatistics( - cacheName, - cacheMode, - size(), - cacheHits.get(), - cacheMisses.get(), - totalInvalidations.get(), - getCurrentUtcTimestamp() - creationTime, - ttlMillis, - getEstimatedMemorySize() - ); - } - - /** - * Cache statistics data class. - */ - public static class CacheStatistics { - public final String cacheName; - public final CacheMode cacheMode; - public final int currentSize; - public final long cacheHits; - public final long cacheMisses; - public final long totalInvalidations; - public final long uptime; - public final long ttlMillis; - public final long estimatedMemoryBytes; - - public CacheStatistics(String cacheName, CacheMode cacheMode, int currentSize, - long cacheHits, long cacheMisses, long totalInvalidations, - long uptime, long ttlMillis, - long estimatedMemoryBytes) { - this.cacheName = cacheName; - this.cacheMode = cacheMode; - this.currentSize = currentSize; - this.cacheHits = cacheHits; - this.cacheMisses = cacheMisses; - this.totalInvalidations = totalInvalidations; - this.uptime = uptime; - this.ttlMillis = ttlMillis; - this.estimatedMemoryBytes = estimatedMemoryBytes; - } - - public double getHitRate() { - long total = cacheHits + cacheMisses; - return total == 0 ? 0.0 : (double) cacheHits / total; - } - - public String getFormattedMemorySize() { - return MemorySizeCalculator.formatMemorySize(estimatedMemoryBytes); - } - } - - // ============================================ - // Abstract Methods for Specialized Cache Updates - // ============================================ - - /** - * Updates all cached data by retrieving fresh values from the game client. - * Each cache implementation must provide its own strategy for refreshing cached data. - * This method should iterate over existing cache entries and refresh them with current data. - */ - public abstract void update(); - - @Override - public void close() { - if (isShutdown.compareAndSet(false, true)) { - if (cleanupTask != null) { - cleanupTask.cancel(true); - } - - // Shutdown cleanup executor - if (cleanupExecutor != null && !cleanupExecutor.isShutdown()) { - cleanupExecutor.shutdown(); - try { - if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) { - cleanupExecutor.shutdownNow(); - } - } catch (InterruptedException e) { - cleanupExecutor.shutdownNow(); - Thread.currentThread().interrupt(); - } - } - - // Detach and close all strategies - for (CacheUpdateStrategy strategy : updateStrategies) { - try { - strategy.onDetach(this); - strategy.close(); // Close the strategy to release resources - } catch (Exception e) { - log.warn("Error detaching/closing strategy {} from cache {}: {}", - strategy.getClass().getSimpleName(), cacheName, e.getMessage()); - } - } - - cache.clear(); - cacheTimestamps.clear(); - log.debug("Closed cache: {}", cacheName); - } - } - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2CacheManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2CacheManager.java deleted file mode 100644 index 594ee2d2630..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2CacheManager.java +++ /dev/null @@ -1,1184 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Player; -import net.runelite.api.gameval.VarPlayerID; -import net.runelite.client.config.ConfigProfile; -import net.runelite.client.eventbus.EventBus; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; -import net.runelite.client.plugins.microbot.util.cache.serialization.CacheSerializationManager; -import net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executors; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -/** - * Central manager for all Rs2UnifiedCache instances in the Microbot framework. - * Handles lifecycle coordination, EventBus registration, cache persistence, and provides unified cache statistics. - * Updated to work with the new unified cache architecture where caches are static utility classes. - */ -@Slf4j -public class Rs2CacheManager implements AutoCloseable { - private static AtomicBoolean isEventRegistered = new AtomicBoolean (false); // Flag to track if event handlers are registered - private static Rs2CacheManager instance; - private static EventBus eventBus; - - private final ScheduledExecutorService cleanupExecutor; - private final ExecutorService cacheManagerExecutor; - private final AtomicBoolean isShutdown; - - // Profile management - similar to Rs2Bank - private static AtomicReference rsProfileKey = new AtomicReference<>(""); - private static AtomicBoolean loggedInCacheStateKnown = new AtomicBoolean(false); - - // Last known player name for shutdown saves when player may no longer be available - private static AtomicReference lastKnownPlayerName = new AtomicReference<>(""); - - // Cache loading retry configuration - private static final int MAX_CACHE_LOAD_ATTEMPTS = 10; // Configurable max retry attempts - private static final long CACHE_LOAD_RETRY_DELAY_MS = 1000; // 1 second between retries - private static final AtomicBoolean cacheLoadingInProgress = new AtomicBoolean(false); - - // Async operation tracking - private static final AtomicReference> currentSaveOperation = new AtomicReference<>(); - private static final AtomicReference> currentLoadOperation = new AtomicReference<>(); - - /** - * Checks if cache data is VALID at the manager level (profile consistency). - * - * VALID = Cache manager state is consistent and trustworthy - * - loggedInCacheStateKnown: Cache manager has initialized properly - * - rsProfileKey exists and matches current RuneLite profile - * - ConfigManager is available for cache operations - * - No cache loading operations in progress (atomic check) - * - No profile switches or stale manager state - * - * This is the TOP-LEVEL validation for all cache systems. - * Individual caches (Rs2Bank, etc.) add their own validation layers. - * - * @return true if cache manager state is valid and consistent, false otherwise - */ - public static boolean isCacheDataValid() { - return loggedInCacheStateKnown.get() && rsProfileKey != null && !rsProfileKey.get().isEmpty() - && Microbot.getConfigManager() != null - && rsProfileKey.get().equals(Microbot.getConfigManager().getRSProfileKey()) && cacheLoadingInProgress.get() == false; - } - - /** - * Checks if bank cache is COMPLETELY READY (all validation layers). - * - * This combines Rs2CacheManager validation + Rs2Bank validation + Rs2BankData states: - * 1. VALID: Cache manager profile consistency (this class) - * 2. VALID: Rs2Bank profile validation - * 3. LOADED: Raw cache data from config (Rs2BankData) - * 4. BUILT: Rs2ItemModel objects ready for scripts (Rs2BankData) - * - * Scripts should use this as the MASTER CHECK before using bank data. - * - * @return true if all cache layers are ready, false otherwise - */ - public static boolean isBankCacheLoaded() { - return Rs2Bank.isCacheLoaded(); - } - - /** - * Checks if bank cache is BUILT (Stage 2: Usable objects ready). - * - * BUILT = Rs2ItemModel objects created and ready for script usage - * - rebuildBankItemsList() completed successfully - * - Raw data converted to full objects with names/properties - * - ItemManager validation done on client thread - * - Scripts can immediately use hasItem(), count(), etc. - * - * @return true if bank items are built and script-ready, false otherwise - */ - public static boolean isBankCacheDataBuild() { - return Rs2Bank.isCacheDataBuilt(); - } - - /** - * Checks if bank cache is LOADED (Stage 1: Raw data from config). - * - * LOADED = Raw cache data exists but NOT yet usable - * - idQuantityAndSlot array contains [id, quantity, slot] triplets - * - Data restored from RuneLite config persistence - * - Items are still integers - NO Rs2ItemModel objects yet - * - Client thread processing NOT required for this stage - * - * @return true if raw cache data is loaded, false otherwise - */ - public static boolean isBankCacheDataLoaded() { - return Rs2Bank.isCacheDataLoaded(); - } - - - /** - * Private constructor for singleton pattern. - */ - private Rs2CacheManager() { - this.cleanupExecutor = Executors.newScheduledThreadPool(1, r -> { - Thread thread = new Thread(r, "Rs2CacheCleanup"); - thread.setDaemon(true); - return thread; - }); - this.cacheManagerExecutor = Executors.newFixedThreadPool(2, runnable -> { - Thread cacheThread = new Thread(runnable, "Rs2Cache-Persistence"); - cacheThread.setDaemon(true); - return cacheThread; - }); - this.isShutdown = new AtomicBoolean(false); - - log.debug("Rs2CacheManager (Unified) initialized with async operations support"); - } - - /** - * Gets the singleton instance of Rs2CacheManager. - * - * @return The singleton instance - */ - public static synchronized Rs2CacheManager getInstance() { - if (instance == null) { - instance = new Rs2CacheManager(); - } - return instance; - } - - /** - * Sets the EventBus instance and registers all cache event handlers. - * This method should be called during plugin startup to ensure all cache events are properly handled. - * Does NOT load persistent caches - that should be done when the profile is available. - * - * @param eventBus The RuneLite EventBus instance - */ - public static void setEventBus(EventBus eventBus) { - Rs2CacheManager.eventBus = eventBus; - - - } - - /** - * Registers all cache event handlers with the EventBus. - */ - public static void registerEventHandlers() { - if (eventBus == null || isEventRegistered.get()) { - log.warn("EventBus is null, cannot register cache event handlers"); - return; - } - - try { - // Register NPC cache events - - eventBus.register(Rs2NpcCache.getInstance()); - //Rs2NpcCache.getInstance().update(); - // Register Object cache events - eventBus.register(Rs2ObjectCache.getInstance()); - Rs2ObjectCache.getInstance().update(600*10); - // Register GroundItem cache events - eventBus.register(Rs2GroundItemCache.getInstance()); - //Rs2GroundItemCache.getInstance().update(); - // Register Varbit cache events - eventBus.register(Rs2VarbitCache.getInstance()); - - // Register VarPlayer cache events - eventBus.register(Rs2VarPlayerCache.getInstance()); - - // Register Skill cache events - eventBus.register(Rs2SkillCache.getInstance()); - - // Register Quest cache events - eventBus.register(Rs2QuestCache.getInstance()); - - // Register SpiritTree cache events - eventBus.register(Rs2SpiritTreeCache.getInstance()); - - Rs2CacheManager.isEventRegistered.set(true); // Set registration flag - log.info("All cache event handlers registered with EventBus"); - } catch (Exception e) { - log.error("Failed to register cache event handlers", e); - } - } - - /** - * Unregisters all cache event handlers from the EventBus. - */ - public static void unregisterEventHandlers() { - if (eventBus == null || !Rs2CacheManager.isEventRegistered.get()) { - return; - } - - try { - eventBus.unregister(Rs2NpcCache.getInstance()); - eventBus.unregister(Rs2ObjectCache.getInstance()); - eventBus.unregister(Rs2GroundItemCache.getInstance()); - eventBus.unregister(Rs2VarbitCache.getInstance()); - eventBus.unregister(Rs2VarPlayerCache.getInstance()); - eventBus.unregister(Rs2SkillCache.getInstance()); - eventBus.unregister(Rs2QuestCache.getInstance()); - eventBus.unregister(Rs2SpiritTreeCache.getInstance()); - Rs2CacheManager.isEventRegistered.set(false); // Reset registration flag - log.debug("All cache event handlers unregistered from EventBus"); - } catch (Exception e) { - log.error("Failed to unregister cache event handlers", e); - } - } - - /** - * Checks if cache event handlers are currently registered with the EventBus. - * - * @return true if event handlers are registered, false otherwise - */ - public static boolean isEventHandlersRegistered() { - return isEventRegistered.get(); - } - - /** - * Invalidates all known unified caches. - */ - public static void invalidateAllCaches(boolean savePersistentCaches) { - try { - if (savePersistentCaches) { - savePersistentCaches(Microbot.getConfigManager().getRSProfileKey()); - } - Rs2NpcCache.getInstance().invalidateAll(); - Rs2GroundItemCache.getInstance().invalidateAll(); - Rs2ObjectCache.getInstance().invalidateAll(); - Rs2VarbitCache.getInstance().invalidateAll(); - Rs2VarPlayerCache.getInstance().invalidateAll(); - Rs2SkillCache.getInstance().invalidateAll(); - Rs2QuestCache.getInstance().invalidateAll(); - Rs2SpiritTreeCache.getInstance().invalidateAll(); - } catch (Exception e) { - log.error("Error invalidating caches: {}", e.getMessage(), e); - } - } - - /** - * Triggers scene scans for all entity caches to repopulate them after clearing. - * This ensures caches are immediately synchronized with the current game scene. - * Should be called after invalidating caches to provide immediate data availability. - */ - public static void triggerSceneScansForAllCaches() { - try { - if (!Microbot.isLoggedIn() || Microbot.getClient() == null || Microbot.getClient().getLocalPlayer() == null) { - log.debug("Cannot trigger scene scans - not logged in"); - return; - } - - log.debug("Triggering scene scans for all entity caches after cache invalidation"); - - // Trigger scene scans for all entity caches with small delays to stagger the operations - Rs2NpcCache.requestSceneScan(); - Rs2GroundItemCache.requestSceneScan(); - Rs2ObjectCache.requestSceneScan(); - - log.debug("Scene scan requests sent to all entity caches"); - } catch (Exception e) { - log.error("Error triggering scene scans: {}", e.getMessage(), e); - } - } - - /** - * Updates the last known player name for use in situations where current player may not be available. - * This is crucial for shutdown saves when the player may have logged out. - * - * @param playerName The current player name to remember - */ - private static void updateLastKnownPlayerName(String playerName) { - if (playerName != null && !playerName.trim().isEmpty()) { - lastKnownPlayerName.set(playerName); - log.debug("Updated last known player name to: {}", playerName); - } - } - - /** - * Gets the current player name with improved tracking and caching. - * This method provides reliable player name access for cache operations. - * - * @return Current player name or null if not available - */ - public static String getCurrentPlayerName() { - try { - if (Microbot.isLoggedIn() && Microbot.getClient() != null) { - String currentPlayerName = Microbot.getClient().getLocalPlayer() != null ? - Microbot.getClient().getLocalPlayer().getName() : null; - if (currentPlayerName != null && !currentPlayerName.trim().isEmpty()) { - updateLastKnownPlayerName(currentPlayerName); - return currentPlayerName; - } - } - } catch (Exception e) { - log.debug("Error getting current player name: {}", e.getMessage()); - } - - return null; - } - - /** - * Gets the last known player name, falling back to current player if available. - * This ensures we can perform character-specific operations even during shutdown. - * - * @return Last known player name or null if never set - */ - public static String getLastKnownPlayerName() { - // try to get current player name first - String currentName = getCurrentPlayerName(); - if (currentName != null) { - return currentName; - } - - // fallback to last known player name - String lastName = lastKnownPlayerName.get(); - return (lastName != null && !lastName.trim().isEmpty()) ? lastName : null; - } - - /** - * Clears the last known player name. Should be called when switching profiles. - */ - private static void clearLastKnownPlayerName() { - lastKnownPlayerName.set(""); - log.debug("Cleared last known player name"); - } - - /** - * Gets the total number of entries across all unified caches. - * - * @return The total cache entry count - */ - public int getTotalCacheSize() { - try { - return Rs2NpcCache.getInstance().size() + - Rs2GroundItemCache.getInstance().size() + - Rs2ObjectCache.getInstance().size() + - Rs2VarbitCache.getInstance().size() + - Rs2VarPlayerCache.getInstance().size() + - Rs2SkillCache.getInstance().size() + - Rs2QuestCache.getInstance().size() + - Rs2SpiritTreeCache.getInstance().size(); - } catch (Exception e) { - log.error("Error calculating total cache size: {}", e.getMessage(), e); - return 0; - } - } - - /** - * Gets the total estimated memory usage across all unified caches. - * - * @return The total estimated memory usage in bytes - */ - public long getTotalMemoryUsage() { - try { - return Rs2NpcCache.getInstance().getEstimatedMemorySize() + - Rs2GroundItemCache.getInstance().getEstimatedMemorySize() + - Rs2ObjectCache.getInstance().getEstimatedMemorySize() + - Rs2VarbitCache.getInstance().getEstimatedMemorySize() + - Rs2VarPlayerCache.getInstance().getEstimatedMemorySize() + - Rs2SkillCache.getInstance().getEstimatedMemorySize() + - Rs2QuestCache.getInstance().getEstimatedMemorySize() + - Rs2SpiritTreeCache.getInstance().getEstimatedMemorySize(); - } catch (Exception e) { - log.error("Error calculating total memory usage: {}", e.getMessage(), e); - return 0; - } - } - - /** - * Provides unified cache statistics for debugging. - * - * @return A string containing cache statistics - */ - public String getCacheStatistics() { - StringBuilder stats = new StringBuilder(); - stats.append("Rs2CacheManager (Unified) Statistics:\n"); - - try { - int npcCount = Rs2NpcCache.getInstance().size(); - int groundItemCount = Rs2GroundItemCache.getInstance().size(); - int objectCount = Rs2ObjectCache.getInstance().size(); - int varbitCount = Rs2VarbitCache.getInstance().size(); - int varPlayerCount = Rs2VarPlayerCache.getInstance().size(); - int skillCount = Rs2SkillCache.getInstance().size(); - int questCount = Rs2QuestCache.getInstance().size(); - int spiritTreeCount = Rs2SpiritTreeCache.getInstance().size(); - - int totalEntries = npcCount + groundItemCount + objectCount + varbitCount + varPlayerCount + skillCount + questCount + spiritTreeCount; - - stats.append("Total entries: ").append(totalEntries).append("\n"); - stats.append("Individual cache sizes:\n"); - stats.append(" NpcCache (EVENT_DRIVEN): ").append(npcCount).append(" entries\n"); - stats.append(" GroundItemCache (EVENT_DRIVEN): ").append(groundItemCount).append(" entries\n"); - stats.append(" ObjectCache (EVENT_DRIVEN): ").append(objectCount).append(" entries\n"); - stats.append(" VarbitCache (AUTO_INVALIDATION): ").append(varbitCount).append(" entries\n"); - stats.append(" VarPlayerCache (EVENT_DRIVEN): ").append(varPlayerCount).append(" entries\n"); - stats.append(" SkillCache (AUTO_INVALIDATION): ").append(skillCount).append(" entries\n"); - stats.append(" QuestCache (AUTO_INVALIDATION): ").append(questCount).append(" entries\n"); - stats.append(" SpiritTreeCache (EVENT_DRIVEN_ONLY): ").append(spiritTreeCount).append(" entries\n"); - } catch (Exception e) { - stats.append("Error collecting statistics: ").append(e.getMessage()).append("\n"); - log.error("Error collecting cache statistics: {}", e.getMessage(), e); - } - - return stats.toString(); - } - - /** - * Gets detailed cache statistics for a specific cache. - * - * @param cacheName The name of the cache to get statistics for - * @return Cache statistics or null if cache not found - */ - public Rs2Cache.CacheStatistics getCacheStatistics(String cacheName) { - try { - switch (cacheName.toLowerCase()) { - case "npccache": - return Rs2NpcCache.getInstance().getStatistics(); - case "grounditemcache": - return Rs2GroundItemCache.getInstance().getStatistics(); - case "objectcache": - return Rs2ObjectCache.getInstance().getStatistics(); - case "varbitcache": - return Rs2VarbitCache.getInstance().getStatistics(); - case "varplayercache": - return Rs2VarPlayerCache.getInstance().getStatistics(); - case "skillcache": - return Rs2SkillCache.getInstance().getStatistics(); - case "questcache": - return Rs2QuestCache.getInstance().getStatistics(); - case "spirittreecache": - return Rs2SpiritTreeCache.getInstance().getStatistics(); - default: - log.warn("Unknown cache name: {}", cacheName); - return null; - } - } catch (Exception e) { - log.error("Error getting statistics for cache {}: {}", cacheName, e.getMessage(), e); - return null; - } - } - - /** - * Triggers cache cleanup for all known caches that support periodic cleanup. - */ - public void triggerCacheCleanup() { - // Note: With the unified architecture, caches handle their own cleanup - // based on their CacheMode. This method is kept for compatibility. - log.debug("Cache cleanup triggered (unified caches handle their own cleanup)"); - } - - /** - * Shuts down the cache manager and all managed caches. - */ - @Override - public void close() { - if (isShutdown.compareAndSet(false, true)) { - log.info("Shutting down Rs2CacheManager"); - - // - // Empty cache state and Save persistent caches before shutdown - emptyCacheState(); - // Unregister event handlers first - unregisterEventHandlers(); - // Close all cache instances to ensure proper resource cleanup - closeAllCaches(); - - - - // Wait for any ongoing cache operations before shutting down - try { - CompletableFuture ongoingSave = currentSaveOperation.get(); - CompletableFuture ongoingLoad = currentLoadOperation.get(); - - if (ongoingSave != null || ongoingLoad != null) { - log.info("Waiting for ongoing cache operations to complete during shutdown"); - if (ongoingSave != null) { - ongoingSave.get(15, TimeUnit.SECONDS); - } - if (ongoingLoad != null) { - ongoingLoad.get(15, TimeUnit.SECONDS); - } - log.info("Cache operations completed during shutdown"); - } - } catch (Exception e) { - log.error("Error waiting for cache operations during shutdown: {}", e.getMessage(), e); - } - - // Shutdown executors - cacheManagerExecutor.shutdown(); - try { - if (!cacheManagerExecutor.awaitTermination(5, TimeUnit.SECONDS)) { - cacheManagerExecutor.shutdownNow(); - } - } catch (InterruptedException e) { - cacheManagerExecutor.shutdownNow(); - Thread.currentThread().interrupt(); - } - - cleanupExecutor.shutdown(); - try { - if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) { - cleanupExecutor.shutdownNow(); - } - } catch (InterruptedException e) { - cleanupExecutor.shutdownNow(); - Thread.currentThread().interrupt(); - } - - log.debug("Rs2CacheManager shutdown complete"); - } - } - - /** - * Closes all cache instances to ensure proper resource cleanup. - * This includes shutting down any schedulers or background tasks. - */ - private void closeAllCaches() { - try { - log.debug("Closing all cache instances"); - - // Close object cache (includes ObjectUpdateStrategy shutdown) - Rs2ObjectCache.getInstance().close(); - - // Close other caches - Rs2NpcCache.getInstance().close(); - Rs2GroundItemCache.getInstance().close(); - Rs2VarbitCache.getInstance().close(); - Rs2VarPlayerCache.getInstance().close(); - Rs2SkillCache.getInstance().close(); - Rs2QuestCache.getInstance().close(); - Rs2SpiritTreeCache.getInstance().close(); - - log.debug("All cache instances closed successfully"); - } catch (Exception e) { - log.error("Error closing cache instances", e); - } - } - - /** - * Resets the singleton instance. Used for testing. - */ - public static synchronized void resetInstance() { - if (instance != null) { - instance.close(); - instance = null; - } - } - - /** - * Loads all persistent caches from RuneLite profile configuration. - * This method should be called when the RS profile is available (on login or profile change). - * Currently unused but kept for potential future use. - */ - @SuppressWarnings("unused") - private static void loadPersistentCaches() { - try { - rsProfileKey.set( Microbot.getConfigManager().getRSProfileKey()); - if (rsProfileKey == null || rsProfileKey.get().isEmpty()) { - log.warn("Cannot load persistent caches: profile key is null"); - return; - } - loadPersistentCaches(rsProfileKey.get()); - } catch (Exception e) { - log.error("Failed to load persistent caches", e); - } - } - - public static void loadVarPlayerCache(){ - try { - if (Microbot.getConfigManager() == null) { - log.warn("Cannot load persistent varplayer cache: ConfigManager is null"); - return; - } - String profileKey = Microbot.getConfigManager().getRSProfileKey(); - if(rsProfileKey == null || rsProfileKey.get().isEmpty()){ - log.warn("Cannot load persistent varplayer cache: profile key is null"); - return; - } - rsProfileKey.set( profileKey); - if (rsProfileKey == null || rsProfileKey.get().isEmpty()) { - log.warn("Cannot load persistent varplayer cache: profile key is null"); - return; - } - } catch (Exception e) { - log.error("Failed to load persistent varplayer cache", e); - return; - } - // Load VarPlayer cache - if (Rs2VarPlayerCache.getCache().isPersistenceEnabled()) { - String playerName = getLastKnownPlayerName(); - if (playerName != null) { - CacheSerializationManager.loadCache(Rs2VarPlayerCache.getCache(), Rs2VarPlayerCache.getCache().getConfigKey(), rsProfileKey.get(), playerName, false); - log.debug ("Loaded VarPlayer cache from configuration for player {}, new cache size: {}", - playerName, Rs2VarPlayerCache.getCache().size()); - } else { - log.warn("Cannot load VarPlayer cache - no player name available"); - } - } - - } - /** - * Loads persistent caches for a specific profile. - * - * @param profileKey The RuneLite profile key to load caches for - */ - private static void loadPersistentCaches(String profileKey) { - try { - if (profileKey == null) { - log.warn("Cannot load persistent caches: profile key is null"); - return; - } - - // get the last known player name for character-specific loading - String playerName = getLastKnownPlayerName(); - if (playerName == null) { - log.warn("Cannot load persistent caches - no player name available"); - return; - } - - Rs2CacheManager.rsProfileKey.set(profileKey); - - log.info("Loading persistent caches from configuration for profile: {} player: {}", profileKey, playerName); - - // Load Skills cache - if (Rs2SkillCache.getCache().isPersistenceEnabled()) { - CacheSerializationManager.loadCache(Rs2SkillCache.getCache(), Rs2SkillCache.getCache().getConfigKey(), profileKey, playerName, false); - log.info("Loaded Skills cache from configuration for player {}, new cache size: {}", - playerName, Rs2SkillCache.getCache().size()); - } - - // Load Quest cache - if (Rs2QuestCache.getCache().isPersistenceEnabled()) { - CacheSerializationManager.loadCache(Rs2QuestCache.getCache(), Rs2QuestCache.getCache().getConfigKey(), profileKey, playerName, false); - // Schedule an async update to populate quest states from client without blocking initialization - //Rs2QuestCache.updateAllFromClientAsync(); - log.debug ("Loaded Quest cache from configuration, new cache size: {}", - Rs2QuestCache.getCache().size()); - } - - // Load Varbit cache - if (Rs2VarbitCache.getCache().isPersistenceEnabled()) { - CacheSerializationManager.loadCache(Rs2VarbitCache.getCache(), Rs2VarbitCache.getCache().getConfigKey(), profileKey, playerName, false); - log.debug ("Loaded Varbit cache from configuration for player {}, new cache size: {}", - playerName, Rs2VarbitCache.getCache().size()); - } - - // Load VarPlayer cache - if (Rs2VarPlayerCache.getCache().isPersistenceEnabled()) { - CacheSerializationManager.loadCache(Rs2VarPlayerCache.getCache(), Rs2VarPlayerCache.getCache().getConfigKey(), profileKey, playerName, false); - log.debug ("Loaded VarPlayer cache from configuration for player {}, new cache size: {}", - playerName, Rs2VarPlayerCache.getCache().size()); - } - if (Rs2SpiritTreeCache.getCache().isPersistenceEnabled()) { - CacheSerializationManager.loadCache(Rs2SpiritTreeCache.getCache(), Rs2SpiritTreeCache.getCache().getConfigKey(), profileKey, playerName, false); - // Update spirit tree cache with current farming handler data after initial load - try { - Rs2SpiritTreeCache.getInstance().update(); - if(Microbot.isDebug()) Rs2SpiritTreeCache.logState(LogOutputMode.CONSOLE_ONLY); - log.debug("Spirit tree cache updated from FarmingHandler after initial load"); - } catch (Exception e) { - log.warn("Failed to update spirit tree cache from FarmingHandler after initial load: {}", e.getMessage()); - } - log.debug ("Loaded SpiritTree cache from configuration, new cache size: {}", - Rs2SpiritTreeCache.getCache().size()); - } - log.info("Finished Try to loaded all persistent caches from configuration for profile: {} - player {}", profileKey, playerName); - } catch (Exception e) { - log.error("Failed to load persistent caches from configuration for profile: {}", profileKey, e); - } - } - - - /** - * Loads the initial cache state from config. Should be called when a player logs in. - * Similar to Rs2Bank.loadInitialCacheFromCurrentConfig(). - * This method handles both Rs2Bank and other cache systems. - */ - public static void loadCacheStateFromCurrentProfile() { - String rsProfileKey = Microbot.getConfigManager().getRSProfileKey(); - loadCacheStateFromConfig(rsProfileKey); - - } - - /** - * Loads the initial cache state from config. Should be called when a player logs in. - * Similar to Rs2Bank.loadCacheFromConfig(). - * This method handles both Rs2Bank and other cache systems. - * Implements retry logic to ensure player is valid before loading. - */ - public static void loadCacheStateFromConfig(String newRsProfileKey) { - if (!isCacheDataValid()) { - // Use async loading to avoid blocking client thread - loadCachesAsync(newRsProfileKey).whenComplete((result, ex) -> { - if (ex != null) { - log.error("Failed to load cache state async for profile: {}", newRsProfileKey, ex); - } else { - log.info("Successfully loaded cache state async for profile: {}", newRsProfileKey); - } - }); - } - } - - /** - * Retries loading cache with player validation up to MAX_CACHE_LOAD_ATTEMPTS times. - * - * @param newRsProfileKey The profile key to load cache for - * @param attemptCount Current attempt number (0-based) - */ - private static void retryLoadCacheWithValidation(String newRsProfileKey, int attemptCount) { - try { - // Check if player is valid - Player localPlayer = Microbot.getClient() != null ? Microbot.getClient().getLocalPlayer() : null; - String playerName = localPlayer != null ? localPlayer.getName() : null; - - if (localPlayer != null && playerName != null && !playerName.trim().isEmpty()) { - log.info("Player validation successful on attempt {}, loading cache state for player: {}", - attemptCount + 1, playerName); - // update last known player name for later use (e.g., shutdown saves) - updateLastKnownPlayerName(playerName); - loadCaches(newRsProfileKey); - cacheLoadingInProgress.set(false); // Reset flag on success - return; - } - - // Player not valid yet, check if we should retry - if (attemptCount < MAX_CACHE_LOAD_ATTEMPTS - 1) { - log.debug("Player not valid on attempt {} (player: {}), retrying in {}ms", - attemptCount + 1, localPlayer != null ? "not null but no name" : "null", CACHE_LOAD_RETRY_DELAY_MS); - - // Schedule next retry - getInstance().cleanupExecutor.schedule(() -> { - retryLoadCacheWithValidation(newRsProfileKey, attemptCount + 1); - }, CACHE_LOAD_RETRY_DELAY_MS, TimeUnit.MILLISECONDS); - } else { - log.warn("Failed to load cache after {} attempts - player validation failed", MAX_CACHE_LOAD_ATTEMPTS); - cacheLoadingInProgress.set(false); // Reset flag on failure - } - - } catch (Exception e) { - log.error("Error during cache loading retry attempt {}: {}", attemptCount + 1, e.getMessage(), e); - cacheLoadingInProgress.set(false); // Reset flag on error - } - } - - /** - * Sets the initial cache state as unknown. Called when logging out or changing profiles. - * Similar to Rs2Bank.setUnknownInitialCacheState(). - * This method handles both Rs2Bank and other cache systems. - */ - public static void setUnknownInitialCacheState() { - if ( isCacheDataValid() - && rsProfileKey != null - && Microbot.getConfigManager() != null - && rsProfileKey.get() == Microbot.getConfigManager().getRSProfileKey()) { - log.info("In Setting initial cache state as unknown for profile \'{}\', saving current cache state", rsProfileKey); - savePersistentCaches(rsProfileKey.get()); - } - // Also handle Rs2Bank cache state - Rs2Bank.setUnknownInitialCacheState(); - loggedInCacheStateKnown.set( false); - rsProfileKey.set(""); - } - - /** - * Loads cache state asynchronously without blocking the client thread. - * Returns immediately and performs load operations in background threads. - * - * @param newRsProfileKey The profile key to load cache for - * @return CompletableFuture that completes when load is done - */ - public static CompletableFuture loadCachesAsync(String newRsProfileKey) { - // Ensure only one load operation at a time - if (cacheLoadingInProgress.compareAndSet(false, true)) { - CompletableFuture loadOperation = CompletableFuture.runAsync(() -> { - try { - retryLoadCacheWithValidation(newRsProfileKey, 0); - } catch (Exception e) { - log.error("Failed during async cache loading for profile: {}", newRsProfileKey, e); - cacheLoadingInProgress.set(false); // reset flag on unexpected error - throw new RuntimeException("Async cache load failed", e); - } - }, getInstance().cacheManagerExecutor); - - // Track the current operation - currentLoadOperation.set(loadOperation); - - return loadOperation.whenComplete((result, ex) -> { - // Clear the operation reference when done - currentLoadOperation.compareAndSet(loadOperation, null); - if (ex != null) { - log.error("Async cache loading failed for profile: {}", newRsProfileKey, ex); - } else { - log.info("Async cache loading completed for profile: {}", newRsProfileKey); - } - }); - } else { - log.debug("Cache loading already in progress, returning existing operation"); - CompletableFuture existingOperation = currentLoadOperation.get(); - return existingOperation != null ? existingOperation : CompletableFuture.completedFuture(null); - } - } - - /** - * Loads cache state from config, handling profile changes. - * This method handles both Rs2Bank and other cache systems. - */ - private static void loadCaches(String newRsProfileKey) { - // Only re-load from config if loading from a new profile - if (newRsProfileKey != null && !newRsProfileKey.equals(rsProfileKey.get())) { - // If we've hopped between profiles, save current state first - if (rsProfileKey != null&& !rsProfileKey.get().isEmpty() && isCacheDataValid()) { - log.info("Saving current cache state before loading new profile: {}, we have valid cache", rsProfileKey.get()); - savePersistentCaches(rsProfileKey.get()); - } - // Load persistent caches - loadPersistentCaches(newRsProfileKey); - // Also handle Rs2Bank cache loading - Rs2Bank.loadCacheFromConfig(newRsProfileKey); - loggedInCacheStateKnown.set(true); - } - } - - /** - * Handles cache state during profile changes. - * Saves current cache state before loading new profile caches. - * Similar to Rs2Bank.handleProfileChange(). - */ - public static void handleProfileChange(String newRsProfileKey, String prvProfile) { - log.info("Handling profile change from '{}' to '{}' with async operations", prvProfile, newRsProfileKey); - - // Save current cache state before loading new profile (async) - CompletableFuture saveOperation = savePersistentCachesAsync(prvProfile); - - // Chain the profile switch operations - saveOperation.thenRunAsync(() -> { - setUnknownInitialCacheState(); - // Load cache state for new profile (this will use async internally) - loadCacheStateFromConfig(newRsProfileKey); - }, getInstance().cacheManagerExecutor) - .whenComplete((result, ex) -> { - if (ex != null) { - log.error("Failed to complete async profile change from '{}' to '{}'", prvProfile, newRsProfileKey, ex); - } else { - log.info("Successfully completed async profile change from '{}' to '{}'", prvProfile, newRsProfileKey); - } - }); - } - - /** - * Clears all cache state. Called when logging out. - * Similar to Rs2Bank.emptyBankState(). - * This method handles both Rs2Bank and other cache systems. - */ - public static void emptyCacheState() { - // Save current state before clearing - if (rsProfileKey != null && !rsProfileKey.get().isEmpty() && isCacheDataValid()) { - // check when the cache was last saved to validate membership expiry - long cacheTimestamp = Rs2VarPlayerCache.getInstance().getCacheTimestamp(VarPlayerID.ACCOUNT_CREDIT); - if (cacheTimestamp <= 0) { - log.info("No valid cache timestamp found for membership validation"); - cacheTimestamp = 0L; - } - - // calculate days since cache was saved - long currentTime = System.currentTimeMillis(); - long daysSinceCached = (currentTime - cacheTimestamp) / (24 * 60 * 60 * 1000); - - // get cached membership days from when data was saved - int cachedMembershipDays = Rs2VarPlayerCache.getVarPlayerValue(VarPlayerID.ACCOUNT_CREDIT); - ConfigProfile profile = Microbot.getConfigManager().getProfile(); - Microbot.getConfigManager().setMemberExpireDays(profile, cachedMembershipDays); - Microbot.getConfigManager().setMemberExpireDaysTimeStemp(profile, currentTime); - log.debug("Saving current cache state before clearing for profile: {}, cached membership days: {}, days since cached: {}, current time: {}", - rsProfileKey.get(), cachedMembershipDays, daysSinceCached , currentTime); - - } - // Clear Rs2Bank state - Rs2Bank.emptyCacheState(); - // Clear cache manager state - rsProfileKey.set(""); - loggedInCacheStateKnown.set(false); - Rs2CacheManager.invalidateAllCaches(false); - log.info("Emptied all cache states"); - } - - /** - * Saves all persistent caches asynchronously without blocking the client thread. - * Returns immediately and performs save operations in background threads. - * - * @return CompletableFuture that completes when all saves are done - */ - public static CompletableFuture savePersistentCachesAsync() { - try { - if (rsProfileKey != null && !rsProfileKey.get().isEmpty()) { - String playerName = getLastKnownPlayerName(); - if (playerName == null) { - log.warn("Cannot save persistent caches - no player name available"); - return CompletableFuture.completedFuture(null); - } - return savePersistentCachesAsync(rsProfileKey.get()); - } - return CompletableFuture.completedFuture(null); - } catch (Exception e) { - log.error("Failed to start async save of persistent caches", e); - return CompletableFuture.failedFuture(e); - } - } - - /** - * Saves all persistent caches asynchronously for a specific profile. - * - * @param profileKey The RuneLite profile key to save caches for - * @return CompletableFuture that completes when all saves are done - */ - public static CompletableFuture savePersistentCachesAsync(String profileKey) { - if (profileKey == null) { - log.warn("Cannot save persistent caches: profile key is null"); - return CompletableFuture.completedFuture(null); - } - - // if we're saving a "previous" profile during a switch, proceed even if ConfigManager has - // already moved to the new profile. otherwise ensure current state is valid. - if (profileKey.equals(rsProfileKey.get())) { - if (!isCacheDataValid()) { - log.warn("Cache data is not valid for profile '{}', cannot save persistent caches", profileKey); - return CompletableFuture.completedFuture(null); - } - } else { - log.debug("Saving caches for previous profile '{}' (active='{}')", profileKey, rsProfileKey.get()); - } - - String playerName = getLastKnownPlayerName(); - if (playerName == null) { - log.warn("Cannot save persistent caches - no player name available"); - return CompletableFuture.completedFuture(null); - } - - log.info("Starting async save of all persistent caches for profile: {}", profileKey); - - // Ensure only one save operation at a time - CompletableFuture saveOperation = CompletableFuture.runAsync(() -> { - try { - // Save Rs2Bank cache first - Rs2Bank.saveCacheToConfig(profileKey); - - // Save other persistent caches - savePersistentCachesInternal(profileKey); - - log.info("Successfully completed async save of all persistent caches for profile: {}", profileKey); - } catch (Exception e) { - log.error("Failed during async save of persistent caches for profile: {}", profileKey, e); - throw new RuntimeException("Async cache save failed", e); - } - }, getInstance().cacheManagerExecutor); - - // Track the current operation - currentSaveOperation.set(saveOperation); - - return saveOperation.whenComplete((result, ex) -> { - // Clear the operation reference when done - currentSaveOperation.compareAndSet(saveOperation, null); - }); - } - - /** - * Saves all persistent caches to RuneLite profile configuration. - * This method handles both Rs2Bank and other cache systems. - */ - public static void savePersistentCaches() { - try { - if (rsProfileKey != null && !rsProfileKey.get().isEmpty()) { - // additional validation - ensure we have a valid player name - String playerName = getLastKnownPlayerName(); - if (playerName == null) { - log.warn("Cannot save persistent caches - no player name available"); - return; - } - savePersistentCaches(rsProfileKey.get()); - } - } catch (Exception e) { - log.error("Failed to save persistent caches", e); - } - } - - /** - * Saves persistent caches for a specific profile. - * This method handles both Rs2Bank and other cache systems. - * - * @param profileKey The RuneLite profile key to save caches for - */ - public static void savePersistentCaches(String profileKey) { - try { - if (!isCacheDataValid() ) { - log.warn("Cache data is not valid, cannot save persistent caches"); - return; - } - if (profileKey == null) { - log.warn("Cannot save persistent caches: profile key is null"); - return; - } - - log.info("Saving all persistent caches to configuration for profile: {}", profileKey); - - // Save Rs2Bank cache first - Rs2Bank.saveCacheToConfig(profileKey); - - // Save other persistent caches - savePersistentCachesInternal(profileKey); - - log.info("Successfully saved all persistent caches to configuration for profile: {}", profileKey); - } catch (Exception e) { - log.error("Failed to save persistent caches to configuration for profile: {}", profileKey, e); - } - } - - /** - * Internal method to save persistent caches (excluding Rs2Bank). - * - * @param profileKey The RuneLite profile key to save caches for - */ - private static void savePersistentCachesInternal(String profileKey) { - try { - // get the last known player name for character-specific saving - String playerName = getLastKnownPlayerName(); - if (playerName == null) { - log.warn("Cannot save persistent caches - no player name available"); - return; - } - - // Save Skills cache - if (Rs2SkillCache.getCache().isPersistenceEnabled()) { - log.info("Saving Skills cache to configuration for player {}, current size: {}", - playerName, Rs2SkillCache.getCache().size()); - CacheSerializationManager.saveCache(Rs2SkillCache.getCache(), Rs2SkillCache.getCache().getConfigKey(), profileKey, playerName); - } - - // Save Quest cache - if (Rs2QuestCache.getCache().isPersistenceEnabled()) { - log.info("Saving Quest cache to configuration for player {}, current size: {}", - playerName, Rs2QuestCache.getCache().size()); - - CacheSerializationManager.saveCache(Rs2QuestCache.getCache(), Rs2QuestCache.getCache().getConfigKey(), profileKey, playerName); - } - - // Save Varbit cache - if (Rs2VarbitCache.getCache().isPersistenceEnabled()) { - CacheSerializationManager.saveCache(Rs2VarbitCache.getCache(), Rs2VarbitCache.getCache().getConfigKey(), profileKey, playerName); - Rs2VarbitCache.printDetailedVarbitInfo(); - log.info("Saving Varbit cache to configuration for player {}, current size: {}", - playerName, Rs2VarbitCache.getCache().size()); - } - - // Save VarPlayer cache - if (Rs2VarPlayerCache.getCache().isPersistenceEnabled()) { - CacheSerializationManager.saveCache(Rs2VarPlayerCache.getCache(), Rs2VarPlayerCache.getCache().getConfigKey(), profileKey, playerName); - log.info("Saving VarPlayer cache to configuration for player {}, current size: {}", - playerName, Rs2VarPlayerCache.getCache().size()); - } - // Save SpiritTree cache - if (Rs2SpiritTreeCache.getCache().isPersistenceEnabled()) { - CacheSerializationManager.saveCache(Rs2SpiritTreeCache.getCache(), Rs2SpiritTreeCache.getCache().getConfigKey(), profileKey, playerName); - log.info("Saving SpiritTree cache to configuration for player {}, current size: {}", - playerName, Rs2SpiritTreeCache.getCache().size()); - } - - } catch (Exception e) { - log.error("Failed to save internal persistent caches for profile: {}", profileKey, e); - } - } - - /** - * Gets statistics for all unified caches as a formatted string with memory usage. - * @return Formatted string containing statistics for all caches including memory usage - */ - public static String getAllCacheStatisticsString() { - StringBuilder sb = new StringBuilder(); - try { - sb.append("=== MICROBOT CACHE STATISTICS ===\n"); - - // Individual cache statistics with memory usage - appendCacheStats(sb, "NPC", Rs2NpcCache.getInstance()); - appendCacheStats(sb, "GroundItems", Rs2GroundItemCache.getInstance()); - appendCacheStats(sb, "Objects", Rs2ObjectCache.getInstance()); - appendCacheStats(sb, "Varbits", Rs2VarbitCache.getInstance()); - appendCacheStats(sb, "VarPlayers", Rs2VarPlayerCache.getInstance()); - appendCacheStats(sb, "Skills", Rs2SkillCache.getInstance()); - appendCacheStats(sb, "Quests", Rs2QuestCache.getInstance()); - appendCacheStats(sb, "SpiritTrees", Rs2SpiritTreeCache.getInstance()); - - sb.append("\n=== SUMMARY ===\n"); - - // Calculate totals - int totalEntries = getInstance().getTotalCacheSize(); - long totalMemoryBytes = getInstance().getTotalMemoryUsage(); - String formattedMemory = MemorySizeCalculator.formatMemorySize(totalMemoryBytes); - - sb.append("Total Cache Entries: ").append(totalEntries).append("\n"); - sb.append("Total Memory Usage: ").append(formattedMemory) - .append(" (").append(totalMemoryBytes).append(" bytes)\n"); - - // Memory breakdown by cache type - sb.append("\n=== MEMORY BREAKDOWN ===\n"); - appendMemoryBreakdown(sb); - - } catch (Exception e) { - log.error("Error getting cache statistics: {}", e.getMessage(), e); - return "Error retrieving cache statistics: " + e.getMessage(); - } - return sb.toString(); - } - - /** - * Appends formatted cache statistics for a single cache. - */ - private static void appendCacheStats(StringBuilder sb, String cacheName, Rs2Cache cache) { - try { - Rs2Cache.CacheStatistics stats = cache.getStatistics(); - sb.append(String.format("%-12s: Size=%-4d | Hits=%-6d | Hit Rate=%5.1f%% | Memory=%s\n", - cacheName, - stats.currentSize, - stats.cacheHits, - stats.getHitRate() * 100, - stats.getFormattedMemorySize())); - } catch (Exception e) { - sb.append(String.format("%-12s: ERROR - %s\n", cacheName, e.getMessage())); - } - } - - /** - * Appends memory usage breakdown by cache type. - */ - private static void appendMemoryBreakdown(StringBuilder sb) { - try { - // Entity caches (volatile) - long entityMemory = Rs2NpcCache.getInstance().getEstimatedMemorySize() + - Rs2ObjectCache.getInstance().getEstimatedMemorySize() + - Rs2GroundItemCache.getInstance().getEstimatedMemorySize(); - - // Player caches (persistent) - long playerMemory = Rs2VarbitCache.getInstance().getEstimatedMemorySize() + - Rs2VarPlayerCache.getInstance().getEstimatedMemorySize() + - Rs2SkillCache.getInstance().getEstimatedMemorySize() + - Rs2QuestCache.getInstance().getEstimatedMemorySize() + - Rs2SpiritTreeCache.getInstance().getEstimatedMemorySize(); - - sb.append("Entity Caches (Volatile): ").append(MemorySizeCalculator.formatMemorySize(entityMemory)).append("\n"); - sb.append("Player Caches (Persistent): ").append(MemorySizeCalculator.formatMemorySize(playerMemory)).append("\n"); - - // Memory efficiency metrics - long totalMemory = entityMemory + playerMemory; - int totalEntries = getInstance().getTotalCacheSize(); - - if (totalEntries > 0) { - long avgMemoryPerEntry = totalMemory / totalEntries; - sb.append("Average Memory per Entry: ").append(MemorySizeCalculator.formatMemorySize(avgMemoryPerEntry)).append("\n"); - } - - } catch (Exception e) { - sb.append("Memory breakdown calculation failed: ").append(e.getMessage()).append("\n"); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2GroundItemCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2GroundItemCache.java deleted file mode 100644 index 0fe2c7f4aae..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2GroundItemCache.java +++ /dev/null @@ -1,787 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Constants; -import net.runelite.api.TileItem; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.ItemDespawned; -import net.runelite.api.events.ItemSpawned; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.strategy.entity.GroundItemUpdateStrategy; -import net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2CacheLoggingUtils; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; - -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Thread-safe cache for tracking ground items using the unified cache architecture. - * Returns Rs2GroundItemModel objects for enhanced item handling. - * Uses EVENT_DRIVEN_ONLY mode to persist items until despawn or game state changes. - * - * This class extends Rs2UnifiedCache and provides specific ground item caching functionality - * with proper EventBus integration for @Subscribe methods. - */ -@Slf4j -public class Rs2GroundItemCache extends Rs2Cache { - - private static Rs2GroundItemCache instance; - - // Reference to the update strategy for scene scanning - private GroundItemUpdateStrategy updateStrategy; - - /** - * Private constructor for singleton pattern. - */ - private Rs2GroundItemCache() { - super("GroundItemCache", CacheMode.EVENT_DRIVEN_ONLY); - this.updateStrategy = new GroundItemUpdateStrategy(); - this.withUpdateStrategy(this.updateStrategy); - } - - /** - * Gets the singleton instance of Rs2GroundItemCache. - * - * @return The singleton ground item cache instance - */ - public static synchronized Rs2GroundItemCache getInstance() { - if (instance == null) { - instance = new Rs2GroundItemCache(); - } - return instance; - } - - /** - * Requests a scene scan to be performed when appropriate. - * This is more efficient than immediate scanning. - */ - public static void requestSceneScan() { - getInstance().updateStrategy.requestSceneScan(getInstance()); - } - - /** - * Starts periodic scene scanning to keep the cache fresh. - * This is useful for long-running scripts that need up-to-date ground item data. - * - * @param intervalSeconds How often to scan the scene in seconds - */ - public static void startPeriodicSceneScan(long intervalSeconds) { - getInstance().updateStrategy.schedulePeriodicSceneScan(getInstance(), intervalSeconds); - } - - /** - * Stops periodic scene scanning. - */ - public static void stopPeriodicSceneScan() { - getInstance().updateStrategy.stopPeriodicSceneScan(); - } - - /** - * Overrides the get method to provide fallback scene scanning when cache is empty or key not found. - * This ensures that even if events are missed, we can still retrieve ground items from the scene. - * - * @param key The unique String key for the ground item - * @return The ground item model if found in cache or scene, null otherwise - */ - @Override - public Rs2GroundItemModel get(String key) { - // First try the regular cache lookup - Rs2GroundItemModel cachedResult = super.get(key); - if (cachedResult != null) { - return cachedResult; - } - - if (Microbot.getClient() == null || Microbot.getClient().getLocalPlayer() == null) { - log.warn("Client or local player is null, cannot perform scene scan"); - return null; - } - - // If not in cache and cache is very small, request and perform scene scan - if (updateStrategy.requestSceneScan(this)) { - log.debug("Cache miss for ground item key '{}' (size: {}), performing scene scan", key, this.size()); - // Try again after scene scan - return super.get(key); - }else { - log.debug("Cache miss for ground item key '{}' but scene scan not successful (size: {})", key, this.size()); - } - - return null; - } - - /** - * Gets a ground item by its unique key. - * - * @param key The unique key for the ground item - * @return Optional containing the ground item model if found - */ - public static Optional getGroundItemByKey(String key) { - return Optional.ofNullable(getInstance().get(key)); - } - - /** - * Gets all ground items matching a specific item ID. - * - * @param itemId The item ID to search for - * @return Stream of matching Rs2GroundItemModel objects - */ - public static Stream getGroundItemsById(int itemId) { - return getInstance().stream() - .filter(item -> item.getId() == itemId); - } - - /** - * Gets all ground items matching a specific name (case-insensitive). - * - * @param name The item name to search for - * @return Stream of matching Rs2GroundItemModel objects - */ - public static Stream getGroundItemsByName(String name) { - return getInstance().stream() - .filter(item -> item.getName() != null && - item.getName().toLowerCase().contains(name.toLowerCase())); - } - - /** - * Gets all ground items within a certain distance from a location. - * - * @param location The center location - * @param maxDistance The maximum distance in tiles - * @return Stream of ground items within the specified distance - */ - public static Stream getGroundItemsWithinDistance(WorldPoint location, int maxDistance) { - return getInstance().stream() - .filter(item -> item.getLocation() != null && - item.getLocation().distanceTo(location) <= maxDistance); - } - - /** - * Gets the first ground item matching the specified ID. - * - * @param itemId The item ID - * @return Optional containing the first matching ground item model - */ - public static Optional getFirstGroundItemById(int itemId) { - return getGroundItemsById(itemId).findFirst(); - } - - /** - * Gets the first ground item matching the specified name. - * - * @param name The item name - * @return Optional containing the first matching ground item model - */ - public static Optional getFirstGroundItemByName(String name) { - return getGroundItemsByName(name).findFirst(); - } - - /** - * Gets all cached ground items as Rs2GroundItemModel objects. - * - * @return Stream of all cached ground items - */ - public static Stream getAllGroundItems() { - return getInstance().stream(); - } - - /** - * Gets ground items with a specific quantity. - * - * @param quantity The quantity to search for - * @return Stream of matching ground items - */ - public static Stream getGroundItemsByQuantity(int quantity) { - return getAllGroundItems() - .filter(item -> item.getQuantity() == quantity); - } - - /** - * Gets ground items within a specific value range. - * - * @param minValue The minimum value (inclusive) - * @param maxValue The maximum value (inclusive) - * @return Stream of matching ground items - */ - public static Stream getGroundItemsByValueRange(int minValue, int maxValue) { - return getAllGroundItems() - .filter(item -> { - int totalValue = item.getHaPrice() * item.getQuantity(); - return totalValue >= minValue && totalValue <= maxValue; - }); - } - - /** - * Gets the closest ground item to the player with the specified ID. - * - * @param itemId The item ID to search for - * @return Optional containing the closest ground item - */ - public static Optional getClosestGroundItemById(int itemId) { - WorldPoint playerLocation = null; - try { - if (Microbot.getClient() != null && Microbot.getClient().getLocalPlayer() != null) { - playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - } catch (Exception e) { - log.debug("Could not get player location for distance calculation: {}", e.getMessage()); - } - - if (playerLocation == null) { - return getGroundItemsById(itemId).findFirst(); - } - - final WorldPoint finalPlayerLocation = playerLocation; - return getGroundItemsById(itemId) - .min((a, b) -> { - try { - int distA = a.getLocation() != null ? a.getLocation().distanceTo(finalPlayerLocation) : Integer.MAX_VALUE; - int distB = b.getLocation() != null ? b.getLocation().distanceTo(finalPlayerLocation) : Integer.MAX_VALUE; - return Integer.compare(distA, distB); - } catch (Exception e) { - return 0; - } - }); - } - - /** - * Gets the closest ground item to the player with the specified name. - * - * @param name The item name to search for - * @return Optional containing the closest ground item - */ - public static Optional getClosestGroundItemByName(String name) { - WorldPoint playerLocation = null; - try { - if (Microbot.getClient() != null && Microbot.getClient().getLocalPlayer() != null) { - playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - } catch (Exception e) { - log.debug("Could not get player location for distance calculation: {}", e.getMessage()); - } - - if (playerLocation == null) { - return getGroundItemsByName(name).findFirst(); - } - - final WorldPoint finalPlayerLocation = playerLocation; - return getGroundItemsByName(name) - .min((a, b) -> { - try { - int distA = a.getLocation() != null ? a.getLocation().distanceTo(finalPlayerLocation) : Integer.MAX_VALUE; - int distB = b.getLocation() != null ? b.getLocation().distanceTo(finalPlayerLocation) : Integer.MAX_VALUE; - return Integer.compare(distA, distB); - } catch (Exception e) { - return 0; - } - }); - } - - /** - * Gets the closest ground item to a specific anchor point with the specified ID. - * - * @param itemId The item ID to search for - * @param anchorPoint The anchor point to calculate distance from - * @return Optional containing the closest ground item - */ - public static Optional getClosestGroundItemById(int itemId, WorldPoint anchorPoint) { - return getGroundItemsById(itemId) - .min((a, b) -> Integer.compare( - a.getLocation().distanceTo(anchorPoint), - b.getLocation().distanceTo(anchorPoint) - )); - } - - /** - * Gets ground items sorted by value (highest first). - * - * @return List of ground items sorted by total value descending - */ - public static List getGroundItemsSortedByValue() { - return getAllGroundItems() - .sorted((a, b) -> Integer.compare( - b.getHaPrice() * b.getQuantity(), - a.getHaPrice() * a.getQuantity() - )) - .collect(Collectors.toList()); - } - - /** - * Gets valuable ground items above a certain threshold. - * - * @param minValue The minimum total value threshold - * @return Stream of valuable ground items - */ - public static Stream getValuableGroundItems(int minValue) { - return getAllGroundItems() - .filter(item -> (item.getHaPrice() * item.getQuantity()) >= minValue); - } - - /** - * Gets the total number of cached ground items. - * - * @return The total ground item count - */ - public static int getGroundItemCount() { - return getInstance().size(); - } - - /** - * Gets the total number of ground items by ID. - * - * @param itemId The item ID to count - * @return The count of ground items with the specified ID - */ - public static long getGroundItemCountById(int itemId) { - return getGroundItemsById(itemId).count(); - } - - /** - * Manually adds a ground item to the cache. - * - * @param tileItem The tile item to add - * @param tile The tile containing the item - */ - public static void addGroundItem(TileItem tileItem, net.runelite.api.Tile tile) { - if (tileItem != null && tile != null) { - String key = generateKey(tileItem, tile.getWorldLocation()); - Rs2GroundItemModel groundItem = new Rs2GroundItemModel(tileItem, tile); - getInstance().put(key, groundItem); - log.debug("Manually added ground item: {} at {}", tileItem.getId(), tile.getWorldLocation()); - } - } - - /** - * Manually removes a ground item from the cache. - * - * @param key The ground item key to remove - */ - public static void removeGroundItem(String key) { - getInstance().remove(key); - log.debug("Manually removed ground item with key: {}", key); - } - - /** - * Invalidates all ground item cache entries. - */ - public static void invalidateAllGroundItems() { - getInstance().invalidateAll(); - log.debug("Invalidated all ground item cache entries"); - } - - /** - - - /** - * Generates a unique key for ground items based on item ID, quantity, and location. - * - * @param item The tile item - * @param location The world location - * @return Unique key string - */ - public static String generateKey(TileItem item, WorldPoint location) { - return String.format("%d_%d_%d_%d_%d", - item.getId(), - item.getQuantity(), - location.getX(), - location.getY(), - location.getPlane()); - } - - /** - * Event handler registration for the unified cache. - * The unified cache handles events through its strategy automatically. - */ - - - @Subscribe(priority = 10) - public void onItemSpawned(ItemSpawned event) { - getInstance().handleEvent(event); - } - - @Subscribe(priority = 20) // Ensure despawn events are handled first - public void onItemDespawned(ItemDespawned event) { - getInstance().handleEvent(event); - } - - @Subscribe(priority = 40) - public void onGameStateChanged(final GameStateChanged event) { - // Removed old region detection - now handled by unified Rs2Cache system - // Also let the strategy handle the event - getInstance().handleEvent(event); - } - - - /** - * Resets the singleton instance. Used for testing. - */ - public static synchronized void resetInstance() { - if (instance != null) { - instance.invalidateAll(); - - instance = null; - } - } - - // ============================================ - // Legacy API Compatibility Methods - // ============================================ - - /** - * Gets ground items by their game ID - Legacy compatibility method. - * - * @param itemId The item ID - * @return Stream of matching ground items - */ - public static Stream getItemsByGameId(int itemId) { - return getInstance().stream() - .filter(item -> item.getId() == itemId); - } - - /** - * Gets first ground item by game ID - Legacy compatibility method. - * - * @param itemId The item ID - * @return Optional containing the first matching ground item - */ - public static Optional getFirstItemByGameId(int itemId) { - return getItemsByGameId(itemId).findFirst(); - } - - /** - * Gets closest ground item by game ID - Legacy compatibility method. - * - * @param itemId The item ID - * @return Optional containing the closest ground item - */ - public static Optional getClosestItemByGameId(int itemId) { - WorldPoint playerLocation = null; - try { - if (Microbot.getClient() != null && Microbot.getClient().getLocalPlayer() != null) { - playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - } catch (Exception e) { - log.debug("Could not get player location for distance calculation: {}", e.getMessage()); - } - - if (playerLocation == null) { - return getItemsByGameId(itemId).findFirst(); - } - - final WorldPoint finalPlayerLocation = playerLocation; - return getItemsByGameId(itemId) - .min((a, b) -> { - try { - int distA = a.getLocation() != null ? a.getLocation().distanceTo(finalPlayerLocation) : Integer.MAX_VALUE; - int distB = b.getLocation() != null ? b.getLocation().distanceTo(finalPlayerLocation) : Integer.MAX_VALUE; - return Integer.compare(distA, distB); - } catch (Exception e) { - return 0; - } - }); - } - - /** - * Gets all ground items - Legacy compatibility method. - * - * @return Stream of all ground items - */ - public static Stream getAllItems() { - return getInstance().stream(); - } - - /** - * Gets item count - Legacy compatibility method. - * - * @return Total number of cached ground items - */ - public static int getItemCount() { - return getInstance().size(); - } - - /** - * Gets cache mode - Legacy compatibility method. - * - * @return The cache mode - */ - public static CacheMode getGroundItemCacheMode() { - return getInstance().getCacheMode(); - } - - /** - * Gets cache statistics - Legacy compatibility method. - * - * @return Statistics string for debugging - */ - public static String getGroundItemCacheStatistics() { - return getInstance().getStatisticsString(); - } - - /** - * Gets closest ground item by name - Legacy compatibility method. - * - * @param itemName The item name to search for - * @return Optional containing the closest ground item - */ - public static Optional getClosestItemByName(String itemName) { - WorldPoint playerLocation = null; - try { - if (Microbot.getClient() != null && Microbot.getClient().getLocalPlayer() != null) { - playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - } catch (Exception e) { - log.debug("Could not get player location for distance calculation: {}", e.getMessage()); - } - - if (playerLocation == null) { - return getGroundItemsByName(itemName).findFirst(); - } - - final WorldPoint finalPlayerLocation = playerLocation; - return getGroundItemsByName(itemName) - .min((a, b) -> { - try { - int distA = a.getLocation() != null ? a.getLocation().distanceTo(finalPlayerLocation) : Integer.MAX_VALUE; - int distB = b.getLocation() != null ? b.getLocation().distanceTo(finalPlayerLocation) : Integer.MAX_VALUE; - return Integer.compare(distA, distB); - } catch (Exception e) { - return 0; - } - }); - } - @Override - public void update(){ - update(Constants.CLIENT_TICK_LENGTH*2); - } - - public void update(long delay) { - log.debug("Starting ground item cache update - clearing cache and performing scene scan, delay: {}ms", delay); - int sizeBefore = this.size(); - - // Clear the entire cache - this.invalidateAll(); - - // Perform a complete scene scan to repopulate the cache - updateStrategy.performSceneScan(this, delay); - - int sizeAfter = this.size(); - log.debug("Ground item cache update completed - items before: {}, after: {}", sizeBefore, sizeAfter); - } - - /** - * Logs the current state of all cached ground items for debugging. - * - * @param dumpToFile Whether to also dump the information to a file - */ - public static void logState(LogOutputMode mode) { - var cache = getInstance(); - var stats = cache.getStatistics(); - - // Create the log content - StringBuilder logContent = new StringBuilder(); - - String header = String.format("=== Ground Item Cache State (%d entries) ===", cache.size()); - logContent.append(header).append("\n"); - - String statsInfo = Rs2CacheLoggingUtils.formatCacheStatistics( - stats.getHitRate(), stats.cacheHits, stats.cacheMisses, stats.cacheMode.toString()); - logContent.append(statsInfo).append("\n\n"); - - if (cache.size() == 0) { - String emptyMsg = "Cache is empty"; - logContent.append(emptyMsg).append("\n"); - } else { - // Table format for ground items with enhanced timing information - String[] headers = {"Name", "Quantity", "ID", "Location", "Distance", "GE Price", "HA Price", "Owned", "Spawn Time UTC", "Despawn Time UTC", "Should Despawn?", "Ticks Left", "Cache Timestamp"}; - int[] columnWidths = {20, 8, 8, 18, 8, 10, 10, 6, 22, 22, 14, 10, 22}; - - String tableHeader = Rs2CacheLoggingUtils.formatTableHeader(headers, columnWidths); - logContent.append("\n").append(tableHeader); - - // Get player location once for distance calculations - WorldPoint playerLocation = null; - try { - if (Microbot.getClient() != null && Microbot.getClient().getLocalPlayer() != null) { - playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - } catch (Exception e) { - log.debug("Could not get player location for distance calculations: {}", e.getMessage()); - } - - final WorldPoint finalPlayerLocation = playerLocation; - - // Convert to list and sort by total value (highest first) - List items = cache.stream() - .limit(50) // Limit early to avoid processing too many items - .collect(Collectors.toList()); - - // Sort by total value with safe calculation - items.sort((a, b) -> { - try { - int valueA = a.getTotalGeValue(); - int valueB = b.getTotalGeValue(); - return Integer.compare(valueB, valueA); // Highest first - } catch (Exception e) { - return 0; // If value calculation fails, consider them equal - } - }); - int maxRows = mode == LogOutputMode.CONSOLE_ONLY ? 50 : cache.size(); - // Process each item safely - for (Rs2GroundItemModel item : items) { - try { - // Calculate distance safely - String distanceStr = "N/A"; - if (finalPlayerLocation != null && item.getLocation() != null) { - try { - int distance = item.getLocation().distanceTo(finalPlayerLocation); - distanceStr = String.valueOf(distance); - } catch (Exception e) { - distanceStr = "Error"; - } - } - - // Get values safely - String geValueStr = "N/A"; - String haValueStr = "N/A"; - try { - geValueStr = String.valueOf(item.getTotalGeValue()); - haValueStr = String.valueOf(item.getTotalHaValue()); - } catch (Exception e) { - log.debug("Error getting item values: {}", e.getMessage()); - } - - // Get cache timestamp for this ground item - String cacheTimestampStr = "N/A"; - try { - // Generate key manually using the same format as generateKey method - String itemKey = String.format("%d_%d_%d_%d_%d", - item.getId(), - item.getQuantity(), - item.getLocation().getX(), - item.getLocation().getY(), - item.getLocation().getPlane()); - Long cacheTimestamp = cache.getCacheTimestamp(itemKey); - if (cacheTimestamp != null) { - cacheTimestampStr = Rs2Cache.formatUtcTimestamp(cacheTimestamp); - } - } catch (Exception e) { - log.debug("Error getting cache timestamp: {}", e.getMessage()); - } - - // Get despawn information safely with enhanced UTC timing - String despawnTimeStr = "N/A"; - String shouldDespawnStr = "No"; - String spawnTimeStr = "N/A"; - String ticksLeftStr = "N/A"; - try { - // Get spawn time in UTC - if (item.getSpawnTimeUtc() != null) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM HH:mm:ss") - .withZone(ZoneOffset.UTC); - spawnTimeStr = formatter.format(item.getSpawnTimeUtc()) + " UTC"; - } - - // Get despawn time in UTC - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM HH:mm:ss") - .withZone(ZoneOffset.UTC); - despawnTimeStr = formatter.format(item.getDespawnTime().atZone(ZoneOffset.UTC)) + " UTC"; - - // Use tick-based despawn detection for accuracy - shouldDespawnStr = item.isDespawned() ? "Yes" : "No"; - ticksLeftStr = String.valueOf(item.getTicksUntilDespawn()); - - } catch (Exception e) { - log.debug("Error getting despawn information: {}", e.getMessage()); - } - - String[] values = { - Rs2CacheLoggingUtils.truncate(item.getName() != null ? item.getName() : "Unknown", 19), - String.valueOf(item.getQuantity()), - String.valueOf(item.getId()), - Rs2CacheLoggingUtils.formatLocation(item.getLocation()), - distanceStr, - geValueStr, - haValueStr, - item.isOwned() ? "Yes" : "No", - Rs2CacheLoggingUtils.truncate(spawnTimeStr, 21), - Rs2CacheLoggingUtils.truncate(despawnTimeStr, 21), - shouldDespawnStr, - ticksLeftStr, - Rs2CacheLoggingUtils.truncate(cacheTimestampStr, 21) - }; - - String row = Rs2CacheLoggingUtils.formatTableRow(values, columnWidths); - logContent.append(row); - } catch (Exception e) { - log.debug("Error processing ground item for logging: {}", e.getMessage()); - // Skip this item and continue with the next one - } - } - - String tableFooter = Rs2CacheLoggingUtils.formatTableFooter(columnWidths); - logContent.append(tableFooter); - - String limitMsg = Rs2CacheLoggingUtils.formatLimitMessage(cache.size(), maxRows); - if (!limitMsg.isEmpty()) { - logContent.append(limitMsg).append("\n"); - } - } - - String footer = "=== End Ground Item Cache State ==="; - logContent.append(footer).append("\n"); - - // Dump to file if requested - Rs2CacheLoggingUtils.outputCacheLog(getInstance().getCacheName(), logContent.toString(), mode); - - } - - /** - * Override periodic cleanup to check for despawned ground items. - * This method is called by the ScheduledExecutorService in the base cache - * to remove items that have naturally despawned based on their game timer. - */ - @Override - protected void performPeriodicCleanup() { - updateStrategy.performSceneScan(instance, Constants.CLIENT_TICK_LENGTH /2); - } - - /** - * Override isExpired to use ground item despawn timing instead of generic TTL. - * This integrates the despawn logic directly with the cache's expiration system. - * - * @param key The cache key to check for expiration - * @return true if the ground item should be considered expired (despawned) - */ - @Override - protected boolean isExpired(String key) { - // For EVENT_DRIVEN_ONLY mode with ground items, check despawn status directly - if (getCacheMode() == CacheMode.EVENT_DRIVEN_ONLY) { - // Access the cached value directly using the protected method to avoid recursion - Rs2GroundItemModel groundItem = getRawCachedValue(key); - if (groundItem != null && groundItem.isDespawned()) { - // Item has despawned - remove it immediately from cache - remove(key); - log.debug("Removed despawned ground item during expiration check: {} (ID: {}) at {}", - groundItem.getName(), groundItem.getId(), groundItem.getLocation()); - return true; - } - // If item is not in cache, consider it expired - if (groundItem == null) { - return true; - } - // Item exists and is not despawned - return false; - } - - // For other modes, fall back to the default TTL behavior - return super.isExpired(key); - } - - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2NpcCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2NpcCache.java deleted file mode 100644 index 655c9583f2d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2NpcCache.java +++ /dev/null @@ -1,502 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Constants; -import net.runelite.api.NPC; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.NpcDespawned; -import net.runelite.api.events.NpcSpawned; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.strategy.entity.NpcUpdateStrategy; -import net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2CacheLoggingUtils; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; - -import java.util.Optional; -import java.util.stream.Stream; - -/** - * Thread-safe cache for tracking NPCs using the unified cache architecture. - * Returns Rs2NpcModel objects for enhanced NPC handling. - * Uses EVENT_DRIVEN_ONLY mode to persist NPCs until despawn or game state changes. - * - * This class extends Rs2UnifiedCache and provides specific NPC caching functionality - * with proper EventBus integration for @Subscribe methods. - */ -@Slf4j -public class Rs2NpcCache extends Rs2Cache { - - private static Rs2NpcCache instance; - - // Reference to the update strategy for scene scanning - private NpcUpdateStrategy updateStrategy; - - /** - * Private constructor for singleton pattern. - */ - private Rs2NpcCache() { - super("NpcCache", CacheMode.EVENT_DRIVEN_ONLY); - this.updateStrategy = new NpcUpdateStrategy(); - this.withUpdateStrategy(this.updateStrategy); - } - - /** - * Gets the singleton instance of Rs2NpcCache. - * - * @return The singleton NPC cache instance - */ - public static synchronized Rs2NpcCache getInstance() { - if (instance == null) { - instance = new Rs2NpcCache(); - } - return instance; - } - - /** - * Requests a scene scan to be performed when appropriate. - * This is more efficient than immediate scanning. - */ - public static void requestSceneScan() { - getInstance().updateStrategy.requestSceneScan(getInstance()); - } - - /** - * Starts periodic scene scanning to keep the cache fresh. - * This is useful for long-running scripts that need up-to-date NPC data. - * - * @param intervalSeconds How often to scan the scene in seconds - */ - public static void startPeriodicSceneScan(long intervalSeconds) { - getInstance().updateStrategy.schedulePeriodicSceneScan(getInstance(), intervalSeconds); - } - - /** - * Stops periodic scene scanning. - */ - public static void stopPeriodicSceneScan() { - getInstance().updateStrategy.stopPeriodicSceneScan(); - } - - /** - * Overrides the get method to provide fallback scene scanning when cache is empty or key not found. - * This ensures that even if events are missed, we can still retrieve NPCs from the scene. - * - * @param key The NPC index key - * @return The NPC model if found in cache or scene, null otherwise - */ - @Override - public Rs2NpcModel get(Integer key) { - // First try the regular cache lookup - Rs2NpcModel cachedResult = super.get(key); - if (cachedResult != null) { - return cachedResult; - } - - if (Microbot.getClient() == null || Microbot.getClient().getLocalPlayer() == null) { - log.warn("Client or local player is null, cannot perform scene scan"); - return null; - } - - // If not in cache and cache is very small, request and perform scene scan - if (updateStrategy.requestSceneScan(this)) { - log.debug("Cache miss for NPC index '{}' (size: {}), performing scene scan", key, this.size()); - // Try again after scene scan if still missing, the npc is not in the scene - return super.get(key); - }else { - log.debug("Cache miss for NPC index '{}' but scene scan not a successful request, returning null", key); - } - - return null; - } - - // ============================================ - // Legacy API Compatibility Methods - // ============================================ - - /** - * Gets an NPC by its index. - * - * @param index The NPC index - * @return Optional containing the NPC model if found - */ - public static Optional getNpcByIndex(int index) { - return Optional.ofNullable(getInstance().get(index)); - } - - - /** - * Gets NPCs by ID - Legacy compatibility method. - * - * @param npcId The NPC ID - * @return Stream of matching NPCs - */ - public static Stream getNpcsById(int npcId) { - return getInstance().stream() - .filter(npc -> npc.getId() == npcId); - } - - /** - * Gets first NPC by ID - Legacy compatibility method. - * - * @param npcId The NPC ID - * @return Optional containing the first matching NPC - */ - public static Optional getFirstNpcById(int npcId) { - return getNpcsById(npcId).findFirst(); - } - - /** - * Gets all NPCs - Legacy compatibility method. - * - * @return Stream of all NPCs - */ - public static Stream getAllNpcs() { - return getInstance().stream(); - } - - /** - * Gets all NPCs matching a specific name (case-insensitive). - * - * @param name The NPC name to search for - * @return Stream of matching Rs2NpcModel objects - */ - public static Stream getNpcsByName(String name) { - return getInstance().stream() - .filter(npc -> npc.getName() != null && - npc.getName().toLowerCase().contains(name.toLowerCase())); - } - - /** - * Gets all NPCs within a certain distance from a location. - * - * @param location The center location - * @param maxDistance The maximum distance in tiles - * @return Stream of NPCs within the specified distance - */ - public static Stream getNpcsWithinDistance(net.runelite.api.coords.WorldPoint location, int maxDistance) { - return getInstance().stream() - .filter(npc -> npc.getWorldLocation() != null && - npc.getWorldLocation().distanceTo(location) <= maxDistance); - } - - /** - * Gets the first NPC matching the specified name. - * - * @param name The NPC name - * @return Optional containing the first matching NPC model - */ - public static Optional getFirstNpcByName(String name) { - return getNpcsByName(name).findFirst(); - } - - /** - * Gets NPCs matching a specific combat level. - * - * @param combatLevel The combat level to search for - * @return Stream of matching NPCs - */ - public static Stream getNpcsByCombatLevel(int combatLevel) { - return getAllNpcs() - .filter(npc -> npc.getCombatLevel() == combatLevel); - } - - /** - * Gets NPCs that are currently in combat. - * - * @return Stream of NPCs in combat - */ - public static Stream getNpcsInCombat() { - return getAllNpcs() - .filter(npc -> npc.isInteracting()); - } - - /** - * Gets the closest NPC to the player with the specified ID. - * - * @param npcId The NPC ID to search for - * @return Optional containing the closest NPC - */ - public static Optional getClosestNpcByGameId(int npcId) { - return getNpcsById(npcId) - .min((a, b) -> Integer.compare(a.getDistanceFromPlayer(), b.getDistanceFromPlayer())); - } - - /** - * Gets the closest NPC to the player with the specified name. - * - * @param name The NPC name to search for - * @return Optional containing the closest NPC - */ - public static Optional getClosestNpcByName(String name) { - return getNpcsByName(name) - .min((a, b) -> Integer.compare(a.getDistanceFromPlayer(), b.getDistanceFromPlayer())); - } - - /** - * Gets the closest NPC to a specific anchor point with the specified ID. - * - * @param npcId The NPC ID to search for - * @param anchorPoint The anchor point to calculate distance from - * @return Optional containing the closest NPC - */ - public static Optional getClosestNpcByGameId(int npcId, net.runelite.api.coords.WorldPoint anchorPoint) { - return getNpcsById(npcId) - .min((a, b) -> Integer.compare( - a.getWorldLocation().distanceTo(anchorPoint), - b.getWorldLocation().distanceTo(anchorPoint) - )); - } - - /** - * Gets the closest NPC to a specific anchor point with the specified name. - * - * @param name The NPC name to search for - * @param anchorPoint The anchor point to calculate distance from - * @return Optional containing the closest NPC - */ - public static Optional getClosestNpcByName(String name, net.runelite.api.coords.WorldPoint anchorPoint) { - return getNpcsByName(name) - .min((a, b) -> Integer.compare( - a.getWorldLocation().distanceTo(anchorPoint), - b.getWorldLocation().distanceTo(anchorPoint) - )); - } - - /** - * Gets the total number of cached NPCs. - * - * @return The total NPC count - */ - public static int getNpcCount() { - return getInstance().size(); - } - - /** - * Manually adds an NPC to the cache. - * - * @param npc The NPC to add - */ - public static void addNpc(NPC npc) { - if (npc != null) { - Rs2NpcModel npcModel = new Rs2NpcModel(npc); - getInstance().put(npc.getIndex(), npcModel); - log.debug("Manually added NPC: {} [{}] at {}", npc.getName(), npc.getId(), npc.getWorldLocation()); - } - } - - /** - * Manually removes an NPC from the cache. - * - * @param index The NPC index to remove - */ - public static void removeNpc(int index) { - getInstance().remove(index); - log.debug("Manually removed NPC with index: {}", index); - } - - /** - * Invalidates all NPC cache entries. - */ - public static void invalidateAllNpcs() { - getInstance().invalidateAll(); - log.debug("Invalidated all NPC cache entries"); - } - - /** - * Event handler registration for the unified cache. - * The unified cache handles events through its strategy automatically. - */ - - - @Subscribe(priority = 10) // High priority to ensure we capture all NPC events - public void onNpcSpawned(final NpcSpawned event) { - getInstance().handleEvent(event); - } - - @Subscribe(priority = 20) // first handle despawn events to ensure NPCs are removed before any other processing - public void onNpcDespawned(final NpcDespawned event) { - getInstance().handleEvent(event); - } - @Subscribe(priority = 40) - public void onGameStateChanged(final GameStateChanged event) { - // Also let the strategy handle the event, region changes and loading of a map trigger despawn events for NPCs correctly - getInstance().handleEvent(event); - } - - /** - * Resets the singleton instance. Used for testing. - */ - public static synchronized void resetInstance() { - if (instance != null) { - instance.invalidateAll(); - instance = null; - } - } - - /** - * Gets cache mode - Legacy compatibility method. - * - * @return The cache mode - */ - public static CacheMode getNpcCacheMode() { - return getInstance().getCacheMode(); - } - - /** - * Gets cache statistics - Legacy compatibility method. - * - * @return Statistics string for debugging - */ - public static String getNpcCacheStatistics() { - return getInstance().getStatisticsString(); - } - - /** - * Immediately updates the cache by invoking the update method with a default parameter of 0. - * This method overrides the parent implementation to provide a default update behavior. - */ - @Override - public void update() { - update(Constants.CLIENT_TICK_LENGTH*2); - - } - /** - * Updates the NPC cache by clearing it and performing a scene scan. - * This is useful for refreshing the cache after significant game state changes. - * - * @param delayMs Optional delay in milliseconds before performing the update - */ - public void update(long delayMs) { - log.debug("Starting NPC cache update - clearing cache and performing scene scan, delay: {} ms", delayMs); - int sizeBefore = this.size(); - - // Clear the entire cache - this.invalidateAll(); - - // Perform a complete scene scan to repopulate the cache - updateStrategy.performSceneScan(this,delayMs); - - int sizeAfter = this.size(); - log.debug("NPC cache update completed - NPCs before: {}, after: {}", sizeBefore, sizeAfter); - } - - /** - * Logs the current state of all cached NPCs for debugging. - * - * @param outputMode Where to direct the output (CONSOLE_ONLY, FILE_ONLY, or BOTH) - */ - public static void logState(LogOutputMode outputMode) { - var cache = getInstance(); - var stats = cache.getStatistics(); - // Create the log content - StringBuilder logContent = new StringBuilder(); - String header = String.format("=== NPC Cache State (%d entries) ===", cache.size()); - logContent.append(header).append("\n"); - - String statsInfo = Rs2CacheLoggingUtils.formatCacheStatistics( - stats.getHitRate(), stats.cacheHits, stats.cacheMisses, stats.cacheMode.toString()); - - logContent.append(statsInfo).append("\n\n"); - - if (cache.size() == 0) { - String emptyMsg = "Cache is empty"; - - logContent.append(emptyMsg).append("\n"); - } else { - // Table format for NPCs - String[] headers = {"Name", "ID", "Combat Level", "Distance", "Location", "Health", "Cache Timestamp"}; - int[] columnWidths = {25, 8, 12, 8, 18, 8, 22}; - - String tableHeader = Rs2CacheLoggingUtils.formatTableHeader(headers, columnWidths); - - logContent.append("\n").append(tableHeader); - int maxRows = outputMode == LogOutputMode.CONSOLE_ONLY ? 50 : cache.size(); - // Sort NPCs by distance (closest first) - cache.stream() - .filter(npc -> { - try { - // Filter out NPCs with invalid distance calculations - return npc != null && npc.getDistanceFromPlayer() < Integer.MAX_VALUE; - } catch (Exception e) { - return false; // Exclude NPCs that cause exceptions - } - }) - .sorted((a, b) -> { - try { - // Ensure both NPCs have valid distance data - if (a == null && b == null) return 0; - if (a == null) return 1; - if (b == null) return -1; - - int distanceA = a.getDistanceFromPlayer(); - int distanceB = b.getDistanceFromPlayer(); - - // Handle negative distances (invalid) by treating them as maximum distance - if (distanceA < 0) distanceA = Integer.MAX_VALUE; - if (distanceB < 0) distanceB = Integer.MAX_VALUE; - - return Integer.compare(distanceA, distanceB); - } catch (Exception e) { - // If comparison fails, use index as fallback to maintain consistency - return Integer.compare( - a != null ? a.getIndex() : Integer.MAX_VALUE, - b != null ? b.getIndex() : Integer.MAX_VALUE - ); - } - }) - .forEach(npc -> { - // Get cache timestamp for this NPC - Long cacheTimestamp = cache.getCacheTimestamp(Integer.valueOf(npc.getIndex())); - String cacheTimestampStr = cacheTimestamp != null ? - Rs2Cache.formatUtcTimestamp(cacheTimestamp) : "N/A"; - - String[] values = { - Rs2CacheLoggingUtils.truncate(npc.getName(), 24), - String.valueOf(npc.getId()), - String.valueOf(npc.getCombatLevel()), - String.valueOf(npc.getDistanceFromPlayer()), - Rs2CacheLoggingUtils.formatLocation(npc.getWorldLocation()), - npc.getHealthRatio() != -1 ? String.valueOf(npc.getHealthRatio()) : "N/A", - Rs2CacheLoggingUtils.truncate(cacheTimestampStr, 21) - }; - - String row = Rs2CacheLoggingUtils.formatTableRow(values, columnWidths); - - logContent.append(row); - }); - - String tableFooter = Rs2CacheLoggingUtils.formatTableFooter(columnWidths); - - logContent.append(tableFooter); - - String limitMsg = Rs2CacheLoggingUtils.formatLimitMessage(cache.size(), maxRows); - if (!limitMsg.isEmpty()) { - - logContent.append(limitMsg).append("\n"); - } - } - - String footer = "=== End NPC Cache State ==="; - logContent.append(footer).append("\n"); - // Use the new output mode utility - Rs2CacheLoggingUtils.outputCacheLog("npc", logContent.toString(), outputMode); - } - - /** - * Logs the current state of all cached NPCs for debugging. - * - * @param dumpToFile Whether to also dump the information to a file - */ - public static void logState(boolean dumpToFile) { - net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode outputMode = - dumpToFile ? net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode.BOTH - : net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode.CONSOLE_ONLY; - logState(outputMode); - } /** - * Logs the current state of all cached NPCs for debugging (console only). - */ - public static void logState() { - logState(false); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2ObjectCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2ObjectCache.java deleted file mode 100644 index 7f62749ae79..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2ObjectCache.java +++ /dev/null @@ -1,604 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Constants; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.*; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.strategy.entity.ObjectUpdateStrategy; -import net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2CacheLoggingUtils; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel.ObjectType; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Thread-safe cache for tracking game objects using the unified cache architecture. - * Handles GameObject, GroundObject, WallObject, and DecorativeObject types. - * Returns Rs2ObjectModel objects for enhanced object handling. - * Uses EVENT_DRIVEN_ONLY mode to persist objects until despawn or game state changes. - * - * This class extends Rs2Cache and provides specific object caching functionality - * with proper EventBus integration for @Subscribe methods. - * - * Key Changes: - * - Uses String-based keys for better tracking across region changes - * - Implements region change detection to clear stale objects - * - Handles the fact that RuneLite doesn't fire despawn events on region changes - */ -@Slf4j -public class Rs2ObjectCache extends Rs2Cache { - - private static Rs2ObjectCache instance; - - // Reference to the update strategy for scene scanning - private ObjectUpdateStrategy updateStrategy; - - /** - * Private constructor for singleton pattern. - */ - private Rs2ObjectCache() { - super("ObjectCache", CacheMode.EVENT_DRIVEN_ONLY); - this.updateStrategy = new ObjectUpdateStrategy(); - this.withUpdateStrategy(this.updateStrategy); - - log.debug("Rs2ObjectCache initialized with String-based keys, region change detection, and scene scanning"); - } - - /** - * Gets the singleton instance of Rs2ObjectCache. - * - * @return The singleton object cache instance - */ - public static synchronized Rs2ObjectCache getInstance() { - if (instance == null) { - instance = new Rs2ObjectCache(); - } - return instance; - } - - /** - * Requests an scene scan to be performed when appropriate. - * This is more efficient than immediate scanning. - */ - public static boolean requestSceneScan() { - return getInstance().updateStrategy.requestSceneScan(getInstance()); - } - - - /** - * Starts periodic scene scanning to keep the cache fresh. - * This is useful for long-running scripts that need up-to-date object data. - * - * @param intervalSeconds How often to scan the scene in seconds - */ - public static void startPeriodicSceneScan(long intervalSeconds) { - getInstance().updateStrategy.schedulePeriodicSceneScan(getInstance(), intervalSeconds); - } - - /** - * Stops periodic scene scanning. - */ - public static void stopPeriodicSceneScan() { - getInstance().updateStrategy.stopPeriodicSceneScan(); - } - - - - - /** - * Overrides the get method to provide fallback scene scanning when cache is empty or key not found. - * This ensures that even if events are missed, we can still retrieve objects from the scene. - * - * @param key The unique String key for the object - * @return The object model if found in cache or scene, null otherwise - */ - @Override - public Rs2ObjectModel get(String key) { - // First try the regular cache lookup - Rs2ObjectModel cachedResult = super.get(key); - if (cachedResult != null) { - return cachedResult; - } - if (Microbot.getClient() == null || Microbot.getClient().getLocalPlayer() == null) { - log.warn("Client or local player is null, cannot perform scene scan"); - return null; - } - // If not in cache and cache is very small, request and perform scene scan - if (updateStrategy.requestSceneScan(this)) { - log.debug("Cache miss for key '{}' (size: {}), performing scene scan", key, this.size()); - //updateStrategy.performSceneScan(this, false); - // Try again after scene scan - return super.get(key); - }else { - log.debug("Cache miss for key '{}' (size: {}), but scene scan not requested not successful", key, this.size()); - } - - return null; - } - - /** - * Gets an object by its unique String-based key. - * - * @param key The unique String key for the object - * @return Optional containing the object model if found - */ - public static Optional getObjectByKey(String key) { - return Optional.ofNullable(getInstance().get(key)); - } - - /** - * Gets all objects matching a specific ID. - * - * @param objectId The object ID to search for - * @return Stream of matching Rs2ObjectModel objects - */ - public static Stream getObjectsById(int objectId) { - return getInstance().stream() - .filter(obj -> obj.getId() == objectId); - } - - /** - * Gets all objects matching a specific name (case-insensitive). - * - * @param name The object name to search for - * @return Stream of matching Rs2ObjectModel objects - */ - public static Stream getObjectsByName(String name) { - return getInstance().stream() - .filter(obj -> obj.getName() != null && - obj.getName().toLowerCase().contains(name.toLowerCase())); - } - - /** - * Gets all objects of a specific type. - * - * @param objectType The type of objects to search for - * @return Stream of matching Rs2ObjectModel objects - */ - public static Stream getObjectsByType(ObjectType objectType) { - return getInstance().stream() - .filter(obj -> obj.getObjectType() == objectType); - } - - /** - * Gets all objects within a certain distance from a location. - * - * @param location The center location - * @param maxDistance The maximum distance in tiles - * @return Stream of objects within the specified distance - */ - public static Stream getObjectsWithinDistance(WorldPoint location, int maxDistance) { - return getInstance().stream() - .filter(obj -> obj.getWorldLocation() != null && - obj.getWorldLocation().distanceTo(location) <= maxDistance); - } - - /** - * Gets the first object matching the specified ID. - * - * @param objectId The object ID - * @return Optional containing the first matching object model - */ - public static Optional getFirstObjectById(int objectId) { - return getObjectsById(objectId).findFirst(); - } - - /** - * Gets the first object matching the specified name. - * - * @param name The object name - * @return Optional containing the first matching object model - */ - public static Optional getFirstObjectByName(String name) { - return getObjectsByName(name).findFirst(); - } - - - - /** - * Gets all objects - Legacy compatibility method. - * - * @return Stream of all objects - */ - public static Stream getAllObjects() { - return getInstance().values().stream(); - } - - /** - * Gets all GameObjects from the cache. - * - * @return Stream of GameObject models - */ - public static Stream getGameObjects() { - return getObjectsByType(ObjectType.GAME_OBJECT); - } - - /** - * Gets all GroundObjects from the cache. - * - * @return Stream of GroundObject models - */ - public static Stream getGroundObjects() { - return getObjectsByType(ObjectType.GROUND_OBJECT); - } - - /** - * Gets all WallObjects from the cache. - * - * @return Stream of WallObject models - */ - public static Stream getWallObjects() { - return getObjectsByType(ObjectType.WALL_OBJECT); - } - - /** - * Gets all DecorativeObjects from the cache. - * - * @return Stream of DecorativeObject models - */ - public static Stream getDecorativeObjects() { - return getObjectsByType(ObjectType.DECORATIVE_OBJECT); - } - - /** - * Gets the closest object to the player with the specified ID. - * - * @param objectId The object ID to search for - * @return Optional containing the closest object - */ - public static Optional getClosestObjectById(int objectId) { - return getClosestObjectById(objectId, Microbot.getClient().getLocalPlayer().getWorldLocation()); - } - - /** - * Gets the closest object to the player with the specified name. - * - * @param name The object name to search for - * @return Optional containing the closest object - */ - public static Optional getClosestObjectByName(String name) { - return getClosestObjectByName(name, Microbot.getClient().getLocalPlayer().getWorldLocation()); - } - - /** - * Gets the closest object to a specific anchor point with the specified ID. - * - * @param objectId The object ID to search for - * @param anchorPoint The anchor point to calculate distance from - * @return Optional containing the closest object - */ - public static Optional getClosestObjectById(int objectId, WorldPoint anchorPoint) { - return getObjectsById(objectId) - .min((o1, o2) -> Integer.compare( - o1.getWorldLocation().distanceTo(anchorPoint), - o2.getWorldLocation().distanceTo(anchorPoint) - )); - } - - /** - * Gets the closest object to a specific anchor point with the specified name. - * - * @param name The object name to search for - * @param anchorPoint The anchor point to calculate distance from - * @return Optional containing the closest object - */ - public static Optional getClosestObjectByName(String name, WorldPoint anchorPoint) { - return getObjectsByName(name) - .min((o1, o2) -> Integer.compare( - o1.getWorldLocation().distanceTo(anchorPoint), - o2.getWorldLocation().distanceTo(anchorPoint) - )); - } - - /** - * Gets objects sorted by distance from player (closest first). - * - * @return List of objects sorted by distance ascending - */ - public static List getObjectsSortedByDistance() { - WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - return getInstance().values().stream() - .sorted((o1, o2) -> Integer.compare( - o1.getWorldLocation().distanceTo(playerLocation), - o2.getWorldLocation().distanceTo(playerLocation) - )) - .collect(Collectors.toList()); - } - - /** - * Gets the total number of cached objects. - * - * @return The total object count - */ - public static int getObjectCount() { - return getInstance().size(); - } - - /** - * Gets the total number of objects by ID. - * - * @param objectId The object ID to count - * @return The count of objects with the specified ID - */ - public static long getObjectCountById(int objectId) { - return getObjectsById(objectId).count(); - } - - /** - * Gets the count of objects by type. - * - * @param objectType The object type to count - * @return The count of objects with the specified type - */ - public static long getObjectCountByType(ObjectType objectType) { - return getObjectsByType(objectType).count(); - } - - - - /** - * Invalidates all object cache entries and performs a fresh scene scan. - */ - public static void invalidateAllObjectsAndScanScene() { - getInstance().invalidateAll(); - requestSceneScan(); - log.debug("Invalidated all object cache entries and triggered scene scan"); - } - - - - /** - * Event handler registration for the unified cache. - * The unified cache handles events through its strategy automatically. - * Region change detection is now handled primarily in onGameTick() to prevent - * redundant checks during burst spawn events. - */ - - @Subscribe(priority = 50) - public void onGameObjectSpawned(final GameObjectSpawned event) { - // Region change check now handled in onGameStateChanged() to prevent redundant checks - getInstance().handleEvent(event); - } - - @Subscribe(priority = 60) - public void onGameObjectDespawned(final GameObjectDespawned event) { - getInstance().handleEvent(event); - } - - @Subscribe(priority = 50) - public void onGroundObjectSpawned(final GroundObjectSpawned event) { - // Region change check now handled in onGameStateChanged() to prevent redundant checks - getInstance().handleEvent(event); - } - - @Subscribe(priority = 60) - public void onGroundObjectDespawned(final GroundObjectDespawned event) { - getInstance().handleEvent(event); - } - - @Subscribe(priority = 50) - public void onWallObjectSpawned(final WallObjectSpawned event) { - // Region change check now handled in onGameStateChanged() to prevent redundant checks - getInstance().handleEvent(event); - } - - @Subscribe(priority = 60) - public void onWallObjectDespawned(final WallObjectDespawned event) { - getInstance().handleEvent(event); - } - - @Subscribe(priority = 50) - public void onDecorativeObjectSpawned(final DecorativeObjectSpawned event) { - // Region change check now handled in onGameStateChanged() to prevent redundant checks - getInstance().handleEvent(event); - } - - @Subscribe(priority = 60) - public void onDecorativeObjectDespawned(final DecorativeObjectDespawned event) { - getInstance().handleEvent(event); - } - @Subscribe(priority = 40) - public void onGameStateChanged(final GameStateChanged event) { - // Removed old region detection - now handled by unified Rs2Cache system - // Also let the strategy handle the event - getInstance().handleEvent(event); - } - - @Subscribe(priority = 110) - public void onGameTick(final GameTick event) { - // Let the strategy handle scanning - getInstance().handleEvent(event); - } - - /** - * Resets the singleton instance. Used for testing. - */ - public static synchronized void resetInstance() { - if (instance != null) { - instance.invalidateAll(); - instance = null; - log.debug("Rs2ObjectCache instance reset"); - } - } - - /** - * Gets object type statistics for display in overlays. - * - * @return Statistics string with object type counts - */ - public static String getObjectTypeStatistics() { - Rs2ObjectCache cache = getInstance(); - - // Count objects by type - int gameObjectCount = 0; - int wallObjectCount = 0; - int decorativeObjectCount = 0; - int groundObjectCount = 0; - int tileObjectCount = 0; - - for (Rs2ObjectModel objectModel : cache.values()) { - switch (objectModel.getObjectType()) { - case GAME_OBJECT: - gameObjectCount++; - break; - case WALL_OBJECT: - wallObjectCount++; - break; - case DECORATIVE_OBJECT: - decorativeObjectCount++; - break; - case GROUND_OBJECT: - groundObjectCount++; - break; - case TILE_OBJECT: - tileObjectCount++; - break; - } - } - - return String.format("Objects by type - Game: %d, Wall: %d, Decorative: %d, Ground: %d, Tile: %d (Total: %d)", - gameObjectCount, wallObjectCount, decorativeObjectCount, groundObjectCount, tileObjectCount, - cache.size()); - } - - /** - * Logs the current state of all cached objects for debugging. - * - * @param dumpToFile Whether to also dump the information to a file - */ - public static void logState(LogOutputMode mode) { - var cache = getInstance(); - var stats = cache.getStatistics(); - - // Create the log content - StringBuilder logContent = new StringBuilder(); - - String header = String.format("=== Object Cache State (%d entries) ===", cache.size()); - logContent.append(header).append("\n"); - - String statsInfo = Rs2CacheLoggingUtils.formatCacheStatistics( - stats.getHitRate(), stats.cacheHits, stats.cacheMisses, stats.cacheMode.toString()); - logContent.append(statsInfo).append("\n\n"); - - if (cache.size() == 0) { - logContent.append("Cache is empty\n"); - } else { - // Table format for objects - final String[] headers = {"Name", "Type", "ID", "Location", "Distance", "Actions"}; - final int[] columnWidths = {25, 12, 8, 18, 8, 30}; - logContent.append("\n").append(Rs2CacheLoggingUtils.formatTableHeader(headers, columnWidths)); - - // Get player location once for distance calculations - WorldPoint playerLocation = null; - try { - if (Microbot.getClient() != null && Microbot.getClient().getLocalPlayer() != null) { - playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - } catch (Exception e) { - log.debug("Could not get player location for distance calculations: {}", e.getMessage()); - } - final WorldPoint finalPlayerLocation = playerLocation; - - // Use a fixed-size buffer to avoid excessive StringBuilder growth - int maxRows = mode == LogOutputMode.CONSOLE_ONLY ? 50 : cache.size(); - int rowCount = 0; - // Precompute distances and actions in parallel for performance - class ObjectLogInfo { - Rs2ObjectModel obj; - int distance; - String actionsStr; - ObjectLogInfo(Rs2ObjectModel obj, int distance, String actionsStr) { - this.obj = obj; - this.distance = distance; - this.actionsStr = actionsStr; - } - } - - List objects = cache.values().parallelStream().limit(50) - .map(obj -> { - int distance = Integer.MAX_VALUE; - if (finalPlayerLocation != null && obj.getLocation() != null) { - try { - distance = obj.getLocation().distanceTo(finalPlayerLocation); - } catch (Exception ignored) {} - } - String actionsStr = ""; - try { - String[] actions = obj.getActions(); - if (actions != null && actions.length > 0) { - actionsStr = Arrays.stream(actions) - .filter(Objects::nonNull) - .filter(action -> !action.trim().isEmpty()) - .collect(Collectors.joining(",")); - } - } catch (Exception ignored) {} - return new ObjectLogInfo(obj, distance, actionsStr); - }) - .collect(Collectors.toList()); - - // Sort by distance (single-threaded, but fast on precomputed values) - if (finalPlayerLocation != null) { - objects.sort(Comparator.comparingInt(info -> info.distance)); - } - - for (ObjectLogInfo info : objects) { - if (rowCount++ >= maxRows) break; - try { - String[] values = { - Rs2CacheLoggingUtils.truncate(info.obj.getName() != null ? info.obj.getName() : "Unknown", 24), - info.obj.getObjectType() != null ? info.obj.getObjectType().name() : "Unknown", - String.valueOf(info.obj.getId()), - Rs2CacheLoggingUtils.formatLocation(info.obj.getLocation()), - info.distance == Integer.MAX_VALUE ? "N/A" : String.valueOf(info.distance), - Rs2CacheLoggingUtils.truncate(info.actionsStr, 29) - }; - logContent.append(Rs2CacheLoggingUtils.formatTableRow(values, columnWidths)); - } catch (Exception e) { - log.debug("Error processing object for logging: {}", e.getMessage()); - } - } - - logContent.append(Rs2CacheLoggingUtils.formatTableFooter(columnWidths)); - String limitMsg = Rs2CacheLoggingUtils.formatLimitMessage(cache.size(), maxRows); - if (!limitMsg.isEmpty()) { - logContent.append(limitMsg).append("\n"); - } - } - - String footer = "=== End Object Cache State ==="; - logContent.append(footer).append("\n"); - Rs2CacheLoggingUtils.outputCacheLog(getInstance().getCacheName(), logContent.toString(), mode); - } - - - - /** - * Implementation of abstract update method from Rs2Cache. - * Clears the cache and performs a complete scene scan to reload all objects from the scene. - * This ensures the cache is fully refreshed with current scene data. - */ - @Override - public void update() { - // Call the update method with a default delay of 0 - update(Constants.CLIENT_TICK_LENGTH /2); - } - /** - * Updates the object cache by clearing it and performing a scene scan. - * This is useful for refreshing the cache after significant game state changes. - * - * @param delayMs Optional delay in milliseconds before performing the update - */ - public void update(long delayMs) { - log.debug("Starting object cache update - clearing cache and performing scene scan after delay: {}ms", delayMs); - // Clear the entire cache - this.invalidateAll(); - // Perform a complete scene scan to repopulate the cache - updateStrategy.performSceneScan(this, delayMs ); - - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2QuestCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2QuestCache.java deleted file mode 100644 index 2c5c08dd30d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2QuestCache.java +++ /dev/null @@ -1,669 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import java.util.function.Supplier; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Quest; -import net.runelite.api.QuestState; -import net.runelite.api.events.ChatMessage; -import net.runelite.api.events.VarbitChanged; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.serialization.CacheSerializable; -import net.runelite.client.plugins.microbot.util.cache.strategy.simple.QuestUpdateStrategy; -import net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2CacheLoggingUtils; - -/** - * Thread-safe cache for quest states using the unified cache architecture. - * Automatically updates when quest-related events are received and supports persistence. - * - * This class extends Rs2UnifiedCache and provides specific quest caching functionality - * with proper EventBus integration for @Subscribe methods. - * - * Threading Strategy: - * - All quest state loading uses async approach with invokeLater() for deferred execution - * - Event handlers: Use invokeLater() to defer execution and avoid blocking event processing - * - Async methods: Use invokeLater() for deferred execution during initialization scenarios - * - * The cache uses only asynchronous operations to avoid nested thread issues - * that can occur when events (already on client thread) need to load quest states. - */ -@Slf4j -public class Rs2QuestCache extends Rs2Cache implements CacheSerializable { - private static Rs2QuestCache instance; - // Async update tracking - private static final AtomicInteger pendingAsyncUpdates = new java.util.concurrent.atomic.AtomicInteger(0); - private static final Set> updateCompletionCallbacks = java.util.concurrent.ConcurrentHashMap.newKeySet(); - - // Quest-specific update coordination to prevent deadlocks and race conditions - private static final java.util.concurrent.ConcurrentHashMap questUpdatesInProgress = new java.util.concurrent.ConcurrentHashMap<>(); - private static final long UPDATE_TIMEOUT_MS = 10000; // 10 seconds timeout for update tracking - - /** - * Private constructor for singleton pattern. - */ - private Rs2QuestCache() { - super("QuestCache", CacheMode.EVENT_DRIVEN_ONLY); - this.withUpdateStrategy(new QuestUpdateStrategy()) - .withPersistence("quests"); - } - - /** - * Gets the singleton instance of Rs2QuestCache. - * - * @return The singleton quest cache instance - */ - public static synchronized Rs2QuestCache getInstance() { - if (instance == null) { - instance = new Rs2QuestCache(); - } - return instance; - } - - /** - * Gets the cache instance for backward compatibility. - * - /** - * Gets the cache instance for backward compatibility. - * - * @return The singleton unified cache instance - */ - public static Rs2Cache getCache() { - return getInstance(); - } - - /** - * Checks if a quest update is currently in progress. - * Cleans up stale update entries older than UPDATE_TIMEOUT_MS. - * - * @param quest The quest to check - * @return true if update is in progress, false otherwise - */ - private static boolean isUpdateInProgress(Quest quest) { - Long updateStartTime = questUpdatesInProgress.get(quest); - if (updateStartTime == null) { - return false; - } - - // Check if update has timed out - long currentTime = System.currentTimeMillis(); - if (currentTime - updateStartTime > UPDATE_TIMEOUT_MS) { - // Remove stale update entry - questUpdatesInProgress.remove(quest); - log.warn("Removed stale quest update entry for {} (timeout after {}ms)", - quest.getName(), UPDATE_TIMEOUT_MS); - return false; - } - - return true; - } - - /** - * Marks a quest as having an update in progress. - * - * @param quest The quest to mark - * @return true if successfully marked (no existing update), false if update already in progress - */ - private static boolean markUpdateInProgress(Quest quest) { - long currentTime = System.currentTimeMillis(); - Long previousTime = questUpdatesInProgress.putIfAbsent(quest, currentTime); - - if (previousTime != null) { - // Check if existing update has timed out - if (currentTime - previousTime > UPDATE_TIMEOUT_MS) { - // Replace stale entry - questUpdatesInProgress.put(quest, currentTime); - log.debug("Replaced stale quest update entry for {}", quest.getName()); - return true; - } - return false; // Update already in progress - } - - return true; // Successfully marked - } - - /** - * Marks a quest update as completed. - * - * @param quest The quest to mark as completed - */ - private static void markUpdateCompleted(Quest quest) { - questUpdatesInProgress.remove(quest); - } - - /** - * Cleans up stale quest update entries. - */ - private static void cleanupStaleUpdates() { - long currentTime = System.currentTimeMillis(); - questUpdatesInProgress.entrySet().removeIf(entry -> { - if (currentTime - entry.getValue() > UPDATE_TIMEOUT_MS) { - log.debug("Cleaned up stale quest update entry for {}", entry.getKey().getName()); - return true; - } - return false; - }); - } - - - - - - - - /** - * Asynchronously loads quest state using invokeLater for deferred execution. - * This is the core method for all quest state loading to ensure consistent async behavior. - * Prevents duplicate updates for the same quest using quest-specific coordination. - * - * @param quest The quest to load state for - * @param callback Callback to handle the loaded quest state - */ - private static void loadQuestStateFromClientAsync(Quest quest, Consumer callback) { - try { - log.debug("Setting up async quest state loading for {}", quest.getName()); - - if (Microbot.getClient() == null) { - log.warn("Client is null when loading quest state for {}", quest); - executeCallback(callback, QuestState.NOT_STARTED); - return; - } - - // Check if update is already in progress for this quest - if (!markUpdateInProgress(quest)) { - log.debug("Quest update already in progress for {}, rejecting duplicate request", quest.getName()); - return; // Simply reject the duplicate update request - } - - // Increment pending updates counter - pendingAsyncUpdates.incrementAndGet(); - - // Always use invokeLater for consistency, even when on client thread - Microbot.getClientThread().invokeLater(() -> { - executeQuestStateLoad(quest, callback); - }); - - } catch (Exception e) { - log.error("Error setting up async quest state loading for {}: {}", quest, e.getMessage(), e); - handleLoadFailure(quest, callback); - } - } - - /** - * Executes the actual quest state loading on the client thread. - * - * @param quest The quest to load state for - * @param callback Callback to handle the loaded quest state - */ - private static void executeQuestStateLoad(Quest quest, Consumer callback) { - try { - QuestState state = quest.getState(Microbot.getClient()); - log.debug("Loaded quest state: {} = {}", quest.getName(), state); - - // Update cache with new state - updateQuestState(quest, state); - - // Execute callback - executeCallback(callback, state); - - } catch (Exception e) { - log.error("Error in quest state loading for {}: {}", quest, e.getMessage(), e); - executeCallback(callback, QuestState.NOT_STARTED); - } finally { - // Always clean up tracking and notify completion - handleLoadCompletion(quest); - } - } - - /** - * Handles cleanup and notification when a quest load completes. - * - * @param quest The quest that completed loading - */ - private static void handleLoadCompletion(Quest quest) { - // Mark quest update as completed - markUpdateCompleted(quest); - - // Decrement pending updates and check for completion - int remaining = pendingAsyncUpdates.decrementAndGet(); - if (remaining == 0) { - // Clean up stale updates - cleanupStaleUpdates(); - - // Notify all completion callbacks - notifyCompletionCallbacks(); - } - } - - /** - * Handles load failure scenarios. - * - * @param quest The quest that failed to load - * @param callback The callback to notify of failure - */ - private static void handleLoadFailure(Quest quest, Consumer callback) { - markUpdateCompleted(quest); - executeCallback(callback, QuestState.NOT_STARTED); - pendingAsyncUpdates.decrementAndGet(); - } - - /** - * Safely executes a callback with error handling. - * - * @param callback The callback to execute - * @param state The quest state to pass to the callback - */ - private static void executeCallback(Consumer callback, QuestState state) { - if (callback != null) { - try { - callback.accept(state); - } catch (Exception e) { - log.error("Error in quest state callback: {}", e.getMessage(), e); - } - } - } - - /** - * Notifies all completion callbacks safely. - */ - private static void notifyCompletionCallbacks() { - updateCompletionCallbacks.forEach(completionCallback -> { - try { - completionCallback.accept(true); - } catch (Exception e) { - log.error("Error in update completion callback: {}", e.getMessage()); - } - }); - } - - /** - * Gets quest state from the cache. If not cached, returns NOT_STARTED and triggers async loading. - * This prevents blocking behavior and deadlocks by not waiting for completion. - * - * @param quest The quest to retrieve state for - * @return The cached QuestState or NOT_STARTED if not in cache - */ - public static QuestState getQuestState(Quest quest) { - // Use the base cache get method directly - QuestState cachedState = getInstance().get(quest); - if (cachedState != null ) { - return cachedState; - } - if ( isUpdateInProgress(quest)) { - //log.info("Quest update in progress for {}, returning NOT_STARTED", quest.getName()); - return QuestState.NOT_STARTED; // Return NOT_STARTED if update is in progress - } - - // Trigger async loading if not cached - updateQuestStateAsync(quest); - return getCache().get(quest); // Default state if not cached - - - - - } - - /** - * Asynchronously updates quest state in cache. Useful during initialization or event processing - * where you want to ensure quest state loading doesn't block current execution. - * - * @param quest The quest to update - */ - public static void updateQuestStateAsync(Quest quest) { - loadQuestStateFromClientAsync(quest, state -> { - log.debug("Async quest update completed: {} = {}", quest.getName(), state); - }); - } - - /** - * Gets quest state asynchronously with a callback. Preferred method for async operations. - * If cached, callback is executed immediately. Otherwise, loads asynchronously. - * - * @param quest The quest to retrieve state for - * @param callback Callback to receive the quest state - */ - public static void getQuestStateAsync(Quest quest, Consumer callback) { - QuestState cachedState = getInstance().get(quest); - if (cachedState != null) { - executeCallback(callback, cachedState); - } else { - loadQuestStateFromClientAsync(quest, callback); - } - } - - /** - * Registers a callback to be notified when all pending async updates are complete. - * - * @param callback Callback to be called with true when all updates are complete - */ - public static void onAllAsyncUpdatesComplete(Consumer callback) { - if (callback != null) { - updateCompletionCallbacks.add(callback); - // If no updates are pending, call immediately - if (pendingAsyncUpdates.get() == 0) { - try { - callback.accept(true); - } catch (Exception e) { - log.error("Error in immediate completion callback: {}", e.getMessage()); - } - } - } - } - - /** - * Gets the number of pending async quest state updates. - * - * @return The number of pending updates - */ - public static int getPendingAsyncUpdates() { - return pendingAsyncUpdates.get(); - } - - /** - * Clears all completion callbacks. Useful for cleanup. - */ - public static void clearCompletionCallbacks() { - updateCompletionCallbacks.clear(); - } - - /** - * Gets quest state from the cache or loads it with a custom supplier. - * - * @param quest The quest to retrieve state for - * @param valueLoader Custom supplier for loading the quest state - * @return The QuestState - */ - public static QuestState getQuestState(Quest quest, Supplier valueLoader) { - return getInstance().get(quest, valueLoader); - } - - /** - * Manually updates a quest state in the cache. - * - * @param quest The quest to update - * @param state The new quest state - */ - private static void updateQuestState(Quest quest, QuestState state) { - getInstance().put(quest, state); - log.debug("Updated quest cache: {} = {}", quest, state); - } - - /** - * Checks if a quest is started (not NOT_STARTED). - * - * @param quest The quest to check - * @return true if the quest is started - */ - public static boolean isQuestStarted(Quest quest) { - return getQuestState(quest) != QuestState.NOT_STARTED; - } - - /** - * Checks if a quest is completed (FINISHED). - * - * @param quest The quest to check - * @return true if the quest is completed - */ - public static boolean isQuestCompleted(Quest quest) { - return getQuestState(quest) == QuestState.FINISHED; - } - - /** - * Checks if a quest is in progress (IN_PROGRESS). - * - * @param quest The quest to check - * @return true if the quest is in progress - */ - public static boolean isQuestInProgress(Quest quest) { - return getQuestState(quest) == QuestState.IN_PROGRESS; - } - - /** - * Schedules an asynchronous update of all cached quests using invokeLater. - * This is useful during initialization or when you want to ensure quest updates - * don't block current event processing, even when already on the client thread. - */ - public static void updateAllFromClientAsync() { - - try { - log.debug("Starting asynchronous quest cache update..."); - getInstance().update(); - log.debug("Completed asynchronous quest cache update"); - } catch (Exception e) { - log.error("Error during asynchronous quest cache update: {}", e.getMessage(), e); - } - - } - - /** - * Updates all cached data by retrieving fresh values from the game client asynchronously. - * Implements the abstract method from Rs2Cache. - * - * Iterates over all currently cached quest keys and refreshes their states asynchronously. - */ - @Override - public void update() { - log.debug("Updating all cached quests from client asynchronously..."); - - if (Microbot.getClient() == null) { - log.warn("Cannot update quests - client is null"); - return; - } - - int beforeSize = size(); - - // Get all currently cached quest keys and update them asynchronously - java.util.Set cachedQuests = entryStream() - .map(java.util.Map.Entry::getKey) - .collect(java.util.stream.Collectors.toSet()); - - if (cachedQuests.isEmpty()) { - log.debug("No cached quests to update"); - return; - } - - log.info("Starting async update of {} cached quests", cachedQuests.size()); - - for (Quest quest : cachedQuests) { - loadQuestStateFromClientAsync(quest, freshState -> { - if (freshState != null) { - put(quest, freshState); - log.debug("Updated quest {} with fresh state: {}", quest.getName(), freshState); - } - }); - } - - log.info("Initiated async update for {} quests (cache had {} entries total)", - cachedQuests.size(), beforeSize); - } - - - - /** - * Event handler registration for the unified cache. - * The unified cache handles events through its strategy automatically. - */ - - @Subscribe - public void onVarbitChanged(final VarbitChanged event) { - try { - getInstance().handleEvent(event); - } catch (Exception e) { - log.error("Error handling VarbitChanged event: {}", e.getMessage(), e); - } - } - - @Subscribe - public void onChatMessage(final ChatMessage chatMessage) { - try { - getInstance().handleEvent(chatMessage); - } catch (Exception e) { - log.error("Error handling ChatMessage event: {}", e.getMessage(), e); - } - } - - /** - * Resets the singleton instance. Used for testing. - */ - public static synchronized void resetInstance() { - if (instance != null) { - instance.invalidateAll(); - instance = null; - } - } - - // ============================================ - // CacheSerializable Implementation - // ============================================ - - @Override - public String getConfigKey() { - return "quests"; - } - - @Override - public String getConfigGroup() { - return "microbot"; - } - - @Override - public boolean shouldPersist() { - return true; // Quest states should be persisted for progress tracking - } - - /** - * Logs the current state of all cached quests for debugging. - * - * @param dumpToFile Whether to also dump the information to a file - */ - public static void logState(LogOutputMode mode) { - var cache = getInstance(); - var stats = cache.getStatistics(); - - // Create the log content - StringBuilder logContent = new StringBuilder(); - - String header = String.format("=== Quest Cache State (%d entries) ===", cache.size()); - logContent.append(header).append("\n"); - - String statsInfo = Rs2CacheLoggingUtils.formatCacheStatistics( - stats.getHitRate(), stats.cacheHits, stats.cacheMisses, stats.cacheMode.toString()); - logContent.append(statsInfo).append("\n\n"); - - if (cache.size() == 0) { - String emptyMsg = "Cache is empty"; - logContent.append(emptyMsg).append("\n"); - } else { - // Table format for quests - String[] headers = {"Quest", "State", "ID", "Cache Timestamp", "Varbit ID", "VarPlayer ID"}; - int[] columnWidths = {40, 15, 8, 22, 10, 12}; - - String tableHeader = Rs2CacheLoggingUtils.formatTableHeader(headers, columnWidths); - logContent.append("\n").append(tableHeader); - - // Sort quests by state (completed first, then in progress, then not started) - cache.entryStream() - .sorted((a, b) -> { - try { - // Handle null entries - if (a == null && b == null) return 0; - if (a == null) return 1; - if (b == null) return -1; - if (a.getKey() == null && b.getKey() == null) return 0; - if (a.getKey() == null) return 1; - if (b.getKey() == null) return -1; - if (a.getValue() == null && b.getValue() == null) return 0; - if (a.getValue() == null) return 1; - if (b.getValue() == null) return -1; - - // Sort by state priority: FINISHED > IN_PROGRESS > NOT_STARTED - int aOrder = getQuestStateOrder(a.getValue()); - int bOrder = getQuestStateOrder(b.getValue()); - if (aOrder != bOrder) { - return Integer.compare(aOrder, bOrder); - } - // Then sort alphabetically by quest name - String nameA = a.getKey().getName(); - String nameB = b.getKey().getName(); - if (nameA == null && nameB == null) return 0; - if (nameA == null) return 1; - if (nameB == null) return -1; - return nameA.compareTo(nameB); - } catch (Exception e) { - // Fallback to ID comparison if anything goes wrong - try { - return Integer.compare( - a != null && a.getKey() != null ? a.getKey().getId() : Integer.MAX_VALUE, - b != null && b.getKey() != null ? b.getKey().getId() : Integer.MAX_VALUE - ); - } catch (Exception e2) { - return 0; // Last resort - consider equal - } - } - }) - .forEach(entry -> { - Quest quest = entry.getKey(); - QuestState questState = entry.getValue(); - - // Get cache timestamp for this quest - Long cacheTimestamp = cache.getCacheTimestamp(quest); - String cacheTimestampStr = cacheTimestamp != null ? - Rs2Cache.formatUtcTimestamp(cacheTimestamp) : "N/A"; - - // Get varbit/varPlayer IDs for this quest - Integer varbitId = QuestUpdateStrategy.getVarbitIdByQuest(quest); - Integer varPlayerId = QuestUpdateStrategy.getVarPlayerIdByQuest(quest); - - String[] values = { - Rs2CacheLoggingUtils.truncate(quest.getName(), 39), - questState.toString(), - String.valueOf(quest.getId()), - Rs2CacheLoggingUtils.truncate(cacheTimestampStr, 21), - varbitId != null ? String.valueOf(varbitId) : "N/A", - varPlayerId != null ? String.valueOf(varPlayerId) : "N/A" - }; - - String row = Rs2CacheLoggingUtils.formatTableRow(values, columnWidths); - logContent.append(row); - }); - - String tableFooter = Rs2CacheLoggingUtils.formatTableFooter(columnWidths); - logContent.append(tableFooter); - - // Summary statistics - long completedCount = cache.entryStream().filter(e -> e.getValue() == QuestState.FINISHED).count(); - long inProgressCount = cache.entryStream().filter(e -> e.getValue() == QuestState.IN_PROGRESS).count(); - long notStartedCount = cache.entryStream().filter(e -> e.getValue() == QuestState.NOT_STARTED).count(); - - String summaryMsg = String.format("Quest Summary: %d Completed, %d In Progress, %d Not Started", - completedCount, inProgressCount, notStartedCount); - logContent.append(summaryMsg).append("\n"); - } - - String footer = "=== End Quest Cache State ==="; - logContent.append(footer).append("\n"); - Rs2CacheLoggingUtils.outputCacheLog(getInstance().getCacheName(), logContent.toString(), mode); - } - - /** - * Helper method to define quest state sorting order. - */ - private static int getQuestStateOrder(QuestState state) { - switch (state) { - case FINISHED: - return 0; // Highest priority - case IN_PROGRESS: - return 1; // Medium priority - case NOT_STARTED: - default: - return 2; // Lowest priority - } - } - - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2SkillCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2SkillCache.java deleted file mode 100644 index f0f944ec859..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2SkillCache.java +++ /dev/null @@ -1,523 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Skill; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.StatChanged; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.model.SkillData; -import net.runelite.client.plugins.microbot.util.cache.strategy.simple.SkillUpdateStrategy; -import net.runelite.client.plugins.microbot.util.cache.serialization.CacheSerializable; -import net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2CacheLoggingUtils; - -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.function.Supplier; - -/** - * Thread-safe cache for skill levels and experience using the unified cache architecture. - * Automatically updates when StatChanged events are received and supports persistence. - * - * This class extends Rs2UnifiedCache and provides specific skill caching functionality - * with proper EventBus integration for @Subscribe methods. - */ -@Slf4j -public class Rs2SkillCache extends Rs2Cache implements CacheSerializable { - - private static Rs2SkillCache instance; - - /** - * Private constructor for singleton pattern. - */ - private Rs2SkillCache() { - super("SkillCache", CacheMode.EVENT_DRIVEN_ONLY); - this.withUpdateStrategy(new SkillUpdateStrategy()) - .withPersistence("skills"); - } - - /** - * Gets the singleton instance of Rs2SkillCache. - * - * @return The singleton skill cache instance - */ - public static synchronized Rs2SkillCache getInstance() { - if (instance == null) { - instance = new Rs2SkillCache(); - } - return instance; - } - - /** - * Gets the cache instance for backward compatibility. - * - * @return The singleton unified cache instance - */ - public static Rs2Cache getCache() { - return getInstance(); - } - - /** - * Loads skill data from the client for a specific skill. - * - * @param skill The skill to load data for - * @return The SkillData containing level, boosted level, and experience - */ - private static SkillData loadSkillDataFromClient(Skill skill) { - try { - if (Microbot.getClient() == null) { - log.warn("Client is null when loading skill data for {}", skill); - return new SkillData(1, 1, 0); - } - final int[] skillValues = new int[3]; // [level, boostedLevel, experience] - boolean loadedSuccessfully = Microbot.getClientThread().runOnClientThreadOptional( () -> { - skillValues[0] = Microbot.getClient().getRealSkillLevel(skill); - skillValues[1] = Microbot.getClient().getBoostedSkillLevel(skill); - skillValues[2] = Microbot.getClient().getSkillExperience(skill); - if (skillValues[0] < 0 || skillValues[1] < 0 || skillValues[2] < 0) { - log.warn("Invalid skill data for {}: level={}, boosted={}, exp={}", skill, skillValues[0], skillValues[1], skillValues[2]); - return false; // Skip if invalid - } - return true; // Ensure this runs on the client thread - }).orElse(false); - - if (!loadedSuccessfully) { - log.warn("Failed to load skill data for {}, using default values", skill); - return new SkillData(1, 1, 0); - } - - log.trace("Loaded skill data from client: {} (level: {}, boosted: {}, exp: {})", - skill, skillValues[0], skillValues[1], skillValues[2]); - return new SkillData(skillValues[0], skillValues[1], skillValues[2]); - } catch (Exception e) { - log.error("Error loading skill data for {}: {}", skill, e.getMessage(), e); - return new SkillData(1, 1, 0); - } - } - - /** - * Gets skill data from the cache or loads it from the client. - * - * @param skill The skill to retrieve data for - * @return The SkillData containing level, boosted level, and experience - */ - public static SkillData getSkillData(Skill skill) { - return getInstance().get(skill, () -> loadSkillDataFromClient(skill)); - } - - /** - * Gets skill data from the cache or loads it with a custom supplier. - * - * @param skill The skill to retrieve data for - * @param valueLoader Custom supplier for loading the skill data - * @return The SkillData - */ - public static SkillData getSkillData(Skill skill, Supplier valueLoader) { - return getInstance().get(skill, valueLoader); - } - - /** - * Gets the real (unboosted) level for a skill from the cache. - * - * @param skill The skill to get the level for - * @return The real skill level - */ - public static int getRealSkillLevel(Skill skill) { - return getSkillData(skill).getLevel(); - } - - /** - * Gets the boosted level for a skill from the cache. - * - * @param skill The skill to get the boosted level for - * @return The boosted skill level - */ - public static int getBoostedSkillLevel(Skill skill) { - return getSkillData(skill).getBoostedLevel(); - } - - /** - * Gets the experience for a skill from the cache. - * - * @param skill The skill to get the experience for - * @return The skill experience - */ - public static int getSkillExperience(Skill skill) { - return getSkillData(skill).getExperience(); - } - - /** - * Manually updates skill data in the cache. - * - * @param skill The skill to update - * @param skillData The new skill data - */ - public static void updateSkillData(Skill skill, SkillData skillData) { - getInstance().put(skill, skillData); - } - - /** - * Manually updates skill data in the cache. - * - * @param skill The skill to update - * @param level The real skill level - * @param boostedLevel The boosted skill level - * @param experience The skill experience - */ - public static void updateSkillData(Skill skill, int level, int boostedLevel, int experience) { - updateSkillData(skill, new SkillData(level, boostedLevel, experience)); - } - - /** - * Updates all cached skills by retrieving fresh data from the game client. - * Implements the abstract method from Rs2Cache. - * - * Iterates over all currently cached skill keys and refreshes their data from the client. - */ - @Override - public void update() { - log.debug("Updating all cached skills from client..."); - - if (Microbot.getClient() == null) { - log.warn("Cannot update skills - client is null"); - return; - } - - int beforeSize = size(); - int updatedCount = 0; - - // Get all currently cached skill keys and update them - java.util.Set cachedSkills = entryStream() - .map(java.util.Map.Entry::getKey) - .collect(java.util.stream.Collectors.toSet()); - - for (Skill skill : cachedSkills) { - try { - // Refresh the skill data from client using the private method - SkillData freshData = loadSkillDataFromClient(skill); - if (freshData != null) { - put(skill, freshData); - updatedCount++; - log.debug("Updated skill {} with fresh data: level={}, boosted={}, xp={}", - skill, freshData.getLevel(), freshData.getBoostedLevel(), freshData.getExperience()); - } - } catch (Exception e) { - log.warn("Failed to update skill {}: {}", skill, e.getMessage()); - } - } - - log.debug("Updated {} skills from client (cache had {} entries total)", - updatedCount, beforeSize); - } - - // ============================================ - // Print Functions for Cache Information - // ============================================ - - /** - * Returns a detailed formatted string containing all skill cache information. - * Includes complete skill data with temporal tracking and change information. - * - * @return Detailed multi-line string representation of all cached skills - */ - public static String printDetailedSkillInfo() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") - .withZone(ZoneId.systemDefault()); - - sb.append("=".repeat(80)).append("\n"); - sb.append(" DETAILED SKILL CACHE INFORMATION\n"); - sb.append("=".repeat(80)).append("\n"); - - // Cache metadata - Rs2Cache cache = getInstance(); - sb.append(String.format("Cache Name: %s\n", cache.getCacheName())); - sb.append(String.format("Cache Mode: %s\n", cache.getCacheMode())); - sb.append(String.format("Total Cached Skills: %d\n", cache.size())); - - // Cache statistics - var stats = cache.getStatistics(); - sb.append(String.format("Cache Hits: %d\n", stats.cacheHits)); - sb.append(String.format("Cache Misses: %d\n", stats.cacheMisses)); - sb.append(String.format("Hit Ratio: %.2f%%\n", stats.getHitRate() * 100)); - sb.append(String.format("Total Invalidations: %d\n", stats.totalInvalidations)); - sb.append(String.format("Uptime: %d ms\n", stats.uptime)); - sb.append(String.format("TTL: %d ms\n", stats.ttlMillis)); - sb.append("\n"); - - sb.append("-".repeat(80)).append("\n"); - sb.append(" SKILL DETAILS\n"); - sb.append("-".repeat(80)).append("\n"); - - // Headers - sb.append(String.format("%-15s %-6s %-8s %-12s %-12s %-10s %-19s\n", - "SKILL", "LEVEL", "BOOSTED", "EXPERIENCE", "EXP GAINED", "LEVEL UP", "LAST UPDATED")); - sb.append("-".repeat(80)).append("\n"); - - // Iterate through all skills - for (Skill skill : Skill.values()) { - SkillData data = cache.get(skill); - if (data != null) { - String lastUpdated = formatter.format(Instant.ofEpochMilli(data.getLastUpdated())); - String expGained = data.getExperienceGained() > 0 ? - String.format("+%d", data.getExperienceGained()) : "-"; - String levelUp = data.isLevelUp() ? "YES" : "-"; - - sb.append(String.format("%-15s %-6d %-8d %-12d %-12s %-10s %-19s\n", - skill.name(), - data.getLevel(), - data.getBoostedLevel(), - data.getExperience(), - expGained, - levelUp, - lastUpdated)); - - // Additional details for skills with changes - if (data.getPreviousLevel() != null || data.getPreviousExperience() != null) { - sb.append(String.format(" └─ Previous: Level %s, Experience %s\n", - data.getPreviousLevel() != null ? data.getPreviousLevel() : "Unknown", - data.getPreviousExperience() != null ? data.getPreviousExperience() : "Unknown")); - } - } - } - - sb.append("-".repeat(80)).append("\n"); - sb.append(String.format("Generated at: %s\n", formatter.format(Instant.now()))); - sb.append("=".repeat(80)); - - return sb.toString(); - } - - /** - * Returns a summary formatted string containing essential skill cache information. - * Compact view showing key metrics and recent changes. - * - * @return Summary multi-line string representation of skill cache - */ - public static String printSkillSummary() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss"); - - sb.append("┌─ SKILL CACHE SUMMARY ").append("─".repeat(45)).append("┐\n"); - - Rs2Cache cache = getInstance(); - var stats = cache.getStatistics(); - - // Summary statistics - sb.append(String.format("│ Skills Cached: %-3d │ Hits: %-6d │ Hit Rate: %5.1f%% │\n", - cache.size(), stats.cacheHits, stats.getHitRate() * 100)); - - // Combat level calculation - int attack = getRealSkillLevel(Skill.ATTACK); - int strength = getRealSkillLevel(Skill.STRENGTH); - int defence = getRealSkillLevel(Skill.DEFENCE); - int hitpoints = getRealSkillLevel(Skill.HITPOINTS); - int prayer = getRealSkillLevel(Skill.PRAYER); - int ranged = getRealSkillLevel(Skill.RANGED); - int magic = getRealSkillLevel(Skill.MAGIC); - - double combatLevel = (defence + hitpoints + Math.floor(prayer / 2)) * 0.25 + - Math.max(attack + strength, Math.max(ranged * 1.5, magic * 1.5)) * 0.325; - - // Calculate total level - int totalLevel = 0; - for (Skill skill : Skill.values()) { - totalLevel += getRealSkillLevel(skill); - } - - sb.append(String.format("│ Combat Level: %-6.1f │ Total Level: %-8d │\n", - combatLevel, totalLevel)); - - sb.append("├─ RECENT CHANGES ").append("─".repeat(46)).append("â”Ī\n"); - - // Show skills with recent changes (level ups or significant exp gains) - boolean hasChanges = false; - for (Skill skill : Skill.values()) { - SkillData data = cache.get(skill); - if (data != null && (data.isLevelUp() || data.getExperienceGained() > 0)) { - hasChanges = true; - String timeStr = formatter.format(Instant.ofEpochMilli(data.getLastUpdated())); - if (data.isLevelUp()) { - sb.append(String.format("│ %-12s LEVEL UP! %d → %d (%s) %-14s │\n", - skill.name(), data.getPreviousLevel(), data.getLevel(), timeStr, "")); - } else if (data.getExperienceGained() > 1000) { - sb.append(String.format("│ %-12s +%-8d exp (%s) %-20s │\n", - skill.name(), data.getExperienceGained(), timeStr, "")); - } - } - } - - if (!hasChanges) { - sb.append("│ No recent skill changes detected ").append(" ".repeat(27)).append("│\n"); - } - - sb.append("└").append("─".repeat(63)).append("┘"); - - return sb.toString(); - } - - // ============================================ - // Legacy API Compatibility Methods - // ============================================ - - /** - * Checks if skill meets level requirement - Legacy compatibility method. - * - * @param skill The skill to check - * @param levelRequired The required level - * @param boosted Whether to use boosted level (true) or real level (false) - * @return true if the requirement is met - */ - public static boolean hasSkillRequirement(Skill skill, int levelRequired, boolean boosted) { - int currentLevel = boosted ? getBoostedSkillLevel(skill) : getRealSkillLevel(skill); - return currentLevel >= levelRequired; - } - - /** - * Invalidates all skill cache entries. - */ - public static void invalidateAllSkills() { - getInstance().invalidateAll(); - log.debug("Invalidated all skill cache entries"); - } - - /** - * Invalidates a specific skill cache entry. - * - * @param skill The skill to invalidate - */ - public static void invalidateSkill(Skill skill) { - getInstance().remove(skill); - log.debug("Invalidated skill cache entry: {}", skill); - } - - /** - * Event handler registration for the unified cache. - * The unified cache handles events through its strategy automatically. - */ - - @Subscribe - public void onStatChanged(StatChanged event) { - try { - getInstance().handleEvent(event); - } catch (Exception e) { - log.error("Error handling StatChanged event: {}", e.getMessage(), e); - } - } - - @Subscribe - public void onGameStateChanged(GameStateChanged event) { - try { - switch (event.getGameState()) { - case LOGGED_IN: - case HOPPING: - case LOGIN_SCREEN: - case CONNECTION_LOST: - // Let the strategy handle cache invalidation - break; - default: - break; - } - } catch (Exception e) { - log.error("Error handling GameStateChanged event: {}", e.getMessage(), e); - } - } - - /** - * Resets the singleton instance. Used for testing. - */ - public static synchronized void resetInstance() { - if (instance != null) { - instance.invalidateAll(); - instance = null; - } - } - - /** - * Logs the current state of all cached skills for debugging. - * - * @param dumpToFile Whether to also dump the information to a file - */ - public static void logState(LogOutputMode mode) { - var cache = getInstance(); - var stats = cache.getStatistics(); - - // Create the log content - StringBuilder logContent = new StringBuilder(); - - String header = String.format("=== Skill Cache State (%d entries) ===", cache.size()); - logContent.append(header).append("\n"); - - String statsInfo = Rs2CacheLoggingUtils.formatCacheStatistics( - stats.getHitRate(), stats.cacheHits, stats.cacheMisses, stats.cacheMode.toString()); - logContent.append(statsInfo).append("\n\n"); - - if (cache.size() == 0) { - String emptyMsg = "Cache is empty"; - logContent.append(emptyMsg).append("\n"); - } else { - // Table format for skills - String[] headers = {"Skill", "Level", "Boosted", "Experience", "Previous", "Last Updated"}; - int[] columnWidths = {15, 8, 8, 12, 20, 12}; - - String tableHeader = Rs2CacheLoggingUtils.formatTableHeader(headers, columnWidths); - logContent.append("\n").append(tableHeader); - - // Sort skills by name for consistent ordering - for (Skill skill : Skill.values()) { - SkillData data = cache.get(skill); - if (data != null) { - String previousInfo = ""; - if (data.getPreviousLevel() != null || data.getPreviousExperience() != null) { - previousInfo = String.format("L%s E%s", - data.getPreviousLevel() != null ? data.getPreviousLevel() : "?", - data.getPreviousExperience() != null ? data.getPreviousExperience() : "?"); - } - - String[] values = { - skill.name(), - String.valueOf(data.getLevel()), - String.valueOf(data.getBoostedLevel()), - String.valueOf(data.getExperience()), - Rs2CacheLoggingUtils.truncate(previousInfo, 19), - Rs2CacheLoggingUtils.formatTimestamp(data.getLastUpdated()) - }; - - String row = Rs2CacheLoggingUtils.formatTableRow(values, columnWidths); - logContent.append(row); - } - } - - String tableFooter = Rs2CacheLoggingUtils.formatTableFooter(columnWidths); - logContent.append(tableFooter); - } - - String footer = "=== End Skill Cache State ==="; - logContent.append(footer).append("\n"); - - - Rs2CacheLoggingUtils.writeCacheLogFile("skill", logContent.toString(), true); - } - - - // ============================================ - // CacheSerializable Implementation - // ============================================ - - @Override - public String getConfigKey() { - return "skills"; - } - - @Override - public String getConfigGroup() { - return "microbot"; - } - - @Override - public boolean shouldPersist() { - return true; // Skills should always be persisted for progress tracking - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2SpiritTreeCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2SpiritTreeCache.java deleted file mode 100644 index 01319ed81b4..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2SpiritTreeCache.java +++ /dev/null @@ -1,688 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.GameState; -import net.runelite.api.Skill; -import net.runelite.api.GameState; -import net.runelite.api.Skill; -import net.runelite.api.coords.WorldArea; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.GameStateChanged; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.questhelper.helpers.mischelpers.farmruns.CropState; -import net.runelite.client.plugins.microbot.shortestpath.Transport; -import net.runelite.client.plugins.microbot.shortestpath.TransportType; -import net.runelite.client.plugins.microbot.util.cache.model.SpiritTreeData; -import net.runelite.client.plugins.microbot.util.cache.serialization.CacheSerializable; -import net.runelite.client.plugins.microbot.util.cache.strategy.farming.SpiritTreeUpdateStrategy; -import net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2CacheLoggingUtils; -import net.runelite.client.plugins.microbot.util.farming.SpiritTree; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -/** - * Thread-safe cache for spirit tree farming states and travel availability using the unified cache architecture. - * Automatically updates when WidgetLoaded, VarbitChanged, and GameObjectSpawned events are received and supports persistence. - * - * This cache tracks both built-in spirit trees (quest unlocked) and farmable spirit trees (player grown), - * storing comprehensive state information including crop states, travel availability, and detection context. - * - * The cache integrates with: - * - Spirit tree widget detection (Adventure Log - Spirit Tree Locations) - * - Farming varbit monitoring for spiritTree state changes - * - Game object spawning for real-time availability detection - * - Persistent storage for cross-session state preservation - */ -@Slf4j -public class Rs2SpiritTreeCache extends Rs2Cache implements CacheSerializable { - - private static Rs2SpiritTreeCache instance; - - // Cache configuration constants - private static final long SPIRIT_TREE_DATA_TTL = 30 * 60 * 1000L; // 30 minutes - private static final long STALE_DATA_THRESHOLD = 10 * 60 * 1000L; // 10 minutes - - /** - * Private constructor for singleton pattern. - */ - private Rs2SpiritTreeCache() { - super("SpiritTreeCache", CacheMode.EVENT_DRIVEN_ONLY); - this.withUpdateStrategy(new SpiritTreeUpdateStrategy()) - .withPersistence("spiritTrees"); - } - - /** - * Gets the singleton instance of Rs2SpiritTreeCache. - * - * @return The singleton spirit tree cache instance - */ - public static synchronized Rs2SpiritTreeCache getInstance() { - if (instance == null) { - instance = new Rs2SpiritTreeCache(); - } - return instance; - } - - /** - * Gets the cache instance for backward compatibility. - * - * @return The singleton cache instance - */ - public static Rs2Cache getCache() { - return getInstance(); - } - - // ============================================ - // Core Spirit Tree Cache Operations - // ============================================ - - /** - * Gets spirit tree data from the cache or initializes it with current state. - * - * @param spiritTree The spirit tree spiritTree to retrieve data for - * @return The SpiritTreeData containing state and availability information - */ - public static SpiritTreeData getSpiritTreeData(SpiritTree spiritTree) { - return getInstance().get(spiritTree, () -> { - try { - // Determine initial state based on spiritTree type - CropState cropState = CropState.HARVESTABLE; - boolean availableForTravel = spiritTree.hasQuestRequirements(); - - if (spiritTree.getType() == SpiritTree.SpiritTreeType.FARMABLE) { - cropState = spiritTree.getPatchState(); - availableForTravel &= spiritTree.isAvailableForTravel(); - } else if (spiritTree.getType() == SpiritTree.SpiritTreeType.POH) { - availableForTravel &= spiritTree.hasLevelRequirement(); - } - - log.debug("Initial spirit tree data for {}: \n\tcropState={}, available={}", - spiritTree.name(), cropState, availableForTravel); - - return new SpiritTreeData(spiritTree, cropState, availableForTravel); - - } catch (Exception e) { - log.error("Error loading initial spirit tree data for {}: {}", spiritTree.name(), e.getMessage(), e); - // Return default state - assume unavailable to be safe - return new SpiritTreeData(spiritTree, null, false); - } - }); - } - - /** - * Gets all available spirit tree locations for travel. - * These are the origins where spirit trees are available for use. - * - * @return Set of world points where spirit trees are available for travel - */ - public static Set getAvailableOrigins() { - return getInstance().stream() - .filter(SpiritTreeData::isAvailableForTravel) - .map(data -> data.getSpiritTree().getLocation()) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - } - - /** - * Gets all available spirit tree destinations for travel. - * This is an alias for getAvailableOrigins() since in spirit tree context, - * available origins can also serve as destinations. - * - * @return Set of world points where spirit trees are available as destinations - */ - public static Set getAvailableDestinations() { - return getAvailableOrigins(); - } - - /** - * Gets all spirit tree patches that are currently available for travel. - * - * @return List of available spirit tree patches - */ - public static List getAvailableSpiritTrees() { - return getInstance().stream() - .filter(SpiritTreeData::isAvailableForTravel) - .map(SpiritTreeData::getSpiritTree) - .collect(Collectors.toList()); - } - - /** - * Gets all farmable spirit tree patches and their current states. - * - * @return List of spirit tree data for farmable patches only - */ - public static List getFarmableTreeStates() { - return getInstance().stream() - .filter(data -> data.getSpiritTree().getType() == SpiritTree.SpiritTreeType.FARMABLE) - .collect(Collectors.toList()); - } - - /** - * Gets farmable patches that require attention (diseased, dead, or ready for harvest). - * - * @return List of patches requiring farming attention - */ - public static List getPatchesRequiringAttention() { - return getInstance().stream() - .filter(data -> data.getSpiritTree().getType() == SpiritTree.SpiritTreeType.FARMABLE) - .filter(data -> { - CropState state = data.getCropState(); - return state == CropState.DISEASED || - state == CropState.DEAD; - }) - .collect(Collectors.toList()); - } - - /** - * Gets patches that are ready for planting (empty). - * - * @return List of empty farmable patches - */ - public static List getEmptyPatches() { - return getInstance().stream() - .filter(data -> data.getSpiritTree().getType() == SpiritTree.SpiritTreeType.FARMABLE) - .filter(data -> data.getCropState() == CropState.EMPTY) - .collect(Collectors.toList()); - } - - /** - * Checks if a spirit tree is available for travel at the given origin location. - * Uses WorldArea for robust location matching around both the spirit tree spiritTree - * and the query location to handle slight coordinate variations. - * - * @param origin The world point to check (where a spirit tree should be standing) - * @return true if a spirit tree is available at this location - */ - public static boolean isOriginAvailable(WorldPoint origin) { - if (origin == null) { - return false; - } - - // Create a search area around the query point for robust matching - WorldArea queryArea = new WorldArea(origin, 3, 3); // 3x3 area around query point - - return getInstance().stream() - .filter(SpiritTreeData::isAvailableForTravel) - .map(data -> data.getSpiritTree().getLocation()) - .filter(Objects::nonNull) - .anyMatch(location -> { - // Create an area around each spirit tree location (accounting for multi-tile objects) - WorldArea spiritTreeArea = new WorldArea(location, 3, 3); // 3x3 area around spirit tree - // Check if the areas intersect (on same plane) - return queryArea.intersectsWith2D(spiritTreeArea) && - queryArea.getPlane() == spiritTreeArea.getPlane(); - }); - } - - /** - * Checks if a spirit tree is available as a destination for travel. - * This is an alias for isOriginAvailable() since any available origin can serve as a destination. - * - * @param destination The world point to check (where you want to travel TO) - * @return true if a spirit tree is available at this destination - */ - public static boolean isDestinationAvailable(WorldPoint destination) { - return isOriginAvailable(destination); - } - - /** - * Checks if a spirit tree transport is available for pathfinding. - * This method is specifically designed for pathfinder integration. - * Validates that the transport is of type SPIRIT_TREE and that both the origin and destination are available. - * - * @param transport The transport object to check - * @return true if the transport is a valid spirit tree and both ends are available for travel - */ - public static boolean isSpiritTreeTransportAvailable(Transport transport) { - if (transport == null) { - return false; - } - - if (transport.getType() != TransportType.SPIRIT_TREE) { - log.warn("Transport type {} is not SPIRIT_TREE, cannot check availability", transport.getType()); - return false; - } - - return isOriginAvailable(transport.getOrigin()) & isDestinationAvailable(transport.getDestination()); - } - - /** - * Gets the closest available spirit tree to a specific location. - * - * @param fromLocation The location to measure distance from - * @return Optional containing the closest available spirit tree data - */ - public static Optional getClosestAvailableTree(WorldPoint fromLocation) { - if (fromLocation == null) { - return Optional.empty(); - } - - return getInstance().stream() - .filter(data -> data.isAvailableForTravel()) - .filter(data -> data.getSpiritTree().getLocation() != null) - .min((data1, data2) -> { - int dist1 = data1.getSpiritTree().getLocation().distanceTo(fromLocation); - int dist2 = data2.getSpiritTree().getLocation().distanceTo(fromLocation); - return Integer.compare(dist1, dist2); - }); - } - - // ============================================ - // Static Update Methods - // ============================================ - - /** - * Static update method that loads and updates spirit tree cache based on FarmingHandler predictions. - * This method checks all spirit tree patches using FarmingHandler to get the most up-to-date information - * and updates the cache accordingly. It handles both initial cache population and updates based on - * current cached state, ensuring the most accurate information is preserved. - * - * This method should be called during cache initialization and when fresh farming data is needed. - * It uses the same logic as dynamic updates but provides static access for initial loading. - */ - @Override - public void update() { - try { - log.debug("Starting static update from FarmingHandler for spirit tree cache"); - - Rs2SpiritTreeCache cache = getInstance(); - - // Get current player context - WorldPoint playerLocation = getPlayerLocationSafely(); - Integer farmingLevel = getFarmingLevelSafely(); - - // Initialize update counters - int updatedCount = 0; - int newEntriesCount = 0; - int preservedCount = 0; - - // Process all spirit tree patches - for (SpiritTree spiritTree : SpiritTree.values()) { - try { - // Get existing cached data - SpiritTreeData existingData = getSpiritTreeData(spiritTree); - // Determine update strategy based on spiritTree type and existing data - SpiritTreeData updatedData = createUpdatedSpiritTreeData( - spiritTree, existingData, playerLocation, farmingLevel); - - // Update cache if we have new or updated data - if (updatedData != null) { - // Check if this is new data or an update - if (existingData == null) { - newEntriesCount++; - log.debug("Added new spirit tree data for {}: {}", - spiritTree.name(), getDataSummary(updatedData)); - } else if (!isDataEquivalent(existingData, updatedData)) { - updatedCount++; - log.debug("Updated spirit tree data for {}: {} -> {}", - spiritTree.name(), getDataSummary(existingData), getDataSummary(updatedData)); - } else { - preservedCount++; - log.debug("Preserved existing spirit tree data for {}: {}", - spiritTree.name(), getDataSummary(existingData)); - continue; // Skip update if data is equivalent - } - - cache.put(spiritTree, updatedData); - } else if (existingData != null) { - // Keep existing data if no new information is available - preservedCount++; - log.trace("Preserved existing spirit tree data for {} (no new information): {}", - spiritTree.name(), getDataSummary(existingData)); - } - - } catch (Exception e) { - log.warn("Failed to update spirit tree data for spiritTree {}: {}", spiritTree.name(), e.getMessage()); - } - } - List farambleTrees = getFarmableTreeStates(); - int availabilityFarmableTrees = (int) farambleTrees.stream() - .filter(SpiritTreeData::isAvailableForTravel) - .count(); - log.debug(getFarmingStatusSummary()); - log.debug("Static spirit tree cache update completed: \n\t{} new entries, {} updated, {} preserved entries, {} farmable trees (available for travel: {})", - newEntriesCount, updatedCount, preservedCount, farambleTrees.size(), availabilityFarmableTrees); - - } catch (Exception e) { - log.error("Failed to update spirit tree cache from FarmingHandler: {}", e.getMessage(), e); - } - } - - /** - * Creates updated spirit tree data based on current farming information and existing cache data. - * This method combines FarmingHandler predictions with existing cache data, - * preserving the most recent and accurate information. - */ - private static SpiritTreeData createUpdatedSpiritTreeData(SpiritTree spiritTree, - SpiritTreeData existingData, - WorldPoint playerLocation, - Integer farmingLevel) { - try { - // For built-in trees, check accessibility based on quest requirements - if (spiritTree.getType() == SpiritTree.SpiritTreeType.BUILT_IN) { - boolean accessible = spiritTree.hasQuestRequirements(); - // If we have existing data and it's recent, preserve travel availability info - if (existingData == null || existingData.isAvailableForTravel() != accessible) { - - // Update with quest accessibility but preserve recent travel information - return new SpiritTreeData( - spiritTree, - null, // Built-in trees don't have crop states - accessible, // Must be both accessible and available - playerLocation, - false, // Not detected via widget in static update - false // Not detected via game object in static update - ); - } - } - // For farmable trees, use FarmingHandler to predict state - else if (spiritTree.getType() == SpiritTree.SpiritTreeType.FARMABLE ) { - CropState predictedState = spiritTree.getPatchState(); // Uses Rs2Farming.predictPatchState internally - CropState lastCropState = existingData != null ? existingData.getCropState() : null; - if (predictedState != null && !predictedState.equals(lastCropState)) { - // Determine travel availability based on crop state - boolean detectedViaWidget = existingData != null && existingData.isDetectedViaWidget(); - boolean detectedViaNearPatch = existingData != null && existingData.isDetectedViaNearBy(); - boolean availableForTravelLast = existingData != null && existingData.isAvailableForTravel(); - boolean availableForTravel = spiritTree.isAvailableForTravel(); - if ((availableForTravel != availableForTravelLast) && - (lastCropState!=null && (lastCropState == CropState.UNCHECKED || lastCropState == CropState.GROWING))) { - log.info("Spirit tree {} is now available, last available for travel was false, and tree was predicted updating to true", spiritTree.name()); - // Use the more specific information: dynamic detection for travel, farming handler for crop state - return new SpiritTreeData( - spiritTree, - predictedState, // Always update with latest farming prediction - availableForTravel && farmingLevel >= 83, // Preserve recent dynamic travel detection - playerLocation, - false, - false - ); - - } else { - log.info("Spirit tree {} not updated, farm state: {}, available for travel last: {}, detected via widget: {}, detected via near patch: {}, last farming state: {}", - spiritTree.name(), predictedState, availableForTravelLast, detectedViaWidget, detectedViaNearPatch, lastCropState); - - } - - - } else { - // If FarmingHandler can't predict the state, preserve existing data if available - if (existingData != null) { - log.trace("No farming prediction available for {}, preserving existing data", spiritTree.name()); - return existingData; // Return existing data unchanged - } else { - log.trace("No farming prediction or existing data for {}, skipping", spiritTree.name()); - return null; // No data to work with - } - } - } - - return null; // Unknown spiritTree type or no data available - - } catch (Exception e) { - log.warn("Failed to create updated spirit tree data for {}: {}", spiritTree.name(), e.getMessage()); - return existingData; // Fallback to existing data on error - } - } - - /** - * Checks if two SpiritTreeData objects are equivalent for update purposes. - * This avoids unnecessary cache updates when data hasn't meaningfully changed. - */ - private static boolean isDataEquivalent(SpiritTreeData data1, SpiritTreeData data2) { - if (data1 == null || data2 == null) { - return data1 == data2; - } - - return data1.getSpiritTree().equals(data2.getSpiritTree()) && - java.util.Objects.equals(data1.getCropState(), data2.getCropState()) && - data1.isAvailableForTravel() == data2.isAvailableForTravel(); - } - - /** - * Creates a summary string for spirit tree data for logging purposes. - */ - private static String getDataSummary(SpiritTreeData data) { - if (data == null) { - return "null"; - } - - return String.format("[%s|%s|travel=%s]", - data.getSpiritTree().name(), - data.getCropState() != null ? data.getCropState().name() : "N/A", - data.isAvailableForTravel()); - } /** - * Forces a refresh of farmable spirit tree states only. - */ - public static void refreshFarmableStates() { - log.debug("Refreshing farmable spirit tree states"); - Rs2SpiritTreeCache.getInstance().update(); - } - - // ============================================ - // Cache Statistics and Monitoring - // ============================================ - - /** - * Gets statistics about the spirit tree cache for debugging. - * - * @return Formatted statistics string - */ - public static String getCacheStatistics() { - Rs2SpiritTreeCache cache = getInstance(); - - long totalEntries = cache.size(); - long availableForTravel = cache.stream().filter(SpiritTreeData::isAvailableForTravel).count(); - long farmableEntries = cache.stream() - .filter(data -> data.getSpiritTree().getType() == SpiritTree.SpiritTreeType.FARMABLE) - .count(); - long staleEntries = cache.stream() - .filter(data -> data.isStale(STALE_DATA_THRESHOLD)) - .count(); - - return String.format( - "SpiritTreeCache Stats: Total=%d, Available=%d, Farmable=%d, Stale=%d", - totalEntries, availableForTravel, farmableEntries, staleEntries - ); - } - - /** - * Logs the current state of all cached spirit trees for debugging. - */ - public static void logState(LogOutputMode mode) { - StringBuilder logContent = new StringBuilder(); - logContent.append("=== Spirit Tree Cache States ===\n"); - logContent.append(String.format("%-20s %-12s %-12s %-10s %-10s %-8s\n", - "Name", "Type", "CropState", "Available", "Updated", "Via")); - - getInstance().stream() - .sorted(Comparator.comparing(data -> data.getSpiritTree().name())) - .forEach(data -> { - String spiritTreeType = data.getSpiritTree().getType().name(); - String cropState = data.getCropState() != null ? data.getCropState().name() : "N/A"; - String lastUpdated = Instant.ofEpochMilli(data.getLastUpdated()) - .atZone(ZoneId.systemDefault()) - .format(DateTimeFormatter.ofPattern("HH:mm:ss")); - String detection = data.isDetectedViaWidget() ? "WIDGET" : - (data.isDetectedViaNearBy() ? "NEARBY" : "INIT"); - - logContent.append(String.format("%-20s %-12s %-12s %-10s %-10s %-8s\n", - data.getSpiritTree().name(), - spiritTreeType, - cropState, - data.isAvailableForTravel(), - lastUpdated, - detection - )); - }); - - logContent.append("=== End Spirit Tree Cache States ===\n"); - - Rs2CacheLoggingUtils.outputCacheLog( - "spirit_tree", - logContent.toString(), - mode - ); - } - - // ============================================ - // CacheSerializable Implementation - // ============================================ - - @Override - public String getConfigKey() { - return "spiritTrees"; - } - - @Override - public boolean shouldPersist() { - return true; - } - - // ============================================ - // Event Handling - // ============================================ - - - // ============================================ - // Event Handling Delegation to Update Strategy - // ============================================ - - - - /** - * Handle WidgetLoaded event and delegate to update strategy. - */ - @Subscribe - public void onWidgetLoaded(net.runelite.api.events.WidgetLoaded event) { - getInstance().handleEvent(event); - } - - /** - * Handle VarbitChanged event and delegate to update strategy. - */ - @Subscribe - public void onVarbitChanged(net.runelite.api.events.VarbitChanged event) { - getInstance().handleEvent(event); - } - - /** - * Handle GameObjectSpawned event and delegate to update strategy. - */ - @Subscribe - public void onGameObjectSpawned(net.runelite.api.events.GameObjectSpawned event) { - getInstance().handleEvent(event); - } - - /** - * Handle GameObjectSpawned event and delegate to update strategy. - */ - @Subscribe - public void onGameObjectDespawned(net.runelite.api.events.GameObjectDespawned event) { - getInstance().handleEvent(event); - } - - /** - * Handle game state changes for cache lifecycle management (unchanged). - */ - @Subscribe - public void onGameStateChanged(GameStateChanged event) { - getInstance().handleEvent(event); - } - - - // ============================================ - // Utility Methods - // ============================================ - - /** - * Validates that the spirit tree cache is properly initialized and functional. - * - * @return true if the cache is ready for use - */ - public static boolean isInitialized() { - try { - return instance != null; - } catch (Exception e) { - log.error("Error checking spirit tree cache initialization: {}", e.getMessage()); - return false; - } - } - - /** - * Gets a summary of spirit tree farming status for user display. - * - * @return Formatted farming status summary - */ - public static String getFarmingStatusSummary() { - List farmableStates = getFarmableTreeStates(); - - if (farmableStates.isEmpty()) { - return "No farmable spirit tree data available"; - } - - long planted = farmableStates.stream() - .filter(data -> data.getCropState() != CropState.EMPTY) - .count(); - long grown = farmableStates.stream() - .filter(data -> data.getCropState() == CropState.HARVESTABLE || - data.getCropState() == CropState.UNCHECKED) - .count(); - long readyForHarvest = farmableStates.stream() - .filter(data -> data.getCropState() == CropState.HARVESTABLE) - .count(); - long needsAttention = farmableStates.stream() - .filter(data -> data.getCropState() == CropState.DISEASED || - data.getCropState() == CropState.DEAD) - .count(); - - return String.format("Spirit Trees: %d/%d planted, %d grown (%d harvest ready), %d need attention", - planted, farmableStates.size(), grown, readyForHarvest, needsAttention); - } - - // ============================================ - // Private Utility Methods - // ============================================ - - /** - * Get current player location safely - */ - private static WorldPoint getPlayerLocationSafely() { - try { - if (Microbot.getClient() != null && - Microbot.getClient().getGameState() == GameState.LOGGED_IN && - Microbot.getClient().getLocalPlayer() != null) { - return Rs2Player.getWorldLocation(); - } - } catch (Exception e) { - log.trace("Could not get player location: {}", e.getMessage()); - } - return null; - } - - /** - * Get current farming level safely - */ - private static Integer getFarmingLevelSafely() { - try { - if (Microbot.getClient() != null && - Microbot.getClient().getGameState() == GameState.LOGGED_IN) { - return Rs2Player.getRealSkillLevel(Skill.FARMING); - } - } catch (Exception e) { - log.trace("Could not get farming level: {}", e.getMessage()); - } - return null; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2VarPlayerCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2VarPlayerCache.java deleted file mode 100644 index 8a63c5d0351..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2VarPlayerCache.java +++ /dev/null @@ -1,423 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.VarbitChanged; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.model.VarbitData; -import net.runelite.client.plugins.microbot.util.cache.serialization.CacheSerializable; -import net.runelite.client.plugins.microbot.util.cache.strategy.simple.VarPlayerUpdateStrategy; -import net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2CacheLoggingUtils; - -import java.util.Map; -import java.util.Optional; -import java.util.stream.Stream; - -/** - * Cache for varplayer (varp) values with enhanced tracking and contextual information. - * Provides thread-safe access to varplayer data with temporal tracking, change history, - * and contextual information about when and where values changed. - * - * Reuses VarbitData model since varplayer and varbit data structures are identical. - */ -@Slf4j -public class Rs2VarPlayerCache extends Rs2Cache implements CacheSerializable { - - private static volatile Rs2VarPlayerCache instance; - - /** - * Private constructor for singleton pattern. - */ - private Rs2VarPlayerCache() { - super("VarPlayerCache", CacheMode.EVENT_DRIVEN_ONLY); - - // Set up update strategy to handle VarbitChanged events for varplayer values - this.withUpdateStrategy(new VarPlayerUpdateStrategy()) - .withPersistence("varplayers"); - - log.debug("Rs2VarPlayerCache initialized with EVENT_DRIVEN_ONLY mode"); - } - - /** - * Gets the singleton instance of Rs2VarPlayerCache. - * - * @return The singleton instance - */ - public static Rs2VarPlayerCache getInstance() { - if (instance == null) { - synchronized (Rs2VarPlayerCache.class) { - if (instance == null) { - instance = new Rs2VarPlayerCache(); - } - } - } - return instance; - } - - /** - * Gets the cache instance for backward compatibility. - * - * @return The singleton cache instance - */ - public static Rs2Cache getCache() { - return getInstance(); - } - - /** - * Loads varplayer data from the client for a specific varp ID. - * - * @param varpId The varp ID to load data for - * @return The VarbitData containing the current value - */ - private static VarbitData loadVarPlayerDataFromClient(int varpId) { - try { - if (Microbot.getClient() == null) { - log.warn("Client is null when loading varp {}", varpId); - return new VarbitData(0); - } - - int value = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getClient().getVarpValue(varpId)).orElse(0); - - log.debug("Loaded varp from client: {} = {}", varpId, value); - return new VarbitData(value); - } catch (Exception e) { - log.error("Error loading varp {}: {}", varpId, e.getMessage(), e); - return new VarbitData(0); - } - } - - /** - * Gets the current value of a varplayer. - * If not cached, retrieves from client and caches the result. - * - * @param varpId The varplayer ID - * @return The current varplayer value, or 0 if not found - */ - public static int getVarPlayerValue(int varpId) { - try { - VarbitData cached = getInstance().get(varpId); - if (cached != null) { - return cached.getValue(); - } - - // Not cached, get from client and cache it - VarbitData freshData = loadVarPlayerDataFromClient(varpId); - getInstance().put(varpId, freshData); - return freshData.getValue(); - - } catch (Exception e) { - log.warn("Failed to get varplayer value for {}: {}", varpId, e.getMessage()); - return 0; - } - } - - /** - * Gets varplayer data with full contextual information. - * - * @param varpId The varplayer ID - * @return Optional containing the VarbitData, or empty if not found - */ - public static Optional getVarPlayerData(int varpId) { - return Optional.ofNullable(getInstance().get(varpId)); - } - - /** - * Gets varplayers that have changed within the specified number of ticks. - * - * @param ticks Number of ticks to look back - * @return Stream of varplayer data that changed recently - */ - public static Stream> getRecentlyChangedVarPlayers(int ticks) { - long cutoffTime = System.currentTimeMillis() - (ticks * 600); // 600ms per tick - return getInstance().entryStream() - .filter(entry -> entry.getValue().getLastUpdated() >= cutoffTime) - .filter(entry -> entry.getValue().hasValueChanged()); - } - - /** - * Gets varplayers with a specific value. - * - * @param value The value to search for - * @return Stream of varplayer IDs that have the specified value - */ - public static Stream getVarPlayersWithValue(int value) { - return getInstance().entryStream() - .filter(entry -> entry.getValue().getValue() == value) - .map(Map.Entry::getKey); - } - - /** - * Gets varplayers that changed at a specific location. - * - * @param x World X coordinate - * @param y World Y coordinate - * @param plane The plane - * @return Stream of varplayer data that changed at the specified location - */ - public static Stream> getVarPlayersChangedAt(int x, int y, int plane) { - return getInstance().entryStream() - .filter(entry -> { - VarbitData data = entry.getValue(); - return data.getPlayerLocation() != null && - data.getPlayerLocation().getX() == x && - data.getPlayerLocation().getY() == y && - data.getPlayerLocation().getPlane() == plane; - }); - } - - /** - * Checks if a varplayer has a specific value. - * - * @param varpId The varplayer ID - * @param value The value to check - * @return true if the varplayer has the specified value - */ - public static boolean hasValue(int varpId, int value) { - return getVarPlayerValue(varpId) == value; - } - - /** - * Checks if a varplayer has changed recently. - * - * @param varpId The varplayer ID - * @param ticks Number of ticks to look back - * @return true if the varplayer changed within the specified time - */ - public static boolean hasChangedRecently(int varpId, int ticks) { - VarbitData data = getInstance().get(varpId); - if (data == null) return false; - - long cutoffTime = System.currentTimeMillis() - (ticks * 600); - return data.getLastUpdated() >= cutoffTime && data.hasValueChanged(); - } - - /** - * Gets the previous value of a varplayer if available. - * - * @param varpId The varplayer ID - * @return The previous value, or null if not available - */ - public static Integer getPreviousValue(int varpId) { - VarbitData data = getInstance().get(varpId); - return data != null ? data.getPreviousValue() : null; - } - - /** - * Updates all cached varplayers by retrieving fresh data from the game client. - * This method iterates over currently cached varplayer IDs and refreshes their values. - */ - public static void updateAllFromClient() { - getInstance().update(); - } - - /** - * Updates all cached data by retrieving fresh values from the game client. - * Implements the abstract method from Rs2Cache. - * - * Iterates over all currently cached varplayer keys and refreshes their values from the client. - */ - @Override - public void update() { - log.debug("Updating all cached varplayers from client..."); - - if (Microbot.getClient() == null) { - log.warn("Cannot update varplayers - client is null"); - return; - } - - int beforeSize = size(); - int updatedCount = 0; - - // Get all currently cached varplayer IDs (keys) and update them - java.util.Set cachedVarpIds = entryStream() - .map(java.util.Map.Entry::getKey) - .collect(java.util.stream.Collectors.toSet()); - - for (Integer varpId : cachedVarpIds) { - try { - // Refresh the data from client using the private method - VarbitData freshData = loadVarPlayerDataFromClient(varpId); - if (freshData != null) { - put(varpId, freshData); - updatedCount++; - log.debug("Updated varp {} with fresh value: {}", varpId, freshData.getValue()); - } - } catch (Exception e) { - log.warn("Failed to update varp {}: {}", varpId, e.getMessage()); - } - } - - log.info("Updated {} varplayers from client (cache had {} entries total)", - updatedCount, beforeSize); - } - - // CacheSerializable implementation - @Override - public String getConfigKey() { - return "varPlayerCache"; - } - - @Override - public boolean shouldPersist() { - // Varplayer values can be persisted as they represent player state - return true; - } - - /** - * Clears all cached varplayer data. - * Useful for testing or when switching profiles. - */ - public static void clearCache() { - getInstance().invalidateAll(); - log.debug("VarPlayer cache cleared"); - } - - /** - * Gets cache statistics. - * - * @return String containing cache statistics - */ - public static String getCacheStats() { - Rs2VarPlayerCache cache = getInstance(); - return String.format("VarPlayerCache - Size: %d", cache.size()); - } - - // ============================================ - // EventBus Integration - // ============================================ - - /** - * Handles VarbitChanged events specifically for varplayer (varp) changes. - * Filters out varbit events and only processes varplayer events. - */ - @Subscribe - public void onVarbitChanged(VarbitChanged event) { - try { - // This is a varplayer event, handle it - getInstance().handleEvent(event); - // Ignore varbit events (handled by Rs2VarbitCache) - } catch (Exception e) { - log.error("Error handling VarbitChanged event for varplayer: {}", e.getMessage(), e); - } - } - - /** - * Handles GameStateChanged events for cache management. - */ - @Subscribe - public void onGameStateChanged(GameStateChanged event) { - try { - switch (event.getGameState()) { - case LOGGED_IN: - case HOPPING: - case LOGIN_SCREEN: - case CONNECTION_LOST: - // Let the strategy handle cache invalidation if needed - break; - default: - break; - } - } catch (Exception e) { - log.error("Error handling GameStateChanged event: {}", e.getMessage(), e); - } - } - - /** - * Logs the current state of all cached varplayers for debugging. - * - * @param dumpToFile Whether to also dump the information to a file - */ - public static void logState(LogOutputMode mode) { - var cache = getInstance(); - var stats = cache.getStatistics(); - - // Create the log content - StringBuilder logContent = new StringBuilder(); - - String header = String.format("=== VarPlayer Cache State (%d entries) ===", cache.size()); - logContent.append(header).append("\n"); - - String statsInfo = Rs2CacheLoggingUtils.formatCacheStatistics( - stats.getHitRate(), stats.cacheHits, stats.cacheMisses, stats.cacheMode.toString()); - logContent.append(statsInfo).append("\n\n"); - - if (cache.size() == 0) { - String emptyMsg = "Cache is empty"; - logContent.append(emptyMsg).append("\n"); - } else { - final int MAXNAME_LENGTH = 45; // Maximum length for names - // Table format for varplayers with VarPlayerID names where available - String[] headers = {"VarPlayer ID", "Name", "Value", "Previous", "Changed", "Last Updated", "Cache Timestamp"}; - int[] columnWidths = {12, MAXNAME_LENGTH, 8, 8, 8, 30, 22}; - - String tableHeader = Rs2CacheLoggingUtils.formatTableHeader(headers, columnWidths); - - logContent.append("\n").append(tableHeader); - - // Sort varplayers by recent changes (most recent first) - cache.entryStream() - .sorted((a, b) -> { - try { - // Handle null entries - if (a == null && b == null) return 0; - if (a == null) return 1; - if (b == null) return -1; - if (a.getValue() == null && b.getValue() == null) return 0; - if (a.getValue() == null) return 1; - if (b.getValue() == null) return -1; - - return Long.compare(b.getValue().getLastUpdated(), a.getValue().getLastUpdated()); - } catch (Exception e) { - // Fallback to key comparison if anything goes wrong - try { - return Integer.compare( - a != null && a.getKey() != null ? a.getKey() : Integer.MAX_VALUE, - b != null && b.getKey() != null ? b.getKey() : Integer.MAX_VALUE - ); - } catch (Exception e2) { - return 0; // Last resort - consider equal - } - } - }) - .forEach(entry -> { - Integer varPlayerId = entry.getKey(); - VarbitData varPlayerData = entry.getValue(); - - // Get cache timestamp for this varPlayer - Long cacheTimestamp = cache.getCacheTimestamp(varPlayerId); - String cacheTimestampStr = cacheTimestamp != null ? - Rs2Cache.formatUtcTimestamp(cacheTimestamp) : "N/A"; - - String varPlayerName = Rs2CacheLoggingUtils.getVarPlayerFieldName(varPlayerId); - String[] values = { - String.valueOf(varPlayerId), - Rs2CacheLoggingUtils.truncate(varPlayerName, MAXNAME_LENGTH), - String.valueOf(varPlayerData.getValue()), - varPlayerData.getPreviousValue() != null ? String.valueOf(varPlayerData.getPreviousValue()) : "null", - varPlayerData.hasValueChanged() ? "Yes" : "No", - Rs2CacheLoggingUtils.formatTimestamp(varPlayerData.getLastUpdated()), - Rs2CacheLoggingUtils.truncate(cacheTimestampStr, 21) - }; - - String row = Rs2CacheLoggingUtils.formatTableRow(values, columnWidths); - logContent.append(row); - }); - - String tableFooter = Rs2CacheLoggingUtils.formatTableFooter(columnWidths); - logContent.append(tableFooter); - - String limitMsg = Rs2CacheLoggingUtils.formatLimitMessage(cache.size(), 50); - if (!limitMsg.isEmpty()) { - logContent.append(limitMsg).append("\n"); - } - } - - String footer = "=== End VarPlayer Cache State ==="; - logContent.append(footer).append("\n"); - Rs2CacheLoggingUtils.outputCacheLog(getInstance().getCacheName(), logContent.toString(), mode); - - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2VarbitCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2VarbitCache.java deleted file mode 100644 index 80da75e25de..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/Rs2VarbitCache.java +++ /dev/null @@ -1,507 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.VarbitChanged; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.strategy.simple.VarbitUpdateStrategy; -import net.runelite.client.plugins.microbot.util.cache.serialization.CacheSerializable; -import net.runelite.client.plugins.microbot.util.cache.model.VarbitData; -import net.runelite.client.plugins.microbot.util.cache.util.LogOutputMode; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2CacheLoggingUtils; - -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; - -/** - * Thread-safe cache for varbit values using the unified cache architecture. - * Automatically updates when VarbitChanged events are received and supports persistence. - * Stores VarbitData with contextual information about when and where changes occurred. - * - * This class extends Rs2UnifiedCache and provides specific varbit caching functionality - * with proper EventBus integration for @Subscribe methods. - */ -@Slf4j -public class Rs2VarbitCache extends Rs2Cache implements CacheSerializable { - - private static Rs2VarbitCache instance; - - /** - * Private constructor for singleton pattern. - */ - private Rs2VarbitCache() { - super("VarbitCache", CacheMode.EVENT_DRIVEN_ONLY); - this.withUpdateStrategy(new VarbitUpdateStrategy()) - .withPersistence("varbits"); - } - - /** - * Gets the singleton instance of Rs2VarbitCache. - * - * @return The singleton varbit cache instance - */ - public static synchronized Rs2VarbitCache getInstance() { - if (instance == null) { - instance = new Rs2VarbitCache(); - } - return instance; - } - - /** - * Gets the cache instance for backward compatibility. - * - * @return The singleton unified cache instance - */ - public static Rs2Cache getCache() { - return getInstance(); - } - - public synchronized void close() { - if (instance != null) { - instance = null; - } - } - - /** - * Gets a varbit value from the cache or loads it from the client. - * - * @param varbitId The varbit ID to retrieve - * @return The varbit value - */ - public static int getVarbitValue(int varbitId) { - VarbitData data = getVarbitData(varbitId); - return data != null ? data.getValue() : 0; - } - - /** - * Loads varbit data from the client for a specific varbit ID. - * - * @param varbitId The varbit ID to load data for - * @return The VarbitData containing the current value - */ - private static VarbitData loadVarbitDataFromClient(int varbitId) { - try { - // Additional safety check - if (Microbot.getClient() == null) { - log.warn("Client is null when loading varbit {}", varbitId); - return new VarbitData(0); - } - - int value = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getClient().getVarbitValue(varbitId)).orElse(-1); - log.debug("Loaded varbit from client: {} = {}", varbitId, value); - return new VarbitData(value); - } catch (Exception e) { - log.error("Error loading varbit {}: {}", varbitId, e.getMessage(), e); - return new VarbitData(-1); - } - } - - /** - * Gets varbit data from the cache or loads it from the client. - * - * @param varbitId The varbit ID to retrieve - * @return The VarbitData containing value and contextual information - */ - public static VarbitData getVarbitData(int varbitId) { - // Validate input - if (varbitId < 0) { - log.warn("Invalid varbit ID: {}", varbitId); - return new VarbitData(0); - } - VarbitData cachedValue = getInstance().get(varbitId, () -> loadVarbitDataFromClient(varbitId)); - if (cachedValue == null) { - log.warn("Varbit {} not found in cache, returning default value", varbitId); - return new VarbitData(-1); - } - return cachedValue; - } - - - - - /** - * Manually updates a varbit value in the cache. - * - * @param varbitId The varbit ID to update - * @param value The new value - */ - public static void updateVarbitValue(int varbitId, int value) { - VarbitData oldData = getInstance().get(Integer.valueOf(varbitId)); - VarbitData newData = oldData != null ? - oldData.withUpdate(value, null, null, null) : - new VarbitData(value); - - getInstance().put(varbitId, newData); - log.debug("Updated varbit cache: {} = {}", varbitId, value); - } - - /** - * Manually updates varbit data in the cache. - * - * @param varbitId The varbit ID to update - * @param varbitData The new varbit data - */ - public static void updateVarbitData(int varbitId, VarbitData varbitData) { - getInstance().put(varbitId, varbitData); - log.debug("Updated varbit cache data: {} = {}", varbitId, varbitData.getValue()); - } - - /** - * Invalidates all varbit cache entries. - */ - public static void invalidateAllVarbits() { - getInstance().invalidateAll(); - log.debug("Invalidated all varbit cache entries"); - } - - /** - * Invalidates a specific varbit cache entry. - * - * @param varbitId The varbit ID to invalidate - */ - public static void invalidateVarbit(int varbitId) { - getInstance().remove(varbitId); - log.debug("Invalidated varbit cache entry: {}", varbitId); - } - - /** - * Updates all cached varbits by retrieving fresh data from the game client. - * This method iterates over all currently cached varbits and refreshes their data. - * Since varbits can have any ID, this only updates currently cached entries. - */ - public static void updateAllFromClient() { - getInstance().update(); - } - - /** - * Updates all cached data by retrieving fresh values from the game client. - * Implements the abstract method from Rs2Cache. - * - * Iterates over all currently cached varbit keys and refreshes their values from the client. - */ - @Override - public void update() { - log.debug("Updating all cached varbits from client..."); - - if (Microbot.getClient() == null) { - log.warn("Cannot update varbits - client is null"); - return; - } - - int beforeSize = size(); - int updatedCount = 0; - - // Get all currently cached varbit IDs (keys) and update them - java.util.Set cachedVarbitIds = entryStream() - .map(java.util.Map.Entry::getKey) - .collect(java.util.stream.Collectors.toSet()); - - for (Integer varbitId : cachedVarbitIds) { - try { - // Refresh the data from client using the private method - VarbitData freshData = loadVarbitDataFromClient(varbitId); - if (freshData != null) { - put(varbitId, freshData); - updatedCount++; - log.debug("Updated varbit {} with fresh value: {}", varbitId, freshData.getValue()); - } - } catch (Exception e) { - log.warn("Failed to update varbit {}: {}", varbitId, e.getMessage()); - } - } - - log.info("Updated {} varbits from client (cache had {} entries total)", - updatedCount, beforeSize); - } - - // ============================================ - // Legacy API Compatibility Methods - // ============================================ - - /** - * Gets varbit value - Legacy compatibility method (already available as getVarbitValue). - * - * @param varbitId The varbit ID - * @return The varbit value - */ - public static int get(int varbitId) { - return getVarbitValue(varbitId); - } - - - - /** - * Event handler registration for the unified cache. - * The unified cache handles events through its strategy automatically. - */ - - - @Subscribe - public void onVarbitChanged(VarbitChanged event) { - try { - getInstance().handleEvent(event); - } catch (Exception e) { - log.error("Error handling VarbitChanged event: {}", e.getMessage(), e); - } - } - @Subscribe - public void onGameStateChanged(GameStateChanged event) { - try { - switch (event.getGameState()) { - case LOGGED_IN: - case HOPPING: - case LOGIN_SCREEN: - case CONNECTION_LOST: - // Let the strategy handle cache invalidation - break; - default: - break; - } - } catch (Exception e) { - log.error("Error handling GameStateChanged event: {}", e.getMessage(), e); - } - } - - - /** - * Resets the singleton instance. Used for testing. - */ - public static synchronized void resetInstance() { - if (instance != null) { - invalidateAllVarbits(); - instance = null; - } - } - - // ============================================ - // CacheSerializable Implementation - // ============================================ - - @Override - public String getConfigKey() { - return "varbits"; - } - - @Override - public String getConfigGroup() { - return "microbot"; - } - - @Override - public boolean shouldPersist() { - return true; // Varbits should be persisted for game state tracking - } - - // ============================================ - // Print Functions for Cache Information - // ============================================ - - /** - * Returns a detailed formatted string containing all varbit cache information. - * Includes complete varbit data with contextual tracking and change information. - * - * @return Detailed multi-line string representation of all cached varbits - */ - public static String printDetailedVarbitInfo() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") - .withZone(ZoneId.systemDefault()); - - sb.append("=".repeat(80)).append("\n"); - sb.append(" DETAILED VARBIT CACHE INFORMATION\n"); - sb.append("=".repeat(80)).append("\n"); - - // Cache metadata - Rs2Cache cache = getInstance(); - sb.append(String.format("Cache Name: %s\n", cache.getCacheName())); - sb.append(String.format("Cache Mode: %s\n", cache.getCacheMode())); - sb.append(String.format("Total Cached Varbits: %d\n", cache.size())); - - // Cache statistics - var stats = cache.getStatistics(); - sb.append(String.format("Cache Hits: %d\n", stats.cacheHits)); - sb.append(String.format("Cache Misses: %d\n", stats.cacheMisses)); - sb.append(String.format("Hit Ratio: %.2f%%\n", stats.getHitRate() * 100)); - sb.append(String.format("Total Invalidations: %d\n", stats.totalInvalidations)); - sb.append(String.format("Uptime: %d ms\n", stats.uptime)); - sb.append(String.format("TTL: %d ms\n", stats.ttlMillis)); - sb.append("\n"); - - sb.append("-".repeat(80)).append("\n"); - sb.append(" VARBIT DETAILS\n"); - sb.append("-".repeat(80)).append("\n"); - - // Headers - sb.append(String.format("%-10s %-8s %-13s %-15s %-25s %-19s\n", - "VARBIT ID", "VALUE", "PREV VALUE", "CHANGED", "PLAYER LOCATION", "LAST UPDATED")); - sb.append("-".repeat(80)).append("\n"); - - // Get all varbit entries and sort by ID - cache.values().stream() - .filter(data -> data != null) - .sorted((data1, data2) -> { - // We need to find the keys for sorting, but we'll sort by last updated for now - return Long.compare(data2.getLastUpdated(), data1.getLastUpdated()); - }) - .forEach(data -> { - String lastUpdated = formatter.format(Instant.ofEpochMilli(data.getLastUpdated())); - String prevValue = data.getPreviousValue() != null ? - data.getPreviousValue().toString() : "-"; - String changed = data.hasValueChanged() ? "YES" : "-"; - String location = data.getPlayerLocation() != null ? - String.format("(%d,%d,%d)", - data.getPlayerLocation().getX(), - data.getPlayerLocation().getY(), - data.getPlayerLocation().getPlane()) : "-"; - - sb.append(String.format("%-10s %-8d %-13s %-15s %-25s %-19s\n", - "Unknown", // We don't have access to the varbit ID in this context - data.getValue(), - prevValue, - changed, - location, - lastUpdated)); - - // Additional contextual information - if (!data.getNearbyNpcIds().isEmpty() || !data.getNearbyObjectIds().isEmpty()) { - if (!data.getNearbyNpcIds().isEmpty()) { - sb.append(String.format(" └─ Nearby NPCs: %s\n", - data.getNearbyNpcIds().toString())); - } - if (!data.getNearbyObjectIds().isEmpty()) { - sb.append(String.format(" └─ Nearby Objects: %s\n", - data.getNearbyObjectIds().toString())); - } - } - }); - - sb.append("-".repeat(80)).append("\n"); - sb.append(String.format("Generated at: %s\n", formatter.format(Instant.now()))); - sb.append("=".repeat(80)); - - return sb.toString(); - } - - /** - * Returns a summary formatted string containing essential varbit cache information. - * Compact view showing key metrics and recent changes. - * - * @return Summary multi-line string representation of varbit cache - */ - public static String printVarbitSummary() { - StringBuilder sb = new StringBuilder(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss"); - - sb.append("┌─ VARBIT CACHE SUMMARY ").append("─".repeat(44)).append("┐\n"); - - Rs2Cache cache = getInstance(); - var stats = cache.getStatistics(); - - // Summary statistics - sb.append(String.format("│ Varbits Cached: %-3d │ Hits: %-6d │ Hit Rate: %5.1f%% │\n", - cache.size(), stats.cacheHits, stats.getHitRate() * 100)); - - // Count changed varbits - long changedCount = cache.values().stream() - .filter(data -> data != null && data.hasValueChanged()) - .count(); - - sb.append(String.format("│ Changed Varbits: %-3d │ Total Changes: %-8d │\n", - changedCount, stats.totalInvalidations)); - - sb.append("├─ RECENT CHANGES ").append("─".repeat(46)).append("â”Ī\n"); - - // Show recent varbit changes - cache.values().stream() - .filter(data -> data != null && data.hasValueChanged()) - .sorted((data1, data2) -> Long.compare(data2.getLastUpdated(), data1.getLastUpdated())) - .forEach(data -> { - String timeStr = formatter.format(Instant.ofEpochMilli(data.getLastUpdated())); - sb.append(String.format("│ Varbit changed: %d → %d (%s) %-18s │\n", - data.getPreviousValue() != null ? data.getPreviousValue() : 0, - data.getValue(), - timeStr, "")); - }); - - if (changedCount == 0) { - sb.append("│ No recent varbit changes detected ").append(" ".repeat(28)).append("│\n"); - } - - sb.append("└").append("─".repeat(63)).append("┘"); - - return sb.toString(); - } - - /** - * Logs the current state of all cached varbits for debugging. - * - * @param dumpToFile Whether to also dump the information to a file - */ - public static void logState(LogOutputMode mode) { - var cache = getInstance(); - var stats = cache.getStatistics(); - - // Create the log content - StringBuilder logContent = new StringBuilder(); - - String header = String.format("=== Varbit Cache State (%d entries) ===", cache.size()); - logContent.append(header).append("\n"); - - String statsInfo = Rs2CacheLoggingUtils.formatCacheStatistics( - stats.getHitRate(), stats.cacheHits, stats.cacheMisses, stats.cacheMode.toString()); - logContent.append(statsInfo).append("\n\n"); - int maxRows = mode == LogOutputMode.CONSOLE_ONLY ? 50 : cache.size(); - if (cache.size() == 0) { - String emptyMsg = "Cache is empty"; - logContent.append(emptyMsg).append("\n"); - } else { - final int MAXNAME_LENGTH = 45; // Maximum length for names - // Table format for varbits with VarbitID names where available - String[] headers = {"Varbit ID", "Name", "Value", "Previous", "Changed", "Last Updated"}; - int[] columnWidths = {10, MAXNAME_LENGTH, 8, 8, 8, 30}; - - String tableHeader = Rs2CacheLoggingUtils.formatTableHeader(headers, columnWidths); - logContent.append("\n").append(tableHeader); - - // Sort varbits by recent changes (most recent first) - cache.entryStream() - .sorted((a, b) -> Long.compare(b.getValue().getLastUpdated(), a.getValue().getLastUpdated())) - .forEach(entry -> { - Integer varbitId = entry.getKey(); - VarbitData varbitData = entry.getValue(); - - String varbitName = Rs2CacheLoggingUtils.getVarbitFieldName(varbitId); - String[] values = { - String.valueOf(varbitId), - Rs2CacheLoggingUtils.truncate(varbitName, MAXNAME_LENGTH), - String.valueOf(varbitData.getValue()), - varbitData.getPreviousValue() != null ? String.valueOf(varbitData.getPreviousValue()) : "null", - varbitData.hasValueChanged() ? "Yes" : "No", - Rs2CacheLoggingUtils.formatTimestamp(varbitData.getLastUpdated()) - }; - - String row = Rs2CacheLoggingUtils.formatTableRow(values, columnWidths); - logContent.append(row); - }); - - String tableFooter = Rs2CacheLoggingUtils.formatTableFooter(columnWidths); - logContent.append(tableFooter); - - String limitMsg = Rs2CacheLoggingUtils.formatLimitMessage(cache.size(), maxRows); - if (!limitMsg.isEmpty()) { - logContent.append(limitMsg).append("\n"); - } - } - - String footer = "=== End Varbit Cache State ==="; - logContent.append(footer).append("\n"); - Rs2CacheLoggingUtils.outputCacheLog("varbit", logContent.toString(), mode); - } - - - - // ============================================ -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/SkillData.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/SkillData.java deleted file mode 100644 index ce86da985dd..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/SkillData.java +++ /dev/null @@ -1,90 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.model; - -import lombok.Data; - -/** - * Data structure to hold skill information with temporal tracking. - */ -@Data -public class SkillData { - - private final int level; - private final int boostedLevel; - private final int experience; - private final long lastUpdated; // Timestamp when this skill data was last updated - private final Integer previousLevel; // Previous level before the update (null if unknown) - private final Integer previousExperience; // Previous experience before the update (null if unknown) - - /** - * Creates a new SkillData instance with current timestamp. - * - * @param level The real (unboosted) skill level - * @param boostedLevel The current boosted skill level - * @param experience The skill experience - */ - public SkillData(int level, int boostedLevel, int experience) { - this(level, boostedLevel, experience, System.currentTimeMillis(), null, null); - } - - /** - * Creates a new SkillData instance with previous values for change tracking. - * - * @param level The real (unboosted) skill level - * @param boostedLevel The current boosted skill level - * @param experience The skill experience - * @param previousLevel The previous level (null if unknown) - * @param previousExperience The previous experience (null if unknown) - */ - public SkillData(int level, int boostedLevel, int experience, Integer previousLevel, Integer previousExperience) { - this(level, boostedLevel, experience, System.currentTimeMillis(), previousLevel, previousExperience); - } - - /** - * Creates a new SkillData instance with full temporal tracking. - * - * @param level The real (unboosted) skill level - * @param boostedLevel The current boosted skill level - * @param experience The skill experience - * @param lastUpdated Timestamp when this data was created/updated - * @param previousLevel The previous level (null if unknown) - * @param previousExperience The previous experience (null if unknown) - */ - public SkillData(int level, int boostedLevel, int experience, long lastUpdated, Integer previousLevel, Integer previousExperience) { - this.level = level; - this.boostedLevel = boostedLevel; - this.experience = experience; - this.lastUpdated = lastUpdated; - this.previousLevel = previousLevel; - this.previousExperience = previousExperience; - } - - /** - * Creates a new SkillData with updated values while preserving previous state. - * - * @param newLevel The new skill level - * @param newBoostedLevel The new boosted level - * @param newExperience The new experience - * @return A new SkillData instance with the current values as previous values - */ - public SkillData withUpdate(int newLevel, int newBoostedLevel, int newExperience) { - return new SkillData(newLevel, newBoostedLevel, newExperience, this.level, this.experience); - } - - /** - * Checks if this skill data represents a level increase. - * - * @return true if the level increased from the previous value - */ - public boolean isLevelUp() { - return previousLevel != null && level > previousLevel; - } - - /** - * Gets the experience gained since the last update. - * - * @return the experience difference, or 0 if no previous experience - */ - public int getExperienceGained() { - return previousExperience != null ? experience - previousExperience : 0; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/SpiritTreeData.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/SpiritTreeData.java deleted file mode 100644 index 2025be7765e..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/SpiritTreeData.java +++ /dev/null @@ -1,226 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.model; - -import lombok.Data; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.questhelper.helpers.mischelpers.farmruns.CropState; -import net.runelite.client.plugins.microbot.util.farming.SpiritTree; - -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; - -/** - * Data structure to hold spirit tree farming patch information with contextual and temporal tracking. - * Includes information about patch state, travel availability, when last detected, - * and detection method tracking. - */ -@Data -public class SpiritTreeData { - - private final SpiritTree spiritTree; - private final CropState cropState; - private final boolean availableForTravel; - private final long lastUpdated; // UTC timestamp when this state was last detected - private final WorldPoint playerLocation; // Player location when the state was detected - private final boolean detectedViaWidget; // Whether this state was detected via spirit tree widget - private final boolean detectedViaNearBy; // Whether this state was detected via varbit when near by - - /** - * Creates a new SpiritTreeData instance with current timestamp and minimal context. - * - * @param spiritTree The spirit tree patch - * @param cropState The current crop state (null for built-in trees) - * @param availableForTravel Whether the tree is available for travel - */ - public SpiritTreeData(SpiritTree spiritTree, CropState cropState, boolean availableForTravel) { - this(spiritTree, cropState, availableForTravel, System.currentTimeMillis(), null, - false, false); - } - - /** - * Creates a new SpiritTreeData instance with detection method tracking. - * - * @param spiritTree The spirit tree patch - * @param cropState The current crop state (null for built-in trees) - * @param availableForTravel Whether the tree is available for travel - * @param detectedViaWidget Whether detected via spirit tree widget - * @param detectedViaNearBy Whether detected via varbit when near by - */ - public SpiritTreeData(SpiritTree spiritTree, CropState cropState, boolean availableForTravel, - boolean detectedViaWidget, boolean detectedViaNearBy) { - this(spiritTree, cropState, availableForTravel, System.currentTimeMillis(), null, - detectedViaWidget, detectedViaNearBy); - } - - /** - * Creates a new SpiritTreeData instance with contextual information. - * - * @param spiritTree The spirit tree patch - * @param cropState The current crop state (null for built-in trees) - * @param availableForTravel Whether the tree is available for travel - * @param playerLocation The player's world location when detected - * @param detectedViaWidget Whether detected via spirit tree widget - * @param detectedViaNearBy Whether detected via varbit when near by - */ - public SpiritTreeData(SpiritTree spiritTree, CropState cropState, boolean availableForTravel, - WorldPoint playerLocation, boolean detectedViaWidget, boolean detectedViaNearBy) { - this(spiritTree, cropState, availableForTravel, System.currentTimeMillis(), playerLocation, - detectedViaWidget, detectedViaNearBy); - } - - /** - * Creates a new SpiritTreeData instance with full temporal and contextual tracking. - * - * @param spiritTree The spirit tree patch - * @param cropState The current crop state (null for built-in trees) - * @param availableForTravel Whether the tree is available for travel - * @param lastUpdated UTC timestamp when this data was created/updated - * @param playerLocation The player's world location when detected - * @param detectedViaWidget Whether detected via spirit tree widget - * @param detectedViaNearBy Whether detected via varbit when near by - */ - public SpiritTreeData(SpiritTree spiritTree, CropState cropState, boolean availableForTravel, long lastUpdated, - WorldPoint playerLocation, boolean detectedViaWidget, boolean detectedViaNearBy) { - this.spiritTree = spiritTree; - this.cropState = cropState; - this.availableForTravel = availableForTravel; - this.lastUpdated = lastUpdated; - this.playerLocation = playerLocation; - this.detectedViaWidget = detectedViaWidget; - this.detectedViaNearBy = detectedViaNearBy; - } - - /** - * Creates a new SpiritTreeData with updated availability while preserving other data. - * - * @param newAvailability The new travel availability status - * @param detectedViaWidget Whether detected via widget - * @param detectedViaNearBy Whether detected via varbit when near by - * @param playerLocation Current player location - * @return A new SpiritTreeData instance with updated availability - */ - public SpiritTreeData withUpdatedAvailability(boolean newAvailability, boolean detectedViaWidget, - boolean detectedViaNearBy, WorldPoint playerLocation) { - return new SpiritTreeData(this.spiritTree, this.cropState, newAvailability, playerLocation, - detectedViaWidget, detectedViaNearBy); - } - - /** - * Creates a new SpiritTreeData with updated crop state. - * - * @param newCropState The new crop state - * @param playerLocation Current player location - * @return A new SpiritTreeData instance with updated crop state - */ - public SpiritTreeData withUpdatedCropState(CropState newCropState, WorldPoint playerLocation) { - // Update availability based on new crop state - boolean newAvailability = isAvailableBasedOnCropState(newCropState); - - return new SpiritTreeData(this.spiritTree, newCropState, newAvailability, playerLocation, - false, true); - } - - /** - * Determines travel availability based on crop state. - * - * @param cropState The crop state to evaluate - * @return true if the tree should be available for travel - */ - private boolean isAvailableBasedOnCropState(CropState cropState) { - if (cropState == null) { - return true; // Built-in trees are always available if quest requirements are met - } - - // Farmable trees are available when healthy and grown - return cropState == CropState.HARVESTABLE || cropState == CropState.UNCHECKED; - } - - /** - * Checks if this spirit tree data represents a travel availability change. - * - * @param previousData The previous data to compare against - * @return true if travel availability changed - */ - public boolean hasAvailabilityChanged(SpiritTreeData previousData) { - if (previousData == null) { - return true; // First detection is considered a change - } - return this.availableForTravel != previousData.availableForTravel; - } - - /** - * Checks if this spirit tree data represents a crop state change. - * - * @param previousData The previous data to compare against - * @return true if crop state changed - */ - public boolean hasCropStateChanged(SpiritTreeData previousData) { - if (previousData == null) { - return this.cropState != null; // First detection of farmable tree - } - - if (this.cropState == null && previousData.cropState == null) { - return false; // Both are built-in trees - } - - if (this.cropState == null || previousData.cropState == null) { - return true; // One is built-in, other is farmable - } - - return !this.cropState.equals(previousData.cropState); - } - - /** - * Checks if this detection occurred at a specific location. - * - * @param location The location to check - * @return true if the detection occurred at the specified location - */ - public boolean occurredAt(WorldPoint location) { - return playerLocation != null && playerLocation.distanceTo(location)<10; // Allow some tolerance - } - - /** - * Gets a human-readable timestamp of when this was last updated. - * - * @return Formatted timestamp string - */ - public String getFormattedLastUpdated() { - return Instant.ofEpochMilli(lastUpdated) - .atZone(ZoneId.systemDefault()) - .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); - } - - /** - * Gets the age of this data in milliseconds. - * - * @return Age in milliseconds since last update - */ - public long getAgeMillis() { - return System.currentTimeMillis() - lastUpdated; - } - - /** - * Checks if this data is considered stale based on age. - * - * @param maxAgeMillis Maximum acceptable age in milliseconds - * @return true if the data is older than the specified age - */ - public boolean isStale(long maxAgeMillis) { - return getAgeMillis() > maxAgeMillis; - } - - /** - * Gets a summary string for debugging purposes. - * - * @return Summary string containing key information - */ - public String getSummary() { - return String.format("SpiritTreeData{spiritTree=%s, state=%s, available=%s, age=%dms, via=%s}", - spiritTree.name(), - cropState != null ? cropState.name() : "BUILT_IN", - availableForTravel, - getAgeMillis(), - detectedViaWidget ? "WIDGET" : (detectedViaNearBy ? "NEAR_BY" : "UNKNOWN")); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/VarbitData.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/VarbitData.java deleted file mode 100644 index a5deded438e..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/model/VarbitData.java +++ /dev/null @@ -1,138 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.model; - -import lombok.Data; -import net.runelite.api.coords.WorldPoint; - -import java.util.Collections; -import java.util.List; - -/** - * Data structure to hold varbit information with contextual and temporal tracking. - * Includes information about when the varbit changed, where the player was, - * and what entities were nearby when the change occurred. - */ -@Data -public class VarbitData { - - private final int value; - private final long lastUpdated; // Timestamp when this varbit was last updated - private final Integer previousValue; // Previous value before the update (null if unknown) - private final WorldPoint playerLocation; // Player location when the varbit changed - private final List nearbyNpcIds; // NPC IDs that were nearby when the change occurred - private final List nearbyObjectIds; // Object IDs that were nearby when the change occurred - - /** - * Creates a new VarbitData instance with current timestamp and no context. - * - * @param value The varbit value - */ - public VarbitData(int value) { - this(value, System.currentTimeMillis(), null, null, Collections.emptyList(), Collections.emptyList()); - } - - /** - * Creates a new VarbitData instance with previous value tracking. - * - * @param value The varbit value - * @param previousValue The previous value (null if unknown) - */ - public VarbitData(int value, Integer previousValue) { - this(value, System.currentTimeMillis(), previousValue, null, Collections.emptyList(), Collections.emptyList()); - } - - /** - * Creates a new VarbitData instance with full contextual information. - * - * @param value The varbit value - * @param previousValue The previous value (null if unknown) - * @param playerLocation The player's world location when the change occurred - * @param nearbyNpcIds List of nearby NPC IDs - * @param nearbyObjectIds List of nearby object IDs - */ - public VarbitData(int value, Integer previousValue, WorldPoint playerLocation, - List nearbyNpcIds, List nearbyObjectIds) { - this(value, System.currentTimeMillis(), previousValue, playerLocation, nearbyNpcIds, nearbyObjectIds); - } - - /** - * Creates a new VarbitData instance with full temporal and contextual tracking. - * - * @param value The varbit value - * @param lastUpdated Timestamp when this data was created/updated - * @param previousValue The previous value (null if unknown) - * @param playerLocation The player's world location when the change occurred - * @param nearbyNpcIds List of nearby NPC IDs - * @param nearbyObjectIds List of nearby object IDs - */ - public VarbitData(int value, long lastUpdated, Integer previousValue, WorldPoint playerLocation, - List nearbyNpcIds, List nearbyObjectIds) { - this.value = value; - this.lastUpdated = lastUpdated; - this.previousValue = previousValue; - this.playerLocation = playerLocation; - this.nearbyNpcIds = nearbyNpcIds != null ? Collections.unmodifiableList(nearbyNpcIds) : Collections.emptyList(); - this.nearbyObjectIds = nearbyObjectIds != null ? Collections.unmodifiableList(nearbyObjectIds) : Collections.emptyList(); - } - - /** - * Creates a new VarbitData with updated value while preserving previous state. - * - * @param newValue The new varbit value - * @param playerLocation The player's current location - * @param nearbyNpcIds List of nearby NPC IDs - * @param nearbyObjectIds List of nearby object IDs - * @return A new VarbitData instance with the current value as previous value - */ - public VarbitData withUpdate(int newValue, WorldPoint playerLocation, - List nearbyNpcIds, List nearbyObjectIds) { - return new VarbitData(newValue, this.value, playerLocation, nearbyNpcIds, nearbyObjectIds); - } - - /** - * Checks if this varbit data represents a value change. - * - * @return true if the value changed from the previous value - */ - public boolean hasValueChanged() { - return previousValue != null && value != previousValue; - } - - /** - * Gets the change in value since the last update. - * - * @return the value difference, or 0 if no previous value - */ - public int getValueChange() { - return previousValue != null ? value - previousValue : 0; - } - - /** - * Checks if this varbit change occurred at a specific location. - * - * @param location The location to check - * @return true if the change occurred at the specified location - */ - public boolean occurredAt(WorldPoint location) { - return playerLocation != null && playerLocation.distanceTo(location)<10; // Allow some tolerance - } - - /** - * Checks if a specific NPC was nearby when this varbit changed. - * - * @param npcId The NPC ID to check for - * @return true if the NPC was nearby during the change - */ - public boolean hadNearbyNpc(int npcId) { - return nearbyNpcIds.contains(npcId); - } - - /** - * Checks if a specific object was nearby when this varbit changed. - * - * @param objectId The object ID to check for - * @return true if the object was nearby during the change - */ - public boolean hadNearbyObject(int objectId) { - return nearbyObjectIds.contains(objectId); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/HoverInfoContainer.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/HoverInfoContainer.java deleted file mode 100644 index 80fa781ffce..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/HoverInfoContainer.java +++ /dev/null @@ -1,142 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.overlay; - -import net.runelite.api.Point; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.ui.overlay.OverlayManager; - -import java.awt.Color; -import java.util.List; - -/** - * Container for hover information shared between cache overlays and the top-level info box overlay. - * This allows info boxes to appear on top of all other overlays. - * - * @author Vox - * @version 1.0 - */ -public class HoverInfoContainer { - private static volatile HoverInfo currentHoverInfo = null; - - /** - * Sets the current hover information. - * - * @param info The hover information to display, or null to clear - */ - public static void setHoverInfo(HoverInfo info) { - currentHoverInfo = info; - } - - /** - * Gets the current hover information. - * - * @return The current hover info or null if none - */ - public static HoverInfo getCurrentHoverInfo() { - return currentHoverInfo; - } - - /** - * Clears the current hover information. - */ - public static void clearHoverInfo() { - currentHoverInfo = null; - } - - /** - * Checks if there is currently hover information available. - * - * @return true if hover info is available - */ - public static boolean hasHoverInfo() { - return currentHoverInfo != null; - } - - /** - * Container for hover information data. - */ - public static class HoverInfo { - private final List infoLines; - private final Point location; - private final Color borderColor; - private final String entityType; - private final long creationTime; - - public HoverInfo(List infoLines, Point location, Color borderColor, String entityType) { - this.infoLines = infoLines; - this.location = location; - this.borderColor = borderColor; - this.entityType = entityType; - this.creationTime = System.currentTimeMillis(); - } - - public List getInfoLines() { - return infoLines; - } - - public Point getLocation() { - return location; - } - - public Color getBorderColor() { - return borderColor; - } - - public String getEntityType() { - return entityType; - } - - public long getCreationTime() { - return creationTime; - } - - /** - * Checks if this hover info is still fresh (not too old). - * - * @param maxAgeMs Maximum age in milliseconds - * @return true if the info is still fresh - */ - public boolean isFresh(long maxAgeMs) { - return (System.currentTimeMillis() - creationTime) <= maxAgeMs; - } - } - - // Static overlay management - private static Rs2CacheInfoBoxOverlay infoBoxOverlay; - private static boolean overlayRegistered = false; - - /** - * Manually registers the info box overlay with an overlay manager. - * This should be called by overlay managers or plugins that use the hover system. - * - * @param overlayManager The overlay manager to register with - */ - public static void registerInfoBoxOverlay(OverlayManager overlayManager) { - if (infoBoxOverlay == null && Microbot.getClient() != null) { - infoBoxOverlay = new Rs2CacheInfoBoxOverlay(Microbot.getClient()); - overlayManager.add(infoBoxOverlay); - overlayRegistered = true; - } - } - - /** - * Manually unregisters the info box overlay from an overlay manager. - * - * @param overlayManager The overlay manager to unregister from - */ - public static void unregisterInfoBoxOverlay(OverlayManager overlayManager) { - if (infoBoxOverlay != null && overlayRegistered) { - overlayManager.remove(infoBoxOverlay); - infoBoxOverlay = null; - overlayRegistered = false; - } - } - - /** - * Checks if the info box overlay is currently registered. - * - * @return true if registered - */ - public static boolean isInfoBoxOverlayRegistered() { - return overlayRegistered && infoBoxOverlay != null; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2BaseCacheOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2BaseCacheOverlay.java deleted file mode 100644 index acede59870c..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2BaseCacheOverlay.java +++ /dev/null @@ -1,215 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.overlay; - -import net.runelite.api.Client; -import net.runelite.client.plugins.microbot.Microbot; -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 net.runelite.client.ui.overlay.outline.ModelOutlineRenderer; - -import java.awt.*; - -/** - * Base overlay class for cache-based entity rendering. - * Provides common rendering utilities and setup for cache overlays. - * - * @author Vox - * @version 1.0 - */ -public abstract class Rs2BaseCacheOverlay extends Overlay { - - protected final Client client; - protected final ModelOutlineRenderer modelOutlineRenderer; - - // Default rendering settings (now abstract - overridden in subclasses) - protected static final float DEFAULT_BORDER_WIDTH = 2.0f; - - /** - * Gets the default border color for this entity type. - * @return The default border color - */ - protected abstract Color getDefaultBorderColor(); - - /** - * Gets the default fill color for this entity type. - * @return The default fill color - */ - protected abstract Color getDefaultFillColor(); - - public Rs2BaseCacheOverlay(Client client, ModelOutlineRenderer modelOutlineRenderer) { - this.client = client; - this.modelOutlineRenderer = modelOutlineRenderer; - setPosition(OverlayPosition.DYNAMIC); - setLayer(OverlayLayer.ABOVE_SCENE); - } - - /** - * Renders a polygon with border and fill colors. - * - * @param graphics The graphics context - * @param shape The shape to render - * @param borderColor The border color - * @param fillColor The fill color - * @param borderWidth The border width - */ - protected void renderShape(Graphics2D graphics, Shape shape, Color borderColor, Color fillColor, float borderWidth) { - if (shape == null) { - return; - } - - // Set stroke - graphics.setStroke(new BasicStroke(borderWidth)); - - // Draw border - graphics.setColor(borderColor); - graphics.draw(shape); - - // Fill shape - if (fillColor != null) { - graphics.setColor(fillColor); - graphics.fill(shape); - } - } - - /** - * Renders a polygon using OverlayUtil for consistency with RuneLite. - * - * @param graphics The graphics context - * @param shape The shape to render - * @param borderColor The border color - * @param fillColor The fill color - * @param stroke The stroke to use - */ - protected void renderPolygon(Graphics2D graphics, Shape shape, Color borderColor, Color fillColor, Stroke stroke) { - if (shape != null) { - OverlayUtil.renderPolygon(graphics, shape, borderColor, fillColor, stroke); - } - } - - /** - * Renders text at a specific location. - * - * @param graphics The graphics context - * @param text The text to render - * @param point The location to render at - * @param color The text color - */ - protected void renderText(Graphics2D graphics, String text, net.runelite.api.Point point, Color color) { - if (point != null && text != null && !text.isEmpty()) { - OverlayUtil.renderTextLocation(graphics, point, text, color); - } - } - - /** - * Checks if the client is available and ready for rendering. - * - * @return true if client is ready - */ - protected boolean isClientReady() { - return Microbot.isLoggedIn() && client != null; - } - - /** - * Gets a color with modified alpha for fill colors. - * - * @param baseColor The base color - * @param alpha The alpha value (0-255) - * @return Color with modified alpha - */ - protected Color withAlpha(Color baseColor, int alpha) { - return new Color(baseColor.getRed(), baseColor.getGreen(), baseColor.getBlue(), alpha); - } - - // ============================================ - // Enhanced Utility Methods - // ============================================ - - /** - * Checks if an entity is within the viewport bounds. - * Uses a simple canvas bounds check as a fast pre-filter. - * - * @param canvasPoint The canvas point to check - * @return true if the point is within viewport bounds - */ - protected boolean isWithinViewportBounds(net.runelite.api.Point canvasPoint) { - if (canvasPoint == null || client == null) { - return false; - } - - // Get canvas dimensions (viewport size) - int canvasWidth = client.getCanvasWidth(); - int canvasHeight = client.getCanvasHeight(); - - // Check if point is within bounds with some margin - return canvasPoint.getX() >= -50 && canvasPoint.getX() <= canvasWidth + 50 && - canvasPoint.getY() >= -50 && canvasPoint.getY() <= canvasHeight + 50; - } - - /** - * Renders a shape with enhanced visual effects. - * - * @param graphics The graphics context - * @param shape The shape to render - * @param borderColor The border color - * @param fillColor The fill color - * @param borderWidth The border width - * @param dashedBorder Whether to use a dashed border - */ - protected void renderShapeEnhanced(Graphics2D graphics, Shape shape, Color borderColor, - Color fillColor, float borderWidth, boolean dashedBorder) { - if (shape == null) { - return; - } - - // Set stroke (dashed or solid) - if (dashedBorder) { - float[] dash = {5.0f, 5.0f}; - graphics.setStroke(new BasicStroke(borderWidth, BasicStroke.CAP_ROUND, - BasicStroke.JOIN_ROUND, 1.0f, dash, 0.0f)); - } else { - graphics.setStroke(new BasicStroke(borderWidth)); - } - - // Fill shape first - if (fillColor != null) { - graphics.setColor(fillColor); - graphics.fill(shape); - } - - // Draw border - graphics.setColor(borderColor); - graphics.draw(shape); - } - - /** - * Renders text with a background for better visibility. - * - * @param graphics The graphics context - * @param text The text to render - * @param point The location to render at - * @param textColor The text color - * @param backgroundColor The background color (null for no background) - */ - protected void renderTextWithBackground(Graphics2D graphics, String text, net.runelite.api.Point point, - Color textColor, Color backgroundColor) { - if (point == null || text == null || text.isEmpty()) { - return; - } - - if (backgroundColor != null) { - // Calculate text bounds for background - FontMetrics metrics = graphics.getFontMetrics(); - int textWidth = metrics.stringWidth(text); - int textHeight = metrics.getHeight(); - - // Draw background rectangle - graphics.setColor(backgroundColor); - graphics.fillRect(point.getX() - 2, point.getY() - textHeight + 2, - textWidth + 4, textHeight); - } - - // Render the text - renderText(graphics, text, point, textColor); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2CacheInfoBoxOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2CacheInfoBoxOverlay.java deleted file mode 100644 index 81746f48b81..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2CacheInfoBoxOverlay.java +++ /dev/null @@ -1,154 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.overlay; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Client; -import net.runelite.client.ui.overlay.Overlay; -import net.runelite.client.ui.overlay.OverlayLayer; -import net.runelite.client.ui.overlay.OverlayPosition; - -import java.awt.*; -import java.util.List; - -/** - * Top-level overlay for displaying hover information boxes. - * This overlay uses ALWAYS_ON_TOP layer to ensure info boxes appear above all other overlays. - * - * @author Vox - * @version 1.0 - */ -@Slf4j -public class Rs2CacheInfoBoxOverlay extends Overlay { - - private final Client client; - - // Info box styling - private static final Color INFO_BOX_BACKGROUND = new Color(0, 0, 0, 180); // Semi-transparent black - private static final Color INFO_BOX_BORDER = new Color(255, 255, 255, 200); // Semi-transparent white - private static final Color INFO_TEXT_COLOR = Color.WHITE; - private static final int INFO_BOX_PADDING = 6; - private static final int INFO_BOX_LINE_SPACING = 2; - private static final long MAX_HOVER_INFO_AGE_MS = 100; // 100ms max age for hover info - - public Rs2CacheInfoBoxOverlay(Client client) { - this.client = client; - setPosition(OverlayPosition.DYNAMIC); - setLayer(OverlayLayer.ALWAYS_ON_TOP); // Ensure this appears on top of everything - } - - @Override - public Dimension render(Graphics2D graphics) { - if (!isClientReady()) { - return null; - } - - // Get current hover information - HoverInfoContainer.HoverInfo hoverInfo = HoverInfoContainer.getCurrentHoverInfo(); - if (hoverInfo == null || !hoverInfo.isFresh(MAX_HOVER_INFO_AGE_MS)) { - // Clear stale hover info - if (hoverInfo != null && !hoverInfo.isFresh(MAX_HOVER_INFO_AGE_MS)) { - HoverInfoContainer.clearHoverInfo(); - } - return null; - } - - try { - renderInfoBox(graphics, hoverInfo); - } catch (Exception e) { - log.warn("Error rendering cache info box overlay: {}", e.getMessage()); - } - - return null; - } - - /** - * Renders the info box with hover information. - * - * @param graphics The graphics context - * @param hoverInfo The hover information to display - */ - private void renderInfoBox(Graphics2D graphics, HoverInfoContainer.HoverInfo hoverInfo) { - List infoLines = hoverInfo.getInfoLines(); - if (infoLines.isEmpty()) { - return; - } - - // Set font for measurements - Font originalFont = graphics.getFont(); - Font infoFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12); - graphics.setFont(infoFont); - - FontMetrics fontMetrics = graphics.getFontMetrics(); - int lineHeight = fontMetrics.getHeight(); - - // Calculate info box dimensions - int maxTextWidth = infoLines.stream() - .mapToInt(fontMetrics::stringWidth) - .max() - .orElse(0); - - int infoBoxWidth = maxTextWidth + (INFO_BOX_PADDING * 2); - int infoBoxHeight = (infoLines.size() * lineHeight) + - ((infoLines.size() - 1) * INFO_BOX_LINE_SPACING) + - (INFO_BOX_PADDING * 2); - - // Calculate position (offset from hover location to avoid overlapping with cursor) - net.runelite.api.Point hoverLocation = hoverInfo.getLocation(); - int infoBoxX = hoverLocation.getX() + 15; // Offset right - int infoBoxY = hoverLocation.getY() - infoBoxHeight - 10; // Offset up - - // Ensure info box stays within viewport bounds - int viewportWidth = client.getCanvasWidth(); - int viewportHeight = client.getCanvasHeight(); - - // Adjust X position if too far right - if (infoBoxX + infoBoxWidth > viewportWidth) { - infoBoxX = hoverLocation.getX() - infoBoxWidth - 15; // Move to left side - } - - // Adjust Y position if too high - if (infoBoxY < 0) { - infoBoxY = hoverLocation.getY() + 25; // Move below cursor - } - - // Ensure final position is still within bounds - infoBoxX = Math.max(0, Math.min(infoBoxX, viewportWidth - infoBoxWidth)); - infoBoxY = Math.max(0, Math.min(infoBoxY, viewportHeight - infoBoxHeight)); - - // Draw info box background - graphics.setColor(INFO_BOX_BACKGROUND); - graphics.fillRoundRect(infoBoxX, infoBoxY, infoBoxWidth, infoBoxHeight, 6, 6); - - // Draw info box border using entity color - Color borderColor = hoverInfo.getBorderColor(); - if (borderColor != null) { - graphics.setColor(borderColor); - } else { - graphics.setColor(INFO_BOX_BORDER); - } - graphics.setStroke(new BasicStroke(2.0f)); - graphics.drawRoundRect(infoBoxX, infoBoxY, infoBoxWidth, infoBoxHeight, 6, 6); - - // Draw info lines - graphics.setColor(INFO_TEXT_COLOR); - int currentY = infoBoxY + INFO_BOX_PADDING + fontMetrics.getAscent(); - - for (String line : infoLines) { - graphics.drawString(line, infoBoxX + INFO_BOX_PADDING, currentY); - currentY += lineHeight + INFO_BOX_LINE_SPACING; - } - - // Restore original font - graphics.setFont(originalFont); - } - - /** - * Checks if the client is ready for rendering. - * - * @return true if ready - */ - private boolean isClientReady() { - return client != null && - client.getGameState() != null && - client.getLocalPlayer() != null; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2CacheOverlayManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2CacheOverlayManager.java deleted file mode 100644 index 5813ff3ab9f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2CacheOverlayManager.java +++ /dev/null @@ -1,229 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.overlay; - -import net.runelite.api.Client; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2NpcCacheUtils; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2ObjectCacheUtils; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2GroundItemCacheUtils; -import net.runelite.client.ui.overlay.OverlayManager; -import net.runelite.client.ui.overlay.outline.ModelOutlineRenderer; - -import javax.inject.Inject; - -/** - * Manager class for cache-based overlays. - * Provides easy setup and configuration of cache overlays for development and debugging. - * - * @author Vox - * @version 1.0 - */ -public class Rs2CacheOverlayManager { - - private final Client client; - private final OverlayManager overlayManager; - private final ModelOutlineRenderer modelOutlineRenderer; - - private Rs2NpcCacheOverlay npcOverlay; - private Rs2ObjectCacheOverlay objectOverlay; - private Rs2GroundItemCacheOverlay groundItemOverlay; - private Rs2CacheInfoBoxOverlay hoverInfoOverlay; - - public Rs2CacheOverlayManager(Client client, OverlayManager overlayManager, ModelOutlineRenderer modelOutlineRenderer) { - this.client = client; - this.overlayManager = overlayManager; - this.modelOutlineRenderer = modelOutlineRenderer; - initializeOverlays(); - } - - /** - * Initializes all cache overlays with default settings. - */ - private void initializeOverlays() { - // Create overlays - npcOverlay = new Rs2NpcCacheOverlay(client, modelOutlineRenderer); - objectOverlay = new Rs2ObjectCacheOverlay(client, modelOutlineRenderer); - groundItemOverlay = new Rs2GroundItemCacheOverlay(client, modelOutlineRenderer); - hoverInfoOverlay = new Rs2CacheInfoBoxOverlay(client); - - // Register the hover info overlay with the container for automatic management - HoverInfoContainer.registerInfoBoxOverlay(overlayManager); - - // Configure default filters and settings - configureDefaultSettings(); - } - - /** - * Configures default settings for all overlays. - */ - private void configureDefaultSettings() { - // NPC overlay - show only NPCs within 10 tiles and visible - npcOverlay.setRenderFilter(npc -> npc.getDistanceFromPlayer() <= 10); - - // Object overlay - show only interactable objects within 15 tiles - objectOverlay.setRenderFilter(obj -> obj.getDistanceFromPlayer() <= 15); - - // Ground item overlay - show only items within 5 tiles - groundItemOverlay.setRenderFilter(item -> item.getDistanceFromPlayer() <= 5); - } - - // ============================================ - // Overlay Management Methods - // ============================================ - - /** - * Enables NPC cache overlay. - */ - public void enableNpcOverlay() { - overlayManager.add(npcOverlay); - } - - /** - * Disables NPC cache overlay. - */ - public void disableNpcOverlay() { - overlayManager.remove(npcOverlay); - } - - /** - * Enables object cache overlay. - */ - public void enableObjectOverlay() { - overlayManager.add(objectOverlay); - } - - /** - * Disables object cache overlay. - */ - public void disableObjectOverlay() { - overlayManager.remove(objectOverlay); - } - - /** - * Enables ground item cache overlay. - */ - public void enableGroundItemOverlay() { - overlayManager.add(groundItemOverlay); - } - - /** - * Disables ground item cache overlay. - */ - public void disableGroundItemOverlay() { - overlayManager.remove(groundItemOverlay); - } - - /** - * Enables hover info overlay (always on top). - */ - public void enableHoverInfoOverlay() { - overlayManager.add(hoverInfoOverlay); - } - - /** - * Disables hover info overlay. - */ - public void disableHoverInfoOverlay() { - overlayManager.remove(hoverInfoOverlay); - } - - /** - * Enables all cache overlays including hover info. - */ - public void enableAllOverlays() { - enableNpcOverlay(); - enableObjectOverlay(); - enableGroundItemOverlay(); - enableHoverInfoOverlay(); - } - - /** - * Disables all cache overlays including hover info. - */ - public void disableAllOverlays() { - disableNpcOverlay(); - disableObjectOverlay(); - disableGroundItemOverlay(); - disableHoverInfoOverlay(); - } - - // ============================================ - // Overlay Configuration Getters - // ============================================ - - public Rs2NpcCacheOverlay getNpcOverlay() { - return npcOverlay; - } - - public Rs2ObjectCacheOverlay getObjectOverlay() { - return objectOverlay; - } - - public Rs2GroundItemCacheOverlay getGroundItemOverlay() { - return groundItemOverlay; - } - - public Rs2CacheInfoBoxOverlay getHoverInfoOverlay() { - return hoverInfoOverlay; - } - - // ============================================ - // Quick Configuration Methods - // ============================================ - - /** - * Quick setup for debugging NPCs by ID. - * - * @param npcId The NPC ID to highlight - */ - public void highlightNpcById(int npcId) { - npcOverlay.setRenderFilter(npc -> npc.getId() == npcId); - enableNpcOverlay(); - } - - /** - * Quick setup for debugging objects by ID. - * - * @param objectId The object ID to highlight - */ - public void highlightObjectById(int objectId) { - objectOverlay.setRenderFilter(obj -> obj.getId() == objectId); - enableObjectOverlay(); - } - - /** - * Quick setup for debugging ground items by ID. - * - * @param itemId The item ID to highlight - */ - public void highlightGroundItemById(int itemId) { - groundItemOverlay.setRenderFilter(item -> item.getId() == itemId); - enableGroundItemOverlay(); - } - - /** - * Quick setup for debugging entities by name (case-insensitive). - * - * @param entityName The entity name to highlight - */ - public void highlightEntitiesByName(String entityName) { - String lowerName = entityName.toLowerCase(); - - npcOverlay.setRenderFilter(npc -> npc.getName() != null && - npc.getName().toLowerCase().contains(lowerName)); - objectOverlay.setRenderFilter(obj -> obj.getName() != null && - obj.getName().toLowerCase().contains(lowerName)); - groundItemOverlay.setRenderFilter(item -> item.getName() != null && - item.getName().toLowerCase().contains(lowerName)); - - enableAllOverlays(); - } - - /** - * Cleanup method to properly unregister all overlays including the hover info overlay. - * Should be called when the overlay manager is no longer needed. - */ - public void cleanup() { - disableAllOverlays(); - HoverInfoContainer.unregisterInfoBoxOverlay(overlayManager); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2GroundItemCacheOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2GroundItemCacheOverlay.java deleted file mode 100644 index ebdb47a7726..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2GroundItemCacheOverlay.java +++ /dev/null @@ -1,645 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.overlay; - -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.util.cache.Rs2GroundItemCache; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2GroundItemCacheUtils; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; -import net.runelite.client.ui.overlay.outline.ModelOutlineRenderer; -import net.runelite.client.util.QuantityFormatter; - -import java.awt.*; -import java.util.List; -import java.util.Map; -import java.util.HashMap; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -/** - * Overlay for rendering cached ground items with various highlight options. - * Based on RuneLite's GroundItemsOverlay patterns but using the cache system. - * - * @author Vox - * @version 1.0 - */ -public class Rs2GroundItemCacheOverlay extends Rs2BaseCacheOverlay { - - /** - * Price calculation modes for value display. - */ - public enum PriceMode { - OFF("Off"), - GE("Grand Exchange"), - HA("High Alchemy"), - STORE("Store Price"), - BOTH("Both GE & HA"); - - private final String displayName; - - PriceMode(String displayName) { - this.displayName = displayName; - } - - @Override - public String toString() { - return displayName; - } - } - - // Ground item-specific colors (Green theme) - private static final Color GROUND_ITEM_BORDER_COLOR = Color.GREEN; - private static final Color GROUND_ITEM_FILL_COLOR = new Color(0, 255, 0, 50); // Green with alpha - - // Text rendering constants - private static final int TEXT_OFFSET_Z = 20; - private static final int STRING_GAP = 15; // Gap between multiple items on same tile - - // Rendering options - private boolean renderTile = true; - private boolean renderText = true; - private boolean renderItemInfo = true; // Show item ID - private boolean renderWorldCoordinates = false; // Show world coordinates - private boolean onlyShowTextOnHover = true; // Only show text when mouse is hovering - private Predicate renderFilter = groundItem -> true; // Default to no filter - - // Advanced rendering options - private boolean renderQuantity = true; // Show quantity for stackable items - private PriceMode priceMode = PriceMode.OFF; // Price display mode - private boolean renderDespawnTimer = false; // Show despawn countdown - private boolean renderOwnershipIndicator = false; // Show ownership status - - // Value thresholds for color coding - private int lowValueThreshold = 1000; - private int mediumValueThreshold = 10000; - private int highValueThreshold = 100000; - - // Map to track text offset for multiple items on same tile - private final Map offsetMap = new HashMap<>(); - - public Rs2GroundItemCacheOverlay(Client client, ModelOutlineRenderer modelOutlineRenderer) { - super(client, modelOutlineRenderer); - } - - @Override - protected Color getDefaultBorderColor() { - return GROUND_ITEM_BORDER_COLOR; - } - - @Override - protected Color getDefaultFillColor() { - return GROUND_ITEM_FILL_COLOR; - } - - @Override - public Dimension render(Graphics2D graphics) { - if (!isClientReady()) { - return null; - } - - // Clear offset map for new frame - offsetMap.clear(); - - // Group ground items by tile location to handle multiple items on same tile - Map> itemsByLocation = Rs2GroundItemCache.getInstance().stream() - .filter(item -> renderFilter == null || renderFilter.test(item)) - .filter(Rs2GroundItemCacheUtils::isVisibleInViewport) - .collect(Collectors.groupingBy(Rs2GroundItemModel::getLocation)); - - // Render each tile - for (Map.Entry> entry : itemsByLocation.entrySet()) { - WorldPoint location = entry.getKey(); - List itemsAtLocation = entry.getValue(); - - renderItemsAtTile(graphics, location, itemsAtLocation); - } - - return null; - } - - /** - * Renders all ground items at a specific tile location. - * Handles multiple items by spacing them vertically. - * - * @param graphics The graphics context - * @param location The tile location - * @param itemsAtLocation List of items at this location - */ - private void renderItemsAtTile(Graphics2D graphics, WorldPoint location, List itemsAtLocation) { - if (itemsAtLocation.isEmpty()) { - return; - } - - // Check if we should only show text on hover for this tile - LocalPoint localPoint = LocalPoint.fromWorld(client.getTopLevelWorldView(), location); - if (localPoint == null) { - return; - } - - boolean shouldShowText = !onlyShowTextOnHover || isMouseHoveringOver(localPoint); - - // Sort items by value (highest first) for better display order - itemsAtLocation.sort((a, b) -> Integer.compare(getItemDisplayValue(b), getItemDisplayValue(a))); - - // Render tile highlight if any item should be highlighted - if (renderTile) { - for (Rs2GroundItemModel item : itemsAtLocation) { - Color borderColor = getBorderColorForItem(item); - Color fillColor = getFillColorForItem(item); - renderItemTile(graphics, item, borderColor, fillColor, DEFAULT_BORDER_WIDTH); - break; // Only render tile once per location - } - } - - // Render text for each item with vertical offset - if (shouldShowText && (renderText || renderItemInfo || renderWorldCoordinates || - renderQuantity || priceMode != PriceMode.OFF || renderDespawnTimer || renderOwnershipIndicator)) { - - for (int i = 0; i < itemsAtLocation.size(); i++) { - Rs2GroundItemModel item = itemsAtLocation.get(i); - renderItemTextWithOffset(graphics, item, i); - } - } - } - - /** - * Gets the display value for an item based on the current price mode. - * - * @param item The ground item model - * @return The display value for sorting and comparison - */ - private int getItemDisplayValue(Rs2GroundItemModel item) { - switch (priceMode) { - case GE: - return item.getTotalGeValue(); - case HA: - return item.getTotalHaValue(); - case STORE: - return item.getTotalValue(); - case BOTH: - return Math.max(item.getTotalGeValue(), item.getTotalHaValue()); - case OFF: - default: - return item.getTotalValue(); // Default to store value - } - } - - /** - * Renders text for a ground item with vertical offset for multiple items. - * - * @param graphics The graphics context - * @param itemModel The ground item model - * @param offset The vertical offset index (0-based) - */ - private void renderItemTextWithOffset(Graphics2D graphics, Rs2GroundItemModel itemModel, int offset) { - LocalPoint localPoint = LocalPoint.fromWorld(client.getTopLevelWorldView(), itemModel.getLocation()); - if (localPoint == null) { - return; - } - - // Build text information for this item - String itemText = buildItemText(itemModel); - if (itemText.isEmpty()) { - return; - } - - // Get canvas point with Z offset for text - net.runelite.api.Point canvasPoint = Perspective.localToCanvas(client, localPoint, - client.getTopLevelWorldView().getPlane(), TEXT_OFFSET_Z); - - if (canvasPoint != null) { - // Apply vertical offset for multiple items on same tile - int adjustedY = canvasPoint.getY() - (STRING_GAP * offset); - net.runelite.api.Point adjustedPoint = new net.runelite.api.Point(canvasPoint.getX(), adjustedY); - - Color textColor = getBorderColorForItem(itemModel); - renderTextWithBackground(graphics, itemText, adjustedPoint, textColor); - } - } - - /** - * Renders a single ground item with the configured options (Legacy method for compatibility). - * - * @param graphics The graphics context - * @param itemModel The ground item model to render - */ - private void renderGroundItemOverlay(Graphics2D graphics, Rs2GroundItemModel itemModel) { - try { - Color borderColor = getBorderColorForItem(itemModel); - Color fillColor = getFillColorForItem(itemModel); - float borderWidth = DEFAULT_BORDER_WIDTH; - - // Render tile highlight - if (renderTile) { - renderItemTile(graphics, itemModel, borderColor, fillColor, borderWidth); - } - - // Render text information if enabled - if (renderText || renderItemInfo || renderWorldCoordinates || - renderQuantity || priceMode != PriceMode.OFF || renderDespawnTimer || renderOwnershipIndicator) { - renderItemText(graphics, itemModel, borderColor); - } - - } catch (Exception e) { - // Silent fail to avoid spam - } - } - - /** - * Renders a tile highlight for the ground item. - * - * @param graphics The graphics context - * @param itemModel The ground item model - * @param borderColor The border color - * @param fillColor The fill color - * @param borderWidth The border width - */ - private void renderItemTile(Graphics2D graphics, Rs2GroundItemModel itemModel, - Color borderColor, Color fillColor, float borderWidth) { - LocalPoint localPoint = LocalPoint.fromWorld(client.getTopLevelWorldView(), itemModel.getLocation()); - if (localPoint == null) { - return; - } - - Polygon tilePoly = Perspective.getCanvasTilePoly(client, localPoint); - if (tilePoly != null) { - Stroke stroke = new BasicStroke(borderWidth); - renderPolygon(graphics, tilePoly, borderColor, fillColor, stroke); - } - } - - /** - * Renders text information for the ground item with hover detection and background. - * - * @param graphics The graphics context - * @param itemModel The ground item model - * @param color The text color - */ - private void renderItemText(Graphics2D graphics, Rs2GroundItemModel itemModel, Color color) { - LocalPoint localPoint = LocalPoint.fromWorld(client.getTopLevelWorldView(), itemModel.getLocation()); - if (localPoint == null) { - return; - } - - // Check if we should only show text on hover - if (onlyShowTextOnHover && !isMouseHoveringOver(localPoint)) { - return; - } - - // Build single line of information - StringBuilder infoText = new StringBuilder(); - - // Main item name and quantity - if (renderText) { - infoText.append(buildItemText(itemModel)); - } - - // Add item ID and coordinates if enabled - if (renderItemInfo || renderWorldCoordinates) { - if (infoText.length() > 0) { - infoText.append(" | "); - } - - if (renderItemInfo) { - infoText.append("ID:").append(itemModel.getId()); - } - - if (renderWorldCoordinates) { - if (renderItemInfo) { - infoText.append(" "); - } - WorldPoint wp = itemModel.getLocation(); - infoText.append("(").append(wp.getX()).append(",").append(wp.getY()).append(")"); - } - } - - // Add additional information based on settings - if (renderQuantity && itemModel.getQuantity() > 1) { - if (infoText.length() > 0) { - infoText.append(" "); - } - infoText.append("x").append(itemModel.getQuantity()); - } - - if (priceMode != PriceMode.OFF) { - if (infoText.length() > 0) { - infoText.append(" "); - } - - switch (priceMode) { - case GE: - infoText.append("(GE: ").append(QuantityFormatter.quantityToStackSize(itemModel.getTotalGeValue())).append(" gp)"); - break; - case HA: - infoText.append("(HA: ").append(QuantityFormatter.quantityToStackSize(itemModel.getTotalHaValue())).append(" gp)"); - break; - case STORE: - infoText.append("(Store: ").append(QuantityFormatter.quantityToStackSize(itemModel.getTotalValue())).append(" gp)"); - break; - case BOTH: - if (itemModel.getTotalGeValue() > 0) { - infoText.append("(GE: ").append(QuantityFormatter.quantityToStackSize(itemModel.getTotalGeValue())).append(" gp)"); - } - if (itemModel.getTotalHaValue() > 0) { - infoText.append(" (HA: ").append(QuantityFormatter.quantityToStackSize(itemModel.getTotalHaValue())).append(" gp)"); - } - break; - case OFF: - default: - // No price display - break; - } - } - - if (renderDespawnTimer && !itemModel.isDespawned()) { - if (infoText.length() > 0) { - infoText.append(" "); - } - infoText.append("⏰").append(formatDespawnTime(itemModel.getSecondsUntilDespawn())); - } - - if (renderOwnershipIndicator) { - if (infoText.length() > 0) { - infoText.append(" "); - } - infoText.append(itemModel.isOwned() ? "ðŸ‘Ī" : "🌐"); - } - - // Get canvas point with Z offset for text - net.runelite.api.Point canvasPoint = Perspective.localToCanvas(client, localPoint, - client.getTopLevelWorldView().getPlane(), TEXT_OFFSET_Z); - - if (canvasPoint != null && infoText.length() > 0) { - renderTextWithBackground(graphics, infoText.toString(), canvasPoint, color); - } - } - - /** - * Checks if the mouse is hovering over a ground item location. - * - * @param localPoint The ground item location - * @return true if mouse is hovering over the location - */ - private boolean isMouseHoveringOver(LocalPoint localPoint) { - net.runelite.api.Point mousePos = client.getMouseCanvasPosition(); - if (mousePos == null) { - return false; - } - - // Check if mouse is over the tile - Polygon tilePoly = Perspective.getCanvasTilePoly(client, localPoint); - return tilePoly != null && tilePoly.contains(mousePos.getX(), mousePos.getY()); - } - - /** - * Renders text with a semi-transparent background for better readability. - * - * @param graphics The graphics context - * @param text The text to render - * @param location The location to render at - * @param color The text color - */ - private void renderTextWithBackground(Graphics2D graphics, String text, - net.runelite.api.Point location, Color color) { - FontMetrics fm = graphics.getFontMetrics(); - int textWidth = fm.stringWidth(text); - int textHeight = fm.getHeight(); - - // Create background rectangle - int padding = 4; - int backgroundX = location.getX() - padding; - int backgroundY = location.getY() - textHeight - padding; - int backgroundWidth = textWidth + (padding * 2); - int backgroundHeight = textHeight + (padding * 2); - - // Draw semi-transparent background - Color backgroundColor = new Color(0, 0, 0, 128); // Semi-transparent black - graphics.setColor(backgroundColor); - graphics.fillRect(backgroundX, backgroundY, backgroundWidth, backgroundHeight); - - // Draw text - renderText(graphics, text, location, color); - } - - /** - * Builds the text to display for a ground item. - * - * @param itemModel The ground item model - * @return The text to display - */ - private String buildItemText(Rs2GroundItemModel itemModel) { - StringBuilder text = new StringBuilder(); - - // Main item name - if (renderText) { - text.append(itemModel.getName()); - } - - // Add quantity if more than 1 - if (renderQuantity && itemModel.getQuantity() > 1) { - if (text.length() > 0) { - text.append(" "); - } - text.append("(").append(QuantityFormatter.quantityToStackSize(itemModel.getQuantity())).append(")"); - } - - // Add item ID and coordinates if enabled - if (renderItemInfo || renderWorldCoordinates) { - if (text.length() > 0) { - text.append(" | "); - } - - if (renderItemInfo) { - text.append("ID:").append(itemModel.getId()); - } - - if (renderWorldCoordinates) { - if (renderItemInfo) { - text.append(" "); - } - WorldPoint wp = itemModel.getLocation(); - text.append("(").append(wp.getX()).append(",").append(wp.getY()).append(")"); - } - } - - // Add price information based on price mode - if (priceMode != PriceMode.OFF) { - if (text.length() > 0) { - text.append(" "); - } - - switch (priceMode) { - case GE: - if (itemModel.getTotalGeValue() > 0) { - text.append("[GE: ").append(QuantityFormatter.quantityToStackSize(itemModel.getTotalGeValue())).append(" gp]"); - } - break; - case HA: - if (itemModel.getTotalHaValue() > 0) { - text.append("[HA: ").append(QuantityFormatter.quantityToStackSize(itemModel.getTotalHaValue())).append(" gp]"); - } - break; - case STORE: - if (itemModel.getTotalValue() > 0) { - text.append("[Store: ").append(QuantityFormatter.quantityToStackSize(itemModel.getTotalValue())).append(" gp]"); - } - break; - case BOTH: - boolean hasGe = itemModel.getTotalGeValue() > 0; - boolean hasHa = itemModel.getTotalHaValue() > 0; - if (hasGe || hasHa) { - text.append("["); - if (hasGe) { - text.append("GE: ").append(QuantityFormatter.quantityToStackSize(itemModel.getTotalGeValue())); - } - if (hasGe && hasHa) { - text.append(" | "); - } - if (hasHa) { - text.append("HA: ").append(QuantityFormatter.quantityToStackSize(itemModel.getTotalHaValue())); - } - text.append(" gp]"); - } - break; - case OFF: - default: - // No price display - break; - } - } - - // Add despawn timer if enabled - if (renderDespawnTimer && !itemModel.isDespawned()) { - if (text.length() > 0) { - text.append(" "); - } - text.append("⏰").append(formatDespawnTime(itemModel.getSecondsUntilDespawn())); - } - - // Add ownership indicator if enabled - if (renderOwnershipIndicator) { - if (text.length() > 0) { - text.append(" "); - } - text.append(itemModel.isOwned() ? "ðŸ‘Ī" : "🌐"); - } - - return text.toString(); - } - - /** - * Formats the item value for display. - * - * @param value The item value - * @return The formatted value string - */ - private String formatValue(int value) { - // Simple color formatting without ColorUtils dependency - if (value < lowValueThreshold) { - return String.format("%d gp", value); - } else if (value < mediumValueThreshold) { - return String.format("%d gp", value); - } else if (value < highValueThreshold) { - return String.format("%d gp", value); - } else { - return String.format("%d gp", value); - } - } - - /** - * Formats the despawn time for display. - * - * @param despawnSeconds The despawn time in seconds - * @return The formatted time string - */ - private String formatDespawnTime(long despawnSeconds) { - if (despawnSeconds <= 0) { - return ""; - } - - long minutes = despawnSeconds / 60; - long seconds = despawnSeconds % 60; - - return String.format("%02d:%02d", minutes, seconds); - } - - // ============================================ - // Configuration Methods - // ============================================ - - public Rs2GroundItemCacheOverlay setRenderTile(boolean renderTile) { - this.renderTile = renderTile; - return this; - } - - public Rs2GroundItemCacheOverlay setRenderText(boolean renderText) { - this.renderText = renderText; - return this; - } - - public Rs2GroundItemCacheOverlay setRenderItemInfo(boolean renderItemInfo) { - this.renderItemInfo = renderItemInfo; - return this; - } - - public Rs2GroundItemCacheOverlay setRenderWorldCoordinates(boolean renderWorldCoordinates) { - this.renderWorldCoordinates = renderWorldCoordinates; - return this; - } - - public Rs2GroundItemCacheOverlay setRenderFilter(Predicate renderFilter) { - this.renderFilter = renderFilter; - return this; - } - - // Configuration methods for advanced rendering options - public void setRenderQuantity(boolean renderQuantity) { - this.renderQuantity = renderQuantity; - } - - public void setPriceMode(PriceMode priceMode) { - this.priceMode = priceMode; - } - - public void setRenderDespawnTimer(boolean renderDespawnTimer) { - this.renderDespawnTimer = renderDespawnTimer; - } - - public void setRenderOwnershipIndicator(boolean renderOwnershipIndicator) { - this.renderOwnershipIndicator = renderOwnershipIndicator; - } - - public void setValueThresholds(int low, int medium, int high) { - this.lowValueThreshold = low; - this.mediumValueThreshold = medium; - this.highValueThreshold = high; - } - - /** - * Gets the border color for a specific ground item. - * Can be overridden by subclasses to provide per-item coloring. - * - * @param itemModel The ground item model - * @return The border color for this item - */ - protected Color getBorderColorForItem(Rs2GroundItemModel itemModel) { - return getDefaultBorderColor(); - } - - /** - * Gets the fill color for a specific ground item. - * Can be overridden by subclasses to provide per-item coloring. - * - * @param itemModel The ground item model - * @return The fill color for this item - */ - protected Color getFillColorForItem(Rs2GroundItemModel itemModel) { - return getDefaultFillColor(); - } - - public Rs2GroundItemCacheOverlay setOnlyShowTextOnHover(boolean onlyShowTextOnHover) { - this.onlyShowTextOnHover = onlyShowTextOnHover; - return this; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2NpcCacheOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2NpcCacheOverlay.java deleted file mode 100644 index cc7b26b98d4..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2NpcCacheOverlay.java +++ /dev/null @@ -1,451 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.overlay; - -import net.runelite.api.*; -import net.runelite.api.coords.LocalPoint; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.util.cache.Rs2NpcCache; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2NpcCacheUtils; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; -import net.runelite.client.ui.overlay.outline.ModelOutlineRenderer; -import net.runelite.client.util.Text; -import java.awt.*; -import java.util.function.Predicate; - -/** - * Overlay for rendering cached NPCs with various highlight options. - * Based on RuneLite's NpcOverlay patterns but using the cache system. - * - * @author Vox - * @version 1.0 - */ -public class Rs2NpcCacheOverlay extends Rs2BaseCacheOverlay { - - // NPC-specific colors - private static final Color NPC_BORDER_COLOR = Color.ORANGE; - private static final Color NPC_FILL_COLOR = new Color(255, 165, 0, 50); // Orange with alpha - private static final Color NPC_INTERACTING_COLOR = Color.RED; // Color for NPCs interacting with player - private static final Color NPC_INTERACTING_FILL_COLOR = new Color(255, 0, 0, 50); // Red with alpha - - // Rendering options - private boolean renderHull = true; - private boolean renderTile = false; - private boolean renderTrueTile = false; - private boolean renderOutline = false; - private boolean renderName = false; - private boolean renderNpcInfo = true; // Show NPC ID - private boolean renderWorldCoordinates = false; // Show world coordinates - private boolean renderCombatLevel = false; // Show combat level - private boolean renderDistance = false; // Show distance from player - private boolean onlyShowTextOnHover = true; // Only show text when mouse is hovering - private Predicate renderFilter = npc -> true; - - public Rs2NpcCacheOverlay(Client client, ModelOutlineRenderer modelOutlineRenderer) { - super(client, modelOutlineRenderer); - } - - @Override - protected Color getDefaultBorderColor() { - return NPC_BORDER_COLOR; - } - - @Override - protected Color getDefaultFillColor() { - return NPC_FILL_COLOR; - } - - @Override - public Dimension render(Graphics2D graphics) { - if (!isClientReady()) { - return null; - } - if (Rs2NpcCache.getInstance() == null || Rs2NpcCache.getInstance().size() == 0) { - return null; // No NPCs to render - } - renderFilter = renderFilter != null ? renderFilter : npc -> true; // Default to no filter - // Render all visible NPCs from cache - Rs2NpcCache.getAllNpcs() - .filter(npc -> renderFilter == null || renderFilter.test(npc)) - .filter(Rs2NpcCacheUtils::isVisibleInViewport) - .forEach(npc -> renderNpcOverlay(graphics, npc)); - - return null; - } - - /** - * Renders a single NPC with the configured options. - * - * @param graphics The graphics context - * @param npcModel The NPC model to render - */ - private void renderNpcOverlay(Graphics2D graphics, Rs2NpcModel npcModel) { - try { - // Get the underlying NPC from the model - NPC npc = npcModel.getRuneliteNpc(); - - NPCComposition npcComposition = npc.getTransformedComposition(); - if (npcComposition == null || !npcComposition.isInteractible()) { - return; - } - - Color borderColor = getBorderColorForNpc(npcModel); - Color fillColor = getFillColorForNpc(npcModel); - - // Check if NPC is interacting with the player to override colors - if (isNpcInteractingWithPlayer(npc)) { - borderColor = NPC_INTERACTING_COLOR; - fillColor = NPC_INTERACTING_FILL_COLOR; - } - - float borderWidth = DEFAULT_BORDER_WIDTH; - Stroke stroke = new BasicStroke(borderWidth); - - // Render convex hull - if (renderHull) { - Shape hull = npc.getConvexHull(); - renderPolygon(graphics, hull, borderColor, fillColor, stroke); - } - - // Render tile - if (renderTile) { - Polygon tilePoly = npc.getCanvasTilePoly(); - renderPolygon(graphics, tilePoly, borderColor, fillColor, stroke); - } - - // Render true tile (centered) - if (renderTrueTile) { - renderTrueTile(graphics, npc, npcComposition, borderColor, fillColor, stroke); - } - - // Render outline - if (renderOutline) { - modelOutlineRenderer.drawOutline(npc, (int) borderWidth, borderColor, 0); - } - - // Render NPC info (including name if enabled and hovering) - if (renderNpcInfo || renderWorldCoordinates || renderCombatLevel || renderDistance || renderName) { - renderNpcInfo(graphics, npc); - } - - } catch (Exception e) { - // Silent fail to avoid spam - } - } - - /** - * Renders the true tile (centered on NPC). - */ - private void renderTrueTile(Graphics2D graphics, NPC npc, NPCComposition composition, - Color borderColor, Color fillColor, Stroke stroke) { - LocalPoint lp = LocalPoint.fromWorld(client.getTopLevelWorldView(), npc.getWorldLocation()); - if (lp != null) { - final int size = composition.getSize(); - final LocalPoint centerLp = lp.plus( - Perspective.LOCAL_TILE_SIZE * (size - 1) / 2, - Perspective.LOCAL_TILE_SIZE * (size - 1) / 2); - Polygon tilePoly = Perspective.getCanvasTileAreaPoly(client, centerLp, size); - renderPolygon(graphics, tilePoly, borderColor, fillColor, stroke); - } - } - - /** - * Checks if the mouse is hovering over an NPC. - - /** - * Renders NPC information (name, ID, coordinates, combat level, distance) above the NPC. - * All information is displayed in a single line with hover detection and background. - */ - private void renderNpcInfo(Graphics2D graphics, NPC npc) { - // Check if we should only show text on hover - boolean isHovering = isMouseHoveringOver(npc); - if (onlyShowTextOnHover && !isHovering) { - return; - } - - // Build detailed information lines (always build them when renderNpcInfo is called) - java.util.List infoLines = new java.util.ArrayList<>(); - - // Add NPC name if enabled - if (renderName && npc.getName() != null) { - infoLines.add("Name: " + Text.removeTags(npc.getName())); - } - - // Add NPC ID if enabled - if (renderNpcInfo) { - infoLines.add("ID: " + npc.getId()); - } - - // Add combat level if enabled - if (renderCombatLevel) { - infoLines.add("Combat Level: " + npc.getCombatLevel()); - } - - // Add world coordinates if enabled - if (renderWorldCoordinates) { - WorldPoint wp = npc.getWorldLocation(); - infoLines.add("Coords: " + wp.getX() + ", " + wp.getY() + " (Plane " + wp.getPlane() + ")"); - } - - // Add distance from player if enabled - if (renderDistance) { - Player player = client.getLocalPlayer(); - if (player != null) { - int distance = (int) player.getWorldLocation().distanceTo(npc.getWorldLocation()); - infoLines.add("Distance: " + distance + " tiles"); - } - } - - // Add NPC composition details - NPCComposition npcComposition = npc.getComposition(); - if (npcComposition != null) { - if (npcComposition.getSize() != 1) { - infoLines.add("Size: " + npcComposition.getSize() + "x" + npcComposition.getSize()); - } - - // Add actions if available - String[] actions = npcComposition.getActions(); - if (actions != null && actions.length > 0) { - java.util.List validActions = new java.util.ArrayList<>(); - for (String action : actions) { - if (action != null && !action.trim().isEmpty()) { - validActions.add(action); - } - } - if (!validActions.isEmpty()) { - infoLines.add("Actions: " + String.join(", ", validActions)); - } - } - } - - // Add interaction status - if (isNpcInteractingWithPlayer(npc)) { - infoLines.add("Status: Interacting"); - } - - // Only set hover info if we have information to display - if (!infoLines.isEmpty()) { - Color borderColor = isNpcInteractingWithPlayer(npc) ? NPC_INTERACTING_COLOR : getDefaultBorderColor(); - String entityType = "NPC"; - - // Use NPC's canvas location for positioning, or mouse position if hovering - net.runelite.api.Point displayLocation; - if (isHovering) { - displayLocation = client.getMouseCanvasPosition(); - } else { - // Use NPC's text location for non-hover display - displayLocation = npc.getCanvasTextLocation(graphics, "", npc.getLogicalHeight() + 60); - } - - if (displayLocation != null) { - HoverInfoContainer.HoverInfo hoverInfo = new HoverInfoContainer.HoverInfo( - infoLines, displayLocation, borderColor, entityType); - //HoverInfoContainer.setHoverInfo(hoverInfo); - renderDetailedInfoBox(graphics, infoLines, - displayLocation, borderColor); - } - - } - } - /** - * Renders a detailed info box with multiple lines of information. - * Each line is rendered separately with a colored border indicating the object type. - * - * @param graphics The graphics context - * @param infoLines List of information lines to display - * @param location The location to render the info box - * @param borderColor The border color (indicates object type) - */ - private void renderDetailedInfoBox(Graphics2D graphics, java.util.List infoLines, - net.runelite.api.Point location, Color borderColor) { - if (infoLines.isEmpty()) return; - - FontMetrics fm = graphics.getFontMetrics(); - int lineHeight = fm.getHeight(); - int maxWidth = 0; - - // Calculate the maximum width needed - for (String line : infoLines) { - int lineWidth = fm.stringWidth(line); - if (lineWidth > maxWidth) { - maxWidth = lineWidth; - } - } - - // Calculate box dimensions - int padding = 6; - int boxWidth = maxWidth + (padding * 2); - int boxHeight = (infoLines.size() * lineHeight) + (padding * 2); - - // Calculate box position (centered above the location) - int boxX = location.getX() - (boxWidth / 2); - int boxY = location.getY() - boxHeight - 10; // 10 pixels above the object - - // Draw the info box background - Color backgroundColor = new Color(0, 0, 0, 180); // Semi-transparent black - graphics.setColor(backgroundColor); - graphics.fillRect(boxX, boxY, boxWidth, boxHeight); - - // Draw the border in object type color - graphics.setColor(borderColor); - graphics.setStroke(new BasicStroke(2.0f)); - graphics.drawRect(boxX, boxY, boxWidth, boxHeight); - - // Draw the text lines - graphics.setColor(Color.WHITE); - for (int i = 0; i < infoLines.size(); i++) { - String line = infoLines.get(i); - int textX = boxX + padding; - int textY = boxY + padding + fm.getAscent() + (i * lineHeight); - graphics.drawString(line, textX, textY); - } - } - - /** - * Checks if the mouse is hovering over an NPC. - * - * @param npc The NPC to check - * @return true if mouse is hovering over the NPC - */ - private boolean isMouseHoveringOver(NPC npc) { - net.runelite.api.Point mousePos = client.getMouseCanvasPosition(); - if (mousePos == null) { - return false; - } - - // Check if mouse is over the NPC's convex hull - Shape hull = npc.getConvexHull(); - if (hull != null) { - return hull.contains(mousePos.getX(), mousePos.getY()); - } - - // Fallback to tile poly - Polygon tilePoly = npc.getCanvasTilePoly(); - return tilePoly != null && tilePoly.contains(mousePos.getX(), mousePos.getY()); - } - - /** - * Renders text with a semi-transparent background for better readability. - * - * @param graphics The graphics context - * @param text The text to render - * @param location The location to render at - * @param color The text color - */ - private void renderTextWithBackground(Graphics2D graphics, String text, - net.runelite.api.Point location, Color color) { - FontMetrics fm = graphics.getFontMetrics(); - int textWidth = fm.stringWidth(text); - int textHeight = fm.getHeight(); - - // Create background rectangle - int padding = 4; - int backgroundX = location.getX() - padding; - int backgroundY = location.getY() - textHeight - padding; - int backgroundWidth = textWidth + (padding * 2); - int backgroundHeight = textHeight + (padding * 2); - - // Draw semi-transparent background - Color backgroundColor = new Color(0, 0, 0, 128); // Semi-transparent black - graphics.setColor(backgroundColor); - graphics.fillRect(backgroundX, backgroundY, backgroundWidth, backgroundHeight); - - // Draw text - renderText(graphics, text, location, color); - } - - /** - * Renders multiple lines of text above an NPC with proper spacing. - * - * @param graphics The graphics context - * @param npc The NPC to render text above - * @param lines The lines of text to render - * @param color The color to use - /** - * Checks if an NPC is interacting with the player. - * An NPC is considered interacting if it's targeting/attacking the player. - */ - private boolean isNpcInteractingWithPlayer(NPC npc) { - Actor interacting = npc.getInteracting(); - return interacting != null && interacting.equals(client.getLocalPlayer()); - } - - /** - * Gets the border color for a specific NPC. - * Can be overridden by subclasses to provide per-NPC coloring. - * - * @param npcModel The NPC model - * @return The border color for this NPC - */ - protected Color getBorderColorForNpc(Rs2NpcModel npcModel) { - return getDefaultBorderColor(); - } - - /** - * Gets the fill color for a specific NPC. - * Can be overridden by subclasses to provide per-NPC coloring. - * - * @param npcModel The NPC model - * @return The fill color for this NPC - */ - protected Color getFillColorForNpc(Rs2NpcModel npcModel) { - return getDefaultFillColor(); - } - - // ============================================ - // Configuration Methods - // ============================================ - - public Rs2NpcCacheOverlay setRenderHull(boolean renderHull) { - this.renderHull = renderHull; - return this; - } - - public Rs2NpcCacheOverlay setRenderTile(boolean renderTile) { - this.renderTile = renderTile; - return this; - } - - public Rs2NpcCacheOverlay setRenderTrueTile(boolean renderTrueTile) { - this.renderTrueTile = renderTrueTile; - return this; - } - - public Rs2NpcCacheOverlay setRenderOutline(boolean renderOutline) { - this.renderOutline = renderOutline; - return this; - } - - public Rs2NpcCacheOverlay setRenderName(boolean renderName) { - this.renderName = renderName; - return this; - } - - public Rs2NpcCacheOverlay setRenderNpcInfo(boolean renderNpcInfo) { - this.renderNpcInfo = renderNpcInfo; - return this; - } - - public Rs2NpcCacheOverlay setRenderWorldCoordinates(boolean renderWorldCoordinates) { - this.renderWorldCoordinates = renderWorldCoordinates; - return this; - } - - public Rs2NpcCacheOverlay setRenderCombatLevel(boolean renderCombatLevel) { - this.renderCombatLevel = renderCombatLevel; - return this; - } - - public Rs2NpcCacheOverlay setRenderDistance(boolean renderDistance) { - this.renderDistance = renderDistance; - return this; - } - - public Rs2NpcCacheOverlay setOnlyShowTextOnHover(boolean onlyShowTextOnHover) { - this.onlyShowTextOnHover = onlyShowTextOnHover; - return this; - } - - public Rs2NpcCacheOverlay setRenderFilter(Predicate renderFilter) { - this.renderFilter = renderFilter; - return this; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2ObjectCacheOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2ObjectCacheOverlay.java deleted file mode 100644 index 0abdfa60a66..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/overlay/Rs2ObjectCacheOverlay.java +++ /dev/null @@ -1,841 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.overlay; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.*; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.util.cache.Rs2ObjectCache; -import net.runelite.client.plugins.microbot.util.cache.util.Rs2ObjectCacheUtils; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel; -import net.runelite.client.ui.overlay.outline.ModelOutlineRenderer; -import java.awt.*; -import java.util.EnumMap; -import java.util.List; -import java.util.Map; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -/** - * Overlay for rendering cached objects with various highlight options. - * Based on RuneLite's ObjectIndicatorsOverlay patterns but using the cache system. - * - * @author Vox - * @version 1.0 - */ -@Slf4j -public class Rs2ObjectCacheOverlay extends Rs2BaseCacheOverlay { - - // Object-specific colors (Blue theme) - Default fallback colors - private static final Color OBJECT_BORDER_COLOR = Color.BLUE; - private static final Color OBJECT_FILL_COLOR = new Color(0, 0, 255, 50); // Blue with alpha - - // Default object type colors - private static final Color GAME_OBJECT_COLOR = new Color(255, 0, 0); // Red - private static final Color WALL_OBJECT_COLOR = new Color(0, 255, 0); // Green - private static final Color DECORATIVE_OBJECT_COLOR = new Color(255, 255, 0); // Yellow - private static final Color GROUND_OBJECT_COLOR = new Color(255, 0, 255); // Magenta - private static final Color TILE_OBJECT_COLOR = new Color(255, 165, 0); // Orange - - // Rendering options - private boolean renderHull = true; - private boolean renderClickbox = false; - private boolean renderTile = false; - private boolean renderOutline = false; - private boolean renderObjectInfo = true; // Show object type and ID - private boolean renderObjectName = true; // Show object names - private boolean renderWorldCoordinates = false; // Show world coordinates - private boolean onlyShowTextOnHover = true; // Only show text when mouse is hovering - - // Object type enable/disable flags - private boolean enableGameObjects = true; - private boolean enableWallObjects = true; - private boolean enableDecorativeObjects = true; - private boolean enableGroundObjects = true; - - // Statistics tracking - private static final long STATISTICS_LOG_INTERVAL_MS = 10_000; // 10 seconds - private long lastStatisticsLogTime = 0; - - private Predicate renderFilter; - - public Rs2ObjectCacheOverlay(Client client, ModelOutlineRenderer modelOutlineRenderer) { - super(client, modelOutlineRenderer); - } - - @Override - protected Color getDefaultBorderColor() { - return OBJECT_BORDER_COLOR; - } - - @Override - protected Color getDefaultFillColor() { - return OBJECT_FILL_COLOR; - } - - @Override - public Dimension render(Graphics2D graphics) { - if (!isClientReady()) { - return null; - } - if (Rs2ObjectCache.getInstance() == null || Rs2ObjectCache.getInstance().size() == 0) { - return null; // No objects to render - } - - // Check if we should log statistics (every 10 seconds) - long currentTime = System.currentTimeMillis(); - boolean shouldLogStatistics = (currentTime - lastStatisticsLogTime) >= STATISTICS_LOG_INTERVAL_MS; - - // Get all objects from cache for statistics - List allObjects = Rs2ObjectCache.getInstance().stream() - .collect(Collectors.toList()); - - // Track statistics by object type for each filtering stage - Map totalByType = new EnumMap<>(Rs2ObjectModel.ObjectType.class); - Map afterRenderFilterByType = new EnumMap<>(Rs2ObjectModel.ObjectType.class); - Map afterTypeEnabledByType = new EnumMap<>(Rs2ObjectModel.ObjectType.class); - Map afterViewportByType = new EnumMap<>(Rs2ObjectModel.ObjectType.class); - - // Initialize counters - for (Rs2ObjectModel.ObjectType type : Rs2ObjectModel.ObjectType.values()) { - totalByType.put(type, 0); - afterRenderFilterByType.put(type, 0); - afterTypeEnabledByType.put(type, 0); - afterViewportByType.put(type, 0); - } - - // Count total objects by type - for (Rs2ObjectModel obj : allObjects) { - Rs2ObjectModel.ObjectType type = obj.getObjectType(); - totalByType.put(type, totalByType.get(type) + 1); - } - // Apply filters and count at each stage - List afterRenderFilter = allObjects.stream() - .filter(obj -> renderFilter == null || renderFilter.test(obj)) - .collect(Collectors.toList()); - - for (Rs2ObjectModel obj : afterRenderFilter) { - Rs2ObjectModel.ObjectType type = obj.getObjectType(); - afterRenderFilterByType.put(type, afterRenderFilterByType.get(type) + 1); - } - - List afterTypeEnabled = afterRenderFilter.stream() - .filter(obj -> isObjectTypeEnabled(obj.getObjectType())) - .collect(Collectors.toList()); - - for (Rs2ObjectModel obj : afterTypeEnabled) { - Rs2ObjectModel.ObjectType type = obj.getObjectType(); - afterTypeEnabledByType.put(type, afterTypeEnabledByType.get(type) + 1); - } - - List afterViewport = afterTypeEnabled.stream() - .filter(Rs2ObjectCacheUtils::isVisibleInViewport) - .collect(Collectors.toList()); - - for (Rs2ObjectModel obj : afterViewport) { - Rs2ObjectModel.ObjectType type = obj.getObjectType(); - afterViewportByType.put(type, afterViewportByType.get(type) + 1); - } - - // Log detailed statistics every 10 seconds - if (shouldLogStatistics) { - lastStatisticsLogTime = currentTime; - logRenderingStatistics(totalByType, afterRenderFilterByType, afterTypeEnabledByType, afterViewportByType); - } - - // Render the visible objects - afterViewport.forEach(obj -> renderObjectOverlay(graphics, obj)); - - return null; - } - - /** - * Logs detailed rendering statistics showing object type distribution at each filtering stage. - * This helps identify which object types are available, filtered out, and why. - * - * @param totalByType Total count of each object type in cache - * @param afterRenderFilterByType Count after custom render filter applied - * @param afterTypeEnabledByType Count after object type enable/disable filter - * @param afterViewportByType Count after viewport visibility filter (final rendered count) - */ - private void logRenderingStatistics(Map totalByType, - Map afterRenderFilterByType, - Map afterTypeEnabledByType, - Map afterViewportByType) { - - // Calculate totals across all types - int totalObjects = totalByType.values().stream().mapToInt(Integer::intValue).sum(); - int afterRenderFilterTotal = afterRenderFilterByType.values().stream().mapToInt(Integer::intValue).sum(); - int afterTypeEnabledTotal = afterTypeEnabledByType.values().stream().mapToInt(Integer::intValue).sum(); - int finalRenderedTotal = afterViewportByType.values().stream().mapToInt(Integer::intValue).sum(); - - log.info("=== Rs2ObjectCacheOverlay Rendering Statistics ==="); - log.info("Cache Total: {} | After RenderFilter: {} | After TypeEnabled: {} | Final Rendered: {}", - totalObjects, afterRenderFilterTotal, afterTypeEnabledTotal, finalRenderedTotal); - - // Log statistics for each object type - for (Rs2ObjectModel.ObjectType type : Rs2ObjectModel.ObjectType.values()) { - int total = totalByType.get(type); - int afterRenderFilter = afterRenderFilterByType.get(type); - int afterTypeEnabled = afterTypeEnabledByType.get(type); - int finalRendered = afterViewportByType.get(type); - - // Skip types with no objects - if (total == 0) { - continue; - } - - // Calculate filtering reasons - int filteredByRenderFilter = total - afterRenderFilter; - int filteredByTypeEnabled = afterRenderFilter - afterTypeEnabled; - int filteredByViewport = afterTypeEnabled - finalRendered; - - boolean typeEnabled = isObjectTypeEnabled(type); - - log.info(" {}: Total={} | RenderFilter={} | TypeEnabled={} ({}) | Viewport={} | Final={}", - type.getTypeName(), - total, - afterRenderFilter, - afterTypeEnabled, - typeEnabled ? "ENABLED" : "DISABLED", - finalRendered, - finalRendered); - - // Log filtering details if objects were filtered - if (filteredByRenderFilter > 0) { - log.info(" -> {} filtered by RenderFilter", filteredByRenderFilter); - } - if (filteredByTypeEnabled > 0) { - log.info(" -> {} filtered by TypeEnabled ({})", - filteredByTypeEnabled, typeEnabled ? "ERROR" : "disabled"); - } - if (filteredByViewport > 0) { - log.info(" -> {} filtered by Viewport (not visible)", filteredByViewport); - } - } - - // Log filtering summary - int totalFilteredByRender = totalObjects - afterRenderFilterTotal; - int totalFilteredByType = afterRenderFilterTotal - afterTypeEnabledTotal; - int totalFilteredByViewport = afterTypeEnabledTotal - finalRenderedTotal; - - if (totalFilteredByRender > 0 || totalFilteredByType > 0 || totalFilteredByViewport > 0) { - log.info("Filtering Summary: RenderFilter removed {} | TypeEnabled removed {} | Viewport removed {}", - totalFilteredByRender, totalFilteredByType, totalFilteredByViewport); - } - - // Log current filter settings - log.info("Current Settings: GameObj={} | WallObj={} | DecorObj={} | GroundObj={} | RenderFilter={}", - enableGameObjects, enableWallObjects, enableDecorativeObjects, enableGroundObjects, - renderFilter != null ? "ACTIVE" : "NONE"); - - log.info("=== End Rs2ObjectCacheOverlay Statistics ==="); - } - - /** - * Renders a single object with the configured options. - * - * @param graphics The graphics context - * @param objectModel The object model to render - */ - private void renderObjectOverlay(Graphics2D graphics, Rs2ObjectModel objectModel) { - try { - // Validate inputs - if (objectModel == null) { - return; - } - if (objectModel.getTileObject() == null) { - return; // No tile object to render - } - - TileObject tileObject = objectModel.getTileObject(); - - // Check if object is on current plane - try { - if (tileObject.getPlane() != client.getTopLevelWorldView().getPlane()) { - return; - } - } catch (Exception e) { - // Skip plane check if there's an issue accessing it - log.debug("Failed to check object plane for object {}: {}", objectModel.getId(), e.getMessage()); - } - - // Get colors for this specific object (supports per-object coloring) - Color borderColor = getBorderColorForObject(objectModel); - Color fillColor = getFillColorForObject(objectModel); - float borderWidth = DEFAULT_BORDER_WIDTH; - Stroke stroke = new BasicStroke(borderWidth); - - // Render convex hull - if (renderHull) { - renderConvexHull(graphics, tileObject, borderColor, fillColor, stroke); - } - - // Render clickbox - if (renderClickbox) { - try { - Shape clickbox = tileObject.getClickbox(); - if (clickbox != null) { - renderPolygon(graphics, clickbox, borderColor, fillColor, stroke); - } - } catch (Exception e) { - log.debug("Failed to render clickbox for object {}: {}", objectModel.getId(), e.getMessage()); - } - } - - // Render tile - if (renderTile) { - try { - Polygon tilePoly = tileObject.getCanvasTilePoly(); - if (tilePoly != null) { - renderPolygon(graphics, tilePoly, borderColor, fillColor, stroke); - } - } catch (Exception e) { - log.debug("Failed to render tile poly for object {}: {}", objectModel.getId(), e.getMessage()); - } - } - - // Render outline - if (renderOutline) { - try { - modelOutlineRenderer.drawOutline(tileObject, (int) borderWidth, borderColor, 0); - } catch (Exception e) { - log.debug("Failed to render outline for object {}: {}", objectModel.getId(), e.getMessage()); - } - } - - // Render object information (type, ID, and name) - if (renderObjectInfo) { - renderObjectInfo(graphics, objectModel, tileObject); - } - - } catch (Exception e) { - log.warn("Failed to render object overlay for object {}: {}", - objectModel != null ? objectModel.getId() : "unknown", e.getMessage(), e); - } - } - - /** - * Renders the convex hull for different object types. - * Based on ObjectIndicatorsOverlay pattern. - * - * @param graphics The graphics context - * @param object The tile object - * @param color The border color - * @param fillColor The fill color - * @param stroke The stroke - */ - private void renderConvexHull(Graphics2D graphics, TileObject object, Color color, Color fillColor, Stroke stroke) { - try { - Shape polygon = null; - Shape polygon2 = null; - - try { - if (object instanceof GameObject) { - polygon = ((GameObject) object).getConvexHull(); - } else if (object instanceof WallObject) { - WallObject wallObject = (WallObject) object; - polygon = wallObject.getConvexHull(); - polygon2 = wallObject.getConvexHull2(); - } else if (object instanceof DecorativeObject) { - DecorativeObject decorativeObject = (DecorativeObject) object; - polygon = decorativeObject.getConvexHull(); - polygon2 = decorativeObject.getConvexHull2(); - } else if (object instanceof GroundObject) { - polygon = ((GroundObject) object).getConvexHull(); - } else { - polygon = object.getCanvasTilePoly(); - } - } catch (Exception e) { - log.debug("Failed to get convex hull shapes: {}", e.getMessage()); - // Fallback to tile poly - try { - polygon = object.getCanvasTilePoly(); - } catch (Exception e2) { - log.debug("Failed to get tile poly as fallback: {}", e2.getMessage()); - } - } - - if (polygon != null) { - renderPolygon(graphics, polygon, color, fillColor, stroke); - } - - if (polygon2 != null) { - renderPolygon(graphics, polygon2, color, fillColor, stroke); - } - } catch (Exception e) { - log.debug("Error rendering convex hull: {}", e.getMessage()); - } - } - - /** - * Renders object information as a detailed info box with each piece of information on a separate line. - * The info box border uses the object type color for visual identification. - * - * @param graphics The graphics context - * @param objectModel The object model - * @param tileObject The tile object - */ - private void renderObjectInfo(Graphics2D graphics, Rs2ObjectModel objectModel, TileObject tileObject) { - try { - // Check if we should only show text on hover - boolean isHovering = isMouseHoveringOver(tileObject); - if (onlyShowTextOnHover && !isHovering) { - return; - } - // Build information lines (always build them when renderObjectInfo is called) - java.util.List infoLines = new java.util.ArrayList<>(); - - // Add object name if enabled and available - String objectName = objectModel.getName(); - if (renderObjectName && objectName != null && !objectName.equals("Unknown Object") && !objectName.trim().isEmpty()) { - infoLines.add("Name: " + objectName); - } - - // Add object type and ID - if (renderObjectInfo) { - infoLines.add("Type: " + objectModel.getObjectType().getTypeName()); - infoLines.add("ID: " + objectModel.getId()); - - // Object size information - infoLines.add("Size: " + objectModel.getSizeX() + "x" + objectModel.getSizeY()); - - // Object composition details - ObjectComposition comp = objectModel.getObjectComposition(); - if (comp != null) { - // Map scene and icon IDs - int mapSceneId = comp.getMapSceneId(); - int mapIconId = comp.getMapIconId(); - if (mapSceneId != -1) { - infoLines.add("MapScene: " + mapSceneId); - } - if (mapIconId != -1) { - infoLines.add("MapIcon: " + mapIconId); - } - - // Varbit/VarPlayer information for multiloc objects - int varbitId = comp.getVarbitId(); - int varPlayerId = comp.getVarPlayerId(); - if (varbitId != -1) { - infoLines.add("VarbitID: " + varbitId); - } - if (varPlayerId != -1) { - infoLines.add("VarPlayerID: " + varPlayerId); - } - - // Impostor information for multiloc objects - int[] impostorIds = comp.getImpostorIds(); - if (impostorIds != null && impostorIds.length > 0) { - infoLines.add("Impostors: " + impostorIds.length + " variants"); - } - } - - // Object properties - if (objectModel.isSolid()) { - infoLines.add("Property: Solid"); - } - if (objectModel.blocksLineOfSight()) { - infoLines.add("Property: Blocks LoS"); - } - - // Cache timing information - infoLines.add("Age: " + objectModel.getTicksSinceCreation() + " ticks"); - } - - // Add world coordinates if enabled - if (renderWorldCoordinates) { - WorldPoint wp = objectModel.getLocation(); - if (wp != null) { - infoLines.add("Coords: " + wp.getX() + ", " + wp.getY() + " (Plane " + wp.getPlane() + ")"); - - // Canonical location for multi-tile objects - WorldPoint canonical = objectModel.getCanonicalLocation(); - if (canonical != null && !canonical.equals(wp)) { - infoLines.add("Canonical: " + canonical.getX() + ", " + canonical.getY()); - } - } - } - - // Add additional object info - infoLines.add("Distance: " + objectModel.getDistanceFromPlayer() + " tiles"); - - // Add object actions if available - prefer ObjectComposition actions for completeness - String[] actions = null; - ObjectComposition comp = objectModel.getObjectComposition(); - if (comp != null) { - actions = comp.getActions(); - } - if (actions == null || actions.length == 0) { - actions = objectModel.getActions(); // Fallback to model actions - } - - if (actions != null && actions.length > 0) { - java.util.List validActions = new java.util.ArrayList<>(); - for (String action : actions) { - if (action != null && !action.trim().isEmpty()) { - validActions.add(action); - } - } - if (!validActions.isEmpty()) { - // Limit display to first 3 actions to avoid clutter - int maxActions = Math.min(validActions.size(), 3); - infoLines.add("Actions: " + String.join(", ", validActions.subList(0, maxActions))); - if (validActions.size() > 3) { - infoLines.add(" ... +" + (validActions.size() - 3) + " more"); - } - } - } - - // Only set hover info if we have information to display - if (!infoLines.isEmpty()) { - Color borderColor = getBorderColorForObject(objectModel); - String entityType = "Object (" + objectModel.getObjectType().getTypeName() + ")"; - - // Use object's canvas location for positioning, or mouse position if hovering - net.runelite.api.Point displayLocation; - if (isHovering) { - displayLocation = client.getMouseCanvasPosition(); - } else { - // Use object's text location for non-hover display - displayLocation = tileObject.getCanvasTextLocation(graphics, "", 0); - } - - if (displayLocation != null) { - HoverInfoContainer.HoverInfo hoverInfo = new HoverInfoContainer.HoverInfo( - infoLines, displayLocation, borderColor, entityType); - ///HoverInfoContainer.setHoverInfo(hoverInfo); - renderDetailedInfoBox(graphics, infoLines, - displayLocation, - borderColor); - } - - } - - } catch (Exception e) { - log.debug("Failed to render object info for object {}: {}", objectModel.getId(), e.getMessage()); - } - } - - /** - * Renders a detailed info box with multiple lines of information. - * Each line is rendered separately with a colored border indicating the object type. - * - * @param graphics The graphics context - * @param infoLines List of information lines to display - * @param location The location to render the info box - * @param borderColor The border color (indicates object type) - */ - private void renderDetailedInfoBox(Graphics2D graphics, java.util.List infoLines, - net.runelite.api.Point location, Color borderColor) { - if (infoLines.isEmpty()) return; - - FontMetrics fm = graphics.getFontMetrics(); - int lineHeight = fm.getHeight(); - int maxWidth = 0; - - // Calculate the maximum width needed - for (String line : infoLines) { - int lineWidth = fm.stringWidth(line); - if (lineWidth > maxWidth) { - maxWidth = lineWidth; - } - } - - // Calculate box dimensions - int padding = 6; - int boxWidth = maxWidth + (padding * 2); - int boxHeight = (infoLines.size() * lineHeight) + (padding * 2); - - // Calculate box position (centered above the location) - int boxX = location.getX() - (boxWidth / 2); - int boxY = location.getY() - boxHeight - 10; // 10 pixels above the object - - // Draw the info box background - Color backgroundColor = new Color(0, 0, 0, 180); // Semi-transparent black - graphics.setColor(backgroundColor); - graphics.fillRect(boxX, boxY, boxWidth, boxHeight); - - // Draw the border in object type color - graphics.setColor(borderColor); - graphics.setStroke(new BasicStroke(2.0f)); - graphics.drawRect(boxX, boxY, boxWidth, boxHeight); - - // Draw the text lines - graphics.setColor(Color.WHITE); - for (int i = 0; i < infoLines.size(); i++) { - String line = infoLines.get(i); - int textX = boxX + padding; - int textY = boxY + padding + fm.getAscent() + (i * lineHeight); - graphics.drawString(line, textX, textY); - } - } - - /** - * Checks if a specific object type should be rendered. - * - * @param objectType The object type to check - * @return true if the object type should be rendered - */ - protected boolean isObjectTypeEnabled(Rs2ObjectModel.ObjectType objectType) { - switch (objectType) { - case GAME_OBJECT: - return enableGameObjects; - case WALL_OBJECT: - return enableWallObjects; - case DECORATIVE_OBJECT: - return enableDecorativeObjects; - case GROUND_OBJECT: - return enableGroundObjects; - default: - return true; - } - } - - /** - * Checks if the mouse is hovering over a tile object. - * - * @param tileObject The tile object to check - * @return true if mouse is hovering over the object - */ - private boolean isMouseHoveringOver(TileObject tileObject) { - try { - net.runelite.api.Point mousePos = client.getMouseCanvasPosition(); - if (mousePos == null) { - return false; - } - - // Check if mouse is over the object's convex hull - Shape shape = null; - try { - if (tileObject instanceof GameObject) { - shape = ((GameObject) tileObject).getConvexHull(); - } else if (tileObject instanceof WallObject) { - shape = ((WallObject) tileObject).getConvexHull(); - } else if (tileObject instanceof DecorativeObject) { - shape = ((DecorativeObject) tileObject).getConvexHull(); - } else if (tileObject instanceof GroundObject) { - shape = ((GroundObject) tileObject).getConvexHull(); - } - } catch (Exception e) { - log.debug("Failed to get convex hull for hover detection: {}", e.getMessage()); - } - - // Fallback to tile poly if no hull available - if (shape == null) { - try { - shape = tileObject.getCanvasTilePoly(); - } catch (Exception e) { - log.debug("Failed to get tile poly for hover detection: {}", e.getMessage()); - return false; - } - } - - return shape != null && shape.contains(mousePos.getX(), mousePos.getY()); - } catch (Exception e) { - log.debug("Error in mouse hover detection: {}", e.getMessage()); - return false; - } - } - - /** - * Renders text with a semi-transparent background for better readability. - * - * @param graphics The graphics context - * @param text The text to render - * @param location The location to render at - * @param color The text color - */ - private void renderTextWithBackground(Graphics2D graphics, String text, - net.runelite.api.Point location, Color color) { - try { - if (text == null || text.trim().isEmpty() || location == null) { - return; - } - - FontMetrics fm = graphics.getFontMetrics(); - if (fm == null) { - return; - } - - int textWidth = fm.stringWidth(text); - int textHeight = fm.getHeight(); - - // Create background rectangle - int padding = 4; - int backgroundX = location.getX() - padding; - int backgroundY = location.getY() - textHeight - padding; - int backgroundWidth = textWidth + (padding * 2); - int backgroundHeight = textHeight + (padding * 2); - - // Draw semi-transparent background - Color backgroundColor = new Color(0, 0, 0, 128); // Semi-transparent black - graphics.setColor(backgroundColor); - graphics.fillRect(backgroundX, backgroundY, backgroundWidth, backgroundHeight); - - // Draw text - renderText(graphics, text, location, color); - } catch (Exception e) { - log.debug("Error rendering text with background: {}", e.getMessage()); - } - } - - /** - * Gets the abbreviated object type string. - * - * @param objectType The object type - * @return Abbreviated type string - */ - private String getObjectTypeAbbreviation(Rs2ObjectModel.ObjectType objectType) { - switch (objectType) { - case GAME_OBJECT: - return "GO"; - case WALL_OBJECT: - return "WO"; - case DECORATIVE_OBJECT: - return "DO"; - case GROUND_OBJECT: - return "Gnd"; - case TILE_OBJECT: - return "TO"; - default: - return "?"; - } - } - - /** - * Gets the border color for a specific object type. - * Can be overridden by subclasses to provide type-specific coloring. - * - * @param objectType The object type - * @return The border color for this object type, or default colors if not overridden - */ - protected Color getBorderColorForObjectType(Rs2ObjectModel.ObjectType objectType) { - switch (objectType) { - case GAME_OBJECT: - return GAME_OBJECT_COLOR; - case WALL_OBJECT: - return WALL_OBJECT_COLOR; - case DECORATIVE_OBJECT: - return DECORATIVE_OBJECT_COLOR; - case GROUND_OBJECT: - return GROUND_OBJECT_COLOR; - default: - return getDefaultBorderColor(); - } - } - - /** - * Gets the fill color for a specific object type. - * Can be overridden by subclasses to provide type-specific coloring. - * - * @param objectType The object type - * @return The fill color for this object type, or null for default - */ - protected Color getFillColorForObjectType(Rs2ObjectModel.ObjectType objectType) { - Color borderColor = getBorderColorForObjectType(objectType); - return new Color(borderColor.getRed(), borderColor.getGreen(), borderColor.getBlue(), 50); - } - - /** - * Gets the border color for a specific object. - * Can be overridden by subclasses to provide per-object coloring. - * - * @param objectModel The object model - * @return The border color for this object - */ - protected Color getBorderColorForObject(Rs2ObjectModel objectModel) { - // Check for object type-specific coloring first - Color typeColor = getBorderColorForObjectType(objectModel.getObjectType()); - if (typeColor != null) { - return typeColor; - } - - return getDefaultBorderColor(); - } - - /** - * Gets the fill color for a specific object. - * Can be overridden by subclasses to provide per-object coloring. - * - * @param objectModel The object model - * @return The fill color for this object - */ - protected Color getFillColorForObject(Rs2ObjectModel objectModel) { - // Check for object type-specific coloring first - Color typeColor = getFillColorForObjectType(objectModel.getObjectType()); - if (typeColor != null) { - return typeColor; - } - - // Create a fill color based on the border color - Color borderColor = getBorderColorForObject(objectModel); - return new Color(borderColor.getRed(), borderColor.getGreen(), borderColor.getBlue(), 50); - } - - // ============================================ - // Configuration Methods - // ============================================ - - public Rs2ObjectCacheOverlay setRenderHull(boolean renderHull) { - this.renderHull = renderHull; - return this; - } - - public Rs2ObjectCacheOverlay setRenderClickbox(boolean renderClickbox) { - this.renderClickbox = renderClickbox; - return this; - } - - public Rs2ObjectCacheOverlay setRenderTile(boolean renderTile) { - this.renderTile = renderTile; - return this; - } - - public Rs2ObjectCacheOverlay setRenderOutline(boolean renderOutline) { - this.renderOutline = renderOutline; - return this; - } - - public Rs2ObjectCacheOverlay setRenderFilter(Predicate renderFilter) { - this.renderFilter = renderFilter; - return this; - } - - public Rs2ObjectCacheOverlay setRenderObjectInfo(boolean renderObjectInfo) { - this.renderObjectInfo = renderObjectInfo; - return this; - } - - public Rs2ObjectCacheOverlay setRenderWorldCoordinates(boolean renderWorldCoordinates) { - this.renderWorldCoordinates = renderWorldCoordinates; - return this; - } - - public Rs2ObjectCacheOverlay setOnlyShowTextOnHover(boolean onlyShowTextOnHover) { - this.onlyShowTextOnHover = onlyShowTextOnHover; - return this; - } - - public Rs2ObjectCacheOverlay setRenderObjectName(boolean renderObjectName) { - this.renderObjectName = renderObjectName; - return this; - } - - public Rs2ObjectCacheOverlay setEnableGameObjects(boolean enableGameObjects) { - this.enableGameObjects = enableGameObjects; - return this; - } - - public Rs2ObjectCacheOverlay setEnableWallObjects(boolean enableWallObjects) { - this.enableWallObjects = enableWallObjects; - return this; - } - - public Rs2ObjectCacheOverlay setEnableDecorativeObjects(boolean enableDecorativeObjects) { - this.enableDecorativeObjects = enableDecorativeObjects; - return this; - } - - public Rs2ObjectCacheOverlay setEnableGroundObjects(boolean enableGroundObjects) { - this.enableGroundObjects = enableGroundObjects; - return this; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/CacheSerializable.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/CacheSerializable.java deleted file mode 100644 index eca420eb3c6..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/CacheSerializable.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.serialization; - -/** - * Interface for cache values that can be serialized to RuneLite profile config. - * Implementing this interface indicates that the cache supports persistence across sessions. - */ -public interface CacheSerializable { - - /** - * Gets the config key for this cache type. - * This should be unique for each cache type. - * - * @return The config key for storing this cache data - */ - String getConfigKey(); - - /** - * Gets the config group for this cache type. - * Typically "microbot" for all microbot caches. - * - * @return The config group for storing this cache data - */ - default String getConfigGroup() { - return "microbot"; - } - - /** - * Determines if this cache should be persisted. - * Some cache types like NPCs, Objects, and Ground Items should not be persisted - * as they are dynamically loaded and change frequently. - * - * @return true if this cache should be saved/loaded from config - */ - boolean shouldPersist(); -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/CacheSerializationManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/CacheSerializationManager.java deleted file mode 100644 index ca429ea449e..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/CacheSerializationManager.java +++ /dev/null @@ -1,757 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.serialization; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonSyntaxException; -import com.google.gson.reflect.TypeToken; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Quest; -import net.runelite.api.QuestState; -import net.runelite.api.Skill; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.Rs2Cache; -import net.runelite.client.plugins.microbot.util.cache.model.SkillData; -import net.runelite.client.plugins.microbot.util.cache.model.SpiritTreeData; -import net.runelite.client.plugins.microbot.util.cache.model.VarbitData; -import net.runelite.client.plugins.microbot.util.farming.SpiritTree; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -import java.lang.reflect.Type; -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; -import java.io.IOException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; -import java.util.Map; -import java.util.UUID; - - -/** - * Serialization manager for Rs2UnifiedCache instances. - * Handles automatic save/load to file-based storage under .runelite/microbot-profiles - * to prevent RuneLite profile bloat and improve performance. - * - * Includes cache freshness tracking to prevent loading stale cache data - * that wasn't properly saved due to ungraceful client shutdowns. - * - * Cache freshness is determined by whether data was saved after being loaded, - * not by session ID or time limits (unless explicitly specified). - * This ensures we only load cache data that was properly persisted after modifications. - */ -@Slf4j -public class CacheSerializationManager { - private static final String VERSION = "1.0.0"; // Version for cache serialization format compatibility - private static final String BASE_DIRECTORY = ".runelite/microbot-profiles"; - private static final String CACHE_SUBDIRECTORY = "caches"; - private static final String METADATA_SUFFIX = ".metadata"; - private static final String JSON_EXTENSION = ".json"; - - private static final Gson gson; - - // Session identifier to track cache freshness across client restarts - private static final String SESSION_ID = UUID.randomUUID().toString(); - - /** - * Enhanced metadata class to track cache freshness, validity, and integrity. - * Inspired by GLite's PersistenceMetadata with data size tracking and cache naming. - */ - private static class CacheMetadata { - private final String version; - private final String sessionId; - private final String saveTimestampUtc; // UTC timestamp in ISO 8601 format - private final boolean stale; - private final int dataSize; // Size of serialized data for integrity checks - private final String cacheName; // Name of cache for debugging - - // UTC formatter for consistent timestamp handling - private static final DateTimeFormatter UTC_FORMATTER = DateTimeFormatter.ISO_INSTANT; - - public CacheMetadata(String version, String sessionId, String saveTimestampUtc, boolean stale, int dataSize, String cacheName) { - this.version = version; - this.sessionId = sessionId; - this.saveTimestampUtc = saveTimestampUtc; - this.stale = stale; - this.dataSize = dataSize; - this.cacheName = cacheName; - } - - /** - * Create CacheMetadata with current UTC timestamp - */ - public static CacheMetadata createWithCurrentUtcTime(String version, String sessionId, boolean stale, int dataSize, String cacheName) { - String utcTimestamp = Instant.now().atOffset(ZoneOffset.UTC).format(UTC_FORMATTER); - return new CacheMetadata(version, sessionId, utcTimestamp, stale, dataSize, cacheName); - } - - /** - * Create CacheMetadata with current UTC timestamp and convenience method for common use - */ - public static CacheMetadata createWithCurrentUtcTime(String version, String sessionId, boolean stale) { - String utcTimestamp = Instant.now().atOffset(ZoneOffset.UTC).format(UTC_FORMATTER); - return new CacheMetadata(version, sessionId, utcTimestamp, stale, 0, "unknown"); - } - - public boolean isNewVersion(String currentVersion){ - // Check if the current version is different from the saved version - return !this.version.equals(currentVersion); - } - - /** - * Checks if this metadata indicates fresh cache data that was properly saved after loading. - * - * @param maxAgeMs Maximum age in milliseconds (0 = ignore time completely) - * @return true if cache data is fresh and was saved after loading - */ - public boolean isFresh(long maxAgeMs) { - // Data is fresh if it was saved after being loaded (indicating proper persistence) - if (stale) { - return false; - } - - // If maxAgeMs is 0, we don't care about time - only that it was saved after load - if (maxAgeMs == 0) { - return true; - } - - // Otherwise check if it's within the time limit - long age = getAgeMs(); - return age <= maxAgeMs; - } - - public boolean isFromCurrentSession() { - return SESSION_ID.equals(sessionId); - } - - /** - * Get age in milliseconds from the UTC timestamp - */ - public long getAgeMs() { - try { - Instant saveTime = Instant.parse(saveTimestampUtc); - return Instant.now().toEpochMilli() - saveTime.toEpochMilli(); - } catch (Exception e) { - log.warn("Failed to parse UTC timestamp '{}', treating as very old", saveTimestampUtc); - return Long.MAX_VALUE; // Treat as very old if parsing fails - } - } - - /** - * Get the save timestamp as human-readable string - */ - public String getSaveTimeFormatted() { - try { - Instant saveTime = Instant.parse(saveTimestampUtc); - return saveTime.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + " UTC"; - } catch (Exception e) { - return saveTimestampUtc; // Return raw if parsing fails - } - } - - public boolean isStale() { - return stale; - } - - /** - * Get the data size for integrity checking - */ - public int getDataSize() { - return dataSize; - } - - /** - * Get the cache name for debugging - */ - public String getCacheName() { - return cacheName; - } - - /** - * Validates that this metadata has reasonable values - */ - public void validate() throws IllegalStateException { - if (version == null || version.trim().isEmpty()) { - throw new IllegalStateException("Version cannot be null or empty"); - } - if (dataSize < 0) { - throw new IllegalStateException("Data size cannot be negative"); - } - if (saveTimestampUtc == null || saveTimestampUtc.trim().isEmpty()) { - throw new IllegalStateException("Save timestamp cannot be null or empty"); - } - } - - /** - * Gets a human-readable age description - */ - public String getFormattedAge() { - long ageMs = getAgeMs(); - - if (ageMs < 1000) { - return ageMs + "ms ago"; - } else if (ageMs < 60_000) { - return (ageMs / 1000) + "s ago"; - } else if (ageMs < 3_600_000) { - return (ageMs / 60_000) + "m ago"; - } else if (ageMs < 86_400_000) { - return (ageMs / 3_600_000) + "h ago"; - } else { - return (ageMs / 86_400_000) + "d ago"; - } - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("CacheMetadata{"); - sb.append("version='").append(version).append("'"); - sb.append(", cacheName='").append(cacheName).append("'"); - sb.append(", timestamp=").append(getSaveTimeFormatted()); - sb.append(" (").append(getFormattedAge()).append(")"); - sb.append(", dataSize=").append(dataSize); - sb.append(", stale=").append(stale); - sb.append(", sessionId='").append(sessionId).append("'"); - sb.append("}"); - return sb.toString(); - } - } - - // Initialize Gson with custom adapters - static { - gson = new GsonBuilder() - .registerTypeAdapter(Skill.class, new SkillAdapter()) - .registerTypeAdapter(Quest.class, new QuestAdapter()) - .registerTypeAdapter(QuestState.class, new QuestStateAdapter()) - .registerTypeAdapter(SkillData.class, new SkillDataAdapter()) - .registerTypeAdapter(VarbitData.class, new VarbitDataAdapter()) - .registerTypeAdapter(SpiritTree.class, new SpiritTreePatchAdapter()) - .registerTypeAdapter(SpiritTreeData.class, new SpiritTreeDataAdapter()) - .create(); - } - - /** - * Saves a cache to file-based storage with character-specific directory structure. - * Also stores metadata to track cache freshness and prevent loading stale data. - * - * @param cache The cache to save - * @param configKey The cache type identifier (skills, quests, etc.) - * @param rsProfileKey The RuneLite profile key - * @param playerName The player name for character-specific caching - * @param The key type - * @param The value type - */ - public static void saveCache(Rs2Cache cache, String configKey, String rsProfileKey, String playerName) { - try { - if (rsProfileKey == null) { - log.warn("Cannot save cache {}: profile key not available", configKey); - return; - } - - if (playerName == null || playerName.trim().isEmpty()) { - log.warn("Cannot save cache {}: player name not available", configKey); - return; - } - - // create directory structure - Path cacheDir = getCacheDirectory(rsProfileKey, playerName); - Files.createDirectories(cacheDir); - - // serialize cache data - String json = serializeCacheData(cache, configKey); - if (json == null || json.trim().isEmpty()) { - log.warn("No data to save for cache {} for player {}", configKey, playerName); - return; - } - - // save cache data file - Path cacheFile = cacheDir.resolve(configKey + JSON_EXTENSION); - Files.write(cacheFile, json.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - - // create and save metadata with data size and cache name - CacheMetadata metadata = CacheMetadata.createWithCurrentUtcTime(VERSION, SESSION_ID, false, json.length(), configKey); - String metadataJson = gson.toJson(metadata, CacheMetadata.class); - Path metadataFile = cacheDir.resolve(configKey + METADATA_SUFFIX); - Files.write(metadataFile, metadataJson.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - - log.info("Saved cache \"{}\" for player \"{}\" to file ({} chars) at {}", - configKey, playerName, json.length(), metadata.getSaveTimeFormatted()); - - } catch (IOException e) { - log.error("Failed to save cache {} to file for player {}", configKey, playerName, e); - } catch (Exception e) { - log.error("Failed to save cache {} for player {}", configKey, playerName, e); - } - } - - - - /** - * Gets the current player name using Rs2Player utility. - * - * @return Player name or null if not available - */ - private static String getCurrentPlayerName() { - try { - if (!Microbot.isLoggedIn()) { - return null; - } - // use Rs2Player to get local player and extract name - var localPlayer = Rs2Player.getLocalPlayer(); - return localPlayer != null ? localPlayer.getName() : null; - } catch (Exception e) { - log.debug("Error getting current player name: {}", e.getMessage()); - return null; - } - } - - - /** - * Loads a cache from file-based storage with character-specific directory structure. - * Checks cache freshness metadata before loading to prevent loading stale data. - * - * @param cache The cache to load into - * @param configKey The cache type identifier (skills, quests, etc.) - * @param rsProfileKey The profile key to load from - * @param playerName The player name for character-specific caching - * @param forceInvalidate Whether to force cache invalidation - * @param The key type - * @param The value type - */ - public static void loadCache(Rs2Cache cache, String configKey, String rsProfileKey, String playerName, boolean forceInvalidate) { - loadCache(cache, configKey, rsProfileKey, playerName, 0, forceInvalidate); // Default: ignore time, only check if saved after load - } - - - /** - * Loads a cache from file-based storage with age limit. - * Checks cache freshness metadata before loading to prevent loading stale data. - * - * @param cache The cache to load into - * @param configKey The cache type identifier - * @param rsProfileKey The profile key to load from - * @param playerName The player name for character-specific caching - * @param maxAgeMs Maximum age in milliseconds (0 = ignore time completely) - * @param forceInvalidate Whether to force cache invalidation - * @param The key type - * @param The value type - */ - public static void loadCache(Rs2Cache cache, String configKey, String rsProfileKey, String playerName, long maxAgeMs, boolean forceInvalidate) { - try { - if (rsProfileKey == null) { - log.warn("Cannot load cache {}: profile key not available", configKey); - return; - } - - if (playerName == null || playerName.trim().isEmpty()) { - log.warn("Cannot load cache {}: player name not available", configKey); - return; - } - - Path cacheDir = getCacheDirectory(rsProfileKey, playerName); - Path metadataFile = cacheDir.resolve(configKey + METADATA_SUFFIX); - Path cacheFile = cacheDir.resolve(configKey + JSON_EXTENSION); - - // check if files exist - if (!Files.exists(metadataFile) || !Files.exists(cacheFile)) { - log.debug("No cache files found for {} player {}, starting fresh", configKey, playerName); - if (forceInvalidate) cache.invalidateAll(); - - // create initial stale metadata to track first load - CacheMetadata loadedMetadata = CacheMetadata.createWithCurrentUtcTime(VERSION, SESSION_ID, true, 0, configKey); - String metadataJson = gson.toJson(loadedMetadata, CacheMetadata.class); - Files.createDirectories(cacheDir); - Files.write(metadataFile, metadataJson.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - return; - } - - // read and validate metadata - String metadataJson = Files.readString(metadataFile); - CacheMetadata metadata = gson.fromJson(metadataJson, CacheMetadata.class); - - if (metadata == null || !metadata.isFresh(maxAgeMs)) { - log.warn("Cache \"{}\" for player \"{}\" metadata indicates stale data (age: {}ms, fresh: {})", - configKey, playerName, metadata != null ? metadata.getAgeMs() : "unknown", - metadata != null ? metadata.isFresh(maxAgeMs) : false); - - if (forceInvalidate) cache.invalidateAll(); - return; - } - - // load cache data - String json = Files.readString(cacheFile); - if (json != null && !json.trim().isEmpty()) { - deserializeCacheData(cache, configKey, json); - log.debug("Loaded cache {} for player {} from file, entries loaded: {}", configKey, playerName, cache.size()); - } else { - log.warn("Cache file exists but contains no data for {} player {}", configKey, playerName); - } - - // mark as loaded but stale until next save - CacheMetadata loadedMetadata = CacheMetadata.createWithCurrentUtcTime(VERSION, SESSION_ID, true, json.length(), configKey); - String updatedMetadataJson = gson.toJson(loadedMetadata, CacheMetadata.class); - Files.write(metadataFile, updatedMetadataJson.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - - } catch (JsonSyntaxException e) { - log.warn("Failed to parse cache data for {} player {}, clearing corrupted cache", configKey, playerName, e); - clearCacheFiles(configKey, rsProfileKey, playerName); - } catch (IOException e) { - log.error("Failed to load cache {} for player {} from file", configKey, playerName, e); - } catch (Exception e) { - log.error("Failed to load cache {} for player {}", configKey, playerName, e); - } - } - - /** - * Clears cache files for current player and profile. - * - * @param configKey The cache type to clear - */ - public static void clearCache(String configKey) { - try { - String rsProfileKey = Microbot.getConfigManager() != null ? Microbot.getConfigManager().getRSProfileKey() : null; - String playerName = getCurrentPlayerName(); - clearCacheFiles(configKey, rsProfileKey, playerName); - } catch (Exception e) { - log.error("Failed to clear cache {} files", configKey, e); - } - } - - /** - * Clears cache files for a specific profile and player. - * - * @param configKey The cache type to clear - * @param rsProfileKey The profile key - */ - public static void clearCache(String configKey, String rsProfileKey) { - String playerName = getCurrentPlayerName(); - clearCacheFiles(configKey, rsProfileKey, playerName); - } - - /** - * Clears cache files for specific cache type, profile, and player. - * - * @param configKey The cache type to clear - * @param rsProfileKey The profile key - * @param playerName The player name - */ - public static void clearCacheFiles(String configKey, String rsProfileKey, String playerName) { - try { - if (rsProfileKey == null || playerName == null || playerName.trim().isEmpty()) { - log.warn("Cannot clear cache files: profile key or player name not available"); - return; - } - - Path cacheDir = getCacheDirectory(rsProfileKey, playerName); - Path cacheFile = cacheDir.resolve(configKey + JSON_EXTENSION); - Path metadataFile = cacheDir.resolve(configKey + METADATA_SUFFIX); - - Files.deleteIfExists(cacheFile); - Files.deleteIfExists(metadataFile); - - log.debug("Cleared cache files for {} player {}", configKey, playerName); - } catch (IOException e) { - log.error("Failed to clear cache files for {} player {}", configKey, playerName, e); - } - } - - /** - * Gets the cache directory path for a profile and player using URL encoding for safety. - * This ensures that different profiles/players don't collide into the same directory. - */ - private static Path getCacheDirectory(String profileKey, String playerName) { - try { - // use URL encoding to safely handle special characters while preserving uniqueness - String encodedPlayerName = URLEncoder.encode(playerName, StandardCharsets.UTF_8); - String encodedProfileKey = URLEncoder.encode(profileKey, StandardCharsets.UTF_8); - - Path userHome = Paths.get(System.getProperty("user.home")); - return userHome.resolve(BASE_DIRECTORY) - .resolve(encodedProfileKey) - .resolve(encodedPlayerName) - .resolve(CACHE_SUBDIRECTORY); - } catch (Exception e) { - log.error("Failed to encode path components, falling back to basic sanitization", e); - // fallback to basic sanitization if URL encoding fails - String sanitizedPlayerName = playerName.replaceAll("[^a-zA-Z0-9_-]", "_"); - String sanitizedProfileKey = profileKey.replaceAll("[^a-zA-Z0-9_-]", "_"); - - Path userHome = Paths.get(System.getProperty("user.home")); - return userHome.resolve(BASE_DIRECTORY) - .resolve(sanitizedProfileKey) - .resolve(sanitizedPlayerName) - .resolve(CACHE_SUBDIRECTORY); - } - } - - /** - * Serializes cache data to JSON based on cache type. - * This method handles different cache types with specific serialization strategies. - * Only persistent caches are serialized (Skills, Quests, Varbits). - * NPC cache is excluded as it's dynamically loaded based on game scene. - */ - @SuppressWarnings("unchecked") - public static String serializeCacheData(Rs2Cache cache, String configKey) { - try { - log.debug("Starting serialization for cache type: {}", configKey); - - // Handle different cache types - using actual config keys from caches - switch (configKey) { - case "skills": - String skillJson = serializeSkillCache((Rs2Cache) cache); - log.debug("Skills serialization completed, JSON length: {}", skillJson != null ? skillJson.length() : 0); - return skillJson; - case "quests": - String questJson = serializeQuestCache((Rs2Cache) cache); - log.debug("Quests serialization completed, JSON length: {}", questJson != null ? questJson.length() : 0); - return questJson; - case "varbits": - String varbitJson = serializeVarbitCache((Rs2Cache) cache); - log.debug("Varbits serialization completed, JSON length: {}", varbitJson != null ? varbitJson.length() : 0); - return varbitJson; - case "varPlayerCache": - String varPlayerJson = serializeVarPlayerCache((Rs2Cache) cache); - log.debug("VarPlayer serialization completed, JSON length: {}", varPlayerJson != null ? varPlayerJson.length() : 0); - return varPlayerJson; - case "spiritTrees": - String spiritTreeJson = serializeSpiritTreeCache((Rs2Cache) cache); - log.debug("SpiritTrees serialization completed, JSON length: {}", spiritTreeJson != null ? spiritTreeJson.length() : 0); - return spiritTreeJson; - default: - log.warn("Unknown cache type for serialization: {}", configKey); - return null; - } - } catch (Exception e) { - log.error("Failed to serialize cache data for {}", configKey, e); - return null; - } - } - - /** - * Deserializes cache data from JSON based on cache type. - * Only persistent caches are deserialized (Skills, Quests, Varbits). - * NPC cache is excluded as it's dynamically loaded based on game scene. - */ - @SuppressWarnings("unchecked") - public static void deserializeCacheData(Rs2Cache cache, String configKey, String json) { - try { - log.debug("Starting deserialization for cache type: {}, JSON length: {}", configKey, json != null ? json.length() : 0); - - switch (configKey) { - case "skills": - deserializeSkillCache((Rs2Cache) cache, json); - break; - case "quests": - deserializeQuestCache((Rs2Cache) cache, json); - break; - case "varbits": - deserializeVarbitCache((Rs2Cache) cache, json); - break; - case "varPlayerCache": - deserializeVarPlayerCache((Rs2Cache) cache, json); - break; - case "spiritTrees": - deserializeSpiritTreeCache((Rs2Cache) cache, json); - break; - default: - log.warn("Unknown cache type for deserialization: {}", configKey); - } - - log.debug("Deserialization completed for cache type: {}, final cache size: {}", configKey, cache.size()); - } catch (Exception e) { - log.error("Failed to deserialize cache data for {}", configKey, e); - } - } - - // Skill cache serialization - private static String serializeSkillCache(Rs2Cache cache) { - // Use the new method to get all entries for serialization - Map data = cache.getEntriesForSerialization(); - log.debug("Serializing {} skill entries", data.size()); - if (data.isEmpty()) { - log.warn("Skills cache is empty during serialization"); - return "{}"; - } - String json = gson.toJson(data); - log.debug("Skills JSON preview: {}", json.length() > 200 ? json.substring(0, 200) + "..." : json); - return json; - } - - private static void deserializeSkillCache(Rs2Cache cache, String json) { - Type type = new TypeToken>(){}.getType(); - Map data = gson.fromJson(json, type); - if (data != null) { - int entriesLoaded = 0; - int entriesSkipped = 0; - for (Map.Entry entry : data.entrySet()) { - // Only load entries that are not already present in cache (cache entries are newer) - if (!cache.containsKey(entry.getKey())) { - cache.put(entry.getKey(), entry.getValue()); - entriesLoaded++; - } else { - entriesSkipped++; - log.debug("Skipped loading skill {} - already present in cache with newer data", entry.getKey()); - } - } - log.debug("Deserialized {} skill entries into cache, skipped {} existing entries", entriesLoaded, entriesSkipped); - } else { - log.warn("Skill cache data was null after JSON parsing"); - } - } - - // Quest cache serialization - private static String serializeQuestCache(Rs2Cache cache) { - // Use the new method to get all entries for serialization - Map data = cache.getEntriesForSerialization(); - log.debug("Serializing {} quest entries", data.size()); - if (data.isEmpty()) { - log.warn("Quest cache is empty during serialization"); - return "{}"; - } - String json = gson.toJson(data); - log.debug("Quest JSON preview: {}", json.length() > 200 ? json.substring(0, 200) + "..." : json); - return json; - } - - private static void deserializeQuestCache(Rs2Cache cache, String json) { - Type type = new TypeToken>(){}.getType(); - Map data = gson.fromJson(json, type); - if (data != null) { - int entriesLoaded = 0; - int entriesSkipped = 0; - for (Map.Entry entry : data.entrySet()) { - // Only load entries that are not already present in cache (cache entries are newer) - if (!cache.containsKey(entry.getKey())) { - cache.put(entry.getKey(), entry.getValue()); - entriesLoaded++; - } else { - entriesSkipped++; - log.debug("Skipped loading quest {} - already present in cache with newer data", entry.getKey()); - } - } - log.debug("Deserialized {} quest entries into cache, skipped {} existing entries", entriesLoaded, entriesSkipped); - } else { - log.warn("Quest cache data was null after JSON parsing"); - } - } - - // Varbit cache serialization - private static String serializeVarbitCache(Rs2Cache cache) { - // Use the new method to get all entries for serialization - Map data = cache.getEntriesForSerialization(); - log.debug("Serializing {} varbit entries", data.size()); - if (data.isEmpty()) { - log.warn("Varbit cache is empty during serialization"); - return "{}"; - } - String json = gson.toJson(data); - log.debug("Varbit JSON preview: {}", json.length() > 200 ? json.substring(0, 200) + "..." : json); - return json; - } - - private static void deserializeVarbitCache(Rs2Cache cache, String json) { - Type type = new TypeToken>(){}.getType(); - Map data = gson.fromJson(json, type); - if (data != null) { - int entriesLoaded = 0; - int entriesSkipped = 0; - for (Map.Entry entry : data.entrySet()) { - // Only load entries that are not already present in cache (cache entries are newer) - if (!cache.containsKey(entry.getKey())) { - cache.put(entry.getKey(), entry.getValue()); - entriesLoaded++; - } else { - entriesSkipped++; - log.debug("Skipped loading varbit {} - already present in cache with newer data", entry.getKey()); - } - } - - log.debug("Deserialized {} varbit entries into cache, skipped {} existing entries", entriesLoaded, entriesSkipped); - } else { - log.warn("Varbit cache data was null after JSON parsing"); - } - } - - // VarPlayer cache serialization - reuses VarbitData structure - private static String serializeVarPlayerCache(Rs2Cache cache) { - // Use the new method to get all entries for serialization - Map data = cache.getEntriesForSerialization(); - log.debug("Serializing {} varplayer entries", data.size()); - if (data.isEmpty()) { - log.warn("VarPlayer cache is empty during serialization"); - return "{}"; - } - String json = gson.toJson(data); - log.debug("VarPlayer JSON preview: {}", json.length() > 200 ? json.substring(0, 200) + "..." : json); - return json; - } - - private static void deserializeVarPlayerCache(Rs2Cache cache, String json) { - Type type = new TypeToken>(){}.getType(); - Map data = gson.fromJson(json, type); - if (data != null) { - int entriesLoaded = 0; - int entriesSkipped = 0; - for (Map.Entry entry : data.entrySet()) { - // Only load entries that are not already present in cache (cache entries are newer) - if (!cache.containsKey(entry.getKey())) { - cache.put(entry.getKey(), entry.getValue()); - entriesLoaded++; - } else { - entriesSkipped++; - log.debug("Skipped loading varplayer {} - already present in cache with newer data", entry.getKey()); - } - } - - log.debug("Deserialized {} varplayer entries into cache, skipped {} existing entries", entriesLoaded, entriesSkipped); - } else { - log.warn("VarPlayer cache data was null after JSON parsing"); - } - } - - // Spirit tree cache serialization - private static String serializeSpiritTreeCache(Rs2Cache cache) { - // Use the new method to get all entries for serialization - Map data = cache.getEntriesForSerialization(); - return gson.toJson(data); - } - - private static void deserializeSpiritTreeCache(Rs2Cache cache, String json) { - Type type = new TypeToken>(){}.getType(); - Map data = gson.fromJson(json, type); - if (data != null) { - int entriesLoaded = 0; - int entriesSkipped = 0; - for (Map.Entry entry : data.entrySet()) { - // Only load entries that are not already present in cache (cache entries are newer) - if (!cache.containsKey(entry.getKey())) { - cache.put(entry.getKey(), entry.getValue()); - entriesLoaded++; - } else { - entriesSkipped++; - log.debug("Skipped loading spirit tree {} - already present in cache with newer data", entry.getKey()); - } - } - log.debug("Deserialized {} spirit tree entries into cache, skipped {} existing entries", entriesLoaded, entriesSkipped); - } else { - log.warn("Spirit tree cache data was null after JSON parsing"); - } - } - - - /** - * Creates a character-specific config key by appending player name. - * - * @param baseKey The base config key - * @param playerName The player name - * @return Character-specific config key - */ - public static String createCharacterSpecificKey(String baseKey, String playerName) { - // sanitize player name for config key usage - String sanitizedPlayerName = playerName.replaceAll("[^a-zA-Z0-9_-]", "_"); - return baseKey + "_" + sanitizedPlayerName; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/QuestAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/QuestAdapter.java deleted file mode 100644 index 3283af1a322..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/QuestAdapter.java +++ /dev/null @@ -1,54 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.serialization; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; -import net.runelite.api.Quest; - -import java.io.IOException; - -/** - * Gson TypeAdapter for Quest enum serialization/deserialization. - * Stores quests as their ordinal values for compact representation. - */ -public class QuestAdapter extends TypeAdapter { - - @Override - public void write(JsonWriter out, Quest value) throws IOException { - if (value == null) { - out.nullValue(); - } else { - out.value(value.ordinal()); - } - } - - @Override - public Quest read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - - // Handle both string names (legacy) and ordinal values (new format) - if (in.peek() == JsonToken.STRING) { - // Legacy format: enum name - String questName = in.nextString(); - try { - return Quest.valueOf(questName); - } catch (IllegalArgumentException e) { - throw new IOException("Invalid quest name: " + questName, e); - } - } else { - // New format: ordinal value - int ordinal = in.nextInt(); - Quest[] quests = Quest.values(); - - if (ordinal >= 0 && ordinal < quests.length) { - return quests[ordinal]; - } else { - throw new IOException("Invalid quest ordinal: " + ordinal); - } - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/QuestStateAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/QuestStateAdapter.java deleted file mode 100644 index cd15eaeaee2..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/QuestStateAdapter.java +++ /dev/null @@ -1,54 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.serialization; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; -import net.runelite.api.QuestState; - -import java.io.IOException; - -/** - * Gson TypeAdapter for QuestState enum serialization/deserialization. - * Stores quest states as their ordinal values for compact representation. - */ -public class QuestStateAdapter extends TypeAdapter { - - @Override - public void write(JsonWriter out, QuestState value) throws IOException { - if (value == null) { - out.nullValue(); - } else { - out.value(value.ordinal()); - } - } - - @Override - public QuestState read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - - // Handle both string names (legacy) and ordinal values (new format) - if (in.peek() == JsonToken.STRING) { - // Legacy format: enum name - String stateName = in.nextString(); - try { - return QuestState.valueOf(stateName); - } catch (IllegalArgumentException e) { - throw new IOException("Invalid quest state name: " + stateName, e); - } - } else { - // New format: ordinal value - int ordinal = in.nextInt(); - QuestState[] states = QuestState.values(); - - if (ordinal >= 0 && ordinal < states.length) { - return states[ordinal]; - } else { - throw new IOException("Invalid quest state ordinal: " + ordinal); - } - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SkillAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SkillAdapter.java deleted file mode 100644 index 85d6d36f708..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SkillAdapter.java +++ /dev/null @@ -1,54 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.serialization; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; -import net.runelite.api.Skill; - -import java.io.IOException; - -/** - * Gson TypeAdapter for Skill enum serialization/deserialization. - * Stores skills as their ordinal values for compact representation. - */ -public class SkillAdapter extends TypeAdapter { - - @Override - public void write(JsonWriter out, Skill value) throws IOException { - if (value == null) { - out.nullValue(); - } else { - out.value(value.ordinal()); - } - } - - @Override - public Skill read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - - // Handle both string names (legacy) and ordinal values (new format) - if (in.peek() == JsonToken.STRING) { - // Legacy format: enum name - String skillName = in.nextString(); - try { - return Skill.valueOf(skillName); - } catch (IllegalArgumentException e) { - throw new IOException("Invalid skill name: " + skillName, e); - } - } else { - // New format: ordinal value - int ordinal = in.nextInt(); - Skill[] skills = Skill.values(); - - if (ordinal >= 0 && ordinal < skills.length) { - return skills[ordinal]; - } else { - throw new IOException("Invalid skill ordinal: " + ordinal); - } - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SkillDataAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SkillDataAdapter.java deleted file mode 100644 index 71817024790..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SkillDataAdapter.java +++ /dev/null @@ -1,88 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.serialization; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; -import net.runelite.client.plugins.microbot.util.cache.model.SkillData; - -import java.io.IOException; - -/** - * Gson TypeAdapter for SkillData serialization/deserialization. - * Stores skill data as a compact array: [level, boostedLevel, experience, lastUpdated, previousLevel, previousExperience]. - * Previous values are optional and stored as null if not available. - */ -public class SkillDataAdapter extends TypeAdapter { - - @Override - public void write(JsonWriter out, SkillData value) throws IOException { - if (value == null) { - out.nullValue(); - } else { - out.beginArray(); - out.value(value.getLevel()); - out.value(value.getBoostedLevel()); - out.value(value.getExperience()); - out.value(value.getLastUpdated()); - - // Write previous level (nullable) - if (value.getPreviousLevel() != null) { - out.value(value.getPreviousLevel()); - } else { - out.nullValue(); - } - - // Write previous experience (nullable) - if (value.getPreviousExperience() != null) { - out.value(value.getPreviousExperience()); - } else { - out.nullValue(); - } - - out.endArray(); - } - } - - @Override - public SkillData read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - - in.beginArray(); - int level = in.nextInt(); - int boostedLevel = in.nextInt(); - int experience = in.nextInt(); - - // Handle backwards compatibility - check if more elements exist - long lastUpdated = System.currentTimeMillis(); - Integer previousLevel = null; - Integer previousExperience = null; - - if (in.hasNext()) { - lastUpdated = in.nextLong(); - - if (in.hasNext()) { - if (in.peek() != JsonToken.NULL) { - previousLevel = in.nextInt(); - } else { - in.nextNull(); - } - - if (in.hasNext()) { - if (in.peek() != JsonToken.NULL) { - previousExperience = in.nextInt(); - } else { - in.nextNull(); - } - } - } - } - - in.endArray(); - - return new SkillData(level, boostedLevel, experience, lastUpdated, previousLevel, previousExperience); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SpiritTreeDataAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SpiritTreeDataAdapter.java deleted file mode 100644 index b79eed59038..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SpiritTreeDataAdapter.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (c) 2024 Microbot - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package net.runelite.client.plugins.microbot.util.cache.serialization; - -import com.google.gson.*; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.questhelper.helpers.mischelpers.farmruns.CropState; -import net.runelite.client.plugins.microbot.util.cache.model.SpiritTreeData; -import net.runelite.client.plugins.microbot.util.farming.SpiritTree; - -import java.lang.reflect.Type; - -/** - * Gson adapter for SpiritTreeData serialization/deserialization. - * Handles safe serialization of spirit tree cache data for persistent storage. - */ -public class SpiritTreeDataAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(SpiritTreeData src, Type typeOfSrc, JsonSerializationContext context) { - JsonObject json = new JsonObject(); - - try { - // Store patch as enum name for safe serialization - json.addProperty("patch", src.getSpiritTree().name()); - - // Store crop state as enum name (nullable) - if (src.getCropState() != null) { - json.addProperty("cropState", src.getCropState().name()); - } - - json.addProperty("availableForTravel", src.isAvailableForTravel()); - json.addProperty("lastUpdated", src.getLastUpdated()); - - // Store player location - if (src.getPlayerLocation() != null) { - JsonObject location = new JsonObject(); - location.addProperty("x", src.getPlayerLocation().getX()); - location.addProperty("y", src.getPlayerLocation().getY()); - location.addProperty("plane", src.getPlayerLocation().getPlane()); - json.add("playerLocation", location); - } - - // Store detection method flags - json.addProperty("detectedViaWidget", src.isDetectedViaWidget()); - json.addProperty("detectedViaNearBy", src.isDetectedViaNearBy()); - - // Remove farming level storage as it's no longer used - - // Store nearby entity IDs (optional, for debugging purposes only) - // Note: We don't serialize these as they're not persistent across sessions - - } catch (Exception e) { - // Create minimal fallback serialization - json.addProperty("patch", src.getSpiritTree().name()); - json.addProperty("availableForTravel", src.isAvailableForTravel()); - json.addProperty("lastUpdated", src.getLastUpdated()); - } - - return json; - } - - @Override - public SpiritTreeData deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - - JsonObject jsonObject = json.getAsJsonObject(); - - try { - // Required fields - String patchName = jsonObject.get("patch").getAsString(); - SpiritTree patch = SpiritTree.valueOf(patchName); - - boolean availableForTravel = jsonObject.get("availableForTravel").getAsBoolean(); - long lastUpdated = jsonObject.get("lastUpdated").getAsLong(); - - // Optional fields - CropState cropState = null; - if (jsonObject.has("cropState") && !jsonObject.get("cropState").isJsonNull()) { - cropState = CropState.valueOf(jsonObject.get("cropState").getAsString()); - } - - WorldPoint playerLocation = null; - if (jsonObject.has("playerLocation") && !jsonObject.get("playerLocation").isJsonNull()) { - JsonObject location = jsonObject.getAsJsonObject("playerLocation"); - playerLocation = new WorldPoint( - location.get("x").getAsInt(), - location.get("y").getAsInt(), - location.get("plane").getAsInt() - ); - } - - boolean detectedViaWidget = jsonObject.has("detectedViaWidget") ? - jsonObject.get("detectedViaWidget").getAsBoolean() : false; - - // Handle backward compatibility: check for old field names first, then new field name - boolean detectedViaNearBy = false; - if (jsonObject.has("detectedViaNearBy")) { - detectedViaNearBy = jsonObject.get("detectedViaNearBy").getAsBoolean(); - } else if (jsonObject.has("detectedViaNearPatch")) { - // Backward compatibility: migrate old field to new field - detectedViaNearBy = jsonObject.get("detectedViaNearPatch").getAsBoolean(); - } else if (jsonObject.has("detectedViaGameObject")) { - // Backward compatibility: migrate old field to new field - detectedViaNearBy = jsonObject.get("detectedViaGameObject").getAsBoolean(); - } - - // Ignore farmingLevel as it's no longer used - - // Create SpiritTreeData with preserved timestamp and available fields - return new SpiritTreeData( - patch, - cropState, - availableForTravel, - lastUpdated, - playerLocation, - detectedViaWidget, - detectedViaNearBy - ); - - } catch (Exception e) { - throw new JsonParseException("Failed to deserialize SpiritTreeData: " + e.getMessage(), e); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SpiritTreePatchAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SpiritTreePatchAdapter.java deleted file mode 100644 index c971bd8dc2f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/SpiritTreePatchAdapter.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) 2024 Microbot - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package net.runelite.client.plugins.microbot.util.cache.serialization; - -import com.google.gson.*; -import net.runelite.client.plugins.microbot.util.farming.SpiritTree; - -import java.lang.reflect.Type; - -/** - * Gson adapter for SpiritTreePatch enum serialization/deserialization. - * Handles safe serialization of spirit tree patch enums for cache storage. - */ -public class SpiritTreePatchAdapter implements JsonSerializer, JsonDeserializer { - - @Override - public JsonElement serialize(SpiritTree src, Type typeOfSrc, JsonSerializationContext context) { - return new JsonPrimitive(src.name()); - } - - @Override - public SpiritTree deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - try { - String patchName = json.getAsString(); - return SpiritTree.valueOf(patchName); - } catch (IllegalArgumentException e) { - throw new JsonParseException("Unknown SpiritTreePatch: " + json.getAsString(), e); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/VarbitDataAdapter.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/VarbitDataAdapter.java deleted file mode 100644 index 96716d2d95f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/serialization/VarbitDataAdapter.java +++ /dev/null @@ -1,147 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.serialization; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.util.cache.model.VarbitData; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Gson TypeAdapter for VarbitData serialization/deserialization. - * Stores varbit data as a compact object with optional contextual information. - */ -public class VarbitDataAdapter extends TypeAdapter { - - @Override - public void write(JsonWriter out, VarbitData value) throws IOException { - if (value == null) { - out.nullValue(); - } else { - out.beginObject(); - - // Core data - out.name("value").value(value.getValue()); - out.name("lastUpdated").value(value.getLastUpdated()); - - // Previous value (nullable) - if (value.getPreviousValue() != null) { - out.name("previousValue").value(value.getPreviousValue()); - } - - // Player location (nullable) - if (value.getPlayerLocation() != null) { - out.name("location"); - out.beginArray(); - out.value(value.getPlayerLocation().getX()); - out.value(value.getPlayerLocation().getY()); - out.value(value.getPlayerLocation().getPlane()); - out.endArray(); - } - - // Nearby NPCs (optional) - if (!value.getNearbyNpcIds().isEmpty()) { - out.name("nearbyNpcs"); - out.beginArray(); - for (Integer npcId : value.getNearbyNpcIds()) { - out.value(npcId); - } - out.endArray(); - } - - // Nearby objects (optional) - if (!value.getNearbyObjectIds().isEmpty()) { - out.name("nearbyObjects"); - out.beginArray(); - for (Integer objectId : value.getNearbyObjectIds()) { - out.value(objectId); - } - out.endArray(); - } - - out.endObject(); - } - } - - @Override - public VarbitData read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - - int value = 0; - long lastUpdated = System.currentTimeMillis(); - Integer previousValue = null; - WorldPoint playerLocation = null; - List nearbyNpcIds = new ArrayList<>(); - List nearbyObjectIds = new ArrayList<>(); - - in.beginObject(); - while (in.hasNext()) { - String name = in.nextName(); - switch (name) { - case "value": - value = in.nextInt(); - break; - case "lastUpdated": - lastUpdated = in.nextLong(); - break; - case "previousValue": - if (in.peek() != JsonToken.NULL) { - previousValue = in.nextInt(); - } else { - in.nextNull(); - } - break; - case "location": - if (in.peek() != JsonToken.NULL) { - in.beginArray(); - int x = in.nextInt(); - int y = in.nextInt(); - int plane = in.nextInt(); - playerLocation = new WorldPoint(x, y, plane); - in.endArray(); - } else { - in.nextNull(); - } - break; - case "nearbyNpcs": - if (in.peek() != JsonToken.NULL) { - in.beginArray(); - while (in.hasNext()) { - nearbyNpcIds.add(in.nextInt()); - } - in.endArray(); - } else { - in.nextNull(); - } - break; - case "nearbyObjects": - if (in.peek() != JsonToken.NULL) { - in.beginArray(); - while (in.hasNext()) { - nearbyObjectIds.add(in.nextInt()); - } - in.endArray(); - } else { - in.nextNull(); - } - break; - default: - in.skipValue(); // Skip unknown fields for forwards compatibility - break; - } - } - in.endObject(); - - return new VarbitData(value, lastUpdated, previousValue, playerLocation, - nearbyNpcIds.isEmpty() ? Collections.emptyList() : nearbyNpcIds, - nearbyObjectIds.isEmpty() ? Collections.emptyList() : nearbyObjectIds); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/CacheOperations.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/CacheOperations.java deleted file mode 100644 index 45e35411e62..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/CacheOperations.java +++ /dev/null @@ -1,95 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy; - -import java.util.Map; -import java.util.stream.Stream; - -/** - * Interface providing controlled access to cache operations for strategies. - * This follows the framework guideline of providing limited, safe access to cache internals. - */ -public interface CacheOperations { - - /** - * Gets a value from the cache. - * - * @param key The key to retrieve - * @return The cached value or null if not found - */ - V get(K key); - - /** - * Gets a raw cached value without triggering additional cache operations like scene scans. - * This method bypasses any cache miss handling and returns only what's currently cached. - * Used by update strategies during scene synchronization to avoid recursive scanning. - * - * @param key The key to retrieve - * @return The raw cached value or null if not present in cache - */ - V getRawValue(K key); - - /** - * Puts a value into the cache. - * - * @param key The key to store - * @param value The value to store - */ - void put(K key, V value); - - /** - * Removes a specific key from the cache. - * - * @param key The key to remove - */ - void remove(K key); - - /** - * Invalidates all cached data. - */ - void invalidateAll(); - - /** - * Checks if the cache contains a specific key. - * - * @param key The key to check - * @return True if the key exists and is not expired - */ - boolean containsKey(K key); - - /** - * Gets the current size of the cache. - * - * @return The number of entries in the cache - */ - int size(); - - /** - * Provides a stream of all cache entries. - * This allows for efficient filtering and processing of cache contents. - * Note: The stream should be used within the same thread context and not cached. - * - * @return A stream of Map.Entry representing all cache entries - */ - Stream> entryStream(); - - /** - * Provides a stream of all cache keys. - * - * @return A stream of keys in the cache - */ - Stream keyStream(); - - /** - * Provides a stream of all cache values. - * - * @return A stream of values in the cache - */ - Stream valueStream(); - - /** - * Gets the name of this cache for logging and debugging. - * - * @return The cache name - */ - String getCacheName(); - -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/CacheUpdateStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/CacheUpdateStrategy.java deleted file mode 100644 index 7316ac8c422..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/CacheUpdateStrategy.java +++ /dev/null @@ -1,64 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy; - -/** - * Strategy interface for handling cache updates based on events. - * Follows the game cache framework guidelines for pluggable cache enhancement. - * - * These strategies handle event-driven cache updates, enriching cache data - * with temporal information, contextual data, and change tracking rather - * than just invalidating entries. - * - * Implements AutoCloseable to ensure proper resource cleanup including - * shutdown of any background tasks, executor services, or other resources. - * - * @param Cache key type - * @param Cache value type - */ -public interface CacheUpdateStrategy extends AutoCloseable { - - /** - * Handles an event and potentially updates cache entries with enhanced data. - * - * @param event The event that occurred - * @param cache The cache to potentially update - */ - void handleEvent(Object event, CacheOperations cache); - - /** - * Gets the event types this strategy handles. - * - * @return Array of event classes this strategy processes - */ - Class[] getHandledEventTypes(); - - /** - * Called when the strategy is attached to a cache. - * - * @param cache The cache this strategy is attached to - */ - default void onAttach(CacheOperations cache) { - // Default: no action - } - - /** - * Called when the strategy is detached from a cache. - * - * @param cache The cache this strategy was attached to - */ - default void onDetach(CacheOperations cache) { - // Default: no action - } - - /** - * Closes this strategy and releases any resources such as scheduled tasks, - * executor services, or other background processing. - * - * Default implementation does nothing - strategies that use resources - * should override this method to ensure proper cleanup. - */ - @Override - default void close() { - // Default: no action - } -} - diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/PredicateQuery.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/PredicateQuery.java deleted file mode 100644 index 2f0c8359368..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/PredicateQuery.java +++ /dev/null @@ -1,48 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy; - -import java.util.function.Predicate; - -/** - * Predicate-based query criteria for simple filtering. - * - * @param The value type to filter - */ -public class PredicateQuery implements QueryCriteria { - - private final Predicate predicate; - private final String description; - - /** - * Creates a new predicate query. - * - * @param predicate The predicate to apply - * @param description A description of what this query does - */ - public PredicateQuery(Predicate predicate, String description) { - this.predicate = predicate; - this.description = description; - } - - /** - * Gets the predicate for this query. - * - * @return The predicate function - */ - public Predicate getPredicate() { - return predicate; - } - - /** - * Gets the description of this query. - * - * @return The query description - */ - public String getDescription() { - return description; - } - - @Override - public String getQueryType() { - return "PREDICATE:" + description; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/QueryCriteria.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/QueryCriteria.java deleted file mode 100644 index 6429159c07a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/QueryCriteria.java +++ /dev/null @@ -1,15 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy; - -/** - * Base interface for query criteria. - * Specific strategies can define their own criteria types. - */ -public interface QueryCriteria { - - /** - * Gets the type identifier for this query criteria. - * - * @return The query type string - */ - String getQueryType(); -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/QueryStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/QueryStrategy.java deleted file mode 100644 index 3a72070939d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/QueryStrategy.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy; - -import java.util.stream.Stream; - -/** - * Strategy interface for specialized cache queries. - * Allows caches to support complex queries without inheritance. - * - * @param Cache key type - * @param Cache value type - */ -public interface QueryStrategy { - - /** - * Executes a query against the cache. - * - * @param cache The cache to query - * @param criteria The query criteria - * @return Stream of matching values - */ - Stream executeQuery(CacheOperations cache, QueryCriteria criteria); - - /** - * Gets the query types this strategy supports. - * - * @return Array of supported query criteria classes - */ - Class[] getSupportedQueryTypes(); -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/ValueWrapper.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/ValueWrapper.java deleted file mode 100644 index f6100019498..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/ValueWrapper.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy; - -/** - * Strategy interface for wrapping values before storing them in cache. - * Allows adding metadata, spawn tracking, etc. without inheritance. - * - * @param The original value type - * @param The wrapped value type - */ -public interface ValueWrapper { - - /** - * Wraps a value before storing in cache. - * - * @param value The original value - * @param key The cache key for context - * @return The wrapped value - */ - W wrap(V value, Object key); - - /** - * Unwraps a value when retrieving from cache. - * - * @param wrappedValue The wrapped value from cache - * @return The original value - */ - V unwrap(W wrappedValue); - - /** - * Gets the wrapped value type for type safety. - * - * @return The wrapped type class - */ - Class getWrappedType(); -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/GroundItemUpdateStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/GroundItemUpdateStrategy.java deleted file mode 100644 index aa3116ac49a..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/GroundItemUpdateStrategy.java +++ /dev/null @@ -1,536 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy.entity; - -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; -import java.util.concurrent.ScheduledExecutorService; - -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.*; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.ItemDespawned; -import net.runelite.api.events.ItemSpawned; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheOperations; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheUpdateStrategy; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; -import net.runelite.client.plugins.microbot.util.cache.Rs2GroundItemCache; - -/** - * Enhanced cache update strategy for ground item data. - * Handles automatic cache updates based on ground item spawn/despawn events and provides scene scanning. - * Follows the same pattern as ObjectUpdateStrategy for consistency. - */ -@Slf4j -public class GroundItemUpdateStrategy implements CacheUpdateStrategy { - - GameState lastGameState = null; - - // ScheduledExecutorService for non-blocking operations - private final ScheduledExecutorService executorService; - private ScheduledFuture periodicSceneScanTask; - private ScheduledFuture sceneScanTask; - - // Scene scan tracking - private final AtomicBoolean scanActive = new AtomicBoolean(false); - private final AtomicBoolean scanRequest = new AtomicBoolean(false); - private static final long MIN_SCAN_INTERVAL_MS = Constants.GAME_TICK_LENGTH; // Minimum interval between scans - private volatile long lastSceneScan = 0; // Last time a scene scan was performed - - /** - * Constructor initializes the executor service for background tasks. - */ - public GroundItemUpdateStrategy() { - this.executorService = Executors.newScheduledThreadPool(2, r -> { - Thread thread = new Thread(r, "GroundItemUpdateStrategy-" + System.currentTimeMillis()); - thread.setDaemon(true); - return thread; - }); - log.debug("GroundItemUpdateStrategy initialized with ScheduledExecutorService"); - } - - @Override - public void handleEvent(Object event, CacheOperations cache) { - if (executorService == null || executorService.isShutdown() || !Microbot.isLoggedIn() || Microbot.getClient() == null || Microbot.getClient().getLocalPlayer() == null) { - log.warn("GroundItemUpdateStrategy is shut down or not logged in, ignoring event: {}", event.getClass().getSimpleName()); - return; // Don't process events if shut down - } - if (scanActive.get()){ - log.debug("Skipping event processing - scan already active: {}", event.getClass().getSimpleName()); - return; // Don't process events if a scan is already active - } - // Submit event handling to executor service for non-blocking processing - //executorService.submit(() -> { - // try { - processEventInternal(event, cache); - //} catch (Exception e) { - // log.error("Error processing event: {}", event.getClass().getSimpleName(), e); - //} - //}); - } - - /** - * Internal method to process events - runs on background thread. - */ - private void processEventInternal(Object event, CacheOperations cache) { - if (event instanceof ItemSpawned) { - handleItemSpawned((ItemSpawned) event, cache); - } else if (event instanceof ItemDespawned) { - handleItemDespawned((ItemDespawned) event, cache); - } else if (event instanceof GameStateChanged) { - handleGameStateChanged((GameStateChanged) event, cache); - } - } - - /** - * Performs an scene scan to populate the cache. - * Only scans if certain conditions are met to avoid unnecessary processing. - * This method runs asynchronously on a background thread to avoid blocking. - * - * @param cache The cache to populate - */ - public void performSceneScan(CacheOperations cache,long delayMs) { - if (executorService == null || executorService.isShutdown()) { - log.debug("Skipping ground item scene scan - strategy is shut down or not logged in"); - return; - } - - // Respect minimum scan interval unless forced - if ((System.currentTimeMillis() - lastSceneScan) < MIN_SCAN_INTERVAL_MS) { - log.debug("Skipping ground item scene scan due to minimum interval not reached"); - scanActive.set(false); - return; - } - - if (sceneScanTask != null && !sceneScanTask.isDone()) { - log.debug("Skipping ground item scene scan - already scheduled or running"); - return; // Don't perform scan if already scheduled or running - } - - if (scanActive.compareAndSet(false, true)) { - // Submit scene scan to executor service for non-blocking execution - sceneScanTask = executorService.schedule(() -> { - try { - performSceneScanInternal(cache); - } catch (Exception e) { - log.error("Error during ground item scene scan", e); - } finally { - scanActive.set(false); - } - }, delayMs, TimeUnit.MILLISECONDS); // delay before scan - } else { - log.debug("Skipping ground item scene scan - already active"); - return; // Don't perform scan if already active - } - } - - /** - * Schedules a periodic scene scan task. - * This is useful for ensuring the cache stays up-to-date even if events are missed. - * - * @param cache The cache to scan - * @param intervalSeconds How often to scan in seconds - */ - public void schedulePeriodicSceneScan(CacheOperations cache, long intervalSeconds) { - if (executorService == null || executorService.isShutdown()) { - log.warn("Cannot schedule periodic scan - strategy is shut down"); - return; - } - - // Cancel existing task if any - stopPeriodicSceneScan(); - - periodicSceneScanTask = executorService.scheduleWithFixedDelay(() -> { - try { - if (scanActive.compareAndSet(false, true)) { - if (scanRequest.get() && Microbot.isLoggedIn()) { - log.debug("Periodic ground item scene scan triggered"); - performSceneScanInternal(cache); - } - } else { - log.debug("Skipping scheduled ground item scene scan - already active"); - } - } catch (Exception e) { - log.error("Error in periodic ground item scene scan", e); - } finally { - scanActive.set(false); - } - }, intervalSeconds, intervalSeconds, TimeUnit.SECONDS); - - log.debug("Scheduled periodic ground item scene scan every {} seconds", intervalSeconds); - } - - /** - * Stops periodic scene scanning. - */ - public void stopPeriodicSceneScan() { - if (periodicSceneScanTask != null && !periodicSceneScanTask.isDone()) { - periodicSceneScanTask.cancel(false); - periodicSceneScanTask = null; - log.debug("Stopped periodic ground item scene scanning"); - } - } - - /** - * Checks if a scene scan is needed based on cache state. - * - * @param cache The cache to check - * @return true if a scan would be beneficial - */ - public boolean requestSceneScan(CacheOperations cache) { - if (scanActive.get()) { - log.debug("Skipping scene scan request - already active"); - return false; // Don't request scan if already active - } - if (scanRequest.compareAndSet(false, true)) { - log.debug("Ground item scene scan requested"); - performSceneScan(cache, 5); // Perform with 5ms delay for stability - } - if (!Microbot.getClient().isClientThread()){ - sleepUntil(()->!scanRequest.get(), 1000); // Wait until scan is requested and reset - } - return !scanRequest.get(); // Return true if scan was requested,reseted - } - - /** - * Performs the actual scene scan to synchronize ground items with the current scene. - * This method both adds new items found in the scene AND removes cached items no longer present. - * Provides complete scene synchronization in a single operation. - */ - private void performSceneScanInternal(CacheOperations cache) { - try { - long currentTime = System.currentTimeMillis(); - - // Check minimum interval to prevent excessive scanning - if (currentTime - lastSceneScan < MIN_SCAN_INTERVAL_MS) { - log.debug("Skipping Ground scene scan - too soon since last scan"); - scanActive.set(false); - return; - } - Player player = Microbot.getClient().getLocalPlayer(); - if (player == null) { - log.debug("Cannot perform ground item scene scan - no player"); - scanActive.set(false); - return; - } - - Scene scene = player.getWorldView().getScene(); - if (scene == null) { - log.debug("Cannot perform ground item scene scan - no scene"); - scanActive.set(false); - return; - } - - Tile[][][] tiles = scene.getTiles(); - if (tiles == null) { - log.debug("Cannot perform ground item scene scan - no tiles"); - scanActive.set(false); - return; - } - - // Build a set of all currently existing ground item keys from the scene - java.util.Set currentSceneKeys = new java.util.HashSet<>(); - int addedItems = 0; - int z = player.getWorldView().getPlane(); - - log.debug("Starting ground item scene synchronization (cache size: {})", cache.size()); - - // Phase 1: Scan scene and add new items - for (int x = 0; x < Constants.SCENE_SIZE; x++) { - for (int y = 0; y < Constants.SCENE_SIZE; y++) { - Tile tile = tiles[z][x][y]; - if (tile == null) continue; - if(tile.getGroundItems() == null) continue; // Ensure ground items are loaded - - // Get ground items on this tile - for (TileItem tileItem : tile.getGroundItems()) { - if (tileItem != null) { - String key = generateKey(tileItem, tile.getWorldLocation()); - currentSceneKeys.add(key); // Track all scene items - - // Only add if not already in cache to avoid recursive calls - if (!cache.containsKey(key)) { - Rs2GroundItemModel groundItemModel = new Rs2GroundItemModel(tileItem, tile); - cache.put(key, groundItemModel); - addedItems++; - } - } - } - } - } - - // Phase 2: Remove cached items no longer in scene - int removedItems = 0; - if (!currentSceneKeys.isEmpty()) { - // Find cached items that are no longer in the scene using CacheOperations streaming - List keysToRemove = cache.entryStream() - .map(java.util.Map.Entry::getKey) - .filter(key -> !currentSceneKeys.contains(key)) - .collect(Collectors.toList()); - - // Remove the items that are no longer in scene - for (String key : keysToRemove) { - Rs2GroundItemModel item = cache.getRawValue(key); // Use raw value to avoid triggering recursive scene scans - cache.remove(key); - if (item != null) { - removedItems++; - log.trace("Removed ground item not in scene: ID {} ({})", item.getId(), key); - } - } - } - - // Log comprehensive results - if (addedItems > 0 || removedItems > 0) { - log.debug("Ground item scene synchronization completed - added {} items, removed {} items (total cache size: {})", - addedItems, removedItems, cache.size()); - } else { - log.debug("Ground item scene synchronization completed - no changes made"); - } - - scanRequest.set(false); //NOT in finally block to allow for rescan if there are an error - }catch (Exception e) { - log.error("Error during ground item scene synchronization", e); - }finally { - scanActive.set(false); - lastSceneScan = System.currentTimeMillis(); // Update last scan time - } - } - - private void handleItemSpawned(ItemSpawned event, CacheOperations cache) { - TileItem item = event.getItem(); - if (item != null) { - String key = generateKey(item, event.getTile().getWorldLocation()); - Rs2GroundItemModel groundItem = new Rs2GroundItemModel(item, event.getTile()); - cache.put(key, groundItem); - log.trace("Added ground item {} at {} to cache via spawn event", item.getId(), event.getTile().getWorldLocation()); - } - } - - private void handleItemDespawned(ItemDespawned event, CacheOperations cache) { - TileItem item = event.getItem(); - //Rs2GroundItemModel groundItem = new Rs2GroundItemModel(item, event.getTile()); - if (item != null) { - String key = generateKey(item, event.getTile().getWorldLocation()); - cache.remove(key); - log.trace("Removed ground item {} at {} from cache via despawn event", item.getId(), event.getTile().getWorldLocation()); - } - } - /** - * Cleanup persistent items that don't naturally despawn. - * This method follows the same pattern as performSceneScan with proper async execution. - * - * @param cache The cache operations interface - * @param delayMs Delay before performing the cleanup - */ - public void cleanupPersistentItems(CacheOperations cache, long delayMs) { - if (executorService == null || executorService.isShutdown()) { - log.debug("Skipping persistent item cleanup - strategy is shut down"); - return; - } - - // Submit cleanup to executor service for non-blocking execution - executorService.schedule(() -> { - try { - cleanupPersistentItemsInternal(cache); - } catch (Exception e) { - log.error("Error during persistent item cleanup", e); - } - }, delayMs, TimeUnit.MILLISECONDS); - } - - /** - * Internal method that performs the actual persistent item cleanup. - * Directly removes persistent ground items that don't naturally despawn without scene scanning. - * - * @param cache The cache operations interface - * @return The number of persistent items cleaned up - */ - private int cleanupPersistentItemsInternal(CacheOperations cache) { - // Use CacheOperations streaming to find and remove persistent items - List keysToRemove = cache.entryStream() - .filter(entry -> { - Rs2GroundItemModel item = entry.getValue(); - // Check if this is a persistent item (using isPersistened method) - return item.isPersistened(); - }) - .map(java.util.Map.Entry::getKey) - .collect(Collectors.toList()); - - if (keysToRemove.isEmpty()) { - log.debug("No persistent ground items found to cleanup"); - return 0; - } - - // Remove all persistent items directly - int removedCount = 0; - for (String key : keysToRemove) { - Rs2GroundItemModel item = cache.getRawValue(key); // Use raw value to avoid triggering recursive scene scans - cache.remove(key); - if (item != null) { - removedCount++; - log.trace("Removed persistent ground item: ID {} ({})", item.getId(), key); - } - } - - log.debug("Cleaned up {} persistent ground items", removedCount); - return removedCount; - } - - /** - * Handles game state changes for ground item cache management. - * - *

Ground Item Despawn Handling Strategy:

- *
    - *
  • Unlike NPCs, ground items have complex despawn timing that isn't always captured by {@link net.runelite.api.events.ItemDespawned}
  • - *
  • Items can despawn based on game ticks elapsed since spawn time, which may not trigger ItemDespawned events
  • - *
  • We rely on {@link Rs2GroundItemCache#performPeriodicCleanup()} to check {@link Rs2GroundItemModel#isDespawned()}
  • - *
  • The {@link Rs2GroundItemCache#isExpired(String)} method integrates despawn timing directly into cache operations
  • - *
  • This dual approach ensures expired ground items are removed even when events are missed
  • - *
- * - *

Why scene scanning is essential for ground items:

- *
    - *
  • Ground items have complex despawn mechanics not always captured by ItemDespawned events
  • - *
  • Persistent items (player drops, quest items) may have indefinite lifespans requiring special detection
  • - *
  • Events can be missed during region changes, network issues, or client restarts
  • - *
  • Scene scanning ensures cache synchronization with actual game state after login/loading
  • - *
  • The {@link #performSceneScan(CacheOperations, long)} method provides complete scene-to-cache synchronization
  • - *
- * - *

Persistent Item Handling:

- *
    - *
  • Persistent items are detected using {@link Rs2GroundItemModel#isPersistened()}
  • - *
  • Scene scanning includes cleanup of persistent items that should no longer exist
  • - *
  • Combines natural despawn cleanup with scene validation for comprehensive item management
  • - *
- * - *

Game State Specific Actions:

- *
    - *
  • LOGGED_IN/LOADING: Perform scene scan with 2-tick delay for stability and cleanup persistent items
  • - *
  • LOGOUT States: Cancel ongoing scan operations and invalidate entire cache
  • - *
  • Other States: Update state tracking without additional actions
  • - *
- * - * @param event The game state change event - * @param cache The ground item cache operations interface - */ - private void handleGameStateChanged(GameStateChanged event, CacheOperations cache) { - switch (event.getGameState()) { - case LOGGED_IN: - // Ground item despawn handling is managed by Rs2GroundItemCache.performPeriodicCleanup() - // and Rs2GroundItemCache.isExpired() using Rs2GroundItemModel.isDespawned() - lastGameState = GameState.LOGGED_IN; - log.debug("Player logged in - ground item despawn handled by periodic cleanup"); - - // Perform scene scan to synchronize cache with current scene and cleanup persistent items - performSceneScan(cache, Constants.GAME_TICK_LENGTH*2); // 2 ticks delay for stability - break; - case LOADING: - // Ground item despawn handling is managed by Rs2GroundItemCache.performPeriodicCleanup() - // and Rs2GroundItemCache.isExpired() using Rs2GroundItemModel.isDespawned() - lastGameState = GameState.LOADING; - log.debug("Game loading - ground item despawn handled by periodic cleanup"); - - // Perform scene scan after loading completes and cleanup persistent items - performSceneScan(cache, Constants.GAME_TICK_LENGTH*2); // 2 ticks delay for stability - break; - case LOGIN_SCREEN: - case LOGGING_IN: - case CONNECTION_LOST: - if (sceneScanTask != null && !sceneScanTask.isDone()) { - sceneScanTask.cancel(true); - sceneScanTask = null; - } - // Clear scan request when logging out and stop periodic scanning - scanRequest.set(false); // Reset scan request - cache.invalidateAll(); - lastGameState = event.getGameState(); - log.debug("Player logged out - cleared ground item cache and stopped operations"); - break; - default: - lastGameState = event.getGameState(); - break; - } - } - - /** - * Generates a unique key for ground items based on item ID, quantity, and location. - * - * @param item The tile item - * @param location The world location - * @return Unique key string - */ - private String generateKey(TileItem item, net.runelite.api.coords.WorldPoint location) { - return String.format("%d_%d_%d_%d_%d", - item.getId(), - item.getQuantity(), - location.getX(), - location.getY(), - location.getPlane()); - } - - @Override - public Class[] getHandledEventTypes() { - return new Class[]{ItemSpawned.class, ItemDespawned.class, GameStateChanged.class}; - } - - @Override - public void onAttach(CacheOperations cache) { - log.debug("GroundItemUpdateStrategy attached to cache"); - } - - @Override - public void onDetach(CacheOperations cache) { - log.debug("GroundItemUpdateStrategy detached from cache"); - // Cancel periodic scanning when detaching - if (periodicSceneScanTask != null && !periodicSceneScanTask.isDone()) { - periodicSceneScanTask.cancel(false); - periodicSceneScanTask = null; - } - } - - @Override - public void close() { - log.debug("Shutting down GroundItemUpdateStrategy"); - stopPeriodicSceneScan(); - if (sceneScanTask != null && !sceneScanTask.isDone()) { - sceneScanTask.cancel(false); - sceneScanTask = null; - log.debug("Cancelled active ground item scene scan task"); - } - shutdownExecutorService(); - } - /** - * Shuts down the executor service gracefully, waiting for currently executing tasks to complete. - * If the executor does not terminate within the initial timeout, it attempts a forced shutdown. - * Logs warnings or errors if the shutdown process does not complete as expected. - * If interrupted during shutdown, the method forces shutdown and re-interrupts the current thread. - */ - private void shutdownExecutorService() { - if (executorService != null && !executorService.isShutdown()) { - // Shutdown executor service - executorService.shutdown(); - try { - if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { - log.warn("Executor service did not terminate gracefully, forcing shutdown"); - executorService.shutdownNow(); - - // Wait a bit more for tasks to respond to being cancelled - if (!executorService.awaitTermination(2, TimeUnit.SECONDS)) { - log.error("Executor service did not terminate after forced shutdown"); - } - } - } catch (InterruptedException e) { - log.warn("Interrupted during executor shutdown", e); - executorService.shutdownNow(); - Thread.currentThread().interrupt(); - } - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/NpcUpdateStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/NpcUpdateStrategy.java deleted file mode 100644 index d1561dd703f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/NpcUpdateStrategy.java +++ /dev/null @@ -1,390 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy.entity; - -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.ScheduledExecutorService; - -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.*; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.NpcDespawned; -import net.runelite.api.events.NpcSpawned; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheUpdateStrategy; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheOperations; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; - -/** - * Enhanced cache update strategy for NPC data. - * Handles NPC spawn/despawn events and provides scene scanning. - * Follows the same pattern as ObjectUpdateStrategy for consistency. - */ -@Slf4j -public class NpcUpdateStrategy implements CacheUpdateStrategy { - - GameState lastGameState = null; - - // ScheduledExecutorService for non-blocking operations - private final ScheduledExecutorService executorService; - private ScheduledFuture periodicSceneScanTask; - private ScheduledFuture sceneScanTask; - - // Scene scan tracking - private final AtomicBoolean scanActive = new AtomicBoolean(false); - private final AtomicBoolean scanRequest = new AtomicBoolean(false); - private volatile long lastSceneScan = 0; - private static final long MIN_SCAN_INTERVAL_MS = Constants.GAME_TICK_LENGTH; - - /** - * Constructor initializes the executor service for background tasks. - */ - public NpcUpdateStrategy() { - this.executorService = Executors.newScheduledThreadPool(2, r -> { - Thread thread = new Thread(r, "NpcUpdateStrategy-" + System.currentTimeMillis()); - thread.setDaemon(true); - return thread; - }); - log.debug("NpcUpdateStrategy initialized with ScheduledExecutorService"); - } - - @Override - public void handleEvent(final Object event, CacheOperations cache) { - if (executorService == null || executorService.isShutdown()) { - return; // Don't process events if shut down - } - processEventInternal(event, cache); - } - - /** - * Internal method to process events - runs on background thread. - */ - private void processEventInternal(Object event, CacheOperations cache) { - if (event instanceof NpcSpawned) { - handleNpcSpawned((NpcSpawned) event, cache); - } else if (event instanceof NpcDespawned) { - handleNpcDespawned((NpcDespawned) event, cache); - } else if (event instanceof GameStateChanged) { - handleGameStateChanged((GameStateChanged) event, cache); - } - } - - /** - * Performs an scene scan to populate the cache. - * Only scans if certain conditions are met to avoid unnecessary processing. - * This method runs asynchronously on a background thread to avoid blocking. - * - * @param cache The cache to populate - */ - public void performSceneScan(CacheOperations cache, long delayMs) { - if (executorService == null || executorService.isShutdown() ) { - log.debug("Skipping NPC scene scan - strategy is shut down or not logged in"); - return; - } - - // Respect minimum scan interval unless forced - if ((System.currentTimeMillis() - lastSceneScan) < MIN_SCAN_INTERVAL_MS) { - log.debug("Skipping NPC scene scan due to minimum interval not reached"); - scanActive.set(false); - return; - } - - if (sceneScanTask != null && !sceneScanTask.isDone()) { - log.debug("Skipping NPC scene scan - already scheduled or running"); - return; // Don't perform scan if already scheduled or running - } - - if (scanActive.compareAndSet(false, true)) { - // Submit scene scan to executor service for non-blocking execution - sceneScanTask = executorService.schedule(() -> { - try { - performSceneScanInternal(cache); - } catch (Exception e) { - log.error("Error during NPC scene scan", e); - } finally { - scanActive.set(false); - scanRequest.set(false); // Reset request after scan - } - }, delayMs, TimeUnit.MILLISECONDS); // delay before scan - } else { - log.debug("Skipping NPC scene scan - already active"); - return; // Don't perform scan if already active - } - } - - /** - * Schedules a periodic scene scan task. - * This is useful for ensuring the cache stays up-to-date even if events are missed. - * - * @param cache The cache to scan - * @param intervalSeconds How often to scan in seconds - */ - public void schedulePeriodicSceneScan(CacheOperations cache, long intervalSeconds) { - if (executorService == null || executorService.isShutdown()) { - log.warn("Cannot schedule periodic scan - strategy is shut down"); - return; - } - - // Cancel existing task if any - stopPeriodicSceneScan(); - - periodicSceneScanTask = executorService.scheduleWithFixedDelay(() -> { - try { - if (scanActive.compareAndSet(false, true)) { - if (scanRequest.get() && Microbot.isLoggedIn()) { - log.debug("Periodic NPC scene scan triggered"); - performSceneScanInternal(cache); - } - } else { - log.debug("Skipping scheduled NPC scene scan - already active"); - } - } catch (Exception e) { - log.error("Error in periodic NPC scene scan", e); - } finally { - scanActive.set(false); - } - }, intervalSeconds, intervalSeconds, TimeUnit.SECONDS); - - log.debug("Scheduled periodic NPC scene scan every {} seconds", intervalSeconds); - } - - /** - * Stops periodic scene scanning. - */ - public void stopPeriodicSceneScan() { - if (periodicSceneScanTask != null && !periodicSceneScanTask.isDone()) { - periodicSceneScanTask.cancel(false); - periodicSceneScanTask = null; - log.debug("Stopped periodic NPC scene scanning"); - } - } - - - /** - * Checks if a scene scan is needed based on cache state. - * - * @param cache The cache to check - * @return true if a scan would be beneficial - */ - public boolean requestSceneScan(CacheOperations cache) { - if (scanActive.get()) { - log.debug("Skipping scene scan request - already active"); - return false; // Don't request scan if already active - } - if (scanRequest.compareAndSet(false, true)) { - log.debug("NPC scene scan requested"); - performSceneScan(cache, 5); // Perform with 5ms delay for stability - } - if (!Microbot.getClient().isClientThread()){ - sleepUntil(()->!scanRequest.get(), 1000); // Wait until scan is requested and reset - } - return !scanRequest.get(); // Return true if scan was requested,reseted - } - - /** - * Performs the actual scene scan to synchronize NPCs with the current scene. - * This method both adds new NPCs found in the scene AND removes cached NPCs no longer present. - * Provides complete scene synchronization in a single operation. - */ - private void performSceneScanInternal(CacheOperations cache) { - try { - long currentTime = System.currentTimeMillis(); - - // Check minimum interval to prevent excessive scanning - if (currentTime - lastSceneScan < MIN_SCAN_INTERVAL_MS) { - log.debug("Skipping NPC scene scan - too soon since last scan"); - scanActive.set(false); - return; - } - - Player player = Microbot.getClient().getLocalPlayer(); - if (player == null) { - log.debug("Cannot perform NPC scene scan - no player"); - scanActive.set(false); - return; - } - - // Build a set of all currently existing NPC indices from the scene - java.util.Set currentSceneIndices = new java.util.HashSet<>(); - int addedNpcs = 0; - log.debug("Starting NPC scene synchronization (cache size: {})", cache.size()); - - // Phase 1: Scan scene and add new NPCs - for (NPC npc : Microbot.getClient().getTopLevelWorldView().npcs()) { - if (npc != null && npc.getId() != -1) { // Use ID instead of getName() to avoid client thread requirement - currentSceneIndices.add(npc.getIndex()); // Track all scene NPCs - - // Only add if not already in cache to avoid recursive calls - if (!cache.containsKey(npc.getIndex())) { - Rs2NpcModel npcModel = new Rs2NpcModel(npc); - cache.put(npc.getIndex(), npcModel); - addedNpcs++; - } - } - } - - // Phase 2: Remove cached NPCs no longer in scene - int removedNpcs = 0; - if (!currentSceneIndices.isEmpty()) { - // Find cached NPCs that are no longer in the scene using CacheOperations streaming - java.util.List keysToRemove = cache.entryStream() - .map(java.util.Map.Entry::getKey) - .filter(key -> !currentSceneIndices.contains(key)) - .collect(java.util.stream.Collectors.toList()); - - // Remove the NPCs that are no longer in scene - for (Integer key : keysToRemove) { - // Use raw cached value to avoid triggering recursive scene scans - Rs2NpcModel npc = cache.getRawValue(key); - cache.remove(key); - if (npc != null) { - removedNpcs++; - log.trace("Removed NPC not in scene: ID {} (index: {})", npc.getId(), key); - } - } - } - - // Log comprehensive results - if (addedNpcs > 0 || removedNpcs > 0) { - log.debug("NPC scene synchronization completed - added {} NPCs, removed {} NPCs (total cache size: {})", - addedNpcs, removedNpcs, cache.size()); - } else { - log.debug("NPC scene synchronization completed - no changes made"); - } - scanRequest.set(false); // Reset request after scan - } catch (Exception e) { - log.error("Error during NPC scene synchronization", e); - } finally { - lastSceneScan = System.currentTimeMillis(); // Update last scan time; - scanActive.set(false); - } - } - - private void handleNpcSpawned(NpcSpawned event, CacheOperations cache) { - NPC npc = event.getNpc(); - if (npc != null) { - Rs2NpcModel npcModel = new Rs2NpcModel(npc); - cache.put(npc.getIndex(), npcModel); - log.trace("Added NPC ID {} (index: {}) to cache via spawn event", npc.getId(), npc.getIndex()); - } - } - - private void handleNpcDespawned(NpcDespawned event, CacheOperations cache) { - NPC npc = event.getNpc(); - if (npc != null) { - cache.remove(npc.getIndex()); - log.trace("Removed NPC ID {} (index: {}) from cache via despawn event", npc.getId(), npc.getIndex()); - } - } - - /** - * Handles game state changes for NPC cache management. - * - *

Why NPCs don't require scene scanning on LOGGED_IN/LOADING:

- *
    - *
  • NPCs are automatically managed by the RuneLite client's event system
  • - *
  • {@link net.runelite.api.events.NpcSpawned} events are reliably triggered when NPCs appear
  • - *
  • {@link net.runelite.api.events.NpcDespawned} events are reliably triggered when NPCs disappear
  • - *
  • Region changes automatically trigger proper spawn/despawn events for all NPCs
  • - *
  • This makes manual scene scanning unnecessary and potentially wasteful
  • - *
- * - *

This is different from ground items, which have timing-based despawn mechanics - * that require additional handling through periodic cleanup.

- * - * @param event The game state change event - * @param cache The NPC cache operations interface - */ - private void handleGameStateChanged(GameStateChanged event, CacheOperations cache) { - switch (event.getGameState()) { - case LOGGED_IN: - // NPCs are handled entirely by spawn/despawn events - no manual scanning needed - lastGameState = GameState.LOGGED_IN; - log.debug("Player logged in - NPC events will handle population automatically"); - break; - case LOADING: - // Region changes during loading automatically trigger NPC despawn/spawn events - lastGameState = GameState.LOADING; - log.debug("Game loading - NPC events will handle region change automatically"); - break; - case LOGIN_SCREEN: - case LOGGING_IN: - case CONNECTION_LOST: - // Clear any ongoing operations and invalidate cache on logout - if (sceneScanTask != null && !sceneScanTask.isDone()) { - sceneScanTask.cancel(true); - sceneScanTask = null; - } - cache.invalidateAll(); - lastGameState = event.getGameState(); - log.debug("Player logged out - cleared NPC cache and stopped operations"); - break; - default: - lastGameState = event.getGameState(); - break; - } - } - - @Override - public Class[] getHandledEventTypes() { - return new Class[]{NpcSpawned.class, NpcDespawned.class, GameStateChanged.class}; - } - - @Override - public void onAttach(CacheOperations cache) { - log.debug("NpcUpdateStrategy attached to cache"); - } - - @Override - public void onDetach(CacheOperations cache) { - log.debug("NpcUpdateStrategy detached from cache"); - // Cancel periodic scanning when detaching - if (periodicSceneScanTask != null && !periodicSceneScanTask.isDone()) { - periodicSceneScanTask.cancel(false); - periodicSceneScanTask = null; - } - } - - @Override - public void close() { - log.debug("Shutting down NpcUpdateStrategy"); - stopPeriodicSceneScan(); - if (sceneScanTask != null && !sceneScanTask.isDone()) { - sceneScanTask.cancel(false); - sceneScanTask = null; - log.debug("Cancelled active NPC scene scan task"); - } - shutdownExecutorService(); - } - /** - * Shuts down the executor service gracefully, waiting for currently executing tasks to complete. - * If the executor does not terminate within the initial timeout, it attempts a forced shutdown. - * Logs warnings or errors if the shutdown process does not complete as expected. - * If interrupted during shutdown, the method forces shutdown and re-interrupts the current thread. - */ - private void shutdownExecutorService() { - if (executorService != null && !executorService.isShutdown()) { - // Shutdown executor service - executorService.shutdown(); - try { - if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { - log.warn("Executor service did not terminate gracefully, forcing shutdown"); - executorService.shutdownNow(); - - // Wait a bit more for tasks to respond to being cancelled - if (!executorService.awaitTermination(2, TimeUnit.SECONDS)) { - log.error("Executor service did not terminate after forced shutdown"); - } - } - } catch (InterruptedException e) { - log.warn("Interrupted during executor shutdown", e); - executorService.shutdownNow(); - Thread.currentThread().interrupt(); - } - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/ObjectUpdateStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/ObjectUpdateStrategy.java deleted file mode 100644 index 375385c1157..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/entity/ObjectUpdateStrategy.java +++ /dev/null @@ -1,720 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy.entity; - -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.ScheduledExecutorService; - -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.*; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.*; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.questhelper.steps.tools.QuestPerspective; -import net.runelite.client.plugins.microbot.util.cache.Rs2Cache; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheOperations; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheUpdateStrategy; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -/** - * Enhanced cache update strategy for game object data. - * Handles all types of object spawn/despawn events and provides scene scanning. - * Uses String-based cache keys for better tracking and region change handling. - */ -@Slf4j -public class ObjectUpdateStrategy implements CacheUpdateStrategy { - GameState lastGameState = null; - - // ScheduledExecutorService for non-blocking operations - private final ScheduledExecutorService executorService; - private ScheduledFuture periodicSceneScanTask; - private ScheduledFuture sceneScanTask; - AtomicBoolean scanActive = new AtomicBoolean(false); - AtomicBoolean scanRequest = new AtomicBoolean(false); - private volatile long lastSceneScan = 0; - private static final long MIN_SCAN_INTERVAL_MS = Constants.GAME_TICK_LENGTH; - - /** - * Constructor initializes the executor service for background tasks. - */ - public ObjectUpdateStrategy() { - this.executorService = Executors.newScheduledThreadPool(2, r -> { - Thread thread = new Thread(r, "ObjectUpdateStrategy-" + System.currentTimeMillis()); - thread.setDaemon(true); - return thread; - }); - } - /** - * Generates a unique cache key for any TileObject (GameObject, WallObject, GroundObject, DecorativeObject). - * Uses canonical location logic for GameObjects, and world location for others. - * - * @param object The TileObject (GameObject, WallObject, etc.) - * @param tile The tile containing the object - * @return The cache key string - */ - public static String generateCacheIdForObject(TileObject object, Tile tile) { - if (object instanceof GameObject) { - // Use canonical location logic for GameObjects - return ObjectUpdateStrategy.generateCacheIdForGameObject((GameObject) object, tile); - } else if (object instanceof WallObject) { - WallObject wallObject = (WallObject) object; - return ObjectUpdateStrategy.generateCacheId("WallObject", wallObject.getId(), wallObject.getWorldLocation()); - } else if (object instanceof GroundObject) { - GroundObject groundObject = (GroundObject) object; - return ObjectUpdateStrategy.generateCacheId("GroundObject", groundObject.getId(), groundObject.getWorldLocation()); - } else if (object instanceof DecorativeObject) { - DecorativeObject decorativeObject = (DecorativeObject) object; - return ObjectUpdateStrategy.generateCacheId("DecorativeObject", decorativeObject.getId(), decorativeObject.getWorldLocation()); - } - // Fallback: use type name and world location if available - String type = object != null ? object.getClass().getSimpleName() : "Unknown"; - WorldPoint location = object != null ? object.getWorldLocation() : null; - int objectId = object != null ? object.getId() : -1; - return location != null ? String.format("%s_%d_%d_%d_%d", type, objectId, location.getX(), location.getY(), location.getPlane()) : type + "_null"; - } - - - @Override - public void handleEvent(final Object event, final CacheOperations cache) { - - if (executorService == null || executorService.isShutdown() || !Microbot.isLoggedIn() || Microbot.getClient() == null || Microbot.getClient().getLocalPlayer() == null) { - log.warn("ObjectUpdateStrategy is shut down, ignoring event: {}", event.getClass().getSimpleName()); - return; // Don't process events if shut down - } - if(scanActive.get()){ - log.debug("Skipping event processing - scan already active: {}", event.getClass().getSimpleName()); - return; // Don't process events if a scan is already active - } - // Submit event handling to executor service for non-blocking processing - //executorService.submit(() -> { - //try { - processEventInternal(event, cache); - //} catch (Exception e) { - //log.error("Error processing event: {}", event.getClass().getSimpleName(), e); - //} - //}); - } - - /** - * Internal method to process events - runs on client thread after coalescing. - * This is the renamed version of the original processEvent method. - */ - private void processEventInternal(final Object event, final CacheOperations cache) { - try { - if (event instanceof GameObjectSpawned) { - if(lastGameState == GameState.LOGGED_IN) handleGameObjectSpawned((GameObjectSpawned) event, cache); - } else if (event instanceof GameObjectDespawned) { - if(lastGameState == GameState.LOGGED_IN) handleGameObjectDespawned((GameObjectDespawned) event, cache); - } else if (event instanceof GroundObjectSpawned) { - if(lastGameState == GameState.LOGGED_IN) handleGroundObjectSpawned((GroundObjectSpawned) event, cache); - } else if (event instanceof GroundObjectDespawned) { - if(lastGameState == GameState.LOGGED_IN) handleGroundObjectDespawned((GroundObjectDespawned) event, cache); - } else if (event instanceof WallObjectSpawned) { - if(lastGameState == GameState.LOGGED_IN) handleWallObjectSpawned((WallObjectSpawned) event, cache); - } else if (event instanceof WallObjectDespawned) { - if(lastGameState == GameState.LOGGED_IN) handleWallObjectDespawned((WallObjectDespawned) event, cache); - } else if (event instanceof DecorativeObjectSpawned) { - if(lastGameState == GameState.LOGGED_IN) handleDecorativeObjectSpawned((DecorativeObjectSpawned) event, cache); - } else if (event instanceof DecorativeObjectDespawned) { - if(lastGameState == GameState.LOGGED_IN) handleDecorativeObjectDespawned((DecorativeObjectDespawned) event, cache); - } else if (event instanceof GameStateChanged) { - handleGameStateChanged((GameStateChanged) event, cache); - } else if (event instanceof GameTick) { - //handleGameTick((GameTick) event, cache); - } - } catch (Exception e) { - log.error("Error handling event: {}", event.getClass().getSimpleName(), e); - } - } - - /** - * Performs an scene scan to populate the cache. - * Only scans if certain conditions are met to avoid unnecessary processing. - * This method now runs asynchronously on a background thread to avoid blocking. - * - * @param cache The cache to populate - * @param force Whether to force a scan regardless of conditions - */ - public void performSceneScan(CacheOperations cache, long delayMs) { - if (executorService == null || executorService.isShutdown()) { - log.debug("Skipping scene scan - is executor service shutdown: {} or null {}, scan active: {}", - executorService.isShutdown() ,executorService == null , scanActive.get()); - return; - } - - // Respect minimum scan interval unless forced - if ((System.currentTimeMillis() - lastSceneScan) < MIN_SCAN_INTERVAL_MS) { - log.debug("Skipping scene scan due to minimum interval not reached"); - scanActive.set(false); - return; - } - if (sceneScanTask != null && !sceneScanTask.isDone()) { - log.debug("Skipping scene scan - already scheduled or running"); - return; // Don't perform scan if already scheduled or running - } - if (scanActive.compareAndSet(false,true)){ - // Submit scene scan to executor service for non-blocking execution - sceneScanTask = executorService.schedule(() -> { - try { - performSceneScanInternal(cache); - } catch (Exception e) { - log.error("Error during scene scan", e); - }finally { - - } - }, delayMs, TimeUnit.MILLISECONDS); // 100ms delay before scan - }else{ - log.debug("Skipping scene scan - already active"); - return; // Don't perform scan if already active - } - } - - /** - * Schedules a periodic scene scan task. - * This is useful for ensuring the cache stays up-to-date even if events are missed. - * - * @param cache The cache to scan - * @param intervalSeconds The interval between scans in seconds - */ - public void schedulePeriodicSceneScan(CacheOperations cache, long intervalSeconds) { - if (executorService == null || executorService.isShutdown()) { - log.debug("Cannot schedule periodic scan - strategy is shut down"); - return; - } - stopPeriodicSceneScan(); - - periodicSceneScanTask = executorService.scheduleWithFixedDelay(() -> { - try { - if (scanActive.compareAndSet(false,true)){ - if (scanRequest.get() && Microbot.isLoggedIn()) { // Only perform scan if request is set and not already active - log.debug("Performing scheduled scene scan"); - performSceneScanInternal(cache); - } - } else { - log.debug("Skipping scheduled scene scan - already active"); - } - } catch (Exception e) { - log.error("Error during scheduled scene scan", e); - }finally { - - } - - }, intervalSeconds, intervalSeconds, TimeUnit.SECONDS); - - log.debug("Scheduled periodic scene scan every {} seconds", intervalSeconds); - } - - /** - * Internal implementation of scene scanning that runs on background thread. - */ - - private void performSceneScanInternal(CacheOperations cache) { - - try { - long currentTime = System.currentTimeMillis(); - log.debug("Performing scene scan (last scan: {}, current time: {}) , loggedin: {}", lastSceneScan, currentTime, Microbot.isLoggedIn()); - if (!Microbot.isLoggedIn() || Microbot.getClient() == null || Microbot.getClient().getLocalPlayer() == null) { - log.warn("Cannot perform scene scan - not logged in"); - scanActive.set(false); - return; - } - - Player player = Rs2Player.getLocalPlayer(); - if (player == null) { - log.warn("Cannot perform scene scan - no player"); - scanActive.set(false); - return; - } - WorldPoint playerPoint = QuestPerspective.getRealWorldPointFromLocal( Microbot.getClient(), player.getWorldLocation()); - if (playerPoint == null) { - log.warn("Cannot perform scene scan - player location is null"); - scanActive.set(false); - return ; - } - - WorldView worldView = player.getWorldView(); - if (worldView == null) { - log.warn("Cannot perform scene scan - no world view"); - scanActive.set(false); - return; - } - WorldView topLevelWorldView = Microbot.getClient().getTopLevelWorldView(); - if (topLevelWorldView == null) { - log.warn("Cannot perform scene scan - no top-level world view"); - scanActive.set(false); - return; - } - Scene scene = worldView.getScene(); - if (scene == null) { - log.warn("Cannot perform scene scan - no scene"); - scanActive.set(false); - return; - } - - - Tile[][][] tiles = scene.getTiles(); - if (tiles == null) { - log.warn("Cannot perform scene scan - no tiles"); - scanActive.set(false); - return; - } - - // Build a set of all currently existing object keys from the scene - java.util.Set currentSceneKeys = new java.util.HashSet<>(); - java.util.Map objectsToAdd = new java.util.HashMap<>(); - int z = worldView.getPlane(); - - log.debug("Starting object scene synchronization (cache size: {})", cache.size()); - - // Phase 1: Scan scene and collect all objects - for (int x = 0; x < Constants.SCENE_SIZE; x++) { - for (int y = 0; y < Constants.SCENE_SIZE; y++) { - Tile tile = tiles[z][x][y]; - if (tile == null) continue; - - // Check GameObjects - GameObject[] gameObjects = tile.getGameObjects(); - if (gameObjects != null) { - for (GameObject gameObject : gameObjects) { - if (gameObject == null) continue; - - // Only add if it's the primary location for multi-tile objects - if (gameObject.getSceneMinLocation().equals(tile.getSceneLocation())) { - String cacheId = generateCacheIdForGameObject(gameObject, tile); - currentSceneKeys.add(cacheId); // Track all scene objects - if (!objectsToAdd.containsKey(cacheId)) { - Rs2ObjectModel objectModel = new Rs2ObjectModel(gameObject, tile); - objectsToAdd.put(cacheId, objectModel); - } - } - } - } - - // Check GroundObject - GroundObject groundObject = tile.getGroundObject(); - if (groundObject != null) { - String cacheId = generateCacheId("GroundObject", groundObject.getId(), groundObject.getWorldLocation()); - currentSceneKeys.add(cacheId); // Track all scene objects - if (!objectsToAdd.containsKey(cacheId)) { - Rs2ObjectModel objectModel = new Rs2ObjectModel(groundObject, tile); - objectsToAdd.put(cacheId, objectModel); - } - } - - // Check WallObject - WallObject wallObject = tile.getWallObject(); - if (wallObject != null) { - String cacheId = generateCacheId("WallObject", wallObject.getId(), wallObject.getWorldLocation()); - currentSceneKeys.add(cacheId); // Track all scene objects - if (!objectsToAdd.containsKey(cacheId)) { - Rs2ObjectModel objectModel = new Rs2ObjectModel(wallObject, tile); - objectsToAdd.put(cacheId, objectModel); - } - } - - // Check DecorativeObject - DecorativeObject decorativeObject = tile.getDecorativeObject(); - if (decorativeObject != null) { - String cacheId = generateCacheId("DecorativeObject", decorativeObject.getId(), decorativeObject.getWorldLocation()); - currentSceneKeys.add(cacheId); // Track all scene objects - if (!objectsToAdd.containsKey(cacheId)) { - Rs2ObjectModel objectModel = new Rs2ObjectModel(decorativeObject, tile); - objectsToAdd.put(cacheId, objectModel); - } - } - } - } - - // Phase 2: Add new objects to cache - int addedObjects = 0; - for (java.util.Map.Entry entry : objectsToAdd.entrySet()) { - String cacheId = entry.getKey(); - Rs2ObjectModel objectModel = entry.getValue(); - - // Only add if not already in cache (avoid recursive get calls by checking internally) - if (!cache.containsKey(cacheId)) { - cache.put(cacheId, objectModel); - addedObjects++; - } - } - - // Phase 3: Remove cached objects no longer in scene - int removedObjects = 0; - if (!currentSceneKeys.isEmpty()) { - // Find cached objects that are no longer in the scene using CacheOperations streaming - java.util.List keysToRemove = cache.entryStream() - .map(java.util.Map.Entry::getKey) - .filter(key -> !currentSceneKeys.contains(key)) - .collect(java.util.stream.Collectors.toList()); - - // Remove the objects that are no longer in scene - for (String key : keysToRemove) { - Rs2ObjectModel object = cache.getRawValue(key); // Use raw value to avoid triggering recursive scene scans - cache.remove(key); - if (object != null) { - removedObjects++; - log.trace("Removed object not in scene: ID {} ({})", object.getId(), key); - } - } - } - - // Log comprehensive results - if (addedObjects > 0 || removedObjects > 0) { - log.debug("Object scene synchronization completed - added {} objects, removed {} objects (total cache size: {}), time taken: {} ms", - addedObjects, removedObjects, cache.size(), System.currentTimeMillis() - currentTime); - } else { - log.debug("Object scene synchronization completed - no changes made"); - } - scanRequest.set(false); //NOT in finally block to allow for rescan if there are an error - } catch (Exception e) { - log.error("Error during scene scan", e); - } finally { - // Reset scan state - lastSceneScan = System.currentTimeMillis(); // Update last scan time; - scanActive.set(false); - } - - - } - - - - /** - * Stops periodic scene scanning if currently active. - */ - public void stopPeriodicSceneScan() { - if (isPeriodicSceneScanActive()) { - periodicSceneScanTask.cancel(false); - periodicSceneScanTask = null; - log.debug("Stopped periodic scene scanning"); - } - } - - /** - * Checks if periodic scene scanning is currently active. - * - * @return true if periodic scanning is running - */ - private boolean isPeriodicSceneScanActive() { - return periodicSceneScanTask != null && !periodicSceneScanTask.isDone(); - } - - /** - * Checks if a scene scan is needed based on cache state. - * - * @param cache The cache to check - * @return true if a scan would be beneficial - */ - public boolean requestSceneScan(CacheOperations cache) { - if (scanActive.get()) { - log.debug("Skipping scene scan request - already active"); - return false; // Don't request scan if already active - } - if (scanRequest.compareAndSet(false, true)) { - log.debug("Object scene scan requested"); - performSceneScan(cache, 5); // Perform immediately - } - if (!Microbot.getClient().isClientThread()){ - sleepUntil(()->!scanRequest.get(), 1000); // Wait until scan is requested and reset - } - return !scanRequest.get(); // Return true if scan was requested,reseted - } - private void handleGameObjectSpawned(GameObjectSpawned event, CacheOperations cache) { - GameObject gameObject = event.getGameObject(); - Tile tile = event.getTile(); - if (gameObject != null && tile != null) { - // Only add multi-tile objects from their primary (southwest) tile to prevent duplicates - String cacheId = generateCacheIdForGameObject(gameObject, tile); - if (cache.containsKey(cacheId)) { - log.warn("GameObject {} already in cache, skipping spawn event", gameObject.getId()); - return; // Already cached, skip - } - if (isPrimaryTile(gameObject, tile) || !cache.containsKey(cacheId)) { - Rs2ObjectModel objectModel = new Rs2ObjectModel(gameObject, tile); - cache.put(cacheId, objectModel); - log.debug("Added GameObject {} (id: {}) to cache via spawn event from primary tile", - gameObject.getId(), cacheId); - } else { - log.warn("Skipped GameObject {} spawn event from non-primary tile", gameObject.getId()); - } - } - } - - private void handleGameObjectDespawned(GameObjectDespawned event, CacheOperations cache) { - GameObject gameObject = event.getGameObject(); - Tile tile = event.getTile(); - if (gameObject != null && tile != null) { - // Only process despawn events from the primary tile to prevent multiple removal attempts - String cacheId = generateCacheIdForGameObject(gameObject, tile); - if (!cache.containsKey(cacheId)) { - log.warn("GameObject {} not in cache, skipping despawn event", gameObject.getId()); - return; // Not in cache, skip - } - if (isPrimaryTile(gameObject, tile)|| cache.containsKey(cacheId)) { - cache.remove(cacheId); - log.debug("Removed GameObject {} (id: {}) from cache via despawn event from primary tile", - gameObject.getId(), cacheId); - } else { - log.warn("Skipped GameObject {} despawn event from non-primary tile", gameObject.getId()); - } - } - } - - private void handleGroundObjectSpawned(GroundObjectSpawned event, CacheOperations cache) { - GroundObject groundObject = event.getGroundObject(); - Tile tile = event.getTile(); - String cacheId = generateCacheId("GroundObject", groundObject.getId(), groundObject.getWorldLocation()); - if (cache.containsKey(cacheId)) { - log.warn("GroundObject {} already in cache, skipping spawn event", groundObject.getId()); - return; // Already cached, skip - } - if (groundObject != null && tile != null) { - Rs2ObjectModel objectModel = new Rs2ObjectModel(groundObject, tile); - cache.put(cacheId, objectModel); - log.debug("Added GroundObject {} (id: {}) to cache via spawn event", groundObject.getId(), cacheId); - } - } - - private void handleGroundObjectDespawned(GroundObjectDespawned event, CacheOperations cache) { - GroundObject groundObject = event.getGroundObject(); - if (groundObject != null) { - String cacheId = generateCacheId("GroundObject", groundObject.getId(), groundObject.getWorldLocation()); - cache.remove(cacheId); - log.debug("Removed GroundObject {} (id: {}) from cache via despawn event", groundObject.getId(), cacheId); - } - } - - private void handleWallObjectSpawned(WallObjectSpawned event, CacheOperations cache) { - WallObject wallObject = event.getWallObject(); - Tile tile = event.getTile(); - - if (wallObject != null && tile != null) { - String cacheId = generateCacheId("WallObject", wallObject.getId(), wallObject.getWorldLocation()); - if (cache.containsKey(cacheId)) { - log.warn("WallObject {} already in cache, skipping spawn event", wallObject.getId()); - return; // Already cached, skip - } - Rs2ObjectModel objectModel = new Rs2ObjectModel(wallObject, tile); - - cache.put(cacheId, objectModel); - log.debug("Added WallObject {} (id: {}) to cache via spawn event", wallObject.getId(), cacheId); - } - } - - private void handleWallObjectDespawned(WallObjectDespawned event, CacheOperations cache) { - WallObject wallObject = event.getWallObject(); - if (wallObject != null) { - String cacheId = generateCacheId("WallObject", wallObject.getId(), wallObject.getWorldLocation()); - cache.remove(cacheId); - log.debug("Removed WallObject {} (id: {}) from cache via despawn event", wallObject.getId(), cacheId); - } - } - - private void handleDecorativeObjectSpawned(DecorativeObjectSpawned event, CacheOperations cache) { - DecorativeObject decorativeObject = event.getDecorativeObject(); - Tile tile = event.getTile(); - if (decorativeObject != null && tile != null) { - String cacheId = generateCacheId("DecorativeObject", decorativeObject.getId(), decorativeObject.getWorldLocation()); - if (cache.containsKey(cacheId)) { - log.warn("DecorativeObject {} already in cache, skipping spawn event", decorativeObject.getId()); - return; // Already cached, skip - } - Rs2ObjectModel objectModel = new Rs2ObjectModel(decorativeObject, tile); - cache.put(cacheId, objectModel); - log.debug("Added DecorativeObject {} (id: {}) to cache via spawn event", decorativeObject.getId(), cacheId); - } - } - - private void handleDecorativeObjectDespawned(DecorativeObjectDespawned event, CacheOperations cache) { - DecorativeObject decorativeObject = event.getDecorativeObject(); - if (decorativeObject != null) { - String cacheId = generateCacheId("DecorativeObject", decorativeObject.getId(), decorativeObject.getWorldLocation()); - cache.remove(cacheId); - log.debug("Removed DecorativeObject {} (id: {}) from cache via despawn event", decorativeObject.getId(), cacheId); - } - } - - private void handleGameStateChanged(GameStateChanged event, CacheOperations cache) { - switch (event.getGameState()) { - case LOGGED_IN: - // Check for region changes and perform scene scan to synchronize - if (Rs2Cache.checkAndHandleRegionChange(cache)) { - log.debug("Region change detected on login - performing scene synchronization"); - performSceneScan(cache, Constants.GAME_TICK_LENGTH *3); - } else if (lastGameState != null && lastGameState != GameState.LOGGED_IN) { - // Perform scene synchronization when logging in - might have missed spawn events - performSceneScan(cache, Constants.GAME_TICK_LENGTH *3); // Perform scan after 2 game ticks to allow scene to stabilize - } - - lastGameState = GameState.LOGGED_IN; - log.debug("Player logged in - checking regions and requesting scene synchronization"); - break; - case LOADING: - // Check for region changes during loading - if (Rs2Cache.checkAndHandleRegionChange(cache)) { - log.debug("Region change detected during loading - performing scene synchronization"); - performSceneScan(cache, Constants.GAME_TICK_LENGTH *1); - } else { - performSceneScan(cache, Constants.GAME_TICK_LENGTH*1); // Perform scan after 4 game ticks to allow scene to stabilize - } - lastGameState = GameState.LOADING; - log.debug("Game loading - checking regions and requesting scene synchronization"); - break; - case LOGIN_SCREEN: - case LOGGING_IN: - case CONNECTION_LOST: - // Clear scan request when logging out and stop periodic scanning - if (sceneScanTask != null && !sceneScanTask.isDone()) { - sceneScanTask.cancel(true); - sceneScanTask = null; - } - scanRequest.set(false); // Reset scan request - cache.invalidateAll(); - lastGameState = event.getGameState(); - log.debug("Player logged out - clearing scan request and stopping periodic scanning"); - break; - default: - lastGameState = event.getGameState(); - break; - } - } - - /** - * Gets the canonical world location for a GameObject. - * For multi-tile objects, this returns the southwest tile location. - * - * @param gameObject The GameObject to get the canonical location for - * @param tile The tile from the event - * @return The canonical world location - */ - private static WorldPoint getCanonicalLocation(GameObject gameObject, Tile tile) { - // For multi-tile objects, we need to ensure we use the southwest tile consistently - Point sceneMinLocation = gameObject.getSceneMinLocation(); - Point currentSceneLocation = tile.getSceneLocation(); - - // If this is the southwest tile, use this tile's location - if (sceneMinLocation != null && currentSceneLocation != null && - sceneMinLocation.getX() == currentSceneLocation.getX() && - sceneMinLocation.getY() == currentSceneLocation.getY()) { - return tile.getWorldLocation(); - } - - // Otherwise, we need to calculate the southwest tile's world location - // This is tricky without scene-to-world conversion, so we'll use a different approach - WorldPoint currentLocation = tile.getWorldLocation(); - if (sceneMinLocation != null && currentSceneLocation != null) { - int deltaX = currentSceneLocation.getX() - sceneMinLocation.getX(); - int deltaY = currentSceneLocation.getY() - sceneMinLocation.getY(); - return new WorldPoint(currentLocation.getX() - deltaX, currentLocation.getY() - deltaY, currentLocation.getPlane()); - } - - return currentLocation; - } - - /** - * Checks if the given tile is the primary (southwest) tile for a GameObject. - * - * @param gameObject The GameObject to check - * @param tile The tile to verify - * @return true if this is the primary tile, false otherwise - */ - private boolean isPrimaryTile(GameObject gameObject, Tile tile) { - Point sceneMinLocation = gameObject.getSceneMinLocation(); - Point currentSceneLocation = tile.getSceneLocation(); - - return sceneMinLocation != null && currentSceneLocation != null && - sceneMinLocation.getX() == currentSceneLocation.getX() && - sceneMinLocation.getY() == currentSceneLocation.getY(); - } - - /** - * Generates a unique object ID for tracking. - * For GameObjects, uses the canonical (southwest) location to ensure consistent caching. - */ - private static String generateCacheId(String type, int objectID, net.runelite.api.coords.WorldPoint location) { - return String.format("%s_%d_%d_%d_%d", type, objectID, location.getX(), location.getY(), location.getPlane()); - } - - /** - * Generates a unique object ID for tracking GameObjects using their canonical location. - * This ensures that multi-tile GameObjects have consistent cache keys. - */ - private static String generateCacheIdForGameObject(GameObject gameObject, Tile tile) { - WorldPoint canonicalLocation = getCanonicalLocation(gameObject, tile); - return generateCacheId("GameObject", gameObject.getId(), canonicalLocation); - } - - @Override - public Class[] getHandledEventTypes() { - return new Class[]{ - GameObjectSpawned.class, GameObjectDespawned.class, - GroundObjectSpawned.class, GroundObjectDespawned.class, - WallObjectSpawned.class, WallObjectDespawned.class, - DecorativeObjectSpawned.class, DecorativeObjectDespawned.class, - GameStateChanged.class, GameTick.class - }; - } - - @Override - public void onAttach(CacheOperations cache) { - log.debug("ObjectUpdateStrategy attached to cache"); - // Start periodic scene scanning if logged in - if (Microbot.isLoggedIn() && lastGameState == GameState.LOGGED_IN) { - //schedulePeriodicSceneScan(cache, 30); // Every 30 seconds - } - } - - @Override - public void onDetach(CacheOperations cache) { - log.debug("ObjectUpdateStrategy detached from cache"); - // Cancel periodic scanning when detaching - if (periodicSceneScanTask != null && !periodicSceneScanTask.isDone()) { - periodicSceneScanTask.cancel(false); - periodicSceneScanTask = null; - } - } - - @Override - public void close() { - log.debug("Shutting down ObjectUpdateStrategy"); - stopPeriodicSceneScan(); - if (sceneScanTask != null && !sceneScanTask.isDone()) { - sceneScanTask.cancel(false); - sceneScanTask = null; - log.debug("Cancelled active scene scan task"); - - } - shutdownExecutorService(); - } - /** - * Shuts down the executor service gracefully, waiting for currently executing tasks to complete. - * If the executor does not terminate within the initial timeout, it attempts a forced shutdown. - * Logs warnings or errors if the shutdown process does not complete as expected. - * If interrupted during shutdown, the method forces shutdown and re-interrupts the current thread. - */ - private void shutdownExecutorService() { - if (executorService != null && !executorService.isShutdown()) { - // Shutdown executor service - executorService.shutdown(); - try { - if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { - log.warn("Executor service did not terminate gracefully, forcing shutdown"); - executorService.shutdownNow(); - - // Wait a bit more for tasks to respond to being cancelled - if (!executorService.awaitTermination(2, TimeUnit.SECONDS)) { - log.error("Executor service did not terminate after forced shutdown"); - } - } - } catch (InterruptedException e) { - log.warn("Interrupted during executor shutdown", e); - executorService.shutdownNow(); - Thread.currentThread().interrupt(); - } - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/farming/SpiritTreeUpdateStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/farming/SpiritTreeUpdateStrategy.java deleted file mode 100644 index 0c47a9b9d82..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/farming/SpiritTreeUpdateStrategy.java +++ /dev/null @@ -1,385 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy.farming; - -import java.util.Objects; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.GameState; -import net.runelite.api.GameObject; -import net.runelite.api.Skill; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.*; -import net.runelite.api.gameval.ObjectID; -import net.runelite.api.gameval.VarbitID; -import net.runelite.api.widgets.Widget; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.questhelper.helpers.mischelpers.farmruns.CropState; -import net.runelite.client.plugins.microbot.util.cache.Rs2Cache; -import net.runelite.client.plugins.microbot.util.cache.model.SpiritTreeData; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheOperations; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheUpdateStrategy; -import net.runelite.client.plugins.microbot.util.farming.SpiritTree; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; -import net.runelite.client.plugins.microbot.util.poh.PohTeleports; -import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; - -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -/** - * Cache update strategy for spirit tree farming data. - * Handles WidgetLoaded, VarbitChanged, and GameObjectSpawned events to detect spirit tree states - * and travel availability with enhanced contextual information. - */ -@Slf4j -public class SpiritTreeUpdateStrategy implements CacheUpdateStrategy { - - // Widget constants for spirit tree detection - private static final int ADVENTURE_LOG_GROUP_ID = 187; - private static final int ADVENTURE_LOG_CONTAINER_CHILD = 0; - private static final String SPIRIT_TREE_WIDGET_TITLE = SpiritTree.SPIRIT_TREE_WIDGET_TITLE; - - // Spirit tree object IDs for game object detection - private static final List SPIRIT_TREE_OBJECT_IDS = Arrays.asList( - ObjectID.FARMING_SPIRIT_TREE_PATCH_5, // Object ID found in farming guild fully grown spirit tree patch - ObjectID.SPIRIT_TREE_FULLYGROWN, // Standard spirit spiritTree id when fully grown and available for travel -> healty ) - ObjectID.SPIRITTREE_PRIF, // Prifddinas spirit tree - ObjectID.POG_SPIRIT_TREE_ALIVE_STATIC, // Poison Waste spirit tree - ObjectID.SPIRITTREE_SMALL, // Small spirit tree in grand Exchange 1295 - ObjectID.ENT, // for the "great" trees in tree gnome village 1293 - ObjectID.STRONGHOLD_ENT, // nd tree gnome stronghold 1294 - ObjectID.POH_SPIRIT_TREE // Player-owned house spirit tree object ID - ); - - // Spirit tree specific farming transmit varbits - private static final List SPIRIT_TREE_VARBIT_IDS = Arrays.asList( - VarbitID.FARMING_TRANSMIT_A, // 4771 - Port Sarim and Farming Guild - VarbitID.FARMING_TRANSMIT_B, // 4772 - Etceteria and Brimhaven patches - VarbitID.FARMING_TRANSMIT_F // 7904 - Hosidius - ); - - /** - * Handle an event from the client and update the cache accordingly. - * @param event The event that occurred - * @param cache The cache to potentially update - */ - @Override - public void handleEvent(Object event, CacheOperations cache) { - try { - if (event instanceof WidgetLoaded) { - handleWidgetLoaded((WidgetLoaded) event, cache); - } else if (event instanceof VarbitChanged) { - handleVarbitChanged((VarbitChanged) event, cache); - } else if (event instanceof GameStateChanged) { - handleGameStateChanged((GameStateChanged) event, cache); - } else if (event instanceof GameObjectSpawned) { - GameObject go = ((GameObjectSpawned) event).getGameObject(); - handleGameObjectChange(go, true, cache); - } else if (event instanceof GameObjectDespawned) { - GameObject go = ((GameObjectDespawned) event).getGameObject(); - handleGameObjectChange(go, false, cache); - } - } catch (Exception e) { - log.error("Error handling event in SpiritTreeUpdateStrategy: {}", e.getMessage(), e); - } - } - - /** - * Get the event types that are handled by this strategy - */ - @Override - public Class[] getHandledEventTypes() { - return new Class[]{WidgetLoaded.class, VarbitChanged.class, GameStateChanged.class, GameObjectSpawned.class, GameObjectDespawned.class}; - } - - /** - * Handle widget loaded events to detect spirit tree widget opening - */ - private void handleWidgetLoaded(WidgetLoaded event, CacheOperations cache) { - // Check if this is the adventure log widget with spirit tree locations - if (event.getGroupId() == ADVENTURE_LOG_GROUP_ID) { - // Small delay to ensure widget is fully loaded - Microbot.getClientThread().invokeLater(() -> { - try { - if(Rs2Widget.isHidden(ADVENTURE_LOG_GROUP_ID, ADVENTURE_LOG_CONTAINER_CHILD)) { - log.debug("Adventure log widget not found, skipping spirit tree update"); - return; - } - Widget titleWidget = Rs2Widget.getWidget(ADVENTURE_LOG_GROUP_ID, ADVENTURE_LOG_CONTAINER_CHILD); - if (titleWidget == null || titleWidget.isHidden()) { - log.debug("Adventure log widget not found, skipping spirit tree update"); - return; - } - log.debug("Adventure log widget loaded (group {}), checking for spirit tree locations", event.getGroupId()); - boolean hasRightTitle = Rs2Widget.hasWidgetText(SPIRIT_TREE_WIDGET_TITLE, ADVENTURE_LOG_GROUP_ID, ADVENTURE_LOG_CONTAINER_CHILD, false); - - if (hasRightTitle) { - log.debug("Spirit tree locations widget detected, updating cache from widget data"); - updateCacheFromWidget(cache); - - } - } catch (Exception e) { - log.debug("Error checking spirit tree widget: {}", e.getMessage()); - } - }); - } - } - - /** - * Handle varbit changed events for spirit tree farming transmit varbits - */ - private void handleVarbitChanged(VarbitChanged event, CacheOperations cache) { - int varbitId = event.getVarbitId(); - // Check if this is a spirit tree farming transmit varbit - if (SPIRIT_TREE_VARBIT_IDS.contains(varbitId)) { - log.debug("Spirit tree farming varbit {} changed to value {}, updating spirit tree farming states", varbitId, event.getValue()); - // Update farming states for all farmable spirit tree patches - updateFarmingStatesFromVarbits(cache); - } - if(varbitId == VarbitID.POH_SPIRIT_TREE_UPROOTED){ - log.debug("TODO update cache for POH spirit tree uprooted varbit change,currently in POH? {} ",PohTeleports.isInHouse()); - } - } - - /** - * Handles changes to game objects related to spirit trees and updates the cache accordingly. - * If the provided game object matches known spirit tree objects, it logs the detection - * and updates the cache with the relevant state. - * - * @param gameObject The game object that has changed. May be null, in which case this method does nothing. - * @param spawned Indicates whether the game object was spawned (true) or despawned (false). - * @param cache The cache instance used for storing and updating {@link SpiritTreeData} for detected spirit trees. - */ - private void handleGameObjectChange(GameObject gameObject, boolean spawned, CacheOperations cache) { - if(gameObject == null){ - return; - } - Arrays.stream(SpiritTree.values()).filter(tree -> tree.getType() == SpiritTree.SpiritTreeType.POH).forEach(tree -> { - if (tree.getObjectId().contains(gameObject.getId())) { - log.info("Found spirit tree object {} for POH spirit tree {}, {} to cache", gameObject.getId(), tree.name(), spawned ? "added" : "removed"); - SpiritTreeData newData = new SpiritTreeData( - tree, - spawned ? CropState.HARVESTABLE: CropState.DEAD, - spawned, - gameObject.getWorldLocation(), - false, // Not detected via widget - true // Detected via nearby tree if present - ); - cache.put(tree, newData); - } - }); - - } - - /** - * Handle GameStateChanged events to detect POH region changes and validate spirit tree presence - */ - private void handleGameStateChanged(GameStateChanged event, CacheOperations cache) { - GameState gameState = event.getGameState(); - - // Only process when entering game or loading regions - if (gameState != GameState.LOGGED_IN && gameState != GameState.LOADING) { - return; - } - - try { - // Use unified region detection from Rs2Cache - Rs2Cache.checkAndHandleRegionChange(cache); - } catch (Exception e) { - log.error("Error handling GameStateChanged in SpiritTreeUpdateStrategy: {}", e.getMessage(), e); - } - } - - /** - * Update cache from spirit tree widget data - */ - private void updateCacheFromWidget(CacheOperations cache) { - try { - // Extract available destinations from the widget - List availableSpiritTrees = SpiritTree.extractAvailableFromWidget(); - WorldPoint playerLocation = getPlayerLocation(); - log.debug("Widget extraction found {} available spirit tree destinations", availableSpiritTrees.size()); - for (SpiritTree spiritTree : SpiritTree.values()) { - boolean isAvailable = availableSpiritTrees.contains(spiritTree); - boolean availableForTravel = spiritTree.hasQuestRequirements(); - - SpiritTreeData existingData = cache.get(spiritTree); - - // Create new data with widget detection - SpiritTreeData newData = new SpiritTreeData( - spiritTree, - existingData != null ? existingData.getCropState() : null, // Preserve existing crop state if available - isAvailable && availableForTravel, - playerLocation, - isAvailable, // Detected via widget - false // Not detected via near tree - ); - - cache.put(spiritTree, newData); - - log.debug("Updated spirit tree cache for via widget ({} for travel)\n\t{}", isAvailable ? "available" : "not available", spiritTree.name()); - } - - } catch (Exception e) { - log.error("Error updating cache from spirit tree widget: {}", e.getMessage(), e); - } - } - - /** - * Update farming states from varbit changes with object detection - only when near spirit tree patches - */ - private void updateFarmingStatesFromVarbits(CacheOperations cache) { - try { - WorldPoint playerLocation = getPlayerLocation(); - - if (playerLocation == null) { - return; // Can't determine player location - } - - // Only update if player is near a spirit tree (within region) - boolean nearSpiritTree = SpiritTree.getFarmableSpirtTrees().stream() - .anyMatch(spiritTree -> Arrays.stream(spiritTree.getRegionIds()) - .anyMatch(regionId -> regionId != -1 && regionId == playerLocation.getRegionID())); - - if (!nearSpiritTree) { - log.trace("Player not near any spirit tree patches, skipping varbit update"); - return; - } - - // Update all farmable spirit tree patches - for (SpiritTree spiritTree : SpiritTree.getFarmableSpirtTrees()) { - try { - CropState currentState = spiritTree.getPatchState(); - boolean availableForTravel = spiritTree.isAvailableForTravel(); - - // Enhanced: Check for nearby spirit tree objects to verify travel availability - boolean hasNearbyTravelObject = checkForNearbyTree(spiritTree, playerLocation); - - // Use object detection to override travel availability if object is found - if (hasNearbyTravelObject) { - availableForTravel = true; - log.debug("Found nearby travel object for {}, setting available=true", spiritTree.name()); - } - - SpiritTreeData existingData = cache.get(spiritTree); - - // Only update if state actually changed or this is new data - if (existingData == null || - existingData.getCropState() != currentState || - existingData.isAvailableForTravel() != availableForTravel) { - - SpiritTreeData newData = new SpiritTreeData( - spiritTree, - currentState, - availableForTravel, - playerLocation, - false, // Not detected via widget - true // Detected via varbit when near patch - ); - - cache.put(spiritTree, newData); - log.debug("Updated spirit tree cache for {} via varbit (state: {}, available: {}, hasObject: {})", - spiritTree.name(), currentState, availableForTravel, hasNearbyTravelObject); - } - } catch (Exception e) { - log.debug("Error updating farming state for spiritTree {}: {}", spiritTree.name(), e.getMessage()); - } - } - } catch (Exception e) { - log.error("Error updating farming states from varbits: {}", e.getMessage(), e); - } - } - - /** - * Check for nearby spirit tree objects that have travel actions. - * This integrates the object detection logic from the former GameObjectSpawned handler. - * - * @param spiritTree The spirit tree patch to check for - * @param playerLocation Current player location - * @return true if a nearby object with travel action is found - */ - private boolean checkForNearbyTree(SpiritTree spiritTree, WorldPoint playerLocation) { - try { - // Get all game objects near the spirit tree patch location - Optional nearbyObject = Rs2GameObject.getGameObjects() - .stream() - .filter(obj -> SPIRIT_TREE_OBJECT_IDS.contains(obj.getId())) - .filter(obj -> spiritTree.getLocation().distanceTo(obj.getWorldLocation()) <= 5) - .findFirst(); - - if (nearbyObject.isPresent()) { - GameObject gameObject = nearbyObject.get(); - - // Check if the game object has "Travel" action (indicates it's usable) - try { - String[] actions = Microbot.getClient().getObjectDefinition(gameObject.getId()).getActions(); - boolean hasTravel = Arrays.stream(actions) - .filter(Objects::nonNull) - .anyMatch(action -> action.equalsIgnoreCase("Travel")); - - if (hasTravel) { - log.debug("Found spirit tree object {} with Travel action at {} for patch {}", - gameObject.getId(), gameObject.getWorldLocation(), spiritTree.name()); - return true; - } - } catch (Exception e) { - log.debug("Could not get actions for spirit tree object {}: {}", gameObject.getId(), e.getMessage()); - } - } - - return false; - } catch (Exception e) { - log.debug("Error checking for nearby travel object for {}: {}", spiritTree.name(), e.getMessage()); - return false; - } - } - - /** - - * Find spirit tree spiritTree by object location - */ - private SpiritTree findSpiritTreeByLocation(WorldPoint objectLocation) { - if (objectLocation == null) { - return null; - } - - for (SpiritTree spiritTree : SpiritTree.values()) { - WorldPoint spiritTreeLocation = spiritTree.getLocation(); - if (spiritTreeLocation != null && spiritTreeLocation.distanceTo(objectLocation) <= 5) { - return spiritTree; - } - } - - return null; - } - - /** - * Get current player location safely - */ - private WorldPoint getPlayerLocation() { - try { - if (Microbot.getClient() != null && - Microbot.getClient().getGameState() == GameState.LOGGED_IN && - Microbot.getClient().getLocalPlayer() != null) { - return Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - } catch (Exception e) { - log.trace("Could not get player location: {}", e.getMessage()); - } - return null; - } - - /** - * Get current farming level safely - */ - private Integer getFarmingLevel() { - try { - if (Microbot.getClient() != null && - Microbot.getClient().getGameState() == GameState.LOGGED_IN) { - return Microbot.getClient().getRealSkillLevel(Skill.FARMING); - } - } catch (Exception e) { - log.trace("Could not get farming level: {}", e.getMessage()); - } - return null; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/QuestUpdateStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/QuestUpdateStrategy.java deleted file mode 100644 index 3f37b3cd20c..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/QuestUpdateStrategy.java +++ /dev/null @@ -1,387 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy.simple; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Quest; -import net.runelite.api.QuestState; -import net.runelite.api.events.ChatMessage; -import net.runelite.api.events.VarbitChanged; -import net.runelite.api.ChatMessageType; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.questhelper.QuestHelperPlugin; -import net.runelite.client.plugins.microbot.questhelper.questhelpers.QuestHelper; -import net.runelite.client.plugins.microbot.questhelper.questinfo.QuestHelperQuest; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheOperations; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheUpdateStrategy; - -import java.lang.reflect.Field; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Cache update strategy for quest data. - * Handles VarbitChanged and ChatMessage events to update quest state information. - * Uses QuestHelperQuest enum to efficiently detect quest-related changes. - */ -@Slf4j -public class QuestUpdateStrategy implements CacheUpdateStrategy { - - // Quest tracking variables moved from Rs2QuestCache - private static Quest trackedQuest = null; - - // Map from varbit/varplayer IDs to corresponding RuneLite Quest objects - private static final Map varbitToQuestMap = new ConcurrentHashMap<>(); - private static final Map varPlayerToQuestMap = new ConcurrentHashMap<>(); - - // Flag to track if quest maps have been initialized - private static volatile boolean mapsInitialized = false; - - /** - * Gets the currently tracked quest. - * - * @return The currently tracked quest, or null if none - */ - public static Quest getTrackedQuest() { - return trackedQuest; - } - - /** - * Sets the quest to track for changes. - * - * @param quest The quest to track, or null to stop tracking - */ - public static void setTrackedQuest(Quest quest) { - if (trackedQuest != quest) { - Quest oldQuest = trackedQuest; - trackedQuest = quest; - log.debug("Tracked quest changed from {} to {}", - oldQuest != null ? oldQuest.getName() : "none", - quest != null ? quest.getName() : "none"); - } - } - - /** - * Initializes the varbit/varPlayer to Quest mapping from QuestHelperQuest enum. - * This is done lazily on first access to avoid initialization order issues. - */ - private static void initializeQuestMaps() { - if (mapsInitialized) { - return; - } - - synchronized (QuestUpdateStrategy.class) { - if (mapsInitialized) { - return; - } - - try { - log.debug("Initializing quest variable maps from QuestHelperQuest enum..."); - - for (QuestHelperQuest questHelperQuest : QuestHelperQuest.values()) { - // Get RuneLite Quest by ID - Quest runeliteQuest = getQuestById(questHelperQuest.getId()); - if (runeliteQuest == null) { - continue; // Skip quests without RuneLite Quest mapping - } - - // Use reflection to access private varbit and varPlayer fields - try { - Field varbitField = QuestHelperQuest.class.getDeclaredField("varbit"); - varbitField.setAccessible(true); - Object varbitValue = varbitField.get(questHelperQuest); - - if (varbitValue != null) { - // Get the ID from the QuestVarbits enum - Field idField = varbitValue.getClass().getDeclaredField("id"); - idField.setAccessible(true); - int varbitId = (Integer) idField.get(varbitValue); - varbitToQuestMap.put(varbitId, runeliteQuest); - log.trace("Mapped varbit {} to quest {}", varbitId, runeliteQuest.getName()); - } - - Field varPlayerField = QuestHelperQuest.class.getDeclaredField("varPlayer"); - varPlayerField.setAccessible(true); - Object varPlayerValue = varPlayerField.get(questHelperQuest); - - if (varPlayerValue != null) { - // Get the ID from the QuestVarPlayer enum - Field idField = varPlayerValue.getClass().getDeclaredField("id"); - idField.setAccessible(true); - int varPlayerId = (Integer) idField.get(varPlayerValue); - varPlayerToQuestMap.put(varPlayerId, runeliteQuest); - log.trace("Mapped varPlayer {} to quest {}", varPlayerId, runeliteQuest.getName()); - } - - } catch (Exception reflectionException) { - log.trace("Reflection failed for quest {}: {}", questHelperQuest.getName(), reflectionException.getMessage()); - } - } - - mapsInitialized = true; - log.info("Initialized quest maps: {} varbits, {} varPlayers", - varbitToQuestMap.size(), varPlayerToQuestMap.size()); - - } catch (Exception e) { - log.error("Error initializing quest variable maps: {}", e.getMessage(), e); - } - } - } - - /** - * Gets a RuneLite Quest by its ID. - * - * @param questId The quest ID - * @return The Quest object, or null if not found - */ - private static Quest getQuestById(int questId) { - try { - for (Quest quest : Quest.values()) { - if (quest.getId() == questId) { - return quest; - } - } - } catch (Exception e) { - log.trace("Error finding quest by ID {}: {}", questId, e.getMessage()); - } - return null; - } - - /** - * Gets the Quest associated with a varbit ID. - * - * @param varbitId The varbit ID - * @return The associated Quest, or null if none found - */ - public static Quest getQuestByVarbit(int varbitId) { - initializeQuestMaps(); - return varbitToQuestMap.get(varbitId); - } - - /** - * Gets the Quest associated with a varPlayer ID. - * - * @param varPlayerId The varPlayer ID - * @return The associated Quest, or null if none found - */ - public static Quest getQuestByVarPlayer(int varPlayerId) { - initializeQuestMaps(); - return varPlayerToQuestMap.get(varPlayerId); - } - - /** - * Gets the varbit ID associated with a quest. - * - * @param quest The quest to look up - * @return The varbit ID, or null if none found - */ - public static Integer getVarbitIdByQuest(Quest quest) { - initializeQuestMaps(); - return varbitToQuestMap.entrySet().stream() - .filter(entry -> entry.getValue().equals(quest)) - .map(Map.Entry::getKey) - .findFirst() - .orElse(null); - } - - /** - * Gets the varPlayer ID associated with a quest. - * - * @param quest The quest to look up - * @return The varPlayer ID, or null if none found - */ - public static Integer getVarPlayerIdByQuest(Quest quest) { - initializeQuestMaps(); - return varPlayerToQuestMap.entrySet().stream() - .filter(entry -> entry.getValue().equals(quest)) - .map(Map.Entry::getKey) - .findFirst() - .orElse(null); - } - - /** - * Gets the QuestHelperPlugin instance. - * - * @return The QuestHelperPlugin instance, or null if not available - */ - private static QuestHelperPlugin getQuestHelperPlugin() { - try { - return (QuestHelperPlugin) Microbot.getPluginManager().getPlugins().stream() - .filter(plugin -> plugin instanceof QuestHelperPlugin && Microbot.getPluginManager().isPluginEnabled(plugin)) - .findFirst() - .orElse(null); - } catch (Exception e) { - log.trace("Error getting QuestHelper plugin: {}", e.getMessage()); - return null; - } - } - - /** - * Gets the currently selected quest from QuestHelperPlugin if available. - * - * @return The currently selected QuestHelper, or null if none selected or plugin unavailable - */ - private static QuestHelper getSelectedQuestHelper() { - QuestHelperPlugin plugin = getQuestHelperPlugin(); - if (plugin != null) { - return plugin.getSelectedQuest(); - } - return null; - } - - /** - * Gets the currently active RuneLite Quest from QuestHelperPlugin. - * Maps QuestHelper to RuneLite Quest objects using QuestHelperQuest info. - * - * @return The currently active Quest, or null if none selected or not mappable - */ - public static Quest getCurrentlyActiveQuest() { - try { - QuestHelper selectedHelper = getSelectedQuestHelper(); - if (selectedHelper == null) { - return null; - } - - // Find the corresponding QuestHelperQuest enum entry - for (QuestHelperQuest questHelperQuest : QuestHelperQuest.values()) { - if (questHelperQuest.getQuestHelper() != null && - questHelperQuest.getQuestHelper().getClass().equals(selectedHelper.getClass())) { - return getQuestById(questHelperQuest.getId()); - } - } - - log.trace("No RuneLite Quest found for selected QuestHelper: {}", selectedHelper.getClass().getSimpleName()); - return null; - } catch (Exception e) { - log.trace("Error getting currently active quest: {}", e.getMessage()); - return null; - } - } - - @Override - public void handleEvent(Object event, CacheOperations cache) { - if (event instanceof VarbitChanged) { - handleVarbitChanged((VarbitChanged) event, cache); - } else if (event instanceof ChatMessage) { - handleChatMessage((ChatMessage) event, cache); - } - } - - private void handleVarbitChanged(VarbitChanged event, CacheOperations cache) { - try { - initializeQuestMaps(); // Ensure maps are initialized - - // Check if the changed varbit/varPlayer corresponds to a known quest - Quest affectedQuest = null; - - // Check varbit mapping - if (event.getVarbitId() > 0) { - affectedQuest = varbitToQuestMap.get(event.getVarbitId()); - if (affectedQuest != null) { - log.debug("VarbitChanged - Detected quest {} affected by varbit {}", - affectedQuest.getName(), event.getVarbitId()); - } - } - - // Check varPlayer mapping (VarbitChanged can also affect varPlayers) - if (affectedQuest == null && event.getVarpId() > 0) { - affectedQuest = varPlayerToQuestMap.get(event.getVarpId()); - if (affectedQuest != null) { - log.debug("VarbitChanged - Detected quest {} affected by varPlayer {}", - affectedQuest.getName(), event.getVarpId()); - } - } - - // If a specific quest is affected, trigger its update - if (affectedQuest != null) { - updateQuestAsync(affectedQuest, cache); - } else { - // Fallback: check tracked quest or currently active quest - Quest questToCheck = trackedQuest != null ? trackedQuest : getCurrentlyActiveQuest(); - if (questToCheck != null) { - log.trace("VarbitChanged - Checking tracked/active quest: {}", questToCheck.getName()); - updateQuestAsync(questToCheck, cache); - } - } - - } catch (Exception e) { - log.error("Error handling VarbitChanged event for quests: {}", e.getMessage(), e); - } - } - - private void handleChatMessage(ChatMessage chatMessage, CacheOperations cache) { - try { - if (chatMessage.getType() != ChatMessageType.GAMEMESSAGE) { - return; - } - - String message = chatMessage.getMessage(); - if (message == null) { - return; - } - - // Check for quest-related messages - if (message.contains("You have completed") || - message.contains("Quest complete") || - message.contains("quest") || - message.contains("Quest")) { - //should not be need, the varbitChanged event should handle this already, only dont work for qeust not in the enum QuestHelperQuest - log.debug("ChatMessage - Quest-related message detected: {}", message); - // Update tracked quest if any - Quest questToUpdate = trackedQuest != null ? trackedQuest : getCurrentlyActiveQuest(); - if (questToUpdate != null) { - updateQuestAsync(questToUpdate, cache); - } - } - - } catch (Exception e) { - log.error("Error handling ChatMessage event for quests: {}", e.getMessage(), e); - } - } - - /** - * Asynchronously updates a quest's state in the cache. - * - * @param quest The quest to update - * @param cache The cache operations interface - */ - private void updateQuestAsync(Quest quest, CacheOperations cache) { - Microbot.getClientThread().invokeLater(() -> { - try { - if (Microbot.getClient() == null) { - return; - } - - QuestState oldState = cache.get(quest); - QuestState newState = quest.getState(Microbot.getClient()); - - if (oldState != newState) { - log.debug("\n\tdetection Quest state changed update cache\n\t\t {}: cached: {} -> client: {} ", - quest.getName(), oldState, newState); - cache.put(quest, newState); - - // If quest is now complete and was being tracked, clear tracking - if (newState == QuestState.FINISHED && trackedQuest == quest) { - log.debug("Quest completed, clearing tracking: {}", quest.getName()); - trackedQuest = null; - } - } - } catch (Exception e) { - log.error("Error updating quest state for {}: {}", quest.getName(), e.getMessage()); - } - }); - } - - @Override - public Class[] getHandledEventTypes() { - return new Class[]{VarbitChanged.class, ChatMessage.class}; - } - - @Override - public void onAttach(CacheOperations cache) { - log.debug("QuestUpdateStrategy attached to cache"); - } - - @Override - public void onDetach(CacheOperations cache) { - log.debug("QuestUpdateStrategy detached from cache"); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/SkillUpdateStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/SkillUpdateStrategy.java deleted file mode 100644 index caa3bc79bba..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/SkillUpdateStrategy.java +++ /dev/null @@ -1,82 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy.simple; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Skill; -import net.runelite.api.events.StatChanged; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.model.SkillData; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheOperations; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheUpdateStrategy; - -/** - * Cache update strategy for skill data. - * Handles StatChanged events to update skill information with enhanced data - * including temporal tracking and change detection. - */ -@Slf4j -public class SkillUpdateStrategy implements CacheUpdateStrategy { - - @Override - public void handleEvent(Object event, CacheOperations cache) { - if (event instanceof StatChanged) { - handleStatChanged((StatChanged) event, cache); - } - } - - private void handleStatChanged(StatChanged event, CacheOperations cache) { - try { - Skill skill = event.getSkill(); - if (skill != null && Microbot.getClient() != null) { - - // Get current skill data from client - int level = Microbot.getClient().getRealSkillLevel(skill); - int boostedLevel = Microbot.getClient().getBoostedSkillLevel(skill); - int experience = Microbot.getClient().getSkillExperience(skill); - - // Get existing data to preserve previous values - SkillData existingData = cache.get(skill); - - // Create new skill data with temporal and change tracking - SkillData skillData; - if (existingData != null) { - // Use withUpdate to preserve previous values - skillData = existingData.withUpdate(level, boostedLevel, experience); - } else { - // No previous data available - skillData = new SkillData(level, boostedLevel, experience); - } - - cache.put(skill, skillData); - - // Log level ups and significant experience gains - if (skillData.isLevelUp()) { - log.debug("Level up detected: {} leveled from {} to {}", skill, skillData.getPreviousLevel(), level); - } - if (skillData.getExperienceGained() > 0) { - log.debug("\n\tUpdated skill cache: {} (level: {}, boosted: {}, exp: {}, gained: {} exp)", - skill, level, boostedLevel, experience, skillData.getExperienceGained()); - } else { - log.trace("Updated skill cache: {} (level: {}, boosted: {}, exp: {})", - skill, level, boostedLevel, experience); - } - } - } catch (Exception e) { - log.error("Error handling StatChanged event: {}", e.getMessage(), e); - } - } - - @Override - public Class[] getHandledEventTypes() { - return new Class[]{StatChanged.class}; - } - - @Override - public void onAttach(CacheOperations cache) { - log.debug("SkillUpdateStrategy attached to cache"); - } - - @Override - public void onDetach(CacheOperations cache) { - log.debug("SkillUpdateStrategy detached from cache"); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/VarPlayerUpdateStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/VarPlayerUpdateStrategy.java deleted file mode 100644 index 60609d26b4d..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/VarPlayerUpdateStrategy.java +++ /dev/null @@ -1,103 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy.simple; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.VarbitChanged; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.Rs2NpcCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2ObjectCache; -import net.runelite.client.plugins.microbot.util.cache.model.VarbitData; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheOperations; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheUpdateStrategy; - -import java.util.List; -import java.util.stream.Collectors; - -/** - * Cache update strategy for varplayer data. - * Handles VarbitChanged events to update varplayer information with enhanced data - * including temporal tracking, player location, and nearby entity context. - */ -@Slf4j -public class VarPlayerUpdateStrategy implements CacheUpdateStrategy { - - @Override - public void handleEvent(Object event, CacheOperations cache) { - if (event instanceof VarbitChanged) { - handleVarbitChanged((VarbitChanged) event, cache); - } - } - - private void handleVarbitChanged(VarbitChanged event, CacheOperations cache) { - try { - int varpId = event.getVarpId(); - if (varpId != -1) { - // Get current value from client - int newValue = Microbot.getClient().getVarpValue(varpId); - - // Get existing data to preserve previous value - VarbitData existingData = cache.get(varpId); - Integer previousValue = existingData != null ? existingData.getValue() : null; - - // Collect contextual information - WorldPoint finalPlayerLocation = null; - List nearbyNpcIds = null; - List nearbyObjectIds = null; - - try { - // Get player location - if (Microbot.getClient().getLocalPlayer() != null) { - finalPlayerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - - // Get nearby NPCs within 10 tiles - if (finalPlayerLocation != null) { - final WorldPoint playerLoc = finalPlayerLocation; - nearbyNpcIds = Rs2NpcCache.getInstance().stream() - .filter(npc -> npc.getWorldLocation() != null && - npc.getWorldLocation().distanceTo(playerLoc) <= 10) - .map(npc -> npc.getId()) - .distinct() - .collect(Collectors.toList()); - - // Get nearby objects within 10 tiles - nearbyObjectIds = Rs2ObjectCache.getInstance().stream() - .filter(obj -> obj.getWorldLocation() != null && - obj.getWorldLocation().distanceTo(playerLoc) <= 10) - .map(obj -> obj.getId()) - .distinct() - .collect(Collectors.toList()); - } - } catch (Exception e) { - log.debug("Could not collect contextual information for varplayer {}: {}", varpId, e.getMessage()); - // Continue with null values - the VarPlayerData constructor handles this gracefully - } - - // Create new VarbitData with contextual information - VarbitData newData = new VarbitData(newValue, previousValue, finalPlayerLocation, nearbyNpcIds, nearbyObjectIds); - - // Update the cache - cache.put(varpId, newData); - log.trace("Updated varplayer cache: {} = {} (previous: {}) at location: {}", - varpId, newValue, previousValue, finalPlayerLocation); - } - } catch (Exception e) { - log.error("Error handling VarbitChanged event for varplayer: {}", e.getMessage(), e); - } - } - - @Override - public Class[] getHandledEventTypes() { - return new Class[]{VarbitChanged.class}; - } - - @Override - public void onAttach(CacheOperations cache) { - log.debug("VarPlayerUpdateStrategy attached to cache"); - } - - @Override - public void onDetach(CacheOperations cache) { - log.debug("VarPlayerUpdateStrategy detached from cache"); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/VarbitUpdateStrategy.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/VarbitUpdateStrategy.java deleted file mode 100644 index 8d66924e38b..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/strategy/simple/VarbitUpdateStrategy.java +++ /dev/null @@ -1,103 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.strategy.simple; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.VarbitChanged; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.cache.Rs2NpcCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2ObjectCache; -import net.runelite.client.plugins.microbot.util.cache.model.VarbitData; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheOperations; -import net.runelite.client.plugins.microbot.util.cache.strategy.CacheUpdateStrategy; - -import java.util.List; -import java.util.stream.Collectors; - -/** - * Cache update strategy for varbit data. - * Handles VarbitChanged events to update varbit information with enhanced data - * including temporal tracking, player location, and nearby entity context. - */ -@Slf4j -public class VarbitUpdateStrategy implements CacheUpdateStrategy { - - @Override - public void handleEvent(Object event, CacheOperations cache) { - if (event instanceof VarbitChanged) { - handleVarbitChanged((VarbitChanged) event, cache); - } - } - - private void handleVarbitChanged(VarbitChanged event, CacheOperations cache) { - try { - int varbitId = event.getVarbitId(); - if (varbitId != -1) { - // Get current value from client - int newValue = Microbot.getClient().getVarbitValue(varbitId); - - // Get existing data to preserve previous value - VarbitData existingData = cache.get(varbitId); - Integer previousValue = existingData != null ? existingData.getValue() : null; - - // Collect contextual information - WorldPoint finalPlayerLocation = null; - List nearbyNpcIds = null; - List nearbyObjectIds = null; - - try { - // Get player location - if (Microbot.getClient().getLocalPlayer() != null) { - finalPlayerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - - // Get nearby NPCs within 10 tiles - if (finalPlayerLocation != null) { - final WorldPoint playerLoc = finalPlayerLocation; - nearbyNpcIds = Rs2NpcCache.getInstance().stream() - .filter(npc -> npc.getWorldLocation() != null && - npc.getWorldLocation().distanceTo(playerLoc) <= 10) - .map(npc -> npc.getId()) - .distinct() - .collect(Collectors.toList()); - - // Get nearby objects within 10 tiles - nearbyObjectIds = Rs2ObjectCache.getInstance().stream() - .filter(obj -> obj.getWorldLocation() != null && - obj.getWorldLocation().distanceTo(playerLoc) <= 10) - .map(obj -> obj.getId()) - .distinct() - .collect(Collectors.toList()); - } - } catch (Exception e) { - log.debug("Could not collect contextual information for varbit {}: {}", varbitId, e.getMessage()); - // Continue with null values - the VarbitData constructor handles this gracefully - } - - // Create new VarbitData with contextual information - VarbitData newData = new VarbitData(newValue, previousValue, finalPlayerLocation, nearbyNpcIds, nearbyObjectIds); - - // Update the cache - cache.put(varbitId, newData); - log.trace("Updated varbit cache: {} = {} (previous: {}) at location: {}", - varbitId, newValue, previousValue, finalPlayerLocation); - } - } catch (Exception e) { - log.error("Error handling VarbitChanged event: {}", e.getMessage(), e); - } - } - - @Override - public Class[] getHandledEventTypes() { - return new Class[]{VarbitChanged.class}; - } - - @Override - public void onAttach(CacheOperations cache) { - log.debug("VarbitUpdateStrategy attached to cache"); - } - - @Override - public void onDetach(CacheOperations cache) { - log.debug("VarbitUpdateStrategy detached from cache"); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/LogOutputMode.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/LogOutputMode.java deleted file mode 100644 index aac6c316285..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/LogOutputMode.java +++ /dev/null @@ -1,24 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.util; - -/** - * Defines where cache logging output should be directed. - * - * @author Vox - * @version 1.0 - */ -public enum LogOutputMode { - /** - * Log only to console/logger - */ - CONSOLE_ONLY, - - /** - * Log only to file - */ - FILE_ONLY, - - /** - * Log to both console and file - */ - BOTH -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2CacheLoggingUtils.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2CacheLoggingUtils.java deleted file mode 100644 index d4585a2c689..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2CacheLoggingUtils.java +++ /dev/null @@ -1,492 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.util; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.gameval.VarbitID; -import net.runelite.api.gameval.VarPlayerID; -import net.runelite.client.RuneLite; -import net.runelite.client.plugins.microbot.util.cache.Rs2GroundItemCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2NpcCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2ObjectCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2QuestCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2SkillCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2VarPlayerCache; -import net.runelite.client.plugins.microbot.util.cache.Rs2VarbitCache; - -import java.io.BufferedWriter; -import java.io.IOException; -import java.lang.reflect.Field; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Utility class for logging cache states and dumping to files. - * Provides reflection-based utilities for VarbitID and VarPlayerID name resolution. - * - * @author Vox - * @version 1.0 - */ -@Slf4j -public class Rs2CacheLoggingUtils { - - private static final String CACHE_LOG_FOLDER = "cache"; - private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"); - - // Cache for VarbitID field mappings to avoid repeated reflection - private static final Map varbitIdCache = new ConcurrentHashMap<>(); - private static final Map varPlayerIdCache = new ConcurrentHashMap<>(); - private static boolean varbitCacheInitialized = false; - private static boolean varPlayerCacheInitialized = false; - - /** - * Gets the RuneLite user directory for cache logs. - * - * @return Path to the cache log directory - */ - public static Path getCacheLogDirectory() { - Path runeliteDir = RuneLite.RUNELITE_DIR.toPath(); - Path microbotPluginsDir = runeliteDir.resolve("microbot-plugins"); - Path cacheDir = microbotPluginsDir.resolve(CACHE_LOG_FOLDER); - - try { - Files.createDirectories(cacheDir); - } catch (IOException e) { - log.warn("Failed to create cache log directory: {}", cacheDir, e); - // Fall back to a temp directory - return Paths.get(System.getProperty("java.io.tmpdir"), "microbot-cache"); - } - - return cacheDir; - } - - /** - * Gets a timestamp-based filename for cache dumps. - * - * @param cacheType The cache type (e.g., "npc", "object") - * @return Filename with timestamp - */ - public static String getCacheLogFilename(String cacheType) { - String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT); - return String.format("%s_cache_%s.log", cacheType, timestamp); - } - - /** - * Writes content to a cache log file. - * - * @param cacheType The cache type identifier - * @param content The content to write - * @param includeTimestamp Whether to include a timestamp in the filename - */ - public static void writeCacheLogFile(String cacheType, String content, boolean includeTimestamp) { - try { - Path logDir = getCacheLogDirectory().resolve(cacheType); - Files.createDirectories(logDir); - String filename = includeTimestamp ? getCacheLogFilename(cacheType) : cacheType + "_cache.log"; - Path logFile = logDir.resolve(filename); - - try (BufferedWriter writer = Files.newBufferedWriter(logFile)) { - writer.write(content); - writer.flush(); - } - - log.info("Cache log written to: {}", logFile.toAbsolutePath()); - - } catch (IOException e) { - log.error("Failed to write cache log file for {}: {}", cacheType, e.getMessage(), e); - } - } - - /** - * Gets the field name for a VarbitID value using reflection. - * - * @param varbitId The varbit ID value - * @return The field name if found, or the ID as string if not found - */ - public static String getVarbitFieldName(int varbitId) { - initializeVarbitCache(); - return varbitIdCache.getOrDefault(varbitId, String.valueOf(varbitId)); - } - - /** - * Gets the field name for a VarPlayerID value using reflection. - * - * @param varPlayerId The var player ID value - * @return The field name if found, or the ID as string if not found - */ - public static String getVarPlayerFieldName(int varPlayerId) { - initializeVarPlayerCache(); - return varPlayerIdCache.getOrDefault(varPlayerId, String.valueOf(varPlayerId)); - } - - /** - * Initializes the VarbitID cache using reflection. - */ - private static synchronized void initializeVarbitCache() { - if (varbitCacheInitialized) { - return; - } - - try { - Field[] fields = VarbitID.class.getDeclaredFields(); - for (Field field : fields) { - if (field.getType() == int.class && java.lang.reflect.Modifier.isStatic(field.getModifiers())) { - try { - int value = field.getInt(null); - varbitIdCache.put(value, field.getName()); - } catch (IllegalAccessException e) { - log.debug("Could not access VarbitID field: {}", field.getName()); - } - } - } - varbitCacheInitialized = true; - log.debug("Initialized VarbitID cache with {} entries", varbitIdCache.size()); - - } catch (Exception e) { - log.warn("Failed to initialize VarbitID cache via reflection", e); - varbitCacheInitialized = true; // Prevent retries - } - } - - /** - * Initializes the VarPlayerID cache using reflection. - */ - private static synchronized void initializeVarPlayerCache() { - if (varPlayerCacheInitialized) { - return; - } - - try { - Field[] fields = VarPlayerID.class.getDeclaredFields(); - for (Field field : fields) { - if (field.getType() == int.class && java.lang.reflect.Modifier.isStatic(field.getModifiers())) { - try { - int value = field.getInt(null); - varPlayerIdCache.put(value, field.getName()); - } catch (IllegalAccessException e) { - log.debug("Could not access VarPlayerID field: {}", field.getName()); - } - } - } - varPlayerCacheInitialized = true; - log.debug("Initialized VarPlayerID cache with {} entries", varPlayerIdCache.size()); - - } catch (Exception e) { - log.warn("Failed to initialize VarPlayerID cache via reflection", e); - varPlayerCacheInitialized = true; // Prevent retries - } - } - - /** - * Formats a table header with specified column headers and widths. - * - * @param headers Array of header strings - * @param columnWidths Array of column widths - * @return Formatted table header - */ - public static String formatTableHeader(String[] headers, int[] columnWidths) { - StringBuilder sb = new StringBuilder(); - sb.append("╔"); - for (int i = 0; i < columnWidths.length; i++) { - for (int j = 0; j < columnWidths[i]; j++) { - sb.append("═"); - } - if (i < columnWidths.length - 1) { - sb.append("â•Ķ"); - } - } - sb.append("╗\n"); - - sb.append("║"); - for (int i = 0; i < headers.length; i++) { - String header = truncate(headers[i], columnWidths[i] - 2); - sb.append(String.format(" %-" + (columnWidths[i] - 2) + "s ", header)); - if (i < headers.length - 1) { - sb.append("║"); - } - } - sb.append("║\n"); - - sb.append("╠"); - for (int i = 0; i < columnWidths.length; i++) { - for (int j = 0; j < columnWidths[i]; j++) { - sb.append("═"); - } - if (i < columnWidths.length - 1) { - sb.append("╮"); - } - } - sb.append("â•Ģ\n"); - - return sb.toString(); - } - - /** - * Formats a table row with specified values and column widths. - * - * @param values Array of cell values - * @param columnWidths Array of column widths - * @return Formatted table row - */ - public static String formatTableRow(String[] values, int[] columnWidths) { - StringBuilder sb = new StringBuilder(); - sb.append("║"); - for (int i = 0; i < values.length; i++) { - String value = truncate(values[i], columnWidths[i] - 2); - sb.append(String.format(" %-" + (columnWidths[i] - 2) + "s ", value)); - if (i < values.length - 1) { - sb.append("║"); - } - } - sb.append("║\n"); - return sb.toString(); - } - - /** - * Formats a table footer with specified column widths. - * - * @param columnWidths Array of column widths - * @return Formatted table footer - */ - public static String formatTableFooter(int[] columnWidths) { - StringBuilder sb = new StringBuilder(); - sb.append("╚"); - for (int i = 0; i < columnWidths.length; i++) { - for (int j = 0; j < columnWidths[i]; j++) { - sb.append("═"); - } - if (i < columnWidths.length - 1) { - sb.append("â•Đ"); - } - } - sb.append("╝\n"); - return sb.toString(); - } - - /** - * Truncates a string to the specified maximum length. - * - * @param str The string to truncate - * @param maxLength Maximum length - * @return Truncated string - */ - public static String truncate(String str, int maxLength) { - if (str == null) { - return ""; - } - if (str.length() <= maxLength) { - return str; - } - return str.substring(0, maxLength - 3) + "..."; - } - - /** - * Safely converts an object to string, handling null values. - * - * @param obj The object to convert - * @return String representation or "null" if object is null - */ - public static String safeToString(Object obj) { - return obj != null ? obj.toString() : "null"; - } - - /** - * Formats a cache header with type, size and mode information. - * - * @param cacheType The cache type name - * @param cacheSize The number of entries in the cache - * @param cacheMode The cache mode - * @return Formatted header string - */ - public static String formatCacheHeader(String cacheType, int cacheSize, String cacheMode) { - return String.format("=== %s Cache State (%d entries, %s mode) ===", - cacheType, cacheSize, cacheMode); - } - - /** - * Formats a WorldPoint location for display. - * - * @param location The WorldPoint to format - * @return Formatted location string - */ - public static String formatLocation(net.runelite.api.coords.WorldPoint location) { - if (location == null) { - return "N/A"; - } - return String.format("(%d,%d,%d)", location.getX(), location.getY(), location.getPlane()); - } - - /** - * Formats a timestamp in milliseconds to a readable date/time string. - * - * @param timestampMillis Timestamp in milliseconds - * @return Formatted timestamp string - */ - public static String formatTimestamp(long timestampMillis) { - if (timestampMillis <= 0) { - return "N/A"; - } - LocalDateTime dateTime = LocalDateTime.ofEpochSecond(timestampMillis / 1000, 0, java.time.ZoneOffset.UTC); - return dateTime.format(TIMESTAMP_FORMAT); - } - - /** - * Formats cache statistics for display. - * - * @param hitRate Cache hit rate as a percentage - * @param hits Number of cache hits - * @param misses Number of cache misses - * @param mode Cache mode - * @return Formatted statistics string - */ - public static String formatCacheStatistics(double hitRate, long hits, long misses, String mode) { - return String.format("Cache Statistics: %.1f%% hit rate (%d hits, %d misses) - Mode: %s", - hitRate, hits, misses, mode); - } - - /** - * Formats a limit message when cache content is truncated. - * - * @param totalSize The total number of items - * @param displayedSize The number of items displayed - * @return Formatted limit message or empty string if no truncation - */ - public static String formatLimitMessage(int totalSize, int displayedSize) { - if (totalSize > displayedSize) { - return String.format("... and %d more entries (showing first %d)", - totalSize - displayedSize, displayedSize); - } - return ""; - } - - /** - * Outputs cache log content based on the specified output mode. - * - * @param cacheType The cache type identifier - * @param content The content to output - * @param outputMode Where to direct the output - */ - public static void outputCacheLog(String cacheType, String content, LogOutputMode outputMode) { - switch (outputMode) { - case CONSOLE_ONLY: - log.info(content); - break; - case FILE_ONLY: - writeCacheLogFile(cacheType, content, false); - break; - case BOTH: - log.info(content); - writeCacheLogFile(cacheType, content, false); - break; - } - } - - /** - * Logs all cache states to files only. - * This is useful for generating comprehensive cache dumps for analysis. - * Uses the existing boolean-based logState methods with dumpToFile=true. - */ - public static void logAllCachesToFiles() { - log.info("Starting cache file dump for all Rs2Cache instances..."); - - try { - // NPC Cache - uses LogOutputMode.FILE_ONLY - if (Rs2NpcCache.getInstance() != null) { - Rs2NpcCache.logState(LogOutputMode.FILE_ONLY); - } - - // Object Cache - uses boolean method - if (Rs2ObjectCache.getInstance() != null) { - try { - Rs2ObjectCache.logState(LogOutputMode.FILE_ONLY); - } catch (Exception e) { - log.error("Failed to log Object Cache state: {}", e.getMessage(), e); - } - - } - - // Ground Item Cache - uses boolean method - if (Rs2GroundItemCache.getInstance() != null) { - Rs2GroundItemCache.logState(LogOutputMode.FILE_ONLY); - } - - // Skill Cache - uses boolean method - if (Rs2SkillCache.getInstance() != null) { - Rs2SkillCache.logState(LogOutputMode.FILE_ONLY); - } - - // Varbit Cache - uses boolean method - if (Rs2VarbitCache.getInstance() != null) { - Rs2VarbitCache.logState(LogOutputMode.FILE_ONLY); - } - - // VarPlayer Cache - uses boolean method - if (Rs2VarPlayerCache.getInstance() != null) { - Rs2VarPlayerCache.logState(LogOutputMode.FILE_ONLY); - } - - // Quest Cache - uses boolean method - if (Rs2QuestCache.getInstance() != null) { - Rs2QuestCache.logState(LogOutputMode.FILE_ONLY); - } - - log.info("Cache file dump completed successfully. Files written to cache log directory."); - - } catch (Exception e) { - log.error("Error during cache file dump: {}", e.getMessage(), e); - } - } - - /** - * Logs all cache states to both console and files. - * This provides comprehensive output for debugging sessions. - */ - public static void logAllCachesToConsoleAndFiles() { - log.info("Starting comprehensive cache dump (console + files)..."); - - try { - // NPC Cache - uses LogOutputMode.BOTH - if (Rs2NpcCache.getInstance() != null) { - Rs2NpcCache.logState(LogOutputMode.BOTH); - } - - // Object Cache - uses boolean method (console + file) - if (Rs2ObjectCache.getInstance() != null) { - Rs2ObjectCache.logState(LogOutputMode.BOTH); - } - - // Ground Item Cache - uses boolean method (console + file) - if (Rs2GroundItemCache.getInstance() != null) { - Rs2GroundItemCache.logState(LogOutputMode.BOTH); - } - - // Skill Cache - uses boolean method (console + file) - if (Rs2SkillCache.getInstance() != null) { - Rs2SkillCache.logState(LogOutputMode.BOTH); - } - - // Varbit Cache - uses boolean method (console + file) - if (Rs2VarbitCache.getInstance() != null) { - Rs2VarbitCache.logState(LogOutputMode.BOTH); - } - - // VarPlayer Cache - uses boolean method (console + file) - if (Rs2VarPlayerCache.getInstance() != null) { - Rs2VarPlayerCache.logState(LogOutputMode.BOTH); - } - - // Quest Cache - uses boolean method (console + file) - if (Rs2QuestCache.getInstance() != null) { - Rs2QuestCache.logState(LogOutputMode.BOTH); - } - - log.info("Comprehensive cache dump completed successfully."); - - } catch (Exception e) { - log.error("Error during comprehensive cache dump: {}", e.getMessage(), e); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2GroundItemCacheUtils.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2GroundItemCacheUtils.java deleted file mode 100644 index 8cba1de008f..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2GroundItemCacheUtils.java +++ /dev/null @@ -1,1480 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.util; - -import net.runelite.api.Client; -import net.runelite.api.Perspective; -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.util.cache.Rs2GroundItemCache; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; - -import java.util.*; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Advanced cache-based utilities for ground items. - * Provides scene-independent methods for finding and filtering ground items. - * - * This class offers high-performance ground item operations using cached data, - * avoiding the need to iterate through scene tiles. - * - * @author Vox - * @version 1.0 - */ -public class Rs2GroundItemCacheUtils { - - // ============================================ - // Core Cache Access Methods - // ============================================ - - /** - * Gets ground items by their game ID. - * - * @param itemId The item ID - * @return Stream of matching ground items - */ - public static Stream getByGameId(int itemId) { - try { - return Rs2GroundItemCache.getItemsByGameId(itemId); - } catch (Exception e) { - return Stream.empty(); - } - } - - /** - * Gets the first ground item matching the criteria. - * - * @param itemId The item ID - * @return Optional containing the first matching ground item - */ - public static Optional getFirst(int itemId) { - try { - return Rs2GroundItemCache.getFirstItemByGameId(itemId); - } catch (Exception e) { - return Optional.empty(); - } - } - - /** - * Gets the closest ground item to the player. - * - * @param itemId The item ID - * @return Optional containing the closest ground item - */ - public static Optional getClosest(int itemId) { - try { - return Rs2GroundItemCache.getClosestItemByGameId(itemId); - } catch (Exception e) { - return Optional.empty(); - } - } - - /** - * Gets all cached ground items. - * - * @return Stream of all ground items - */ - public static Stream getAll() { - try { - return Rs2GroundItemCache.getAllItems(); - } catch (Exception e) { - return Stream.empty(); - } - } - - // ============================================ - // Advanced Finding Methods - // ============================================ - - /** - * Finds the first ground item matching a predicate. - * - * @param predicate The predicate to match - * @return Optional containing the first matching ground item - */ - public static Optional find(Predicate predicate) { - try { - return Rs2GroundItemCache.getAllItems() - .filter(predicate) - .findFirst(); - } catch (Exception e) { - return Optional.empty(); - } - } - - /** - * Finds all ground items matching a predicate. - * - * @param predicate The predicate to match - * @return Stream of matching ground items - */ - public static Stream findAll(Predicate predicate) { - try { - return Rs2GroundItemCache.getAllItems().filter(predicate); - } catch (Exception e) { - return Stream.empty(); - } - } - - /** - * Finds the closest ground item matching a predicate. - * - * @param predicate The predicate to match - * @return Optional containing the closest matching ground item - */ - public static Optional findClosest(Predicate predicate) { - try { - return Rs2GroundItemCache.getAllItems() - .filter(predicate) - .min(Comparator.comparingInt(Rs2GroundItemModel::getDistanceFromPlayer)); - } catch (Exception e) { - return Optional.empty(); - } - } - - // ============================================ - // Distance-Based Finding Methods - // ============================================ - - /** - * Finds the first ground item within distance from player by ID. - * - * @param itemId The item ID - * @param distance Maximum distance in tiles - * @return Optional containing the first matching ground item within distance - */ - public static Optional findWithinDistance(int itemId, int distance) { - return find(item -> item.getId() == itemId && item.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds all ground items within distance from player by ID. - * - * @param itemId The item ID - * @param distance Maximum distance in tiles - * @return Stream of matching ground items within distance - */ - public static Stream findAllWithinDistance(int itemId, int distance) { - return findAll(item -> item.getId() == itemId && item.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest ground item by ID within distance from player. - * - * @param itemId The item ID - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching ground item within distance - */ - public static Optional findClosestWithinDistance(int itemId, int distance) { - return findClosest(item -> item.getId() == itemId && item.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds ground items within distance from an anchor point. - * - * @param predicate The predicate to match - * @param anchor The anchor point - * @param distance Maximum distance in tiles - * @return Stream of matching ground items within distance from anchor - */ - public static Stream findWithinDistance(Predicate predicate, WorldPoint anchor, int distance) { - return findAll(item -> predicate.test(item) && item.getLocation().distanceTo(anchor) <= distance); - } - - /** - * Finds the closest ground item to an anchor point. - * - * @param predicate The predicate to match - * @param anchor The anchor point - * @return Optional containing the closest matching ground item to anchor - */ - public static Optional findClosest(Predicate predicate, WorldPoint anchor) { - return findAll(predicate) - .min(Comparator.comparingInt(item -> item.getLocation().distanceTo(anchor))); - } - - /** - * Finds the closest ground item to an anchor point within distance. - * - * @param predicate The predicate to match - * @param anchor The anchor point - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching ground item to anchor within distance - */ - public static Optional findClosest(Predicate predicate, WorldPoint anchor, int distance) { - return findWithinDistance(predicate, anchor, distance) - .min(Comparator.comparingInt(item -> item.getLocation().distanceTo(anchor))); - } - - // ============================================ - // Name-Based Finding Methods - // ============================================ - - /** - * Creates a predicate that matches ground items whose name contains the given string (case-insensitive). - * - * @param itemName The name to match (partial or full) - * @param exact Whether to match exactly or contain - * @return Predicate for name matching - */ - public static Predicate nameMatches(String itemName, boolean exact) { - String lower = itemName.toLowerCase(); - return item -> { - String name = item.getName(); - if (name == null) return false; - return exact ? name.equalsIgnoreCase(itemName) : name.toLowerCase().contains(lower); - }; - } - - /** - * Creates a predicate that matches ground items whose name contains the given string (case-insensitive). - * - * @param itemName The name to match (partial) - * @return Predicate for name matching - */ - public static Predicate nameMatches(String itemName) { - return nameMatches(itemName, false); - } - - /** - * Finds the first ground item by name. - * - * @param itemName The item name - * @param exact Whether to match exactly or contain - * @return Optional containing the first matching ground item - */ - public static Optional findByName(String itemName, boolean exact) { - return find(nameMatches(itemName, exact)); - } - - /** - * Finds the first ground item by name (partial match). - * - * @param itemName The item name - * @return Optional containing the first matching ground item - */ - public static Optional findByName(String itemName) { - return findByName(itemName, false); - } - - /** - * Finds the closest ground item by name. - * - * @param itemName The item name - * @param exact Whether to match exactly or contain - * @return Optional containing the closest matching ground item - */ - public static Optional findClosestByName(String itemName, boolean exact) { - return findClosest(nameMatches(itemName, exact)); - } - - /** - * Finds the closest ground item by name (partial match). - * - * @param itemName The item name - * @return Optional containing the closest matching ground item - */ - public static Optional findClosestByName(String itemName) { - return findClosestByName(itemName, false); - } - - /** - * Finds ground items by name within distance from player. - * - * @param itemName The item name - * @param exact Whether to match exactly or contain - * @param distance Maximum distance in tiles - * @return Stream of matching ground items within distance - */ - public static Stream findByNameWithinDistance(String itemName, boolean exact, int distance) { - return findAll(item -> nameMatches(itemName, exact).test(item) && item.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds ground items by name within distance from player (partial match). - * - * @param itemName The item name - * @param distance Maximum distance in tiles - * @return Stream of matching ground items within distance - */ - public static Stream findByNameWithinDistance(String itemName, int distance) { - return findByNameWithinDistance(itemName, false, distance); - } - - /** - * Finds the closest ground item by name within distance from player. - * - * @param itemName The item name - * @param exact Whether to match exactly or contain - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching ground item within distance - */ - public static Optional findClosestByNameWithinDistance(String itemName, boolean exact, int distance) { - return findClosest(item -> nameMatches(itemName, exact).test(item) && item.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest ground item by name within distance from player (partial match). - * - * @param itemName The item name - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching ground item within distance - */ - public static Optional findClosestByNameWithinDistance(String itemName, int distance) { - return findClosestByNameWithinDistance(itemName, false, distance); - } - - // ============================================ - // Array-Based ID Methods - // ============================================ - - /** - * Finds the first ground item matching any of the given IDs. - * - * @param itemIds Array of item IDs - * @return Optional containing the first matching ground item - */ - public static Optional findByIds(Integer[] itemIds) { - Set idSet = Set.of(itemIds); - return find(item -> idSet.contains(item.getId())); - } - - /** - * Finds the closest ground item matching any of the given IDs. - * - * @param itemIds Array of item IDs - * @return Optional containing the closest matching ground item - */ - public static Optional findClosestByIds(Integer[] itemIds) { - Set idSet = Set.of(itemIds); - return findClosest(item -> idSet.contains(item.getId())); - } - - /** - * Finds ground items matching any of the given IDs within distance. - * - * @param itemIds Array of item IDs - * @param distance Maximum distance in tiles - * @return Stream of matching ground items within distance - */ - public static Stream findByIdsWithinDistance(Integer[] itemIds, int distance) { - Set idSet = Set.of(itemIds); - return findAll(item -> idSet.contains(item.getId()) && item.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest ground item matching any of the given IDs within distance. - * - * @param itemIds Array of item IDs - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching ground item within distance - */ - public static Optional findClosestByIdsWithinDistance(Integer[] itemIds, int distance) { - Set idSet = Set.of(itemIds); - return findClosest(item -> idSet.contains(item.getId()) && item.isWithinDistanceFromPlayer(distance)); - } - - // ============================================ - // Value and Property-Based Methods - // ============================================ - - /** - * Finds ground items with value greater than or equal to the specified amount. - * - * @param minValue Minimum value - * @return Stream of ground items with value >= minValue - */ - public static Stream findByMinValue(int minValue) { - return findAll(item -> item.getValue() >= minValue); - } - - /** - * Finds the closest ground item with value greater than or equal to the specified amount. - * - * @param minValue Minimum value - * @return Optional containing the closest valuable ground item - */ - public static Optional findClosestByMinValue(int minValue) { - return findClosest(item -> item.getValue() >= minValue); - } - - /** - * Finds ground items with value in the specified range. - * - * @param minValue Minimum value (inclusive) - * @param maxValue Maximum value (inclusive) - * @return Stream of ground items with value in range - */ - public static Stream findByValueRange(int minValue, int maxValue) { - return findAll(item -> item.getValue() >= minValue && item.getValue() <= maxValue); - } - - /** - * Finds stackable ground items. - * - * @return Stream of stackable ground items - */ - public static Stream findStackable() { - return findAll(Rs2GroundItemModel::isStackable); - } - - /** - * Finds noted ground items. - * - * @return Stream of noted ground items - */ - public static Stream findNoted() { - return findAll(Rs2GroundItemModel::isNoted); - } - - /** - * Finds tradeable ground items. - * - * @return Stream of tradeable ground items - */ - public static Stream findTradeable() { - return findAll(Rs2GroundItemModel::isTradeable); - } - - /** - * Finds ground items owned by the player. - * - * @return Stream of owned ground items - */ - public static Stream findOwned() { - return findAll(Rs2GroundItemModel::isOwned); - } - - /** - * Finds ground items not owned by the player. - * - * @return Stream of unowned ground items - */ - public static Stream findUnowned() { - return findAll(item -> !item.isOwned()); - } - - /** - * Finds the most valuable ground item. - * - * @return Optional containing the most valuable ground item - */ - public static Optional findMostValuable() { - return getAll().max(Comparator.comparingInt(Rs2GroundItemModel::getValue)); - } - - /** - * Finds the most valuable ground item within distance. - * - * @param distance Maximum distance in tiles - * @return Optional containing the most valuable ground item within distance - */ - public static Optional findMostValuableWithinDistance(int distance) { - return findAll(item -> item.isWithinDistanceFromPlayer(distance)) - .max(Comparator.comparingInt(Rs2GroundItemModel::getValue)); - } - - // ============================================ - // Quantity-Based Methods - // ============================================ - - /** - * Finds ground items with quantity greater than or equal to the specified amount. - * - * @param minQuantity Minimum quantity - * @return Stream of ground items with quantity >= minQuantity - */ - public static Stream findByMinQuantity(int minQuantity) { - return findAll(item -> item.getQuantity() >= minQuantity); - } - - /** - * Finds ground items with quantity in the specified range. - * - * @param minQuantity Minimum quantity (inclusive) - * @param maxQuantity Maximum quantity (inclusive) - * @return Stream of ground items with quantity in range - */ - public static Stream findByQuantityRange(int minQuantity, int maxQuantity) { - return findAll(item -> item.getQuantity() >= minQuantity && item.getQuantity() <= maxQuantity); - } - - /** - * Finds the ground item with the highest quantity for a specific item ID. - * - * @param itemId The item ID - * @return Optional containing the ground item with highest quantity - */ - public static Optional findHighestQuantity(int itemId) { - return getByGameId(itemId).max(Comparator.comparingInt(Rs2GroundItemModel::getQuantity)); - } - - // ============================================ - // Age-Based Methods - // ============================================ - - /** - * Finds ground items that have been on the ground for at least the specified number of ticks. - * - * @param minTicks Minimum ticks since spawn - * @return Stream of ground items aged at least minTicks - */ - public static Stream findByMinAge(int minTicks) { - return findAll(item -> item.getTicksSinceSpawn() >= minTicks); - } - - /** - * Finds ground items that have been on the ground for less than the specified number of ticks. - * - * @param maxTicks Maximum ticks since spawn - * @return Stream of fresh ground items - */ - public static Stream findFresh(int maxTicks) { - return findAll(item -> item.getTicksSinceSpawn() <= maxTicks); - } - - /** - * Finds the oldest ground item. - * - * @return Optional containing the oldest ground item - */ - public static Optional findOldest() { - return getAll().max(Comparator.comparingInt(Rs2GroundItemModel::getTicksSinceSpawn)); - } - - /** - * Finds the newest ground item. - * - * @return Optional containing the newest ground item - */ - public static Optional findNewest() { - return getAll().min(Comparator.comparingInt(Rs2GroundItemModel::getTicksSinceSpawn)); - } - - // ============================================ - // Scene and Viewport Extraction Methods - // ============================================ - - /** - * Gets all ground items currently in the scene (all cached ground items). - * This includes ground items that may not be visible in the current viewport. - * - * @return Stream of all ground items in the scene - */ - public static Stream getAllInScene() { - return getAll(); - } - - /** - * Gets all ground items currently visible in the viewport (on screen). - * Only includes ground items whose location can be converted to screen coordinates. - * - * @return Stream of ground items visible in viewport - */ - public static Stream getAllInViewport() { - return filterVisibleInViewport(getAll()); - } - - /** - * Gets all ground items by ID that are currently visible in the viewport. - * - * @param itemId The item ID to filter by - * @return Stream of ground items with the specified ID that are visible in viewport - */ - public static Stream getAllInViewport(int itemId) { - return filterVisibleInViewport(getByGameId(itemId)); - } - - /** - * Gets the closest ground item in the viewport by ID. - * - * @param itemId The item ID - * @return Optional containing the closest ground item in viewport - */ - public static Optional getClosestInViewport(int itemId) { - return getAllInViewport(itemId) - .min(Comparator.comparingInt(Rs2GroundItemModel::getDistanceFromPlayer)); - } - - /** - * Gets all ground items in the viewport that are interactable (within reasonable distance). - * - * @param maxDistance Maximum distance for interaction - * @return Stream of interactable ground items in viewport - */ - public static Stream getAllInteractable(int maxDistance) { - return getAllInViewport() - .filter(item -> isInteractable(item, maxDistance)); - } - - /** - * Gets all ground items by ID in the viewport that are interactable. - * - * @param itemId The item ID - * @param maxDistance Maximum distance for interaction - * @return Stream of interactable ground items with the specified ID - */ - public static Stream getAllInteractable(int itemId, int maxDistance) { - return getAllInViewport(itemId) - .filter(item -> isInteractable(item, maxDistance)); - } - - /** - * Gets the closest interactable ground item by ID. - * - * @param itemId The item ID - * @param maxDistance Maximum distance for interaction - * @return Optional containing the closest interactable ground item - */ - public static Optional getClosestInteractable(int itemId, int maxDistance) { - return getAllInteractable(itemId, maxDistance) - .min(Comparator.comparingInt(Rs2GroundItemModel::getDistanceFromPlayer)); - } - - // ============================================ - // Line of Sight Utilities - // ============================================ - - /** - * Checks if there is a line of sight between the player and a ground item. - * Uses RuneLite's WorldArea.hasLineOfSightTo for accurate scene collision detection. - * - * @param groundItem The ground item to check - * @return True if line of sight exists, false otherwise - */ - public static boolean hasLineOfSight(Rs2GroundItemModel groundItem) { - if (groundItem == null) return false; - - try { - // Get player's current world location and create a small area (1x1) - WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - return hasLineOfSight(playerLocation, groundItem); - } catch (Exception e) { - return false; - } - } - - /** - * Checks if there is a line of sight between a specific point and a ground item. - * - * @param point The world point to check from - * @param groundItem The ground item to check against - * @return True if line of sight exists, false otherwise - */ - public static boolean hasLineOfSight(WorldPoint point, Rs2GroundItemModel groundItem) { - if (groundItem == null || point == null) return false; - - try { - WorldPoint itemLocation = groundItem.getLocation(); - - // Check same plane - if (point.getPlane() != itemLocation.getPlane()) { - return false; - } - - // Ground items are always 1x1 - return new WorldArea(itemLocation, 1, 1) - .hasLineOfSightTo( - Microbot.getClient().getTopLevelWorldView(), - new WorldArea(point, 1, 1)); - } catch (Exception e) { - return false; - } - } - - /** - * Gets all ground items that have line of sight to the player. - * Useful for identifying interactive items. - * - * @return Stream of ground items with line of sight to player - */ - public static Stream getGroundItemsWithLineOfSightToPlayer() { - return getAll().filter(Rs2GroundItemCacheUtils::hasLineOfSight); - } - - /** - * Gets all ground items that have line of sight to a specific world point. - * - * @param point The world point to check from - * @return Stream of ground items with line of sight to the point - */ - public static Stream getGroundItemsWithLineOfSightTo(WorldPoint point) { - return getAll().filter(item -> hasLineOfSight(point, item)); - } - - /** - * Gets all ground items at a location that have line of sight to the player. - * - * @param worldPoint The world point to check at - * @param maxDistance Maximum distance from the world point - * @return Stream of ground items at the location with line of sight - */ - public static Stream getGroundItemsAtLocationWithLineOfSight(WorldPoint worldPoint, int maxDistance) { - return getAll() - .filter(item -> item.getLocation().distanceTo(worldPoint) <= maxDistance) - .filter(Rs2GroundItemCacheUtils::hasLineOfSight); - } - - // ============================================ - // Looting-Specific Utility Methods - // ============================================ - - /** - * Gets all ground items that are lootable by the player. - * - * @return Stream of lootable ground items - */ - public static Stream getLootableItems() { - return findAll(Rs2GroundItemModel::isLootAble); - } - - /** - * Gets all lootable ground items within distance from player. - * - * @param distance Maximum distance from player - * @return Stream of lootable ground items within distance - */ - public static Stream getLootableItemsWithinDistance(int distance) { - return findAll(item -> item.isLootAble() && item.isWithinDistanceFromPlayer(distance)); - } - - /** - * Gets lootable ground items with value greater than or equal to the specified amount. - * - * @param minValue Minimum total value threshold - * @return Stream of valuable lootable ground items - */ - public static Stream getLootableItemsByValue(int minValue) { - return findAll(item -> item.isLootAble() && item.isWorthLooting(minValue)); - } - - /** - * Gets lootable ground items with Grand Exchange value greater than or equal to the specified amount. - * - * @param minGeValue Minimum GE value threshold - * @return Stream of valuable lootable ground items - */ - public static Stream getLootableItemsByGeValue(int minGeValue) { - return findAll(item -> item.isLootAble() && item.isWorthLootingGe(minGeValue)); - } - - /** - * Gets lootable ground items in value range. - * - * @param minValue Minimum total value (inclusive) - * @param maxValue Maximum total value (inclusive) - * @return Stream of lootable ground items in value range - */ - public static Stream getLootableItemsByValueRange(int minValue, int maxValue) { - return findAll(item -> item.isLootAble() && - item.getTotalValue() >= minValue && - item.getTotalValue() <= maxValue); - } - - /** - * Gets commonly desired loot items that are available for pickup. - * - * @return Stream of common loot ground items - */ - public static Stream getCommonLootItems() { - return findAll(item -> item.isLootAble() && item.isCommonLoot()); - } - - /** - * Gets high-priority items that should be looted urgently. - * - * @return Stream of priority loot ground items - */ - public static Stream getPriorityLootItems() { - return findAll(item -> item.isLootAble() && item.shouldPrioritize()); - } - - /** - * Gets items that are profitable to high alch. - * - * @param minProfit The minimum profit threshold - * @return Stream of profitable high alch ground items - */ - public static Stream getHighAlchProfitableItems(int minProfit) { - return findAll(item -> item.isLootAble() && item.isProfitableToHighAlch(minProfit)); - } - - /** - * Gets the most valuable lootable item within distance. - * - * @param maxDistance Maximum distance from player - * @return Optional containing the most valuable lootable item - */ - public static Optional getMostValuableLootableItem(int maxDistance) { - return getLootableItemsWithinDistance(maxDistance) - .max(Comparator.comparingInt(Rs2GroundItemModel::getTotalValue)); - } - - /** - * Gets the closest valuable item to the player. - * - * @param minValue Minimum value threshold - * @return Optional containing the closest valuable lootable item - */ - public static Optional getClosestValuableItem(int minValue) { - return findClosest(item -> item.isLootAble() && item.isWorthLooting(minValue)); - } - - /** - * Gets the closest lootable item to the player. - * - * @return Optional containing the closest lootable item - */ - public static Optional getClosestLootableItem() { - return findClosest(Rs2GroundItemModel::isLootAble); - } - - /** - * Gets the closest lootable item to the player within distance. - * - * @param maxDistance Maximum distance from player - * @return Optional containing the closest lootable item within distance - */ - public static Optional getClosestLootableItemWithinDistance(int maxDistance) { - return findClosest(item -> item.isLootAble() && item.isWithinDistanceFromPlayer(maxDistance)); - } - - /** - * Gets lootable items that match any of the provided item IDs. - * - * @param itemIds Array of item IDs to match - * @return Stream of matching lootable ground items - */ - public static Stream getLootableItemsByIds(Integer[] itemIds) { - Set idSet = Set.of(itemIds); - return findAll(item -> item.isLootAble() && idSet.contains(item.getId())); - } - - /** - * Gets lootable items by name pattern. - * - * @param namePattern The name pattern (supports partial matches) - * @param exact Whether to match exactly or use contains - * @return Stream of matching lootable ground items - */ - public static Stream getLootableItemsByName(String namePattern, boolean exact) { - return findAll(item -> item.isLootAble() && nameMatches(namePattern, exact).test(item)); - } - - /** - * Gets lootable items by name pattern (partial match). - * - * @param namePattern The name pattern - * @return Stream of matching lootable ground items - */ - public static Stream getLootableItemsByName(String namePattern) { - return getLootableItemsByName(namePattern, false); - } - - // ============================================ - // Despawn-Based Utility Methods - // ============================================ - - /** - * Gets ground items that will despawn within the specified number of seconds. - * - * @param seconds The time threshold in seconds - * @return Stream of ground items about to despawn - */ - public static Stream getItemsDespawningWithin(long seconds) { - return findAll(item -> item.willDespawnWithin(seconds)); - } - - /** - * Gets ground items that will despawn within the specified number of ticks. - * - * @param ticks The time threshold in ticks - * @return Stream of ground items about to despawn - */ - public static Stream getItemsDespawningWithinTicks(int ticks) { - return findAll(item -> item.willDespawnWithinTicks(ticks)); - } - - /** - * Gets lootable ground items that will despawn within the specified number of seconds. - * - * @param seconds The time threshold in seconds - * @return Stream of lootable ground items about to despawn - */ - public static Stream getLootableItemsDespawningWithin(long seconds) { - return findAll(item -> item.isLootAble() && item.willDespawnWithin(seconds)); - } - - /** - * Gets the ground item that will despawn next. - * - * @return Optional containing the next ground item to despawn - */ - public static Optional getNextItemToDespawn() { - return findAll(item -> !item.isDespawned()) - .min(Comparator.comparingLong(Rs2GroundItemModel::getSecondsUntilDespawn)); - } - - /** - * Gets the lootable ground item that will despawn next. - * - * @return Optional containing the next lootable ground item to despawn - */ - public static Optional getNextLootableItemToDespawn() { - return findAll(item -> item.isLootAble() && !item.isDespawned()) - .min(Comparator.comparingLong(Rs2GroundItemModel::getSecondsUntilDespawn)); - } - - /** - * Gets the time in seconds until the next item despawns. - * - * @return Seconds until next despawn, or -1 if no items - */ - public static long getSecondsUntilNextDespawn() { - return getNextItemToDespawn() - .map(Rs2GroundItemModel::getSecondsUntilDespawn) - .orElse(-1L); - } - - /** - * Gets the time in seconds until the next lootable item despawns. - * - * @return Seconds until next lootable item despawn, or -1 if no items - */ - public static long getSecondsUntilNextLootableDespawn() { - return getNextLootableItemToDespawn() - .map(Rs2GroundItemModel::getSecondsUntilDespawn) - .orElse(-1L); - } - - /** - * Gets ground items that have despawned and should be cleaned up. - * - * @return Stream of despawned ground items - */ - public static Stream getDespawnedItems() { - return findAll(Rs2GroundItemModel::isDespawned); - } - - // ============================================ - // Statistics and Analysis Methods - // ============================================ - - /** - * Gets the total value of all lootable items. - * - * @return Total value of all lootable ground items - */ - public static int getTotalLootableValue() { - return getLootableItems() - .mapToInt(Rs2GroundItemModel::getTotalValue) - .sum(); - } - - /** - * Gets the total Grand Exchange value of all lootable items. - * - * @return Total GE value of all lootable ground items - */ - public static int getTotalLootableGeValue() { - return getLootableItems() - .mapToInt(Rs2GroundItemModel::getTotalGeValue) - .sum(); - } - - /** - * Gets the count of lootable items. - * - * @return Number of lootable ground items - */ - public static int getLootableItemCount() { - return (int) getLootableItems().count(); - } - - /** - * Gets the count of lootable items within distance. - * - * @param distance Maximum distance from player - * @return Number of lootable ground items within distance - */ - public static int getLootableItemCountWithinDistance(int distance) { - return (int) getLootableItemsWithinDistance(distance).count(); - } - - - - /** - * Gets statistics about lootable items. - * - * @return Map containing lootable item statistics - */ - public static Map getLootableItemStatistics() { - Map stats = new HashMap<>(); - - List allItems = getAll().collect(Collectors.toList()); - List lootableItems = getLootableItems().collect(Collectors.toList()); - - stats.put("totalItems", allItems.size()); - stats.put("lootableItems", lootableItems.size()); - stats.put("lootablePercentage", allItems.isEmpty() ? 0 : (lootableItems.size() * 100.0 / allItems.size())); - stats.put("totalLootableValue", lootableItems.stream().mapToInt(Rs2GroundItemModel::getTotalValue).sum()); - stats.put("totalLootableGeValue", lootableItems.stream().mapToInt(Rs2GroundItemModel::getTotalGeValue).sum()); - stats.put("averageLootableValue", lootableItems.isEmpty() ? 0 : - lootableItems.stream().mapToInt(Rs2GroundItemModel::getTotalValue).average().orElse(0)); - stats.put("lootableItemsDespawningIn30s", lootableItems.stream().filter(item -> item.willDespawnWithin(30)).count()); - stats.put("secondsUntilNextLootableDespawn", getSecondsUntilNextLootableDespawn()); - stats.put("priorityLootItems", getPriorityLootItems().count()); - stats.put("commonLootItems", getCommonLootItems().count()); - - return stats; - } - - // ============================================ - // Advanced Filtering Methods - // ============================================ - - /** - * Gets ground items matching multiple criteria with custom predicates. - * - * @param isLootable Whether to filter for lootable items only - * @param minValue Minimum value filter (0 to ignore) - * @param maxDistance Maximum distance filter (0 to ignore) - * @param customPredicate Additional custom predicate (null to ignore) - * @return Stream of matching ground items - */ - public static Stream getItemsWithCriteria( - boolean isLootable, - int minValue, - int maxDistance, - Predicate customPredicate) { - - Stream stream = getAll(); - - if (isLootable) { - stream = stream.filter(Rs2GroundItemModel::isLootAble); - } - - if (minValue > 0) { - stream = stream.filter(item -> item.getTotalValue() >= minValue); - } - - if (maxDistance > 0) { - stream = stream.filter(item -> item.isWithinDistanceFromPlayer(maxDistance)); - } - - if (customPredicate != null) { - stream = stream.filter(customPredicate); - } - - return stream; - } - - /** - * Gets items that are both lootable and have line of sight to the player. - * Useful for identifying immediately interactable loot. - * - * @return Stream of lootable ground items with line of sight - */ - public static Stream getLootableItemsWithLineOfSight() { - return getLootableItems().filter(Rs2GroundItemCacheUtils::hasLineOfSight); - } - - /** - * Gets lootable items within distance that have line of sight to the player. - * - * @param maxDistance Maximum distance from player - * @return Stream of lootable ground items within distance with line of sight - */ - public static Stream getLootableItemsWithLineOfSightWithinDistance(int maxDistance) { - return getLootableItemsWithinDistance(maxDistance) - .filter(Rs2GroundItemCacheUtils::hasLineOfSight); - } - - /** - * Gets the closest lootable item with line of sight to the player. - * - * @return Optional containing the closest lootable item with line of sight - */ - public static Optional getClosestLootableItemWithLineOfSight() { - return getLootableItemsWithLineOfSight() - .min(Comparator.comparingInt(Rs2GroundItemModel::getDistanceFromPlayer)); - } - - /** - * Gets the closest valuable lootable item with line of sight to the player. - * - * @param minValue Minimum value threshold - * @return Optional containing the closest valuable lootable item with line of sight - */ - public static Optional getClosestValuableLootableItemWithLineOfSight(int minValue) { - return getLootableItemsWithLineOfSight() - .filter(item -> item.isWorthLooting(minValue)) - .min(Comparator.comparingInt(Rs2GroundItemModel::getDistanceFromPlayer)); - } - - // ============================================ - // Tile-Based Access Methods (Rs2GroundItem replacement support) - // ============================================ - - /** - * Gets all ground items at a specific tile location. - * Direct replacement for Rs2GroundItem.getAllAt(x, y). - * - * @param location The world point location of the tile - * @return Stream of ground items at the specified tile - */ - public static Stream getItemsAtTile(WorldPoint location) { - return getAll().filter(item -> item.getLocation().equals(location)); - } - - /** - * Gets all ground items at a specific tile coordinate. - * Direct replacement for Rs2GroundItem.getAllAt(x, y). - * - * @param x The x coordinate of the tile - * @param y The y coordinate of the tile - * @return Stream of ground items at the specified tile - */ - public static Stream getItemsAtTile(int x, int y) { - return getItemsAtTile(new WorldPoint(x, y, Microbot.getClient().getLocalPlayer().getWorldLocation().getPlane())); - } - - /** - * Gets all ground items within a range of a specific WorldPoint. - * Direct replacement for Rs2GroundItem.getAllFromWorldPoint(range, worldPoint). - * - * @param range The radius in tiles to search around the given world point - * @param worldPoint The center WorldPoint to search around - * @return Stream of ground items found within the specified range, sorted by proximity - */ - public static Stream getItemsFromWorldPoint(int range, WorldPoint worldPoint) { - return getAll() - .filter(item -> item.getLocation().distanceTo(worldPoint) <= range) - .sorted(Comparator.comparingInt(item -> item.getLocation().distanceTo(worldPoint))); - } - - /** - * Gets all ground items within a range of the player. - * Direct replacement for Rs2GroundItem.getAll(range). - * - * @param range The radius in tiles to search around the player - * @return Stream of ground items found within the specified range, sorted by proximity to player - */ - public static Stream getItemsAroundPlayer(int range) { - try { - WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - return getItemsFromWorldPoint(range, playerLocation); - } catch (Exception e) { - return Stream.empty(); - } - } - - // ============================================ - // Enhanced Interaction Utilities - // ============================================ - - /** - * Finds the best ground item to interact with based on criteria. - * - * @param itemId The item ID to search for - * @param action The action to perform (e.g., "Take") - * @param range Maximum search range - * @return Optional containing the best item to interact with - */ - public static Optional findBestInteractionTarget(int itemId, String action, int range) { - return getItemsAroundPlayer(range) - .filter(item -> item.getId() == itemId) - .filter(item -> item.isLootAble()) - .filter(Rs2GroundItemCacheUtils::hasLineOfSight) - .min(Comparator.comparingInt(Rs2GroundItemModel::getDistanceFromPlayer)); - } - - /** - * Finds the best ground item to interact with based on name. - * - * @param itemName The item name to search for - * @param action The action to perform (e.g., "Take") - * @param range Maximum search range - * @return Optional containing the best item to interact with - */ - public static Optional findBestInteractionTarget(String itemName, String action, int range) { - return getItemsAroundPlayer(range) - .filter(item -> nameMatches(itemName, false).test(item)) - .filter(item -> item.isLootAble()) - .filter(Rs2GroundItemCacheUtils::hasLineOfSight) - .min(Comparator.comparingInt(Rs2GroundItemModel::getDistanceFromPlayer)); - } - - /** - * Checks if any ground item exists with the specified ID within range. - * Direct replacement for Rs2GroundItem.exists(id, range). - * - * @param itemId The item ID to check for - * @param range Maximum search range - * @return true if the item exists within range - */ - public static boolean exists(int itemId, int range) { - return getItemsAroundPlayer(range) - .anyMatch(item -> item.getId() == itemId); - } - - /** - * Checks if any ground item exists with the specified name within range. - * Direct replacement for Rs2GroundItem.exists(itemName, range). - * - * @param itemName The item name to check for - * @param range Maximum search range - * @return true if the item exists within range - */ - public static boolean exists(String itemName, int range) { - return getItemsAroundPlayer(range) - .anyMatch(item -> nameMatches(itemName, false).test(item)); - } - - /** - * Checks if valuable items exist on the ground based on minimum value. - * Direct replacement for Rs2GroundItem.isItemBasedOnValueOnGround(value, range). - * - * @param minValue Minimum value threshold - * @param range Maximum search range - * @return true if valuable items exist within range - */ - public static boolean existsValueableItems(int minValue, int range) { - return getLootableItemsWithinDistance(range) - .anyMatch(item -> item.isWorthLooting(minValue)); - } - - // ============================================ - // Batch Processing Methods - // ============================================ - - /** - * Gets multiple ground items of different IDs within range. - * Useful for batch operations. - * - * @param itemIds Array of item IDs to search for - * @param range Maximum search range - * @return Map of item ID to list of ground items - */ - public static Map> getBatchItems(Integer[] itemIds, int range) { - Set idSet = Set.of(itemIds); - return getItemsAroundPlayer(range) - .filter(item -> idSet.contains(item.getId())) - .filter(item -> item.isLootAble()) - .collect(Collectors.groupingBy(Rs2GroundItemModel::getId)); - } - - /** - * Gets ground items sorted by value within range. - * Useful for priority-based looting. - * - * @param range Maximum search range - * @param minValue Minimum value threshold - * @return Stream of ground items sorted by value (highest first) - */ - public static Stream getItemsSortedByValue(int range, int minValue) { - return getLootableItemsWithinDistance(range) - .filter(item -> item.getTotalValue() >= minValue) - .sorted((a, b) -> Integer.compare(b.getTotalValue(), a.getTotalValue())); - } - - /** - * Gets ground items sorted by despawn urgency. - * Items closest to despawning are returned first. - * - * @param range Maximum search range - * @return Stream of ground items sorted by despawn urgency - */ - public static Stream getItemsSortedByDespawnUrgency(int range) { - return getLootableItemsWithinDistance(range) - .filter(item -> !item.isDespawned()) - .sorted(Comparator.comparingLong(Rs2GroundItemModel::getSecondsUntilDespawn)); - } - - // ============================================ - // Viewport Visibility and Interactability Utilities - // ============================================ - - /** - * Checks if a ground item is visible in the current viewport. - * Uses the tile location with client thread safety to determine visibility. - * - * @param groundItem The ground item to check - * @return true if the ground item's location is visible on screen - */ - public static boolean isVisibleInViewport(Rs2GroundItemModel groundItem) { - try { - if (groundItem == null || groundItem.getLocation() == null) { - return false; - } - - // Use client thread for safe access to client state - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - Client client = Microbot.getClient(); - if (client == null) { - return false; - } - - // Convert world point to local point - LocalPoint localPoint = LocalPoint.fromWorld(client.getTopLevelWorldView(), groundItem.getLocation()); - if (localPoint == null) { - return false; - } - - // Check if the local point can be converted to canvas coordinates - net.runelite.api.Point canvasPoint = Perspective.localToCanvas(client, localPoint, client.getTopLevelWorldView().getPlane()); - return canvasPoint != null; - }).orElse(false); - } catch (Exception e) { - return false; - } - } - - /** - * Checks if any entity with a location is within the viewport by checking canvas conversion. - * This is a generic method that can work with any entity that has a world location. - * Uses client thread for safe access to client state. - * - * @param worldPoint The world point to check - * @return true if the location is visible on screen - */ - public static boolean isLocationVisibleInViewport(net.runelite.api.coords.WorldPoint worldPoint) { - try { - if (worldPoint == null) { - return false; - } - - // Use client thread for safe access to client state - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - Client client = Microbot.getClient(); - if (client == null) { - return false; - } - - LocalPoint localPoint = LocalPoint.fromWorld(client.getTopLevelWorldView(), worldPoint); - if (localPoint == null) { - return false; - } - - net.runelite.api.Point canvasPoint = Perspective.localToCanvas(client, localPoint, client.getTopLevelWorldView().getPlane()); - return canvasPoint != null; - }).orElse(false); - } catch (Exception e) { - return false; - } - } - - /** - * Filters a stream of ground items to only include those visible in viewport. - * - * @param groundItemStream Stream of ground items to filter - * @return Stream of ground items visible in viewport - */ - public static Stream filterVisibleInViewport(Stream groundItemStream) { - return groundItemStream.filter(Rs2GroundItemCacheUtils::isVisibleInViewport); - } - - /** - * Checks if a ground item is interactable (visible and within reasonable distance). - * - * @param groundItem The ground item to check - * @param maxDistance Maximum distance in tiles for interaction - * @return true if the ground item is interactable - */ - public static boolean isInteractable(Rs2GroundItemModel groundItem, int maxDistance) { - try { - if (groundItem == null) { - return false; - } - - // Check if visible in viewport first - if (!isVisibleInViewport(groundItem)) { - return false; - } - - // Check distance from player - return groundItem.getDistanceFromPlayer() <= maxDistance; - } catch (Exception e) { - return false; - } - } - - /** - * Checks if an entity at a world point is interactable (within reasonable distance and visible). - * Uses client thread for safe access to player location. - * - * @param worldPoint The world point to check - * @param maxDistance Maximum distance in tiles for interaction - * @return true if the location is potentially interactable - */ - public static boolean isInteractable(net.runelite.api.coords.WorldPoint worldPoint, int maxDistance) { - try { - if (worldPoint == null) { - return false; - } - - // Use client thread for safe access to player location - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - net.runelite.api.coords.WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - if (playerLocation.distanceTo(worldPoint) > maxDistance) { - return false; - } - - // Check if visible in viewport (already uses client thread internally) - return isLocationVisibleInViewport(worldPoint); - }).orElse(false); - } catch (Exception e) { - return false; - } - } - - // ============================================ - // Updated Existing Methods to Use Local Functions - // ============================================ - - /** - * Gets all ground items visible in the viewport. - * - * @return Stream of ground items visible in viewport - */ - public static Stream getVisibleInViewport() { - return filterVisibleInViewport(getAll()); - } - - /** - * Gets ground items by ID that are visible in the viewport. - * - * @param itemId The item ID - * @return Stream of ground items with the specified ID visible in viewport - */ - public static Stream getVisibleInViewportById(int itemId) { - return filterVisibleInViewport(getByGameId(itemId)); - } - - /** - * Finds interactable ground items by ID within distance from player. - * - * @param itemId The item ID - * @param maxDistance Maximum distance in tiles - * @return Stream of interactable ground items with the specified ID - */ - public static Stream findInteractableById(int itemId, int maxDistance) { - return getByGameId(itemId) - .filter(item -> isInteractable(item, maxDistance)); - } - - /** - * Finds interactable ground items by name within distance from player. - * - * @param name The item name - * @param maxDistance Maximum distance in tiles - * @return Stream of interactable ground items with the specified name - */ - public static Stream findInteractableByName(String name, int maxDistance) { - return findAll(nameMatches(name, false)) - .filter(item -> isInteractable(item, maxDistance)); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2NpcCacheUtils.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2NpcCacheUtils.java deleted file mode 100644 index 57e7faaa1df..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2NpcCacheUtils.java +++ /dev/null @@ -1,1185 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.util; - -import net.runelite.api.Client; -import net.runelite.api.NPCComposition; -import net.runelite.api.Perspective; -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.util.cache.Rs2NpcCache; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -import java.util.*; -import java.util.function.Predicate; -import java.util.stream.Stream; - -/** - * Cache-based utility class for NPC operations. - * Provides comprehensive utilities for finding and filtering NPCs using the cache system. - * This is a cache-based alternative to Rs2Npc for persistent NPC tracking. - */ -public class Rs2NpcCacheUtils { - - // ============================================ - // Primary Player-Based Utility Methods - // ============================================ - - /** - * Gets all NPCs within a specified radius from the player's current location. - * This is the method called by toggleNearestNpcTracking() and similar functions. - * - * @param radius Maximum distance in tiles from player - * @return Stream of NPCs within the specified radius from player - */ - public static Stream getNearBy(int radius) { - WorldPoint playerLocation = Rs2Player.getWorldLocation(); - if (playerLocation == null) { - return Stream.empty(); - } - return findWithinDistance(npc -> true, playerLocation, radius); - } - - /** - * Gets all NPCs within a specified radius from the player's current location, - * sorted by distance from closest to furthest. - * - * @param radius Maximum distance in tiles from player - * @return Stream of NPCs within the specified radius, sorted by distance - */ - public static Stream getNearBySorted(int radius) { - return getNearBy(radius) - .sorted(Comparator.comparingInt(Rs2NpcModel::getDistanceFromPlayer)); - } - - /** - * Gets the nearest NPC to the player within the specified radius. - * - * @param radius Maximum distance in tiles from player - * @return Optional containing the nearest NPC, or empty if none found - */ - public static Optional getNearestWithinRadius(int radius) { - return getNearBy(radius) - .min(Comparator.comparingInt(Rs2NpcModel::getDistanceFromPlayer)); - } - - // ============================================ - // Action-Based Utility Methods (Rs2Npc compatibility) - // ============================================ - - /** - * Creates a predicate that matches NPCs with a specific action. - * - * @param action The action to check for (e.g., "Talk-to", "Bank", "Trade") - * @return Predicate for action matching - */ - public static Predicate hasAction(String action) { - return npc -> { - try { - NPCComposition baseComposition = npc.getComposition(); - NPCComposition transformedComposition = npc.getTransformedComposition(); - - List baseActions = baseComposition != null ? - Arrays.asList(baseComposition.getActions()) : Collections.emptyList(); - List transformedActions = transformedComposition != null ? - Arrays.asList(transformedComposition.getActions()) : Collections.emptyList(); - - return baseActions.contains(action) || transformedActions.contains(action); - } catch (Exception e) { - return false; - } - }; - } - - /** - * Finds NPCs with a specific action. - * - * @param action The action to search for - * @return Stream of NPCs with the specified action - */ - public static Stream findWithAction(String action) { - return findAll(hasAction(action)); - } - - /** - * Finds the nearest NPC with a specific action. - * Equivalent to Rs2Npc.getNearestNpcWithAction(). - * - * @param action The action to search for - * @return Optional containing the nearest NPC with the action - */ - public static Optional findNearestWithAction(String action) { - return findClosest(hasAction(action)); - } - - /** - * Finds NPCs with a specific action within distance from player. - * - * @param action The action to search for - * @param distance Maximum distance in tiles - * @return Stream of NPCs with the action within distance - */ - public static Stream findWithActionWithinDistance(String action, int distance) { - return findAll(npc -> hasAction(action).test(npc) && npc.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the nearest NPC with a specific action within distance from player. - * - * @param action The action to search for - * @param distance Maximum distance in tiles - * @return Optional containing the nearest NPC with the action within distance - */ - public static Optional findNearestWithActionWithinDistance(String action, int distance) { - return findClosest(npc -> hasAction(action).test(npc) && npc.isWithinDistanceFromPlayer(distance)); - } - - /** - * Gets the first available action from a list of possible actions for an NPC. - * Equivalent to Rs2Npc.getAvailableAction(). - * - * @param npc The NPC to check - * @param possibleActions List of actions to check for - * @return The first available action, or null if none found - */ - public static String getAvailableAction(Rs2NpcModel npc, List possibleActions) { - if (npc == null || possibleActions == null) return null; - - try { - NPCComposition baseComposition = npc.getComposition(); - NPCComposition transformedComposition = npc.getTransformedComposition(); - - List baseActions = baseComposition != null ? - Arrays.asList(baseComposition.getActions()) : Collections.emptyList(); - List transformedActions = transformedComposition != null ? - Arrays.asList(transformedComposition.getActions()) : Collections.emptyList(); - - for (String action : possibleActions) { - if (baseActions.contains(action) || transformedActions.contains(action)) { - return action; - } - } - return null; - } catch (Exception e) { - return null; - } - } - - // ============================================ - // Specialized NPC Type Utilities - // ============================================ - - /** - * Finds pet NPCs (NPCs with "Dismiss" action that are interacting with player). - * Equivalent to Rs2Npc pet detection logic. - * - * @return Stream of pet NPCs - */ - public static Stream findPets() { - return findAll(npc -> { - try { - NPCComposition npcComposition = npc.getComposition(); - if (npcComposition == null) return false; - - List npcActions = Arrays.asList(npcComposition.getActions()); - if (npcActions.isEmpty()) return false; - - return npcActions.contains("Dismiss") && - Objects.equals(npc.getInteracting(), Microbot.getClient().getLocalPlayer()); - } catch (Exception e) { - return false; - } - }); - } - - /** - * Finds the closest pet NPC. - * - * @return Optional containing the closest pet NPC - */ - public static Optional findClosestPet() { - return findClosest(npc -> { - try { - NPCComposition npcComposition = npc.getComposition(); - if (npcComposition == null) return false; - - List npcActions = Arrays.asList(npcComposition.getActions()); - if (npcActions.isEmpty()) return false; - - return npcActions.contains("Dismiss") && - Objects.equals(npc.getInteracting(), Microbot.getClient().getLocalPlayer()); - } catch (Exception e) { - return false; - } - }); - } - - /** - * Finds bank NPCs (NPCs with "Bank" action). - * Equivalent to Rs2Npc bank detection logic. - * - * @return Stream of bank NPCs - */ - public static Stream findBankNpcs() { - return findWithAction("Bank"); - } - - /** - * Finds the closest bank NPC. - * - * @return Optional containing the closest bank NPC - */ - public static Optional findClosestBankNpc() { - return findNearestWithAction("Bank"); - } - - /** - * Finds shop NPCs (NPCs with "Trade" action). - * - * @return Stream of shop NPCs - */ - public static Stream findShopNpcs() { - return findWithAction("Trade"); - } - - /** - * Finds the closest shop NPC. - * - * @return Optional containing the closest shop NPC - */ - public static Optional findClosestShopNpc() { - return findNearestWithAction("Trade"); - } - - // ============================================ - // Cache-Based NPC Retrieval Methods - // ============================================ - - /** - * Gets an NPC by its index. - * - * @param index The NPC index - * @return Optional containing the NPC model if found - */ - public static Optional getByIndex(int index) { - try { - return Rs2NpcCache.getNpcByIndex(index); - } catch (Exception e) { - return Optional.empty(); - } - } - - /** - * Gets NPCs by their game ID. - * - * @param npcId The NPC ID - * @return Stream of matching NPCs - */ - public static Stream getById(int npcId) { - try { - return Rs2NpcCache.getNpcsById(npcId); - } catch (Exception e) { - return Stream.empty(); - } - } - - /** - * Gets the first NPC matching the criteria. - * - * @param npcId The NPC ID - * @return Optional containing the first matching NPC - */ - public static Optional getFirst(int npcId) { - try { - return Rs2NpcCache.getFirstNpcById(npcId); - } catch (Exception e) { - return Optional.empty(); - } - } - - /** - * Gets all cached NPCs. - * - * @return Stream of all NPCs - */ - public static Stream getAll() { - try { - return Rs2NpcCache.getAllNpcs(); - } catch (Exception e) { - return Stream.empty(); - } - } - - // Advanced cache-based finding utilities - - /** - * Finds the first NPC matching a predicate. - * - * @param predicate The predicate to match - * @return Optional containing the first matching NPC - */ - public static Optional find(Predicate predicate) { - try { - return Rs2NpcCache.getAllNpcs() - .filter(predicate) - .findFirst(); - } catch (Exception e) { - return Optional.empty(); - } - } - - /** - * Finds all NPCs matching a predicate. - * - * @param predicate The predicate to match - * @return Stream of matching NPCs - */ - public static Stream findAll(Predicate predicate) { - try { - return Rs2NpcCache.getAllNpcs().filter(predicate); - } catch (Exception e) { - return Stream.empty(); - } - } - - /** - * Finds the closest NPC matching a predicate. - * - * @param predicate The predicate to match - * @return Optional containing the closest matching NPC - */ - public static Optional findClosest(Predicate predicate) { - try { - return Rs2NpcCache.getAllNpcs() - .filter(predicate) - .min(Comparator.comparingInt(Rs2NpcModel::getDistanceFromPlayer)); - } catch (Exception e) { - return Optional.empty(); - } - } - - /** - * Finds the first NPC within distance from player by ID. - * - * @param npcId The NPC ID - * @param distance Maximum distance in tiles - * @return Optional containing the first matching NPC within distance - */ - public static Optional findWithinDistance(int npcId, int distance) { - return find(npc -> npc.getId() == npcId && npc.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds all NPCs within distance from player by ID. - * - * @param npcId The NPC ID - * @param distance Maximum distance in tiles - * @return Stream of matching NPCs within distance - */ - public static Stream findAllWithinDistance(int npcId, int distance) { - return findAll(npc -> npc.getId() == npcId && npc.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest NPC by ID within distance from player. - * - * @param npcId The NPC ID - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching NPC within distance - */ - public static Optional findClosestWithinDistance(int npcId, int distance) { - return findClosest(npc -> npc.getId() == npcId && npc.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds NPCs within distance from an anchor point. - * - * @param predicate The predicate to match - * @param anchor The anchor point - * @param distance Maximum distance in tiles - * @return Stream of matching NPCs within distance from anchor - */ - public static Stream findWithinDistance(Predicate predicate, WorldPoint anchor, int distance) { - return findAll(npc -> predicate.test(npc) && npc.getWorldLocation().distanceTo(anchor) <= distance); - } - - /** - * Finds the closest NPC to an anchor point. - * - * @param predicate The predicate to match - * @param anchor The anchor point - * @return Optional containing the closest matching NPC to anchor - */ - public static Optional findClosest(Predicate predicate, WorldPoint anchor) { - return findAll(predicate) - .min(Comparator.comparingInt(npc -> npc.getWorldLocation().distanceTo(anchor))); - } - - /** - * Finds the closest NPC to an anchor point within distance. - * - * @param predicate The predicate to match - * @param anchor The anchor point - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching NPC to anchor within distance - */ - public static Optional findClosest(Predicate predicate, WorldPoint anchor, int distance) { - return findWithinDistance(predicate, anchor, distance) - .min(Comparator.comparingInt(npc -> npc.getWorldLocation().distanceTo(anchor))); - } - - // Name-based finding utilities - - /** - * Creates a predicate that matches NPCs whose name contains the given string (case-insensitive). - * - * @param npcName The name to match (partial or full) - * @param exact Whether to match exactly or contain - * @return Predicate for name matching - */ - public static Predicate nameMatches(String npcName, boolean exact) { - String lower = npcName.toLowerCase(); - return npc -> { - String name = npc.getName(); - if (name == null) return false; - return exact ? name.equalsIgnoreCase(npcName) : name.toLowerCase().contains(lower); - }; - } - - /** - * Creates a predicate that matches NPCs whose name contains the given string (case-insensitive). - * - * @param npcName The name to match (partial) - * @return Predicate for name matching - */ - public static Predicate nameMatches(String npcName) { - return nameMatches(npcName, false); - } - - /** - * Finds the first NPC by name. - * - * @param npcName The NPC name - * @param exact Whether to match exactly or contain - * @return Optional containing the first matching NPC - */ - public static Optional findByName(String npcName, boolean exact) { - return find(nameMatches(npcName, exact)); - } - - /** - * Finds the first NPC by name (partial match). - * - * @param npcName The NPC name - * @return Optional containing the first matching NPC - */ - public static Optional findByName(String npcName) { - return findByName(npcName, false); - } - - /** - * Finds the closest NPC by name. - * - * @param npcName The NPC name - * @param exact Whether to match exactly or contain - * @return Optional containing the closest matching NPC - */ - public static Optional findClosestByName(String npcName, boolean exact) { - return findClosest(nameMatches(npcName, exact)); - } - - /** - * Finds the closest NPC by name (partial match). - * - * @param npcName The NPC name - * @return Optional containing the closest matching NPC - */ - public static Optional findClosestByName(String npcName) { - return findClosestByName(npcName, false); - } - - /** - * Finds NPCs by name within distance from player. - * - * @param npcName The NPC name - * @param exact Whether to match exactly or contain - * @param distance Maximum distance in tiles - * @return Stream of matching NPCs within distance - */ - public static Stream findByNameWithinDistance(String npcName, boolean exact, int distance) { - return findAll(npc -> nameMatches(npcName, exact).test(npc) && npc.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds NPCs by name within distance from player (partial match). - * - * @param npcName The NPC name - * @param distance Maximum distance in tiles - * @return Stream of matching NPCs within distance - */ - public static Stream findByNameWithinDistance(String npcName, int distance) { - return findByNameWithinDistance(npcName, false, distance); - } - - /** - * Finds the closest NPC by name within distance from player. - * - * @param npcName The NPC name - * @param exact Whether to match exactly or contain - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching NPC within distance - */ - public static Optional findClosestByNameWithinDistance(String npcName, boolean exact, int distance) { - return findClosest(npc -> nameMatches(npcName, exact).test(npc) && npc.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest NPC by name within distance from player (partial match). - * - * @param npcName The NPC name - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching NPC within distance - */ - public static Optional findClosestByNameWithinDistance(String npcName, int distance) { - return findClosestByNameWithinDistance(npcName, false, distance); - } - - // Array-based ID utilities - - /** - * Finds the first NPC matching any of the given IDs. - * - * @param npcIds Array of NPC IDs - * @return Optional containing the first matching NPC - */ - public static Optional findByIds(Integer[] npcIds) { - Set idSet = Set.of(npcIds); - return find(npc -> idSet.contains(npc.getId())); - } - - /** - * Finds the closest NPC matching any of the given IDs. - * - * @param npcIds Array of NPC IDs - * @return Optional containing the closest matching NPC - */ - public static Optional findClosestByIds(Integer[] npcIds) { - Set idSet = Set.of(npcIds); - return findClosest(npc -> idSet.contains(npc.getId())); - } - - /** - * Finds NPCs matching any of the given IDs within distance. - * - * @param npcIds Array of NPC IDs - * @param distance Maximum distance in tiles - * @return Stream of matching NPCs within distance - */ - public static Stream findByIdsWithinDistance(Integer[] npcIds, int distance) { - Set idSet = Set.of(npcIds); - return findAll(npc -> idSet.contains(npc.getId()) && npc.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest NPC matching any of the given IDs within distance. - * - * @param npcIds Array of NPC IDs - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching NPC within distance - */ - public static Optional findClosestByIdsWithinDistance(Integer[] npcIds, int distance) { - Set idSet = Set.of(npcIds); - return findClosest(npc -> idSet.contains(npc.getId()) && npc.isWithinDistanceFromPlayer(distance)); - } - - // Combat-specific utilities - - /** - * Finds attackable NPCs (combat level > 0, not dead). - * - * @return Stream of attackable NPCs - */ - public static Stream findAttackable() { - return findAll(npc -> npc.getCombatLevel() > 0 && !npc.isDead()); - } - - /** - * Finds the closest attackable NPC. - * - * @return Optional containing the closest attackable NPC - */ - public static Optional findClosestAttackable() { - return findClosest(npc -> npc.getCombatLevel() > 0 && !npc.isDead()); - } - - /** - * Finds attackable NPCs by name. - * - * @param npcName The NPC name - * @param exact Whether to match exactly or contain - * @return Stream of attackable NPCs matching the name - */ - public static Stream findAttackableByName(String npcName, boolean exact) { - return findAll(npc -> - npc.getCombatLevel() > 0 && - !npc.isDead() && - nameMatches(npcName, exact).test(npc)); - } - - /** - * Finds attackable NPCs by name (partial match). - * - * @param npcName The NPC name - * @return Stream of attackable NPCs matching the name - */ - public static Stream findAttackableByName(String npcName) { - return findAttackableByName(npcName, false); - } - - /** - * Finds the closest attackable NPC by name. - * - * @param npcName The NPC name - * @param exact Whether to match exactly or contain - * @return Optional containing the closest attackable NPC matching the name - */ - public static Optional findClosestAttackableByName(String npcName, boolean exact) { - return findClosest(npc -> - npc.getCombatLevel() > 0 && - !npc.isDead() && - nameMatches(npcName, exact).test(npc)); - } - - /** - * Finds the closest attackable NPC by name (partial match). - * - * @param npcName The NPC name - * @return Optional containing the closest attackable NPC matching the name - */ - public static Optional findClosestAttackableByName(String npcName) { - return findClosestAttackableByName(npcName, false); - } - - /** - * Finds attackable NPCs within distance. - * - * @param distance Maximum distance in tiles - * @return Stream of attackable NPCs within distance - */ - public static Stream findAttackableWithinDistance(int distance) { - return findAll(npc -> - npc.getCombatLevel() > 0 && - !npc.isDead() && - npc.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest attackable NPC within distance. - * - * @param distance Maximum distance in tiles - * @return Optional containing the closest attackable NPC within distance - */ - public static Optional findClosestAttackableWithinDistance(int distance) { - return findClosest(npc -> - npc.getCombatLevel() > 0 && - !npc.isDead() && - npc.isWithinDistanceFromPlayer(distance)); - } - - // Player interaction utilities - - /** - * Finds NPCs that are interacting with the player. - * - * @return Stream of NPCs interacting with the player - */ - public static Stream findInteractingWithPlayer() { - return findAll(npc -> npc.isInteractingWithPlayer()); - } - - /** - * Finds the closest NPC that is interacting with the player. - * - * @return Optional containing the closest NPC interacting with the player - */ - public static Optional findClosestInteractingWithPlayer() { - return findClosest(npc -> npc.isInteractingWithPlayer()); - } - - /** - * Finds NPCs that are not interacting with anyone. - * - * @return Stream of NPCs not interacting - */ - public static Stream findNotInteracting() { - return findAll(npc -> !npc.isInteracting()); - } - - /** - * Finds the closest NPC that is not interacting with anyone. - * - * @return Optional containing the closest non-interacting NPC - */ - public static Optional findClosestNotInteracting() { - return findClosest(npc -> !npc.isInteracting()); - } - - // Health and status utilities - - /** - * Finds NPCs with health below a certain percentage. - * - * @param healthPercentage Maximum health percentage (0-100) - * @return Stream of NPCs with health below the threshold - */ - public static Stream findWithHealthBelow(double healthPercentage) { - return findAll(npc -> npc.getHealthPercentage() < healthPercentage); - } - - /** - * Finds the closest NPC with health below a certain percentage. - * - * @param healthPercentage Maximum health percentage (0-100) - * @return Optional containing the closest NPC with health below the threshold - */ - public static Optional findClosestWithHealthBelow(double healthPercentage) { - return findClosest(npc -> npc.getHealthPercentage() < healthPercentage); - } - - /** - * Finds NPCs that are moving. - * - * @return Stream of moving NPCs - */ - public static Stream findMoving() { - return findAll(npc -> npc.isMoving()); - } - - /** - * Finds NPCs that are not moving (idle). - * - * @return Stream of idle NPCs - */ - public static Stream findIdle() { - return findAll(npc -> !npc.isMoving()); - } - - /** - * Finds the closest moving NPC. - * - * @return Optional containing the closest moving NPC - */ - public static Optional findClosestMoving() { - return findClosest(npc -> npc.isMoving()); - } - - /** - * Finds the closest idle NPC. - * - * @return Optional containing the closest idle NPC - */ - public static Optional findClosestIdle() { - return findClosest(npc -> !npc.isMoving()); - } - - // ============================================ - // Scene and Viewport Extraction Methods - // ============================================ - - /** - * Gets all NPCs currently in the scene (all cached NPCs). - * This includes NPCs that may not be visible in the current viewport. - * - * @return Stream of all NPCs in the scene - */ - public static Stream getAllInScene() { - return getAll(); - } - - /** - * Gets all NPCs currently visible in the viewport (on screen). - * Only includes NPCs that have a convex hull and are rendered. - * - * @return Stream of NPCs visible in viewport - */ - public static Stream getAllInViewport() { - return filterVisibleInViewport(getAll()); - } - - /** - * Gets all NPCs by ID that are currently visible in the viewport. - * - * @param npcId The NPC ID to filter by - * @return Stream of NPCs with the specified ID that are visible in viewport - */ - public static Stream getAllInViewport(int npcId) { - return filterVisibleInViewport(getById(npcId)); - } - - /** - * Gets the closest NPC in the viewport by ID. - * - * @param npcId The NPC ID - * @return Optional containing the closest NPC in viewport - */ - public static Optional getClosestInViewport(int npcId) { - return getAllInViewport(npcId) - .min(Comparator.comparingInt(Rs2NpcModel::getDistanceFromPlayer)); - } - - /** - * Gets all NPCs in the viewport that are interactable (within reasonable distance). - * - * @param maxDistance Maximum distance for interaction - * @return Stream of interactable NPCs in viewport - */ - public static Stream getAllInteractable(int maxDistance) { - return getAllInViewport() - .filter(npc -> isInteractable(npc, maxDistance)); - } - - /** - * Gets all NPCs by ID in the viewport that are interactable. - * - * @param npcId The NPC ID - * @param maxDistance Maximum distance for interaction - * @return Stream of interactable NPCs with the specified ID - */ - public static Stream getAllInteractable(int npcId, int maxDistance) { - return getAllInViewport(npcId) - .filter(npc -> isInteractable(npc, maxDistance)); - } - - /** - * Gets the closest interactable NPC by ID. - * - * @param npcId The NPC ID - * @param maxDistance Maximum distance for interaction - * @return Optional containing the closest interactable NPC - */ - public static Optional getClosestInteractable(int npcId, int maxDistance) { - return getAllInteractable(npcId, maxDistance) - .min(Comparator.comparingInt(Rs2NpcModel::getDistanceFromPlayer)); - } - - // ============================================ - // Line of Sight Methods - // ============================================ - - - - /** - * Finds NPCs that are in line of sight from a specific point. - * This is useful for finding NPCs that the player can see. - * - * @param from Starting world point - * @param maxDistance Maximum distance to search (in tiles) - * @return Stream of NPCs that are in line of sight - */ - public static Stream getNpcsInLineOfSight(WorldPoint from, int maxDistance) { - if (from == null) { - return Stream.empty(); - } - - int plane = from.getPlane(); - - return getAll() - .filter(npc -> { - WorldPoint npcLocation = npc.getWorldLocation(); - if (npcLocation == null || npcLocation.getPlane() != plane) { - return false; - } - - // Check distance first as it's a cheaper operation - int distance = from.distanceTo(npcLocation); - if (distance > maxDistance) { - return false; - } - - // Then check line of sight - return hasLineOfSight(from, npc); - }); - } - - /** - * Finds the nearest NPC in line of sight from a specific point. - * - * @param from Starting world point - * @param maxDistance Maximum distance to search (in tiles) - * @return Optional containing the nearest NPC in line of sight - */ - public static Optional getNearestNpcInLineOfSight(WorldPoint from, int maxDistance) { - return getNpcsInLineOfSight(from, maxDistance) - .min(Comparator.comparingInt(npc -> from.distanceTo(npc.getWorldLocation()))); - } - - /** - * Finds NPCs that match a predicate and are in line of sight from a specific point. - * - * @param from Starting world point - * @param maxDistance Maximum distance to search (in tiles) - * @param predicate Filter to apply to NPCs - * @return Stream of NPCs that match the predicate and are in line of sight - */ - public static Stream getNpcsInLineOfSight(WorldPoint from, int maxDistance, Predicate predicate) { - return getNpcsInLineOfSight(from, maxDistance).filter(predicate); - } - - // ============================================ - // Line of Sight Utilities - // ============================================ - - /** - * Checks if there is a line of sight between the player and an NPC. - * Uses RuneLite's WorldArea.hasLineOfSightTo for accurate scene collision detection. - * - * @param npc The NPC to check - * @return True if line of sight exists, false otherwise - */ - public static boolean hasLineOfSight(Rs2NpcModel npc) { - if (npc == null) return false; - - try { - // Get player's current world location and create a small area (1x1) - WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - return hasLineOfSight(playerLocation, npc); - } catch (Exception e) { - return false; - } - } - - /** - * Checks if there is a line of sight between a specific point and an NPC. - * - * @param point The world point to check from - * @param npc The NPC to check against - * @return True if line of sight exists, false otherwise - */ - public static boolean hasLineOfSight(WorldPoint point, Rs2NpcModel npc) { - if (npc == null || point == null) return false; - - try { - WorldPoint npcLocation = npc.getWorldLocation(); - - // Check same plane - if (point.getPlane() != npcLocation.getPlane()) { - return false; - } - - // Create WorldAreas for the point and NPC - int npcSize = npc.getComposition() != null ? npc.getComposition().getSize() : 1; - - return new WorldArea(npcLocation, npcSize, npcSize) - .hasLineOfSightTo( - Microbot.getClient().getTopLevelWorldView(), - new WorldArea(point, 1, 1)); - } catch (Exception e) { - return false; - } - } - - /** - * Gets all NPCs that have line of sight to the player. - * Useful for identifying potential threats or interactive NPCs. - * - * @return Stream of NPCs with line of sight to player - */ - public static Stream getNpcsWithLineOfSightToPlayer() { - return getAll().filter(Rs2NpcCacheUtils::hasLineOfSight); - } - - /** - * Gets all NPCs that have line of sight to a specific world point. - * - * @param point The world point to check from - * @return Stream of NPCs with line of sight to the point - */ - public static Stream getNpcsWithLineOfSightTo(WorldPoint point) { - return getAll().filter(npc -> hasLineOfSight(point, npc)); - } - - /** - * Gets all NPCs at a location that have line of sight to the player. - * - * @param worldPoint The world point to check at - * @param maxDistance Maximum distance from the world point - * @return Stream of NPCs at the location with line of sight - */ - public static Stream getNpcsAtLocationWithLineOfSight(WorldPoint worldPoint, int maxDistance) { - return getAll() - .filter(npc -> npc.getWorldLocation().distanceTo(worldPoint) <= maxDistance) - .filter(Rs2NpcCacheUtils::hasLineOfSight); - } - - // ============================================ - // Viewport Visibility and Interactability Utilities - // ============================================ - - /** - * Checks if an NPC is visible in the current viewport using convex hull detection. - * Uses client thread for safe access to NPC state. - * - * @param npc The NPC to check - * @return true if the NPC is visible on screen - */ - public static boolean isVisibleInViewport(Rs2NpcModel npc) { - try { - if (npc == null) { - return false; - } - - // Use client thread for safe access to convex hull - return Microbot.getClientThread().runOnClientThreadOptional(() -> - npc.getConvexHull() != null - ).orElse(false); - } catch (Exception e) { - return false; - } - } - - /** - * Checks if any entity with a location is within the viewport by checking canvas conversion. - * This is a generic method that can work with any entity that has a world location. - * Uses client thread for safe access to client state. - * - * @param worldPoint The world point to check - * @return true if the location is visible on screen - */ - public static boolean isLocationVisibleInViewport(net.runelite.api.coords.WorldPoint worldPoint) { - try { - if (worldPoint == null) { - return false; - } - - // Use client thread for safe access to client state - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - Client client = Microbot.getClient(); - if (client == null) { - return false; - } - - LocalPoint localPoint = LocalPoint.fromWorld(client.getTopLevelWorldView(), worldPoint); - if (localPoint == null) { - return false; - } - - net.runelite.api.Point canvasPoint = Perspective.localToCanvas(client, localPoint, client.getTopLevelWorldView().getPlane()); - return canvasPoint != null; - }).orElse(false); - } catch (Exception e) { - return false; - } - } - - /** - * Filters a stream of NPCs to only include those visible in viewport. - * - * @param npcStream Stream of NPCs to filter - * @return Stream of NPCs visible in viewport - */ - public static Stream filterVisibleInViewport(Stream npcStream) { - return npcStream.filter(Rs2NpcCacheUtils::isVisibleInViewport); - } - - /** - * Checks if an NPC is interactable (visible and within reasonable distance). - * - * @param npc The NPC to check - * @param maxDistance Maximum distance in tiles for interaction - * @return true if the NPC is interactable - */ - public static boolean isInteractable(Rs2NpcModel npc, int maxDistance) { - try { - if (npc == null) { - return false; - } - - // Check if visible in viewport first - if (!isVisibleInViewport(npc)) { - return false; - } - - // Check distance from player - return npc.getDistanceFromPlayer() <= maxDistance; - } catch (Exception e) { - return false; - } - } - - /** - * Checks if an entity at a world point is interactable (within reasonable distance and visible). - * Uses client thread for safe access to player location. - * - * @param worldPoint The world point to check - * @param maxDistance Maximum distance in tiles for interaction - * @return true if the location is potentially interactable - */ - public static boolean isInteractable(net.runelite.api.coords.WorldPoint worldPoint, int maxDistance) { - try { - if (worldPoint == null) { - return false; - } - - // Use client thread for safe access to player location - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - net.runelite.api.coords.WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - if (playerLocation.distanceTo(worldPoint) > maxDistance) { - return false; - } - - // Check if visible in viewport (already uses client thread internally) - return isLocationVisibleInViewport(worldPoint); - }).orElse(false); - } catch (Exception e) { - return false; - } - } - - // ============================================ - // Updated Existing Methods to Use Local Functions - // ============================================ - - /** - * Gets all NPCs visible in the viewport. - * - * @return Stream of NPCs visible in viewport - */ - public static Stream getVisibleInViewport() { - return filterVisibleInViewport(getAll()); - } - - /** - * Gets NPCs by ID that are visible in the viewport. - * - * @param npcId The NPC ID - * @return Stream of NPCs with the specified ID visible in viewport - */ - public static Stream getVisibleInViewportById(int npcId) { - return filterVisibleInViewport(getById(npcId)); - } - - /** - * Finds interactable NPCs by ID within distance from player. - * - * @param npcId The NPC ID - * @param maxDistance Maximum distance in tiles - * @return Stream of interactable NPCs with the specified ID - */ - public static Stream findInteractableById(int npcId, int maxDistance) { - return getById(npcId) - .filter(npc -> isInteractable(npc, maxDistance)); - } - - /** - * Finds interactable NPCs by name within distance from player. - * - * @param name The NPC name - * @param maxDistance Maximum distance in tiles - * @return Stream of interactable NPCs with the specified name - */ - public static Stream findInteractableByName(String name, int maxDistance) { - return findAll(nameMatches(name, false)) - .filter(npc -> isInteractable(npc, maxDistance)); - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2ObjectCacheUtils.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2ObjectCacheUtils.java deleted file mode 100644 index 65fb38650ff..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/cache/util/Rs2ObjectCacheUtils.java +++ /dev/null @@ -1,1423 +0,0 @@ -package net.runelite.client.plugins.microbot.util.cache.util; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Client; -import net.runelite.api.DecorativeObject; -import net.runelite.api.GameObject; -import net.runelite.api.GroundObject; -import net.runelite.api.Model; -import net.runelite.api.Perspective; -import net.runelite.api.Point; -import net.runelite.api.Renderable; -import net.runelite.api.TileObject; -import net.runelite.api.WallObject; -import net.runelite.api.coords.LocalPoint; -import net.runelite.api.coords.WorldArea; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.util.cache.Rs2ObjectCache; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2ObjectModel; -import net.runelite.client.plugins.microbot.Microbot; - -import java.awt.Rectangle; -import java.awt.Shape; -import java.util.Comparator; -import java.util.Optional; -import java.util.Set; -import java.util.function.Predicate; -import java.util.stream.Stream; - -/** - * Advanced cache-based utilities for game objects. - * Provides scene-independent methods for finding and filtering objects. - * - * This class offers high-performance object operations using cached data, - * avoiding the need to iterate through scene tiles. Supports all object types: - * GameObject, GroundObject, WallObject, and DecorativeObject. - * - * @author Vox - * @version 1.0 - */ -@Slf4j -public class Rs2ObjectCacheUtils { - - // ============================================ - // Core Cache Access Methods - // ============================================ - - /** - * Gets objects by their game ID. - * - * @param objectId The object ID - * @return Stream of matching objects - */ - public static Stream getByGameId(int objectId) { - try { - return Rs2ObjectCache.getInstance().stream() - .filter(obj -> obj.getId() == objectId); - } catch (Exception e) { - return Stream.empty(); - } - } - - /** - * Gets the first object matching the criteria. - * - * @param objectId The object ID - * @return Optional containing the first matching object - */ - public static Optional getFirst(int objectId) { - try { - return Rs2ObjectCache.getInstance().stream() - .filter(obj -> obj.getId() == objectId) - .findFirst(); - } catch (Exception e) { - return Optional.empty(); - } - } - - /** - * Gets the closest object to the player. - * - * @param objectId The object ID - * @return Optional containing the closest object - */ - public static Optional getClosest(int objectId) { - try { - return Rs2ObjectCache.getInstance().stream() - .filter(obj -> obj.getId() == objectId) - .min(Comparator.comparingInt(Rs2ObjectModel::getDistanceFromPlayer)); - } catch (Exception e) { - return Optional.empty(); - } - } - - /** - * Gets all cached objects. - * - * @return Stream of all objects - */ - public static Stream getAll() { - try { - return Rs2ObjectCache.getInstance().stream(); - } catch (Exception e) { - return Stream.empty(); - } - } - - // ============================================ - // Advanced Finding Methods - // ============================================ - - /** - * Finds the nearest object matching a predicate. - * - * @param predicate The predicate to match - * @return Optional containing the first matching object - */ - public static Optional findNearest(Predicate predicate) { - try { - return findClosest(predicate); - } catch (Exception e) { - return Optional.empty(); - } - } - - /** - * Finds all objects matching a predicate. - * - * @param predicate The predicate to match - * @return Stream of matching objects - */ - public static Stream findAll(Predicate predicate) { - try { - return Rs2ObjectCache.getInstance().stream().filter(predicate); - } catch (Exception e) { - return Stream.empty(); - } - } - - /** - * Finds the closest object matching a predicate. - * - * @param predicate The predicate to match - * @return Optional containing the closest matching object - */ - public static Optional findClosest(Predicate predicate) { - try { - return Rs2ObjectCache.getInstance().stream() - .filter(predicate) - .min(Comparator.comparingInt(Rs2ObjectModel::getDistanceFromPlayer)); - } catch (Exception e) { - return Optional.empty(); - } - } - - // ============================================ - // Distance-Based Finding Methods - // ============================================ - - /** - * Finds the first object within distance from player by ID. - * - * @param objectId The object ID - * @param distance Maximum distance in tiles - * @return Optional containing the first matching object within distance - */ - public static Optional findWithinDistance(int objectId, int distance) { - return findNearest(obj -> obj.getId() == objectId && obj.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds all objects within distance from player by ID. - * - * @param objectId The object ID - * @param distance Maximum distance in tiles - * @return Stream of matching objects within distance - */ - public static Stream findAllWithinDistance(int objectId, int distance) { - return findAll(obj -> obj.getId() == objectId && obj.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest object by ID within distance from player. - * - * @param objectId The object ID - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching object within distance - */ - public static Optional findClosestWithinDistance(int objectId, int distance) { - return findClosest(obj -> obj.getId() == objectId && obj.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds objects within distance from an anchor point. - * - * @param predicate The predicate to match - * @param anchor The anchor point - * @param distance Maximum distance in tiles - * @return Stream of matching objects within distance from anchor - */ - public static Stream findWithinDistance(Predicate predicate, WorldPoint anchor, int distance) { - return findAll(obj -> predicate.test(obj) && obj.getLocation().distanceTo(anchor) <= distance); - } - - /** - * Finds the closest object to an anchor point. - * - * @param predicate The predicate to match - * @param anchor The anchor point - * @return Optional containing the closest matching object to anchor - */ - public static Optional findClosest(Predicate predicate, WorldPoint anchor) { - return findAll(predicate) - .min(Comparator.comparingInt(obj -> obj.getLocation().distanceTo(anchor))); - } - - /** - * Finds the closest object to an anchor point within distance. - * - * @param predicate The predicate to match - * @param anchor The anchor point - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching object to anchor within distance - */ - public static Optional findClosest(Predicate predicate, WorldPoint anchor, int distance) { - return findWithinDistance(predicate, anchor, distance) - .min(Comparator.comparingInt(obj -> obj.getLocation().distanceTo(anchor))); - } - - // ============================================ - // Name-Based Finding Methods - // ============================================ - - /** - * Creates a predicate that matches objects whose name contains the given string (case-insensitive). - * - * @param objectName The name to match (partial or full) - * @param exact Whether to match exactly or contain - * @return Predicate for name matching - */ - public static Predicate nameMatches(String objectName, boolean exact) { - String lower = objectName.toLowerCase(); - return obj -> { - String objName = obj.getName(); - if (objName == null) return false; - return exact ? objName.equalsIgnoreCase(objectName) : objName.toLowerCase().contains(lower); - }; - } - - /** - * Creates a predicate that matches objects whose name contains the given string (case-insensitive). - * - * @param objectName The name to match (partial) - * @return Predicate for name matching - */ - public static Predicate nameMatches(String objectName) { - return nameMatches(objectName, false); - } - - /** - * Finds the first object by name. - * - * @param objectName The object name - * @param exact Whether to match exactly or contain - * @return Optional containing the first matching object - */ - public static Optional findNearestByName(String objectName, boolean exact) { - return findNearest(nameMatches(objectName, exact)); - } - - /** - * Finds the first object by name (partial match). - * - * @param objectName The object name - * @return Optional containing the first matching object - */ - public static Optional findNearestByName(String objectName) { - return findNearestByName(objectName, false); - } - - /** - * Finds the closest object by name. - * - * @param objectName The object name - * @param exact Whether to match exactly or contain - * @return Optional containing the closest matching object - */ - public static Optional findClosestByName(String objectName, boolean exact) { - return findClosest(nameMatches(objectName, exact)); - } - - /** - * Finds the closest object by name (partial match). - * - * @param objectName The object name - * @return Optional containing the closest matching object - */ - public static Optional findClosestByName(String objectName) { - return findClosestByName(objectName, false); - } - - /** - * Finds objects by name within distance from player. - * - * @param objectName The object name - * @param exact Whether to match exactly or contain - * @param distance Maximum distance in tiles - * @return Stream of matching objects within distance - */ - public static Stream findByNameWithinDistance(String objectName, boolean exact, int distance) { - return findAll(obj -> nameMatches(objectName, exact).test(obj) && obj.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds objects by name within distance from player (partial match). - * - * @param objectName The object name - * @param distance Maximum distance in tiles - * @return Stream of matching objects within distance - */ - public static Stream findByNameWithinDistance(String objectName, int distance) { - return findByNameWithinDistance(objectName, false, distance); - } - - /** - * Finds the closest object by name within distance from player. - * - * @param objectName The object name - * @param exact Whether to match exactly or contain - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching object within distance - */ - public static Optional findClosestByNameWithinDistance(String objectName, boolean exact, int distance) { - return findClosest(obj -> nameMatches(objectName, exact).test(obj) && obj.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest object by name within distance from player (partial match). - * - * @param objectName The object name - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching object within distance - */ - public static Optional findClosestByNameWithinDistance(String objectName, int distance) { - return findClosestByNameWithinDistance(objectName, false, distance); - } - - // ============================================ - // Array-Based ID Methods - // ============================================ - - /** - * Finds the first object matching any of the given IDs. - * - * @param objectIds Array of object IDs - * @return Optional containing the first matching object - */ - public static Optional findNearestByIds(Integer[] objectIds) { - Set idSet = Set.of(objectIds); - return findClosest(obj -> idSet.contains(obj.getId())); - } - - /** - * Finds the closest object matching any of the given IDs. - * - * @param objectIds Array of object IDs - * @return Optional containing the closest matching object - */ - public static Optional findClosestByIds(Integer[] objectIds) { - Set idSet = Set.of(objectIds); - return findClosest(obj -> idSet.contains(obj.getId())); - } - - /** - * Finds objects matching any of the given IDs within distance. - * - * @param objectIds Array of object IDs - * @param distance Maximum distance in tiles - * @return Stream of matching objects within distance - */ - public static Stream findByIdsWithinDistance(Integer[] objectIds, int distance) { - Set idSet = Set.of(objectIds); - return findAll(obj -> idSet.contains(obj.getId()) && obj.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest object matching any of the given IDs within distance. - * - * @param objectIds Array of object IDs - * @param distance Maximum distance in tiles - * @return Optional containing the closest matching object within distance - */ - public static Optional findClosestByIdsWithinDistance(Integer[] objectIds, int distance) { - Set idSet = Set.of(objectIds); - return findClosest(obj -> idSet.contains(obj.getId()) && obj.isWithinDistanceFromPlayer(distance)); - } - - // ============================================ - // Type-Specific Finding Methods - // ============================================ - - /** - * Finds objects of a specific type. - * - * @param objectType The object type to find - * @return Stream of objects of the specified type - */ - public static Stream findByType(Rs2ObjectModel.ObjectType objectType) { - return findAll(obj -> obj.getObjectType() == objectType); - } - - /** - * Finds the closest object of a specific type. - * - * @param objectType The object type to find - * @return Optional containing the closest object of the specified type - */ - public static Optional findClosestByType(Rs2ObjectModel.ObjectType objectType) { - return findClosest(obj -> obj.getObjectType() == objectType); - } - - /** - * Finds objects of a specific type within distance. - * - * @param objectType The object type to find - * @param distance Maximum distance in tiles - * @return Stream of objects of the specified type within distance - */ - public static Stream findByTypeWithinDistance(Rs2ObjectModel.ObjectType objectType, int distance) { - return findAll(obj -> obj.getObjectType() == objectType && obj.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest object of a specific type within distance. - * - * @param objectType The object type to find - * @param distance Maximum distance in tiles - * @return Optional containing the closest object of the specified type within distance - */ - public static Optional findClosestByTypeWithinDistance(Rs2ObjectModel.ObjectType objectType, int distance) { - return findClosest(obj -> obj.getObjectType() == objectType && obj.isWithinDistanceFromPlayer(distance)); - } - - // ============================================ - // Convenience Methods for Specific Object Types - // ============================================ - - /** - * Finds GameObjects only. - * - * @return Stream of GameObjects - */ - public static Stream findGameObjects() { - return findByType(Rs2ObjectModel.ObjectType.GAME_OBJECT); - } - - /** - * Finds the closest GameObject. - * - * @return Optional containing the closest GameObject - */ - public static Optional findClosestGameObject() { - return findClosestByType(Rs2ObjectModel.ObjectType.GAME_OBJECT); - } - - /** - * Finds GameObjects within distance. - * - * @param distance Maximum distance in tiles - * @return Stream of GameObjects within distance - */ - public static Stream findGameObjectsWithinDistance(int distance) { - return findByTypeWithinDistance(Rs2ObjectModel.ObjectType.GAME_OBJECT, distance); - } - - /** - * Finds GroundObjects only. - * - * @return Stream of GroundObjects - */ - public static Stream findGroundObjects() { - return findByType(Rs2ObjectModel.ObjectType.GROUND_OBJECT); - } - - /** - * Finds the closest GroundObject. - * - * @return Optional containing the closest GroundObject - */ - public static Optional findClosestGroundObject() { - return findClosestByType(Rs2ObjectModel.ObjectType.GROUND_OBJECT); - } - - /** - * Finds GroundObjects within distance. - * - * @param distance Maximum distance in tiles - * @return Stream of GroundObjects within distance - */ - public static Stream findGroundObjectsWithinDistance(int distance) { - return findByTypeWithinDistance(Rs2ObjectModel.ObjectType.GROUND_OBJECT, distance); - } - - /** - * Finds WallObjects only. - * - * @return Stream of WallObjects - */ - public static Stream findWallObjects() { - return findByType(Rs2ObjectModel.ObjectType.WALL_OBJECT); - } - - /** - * Finds the closest WallObject. - * - * @return Optional containing the closest WallObject - */ - public static Optional findClosestWallObject() { - return findClosestByType(Rs2ObjectModel.ObjectType.WALL_OBJECT); - } - - /** - * Finds WallObjects within distance. - * - * @param distance Maximum distance in tiles - * @return Stream of WallObjects within distance - */ - public static Stream findWallObjectsWithinDistance(int distance) { - return findByTypeWithinDistance(Rs2ObjectModel.ObjectType.WALL_OBJECT, distance); - } - - /** - * Finds DecorativeObjects only. - * - * @return Stream of DecorativeObjects - */ - public static Stream findDecorativeObjects() { - return findByType(Rs2ObjectModel.ObjectType.DECORATIVE_OBJECT); - } - - /** - * Finds the closest DecorativeObject. - * - * @return Optional containing the closest DecorativeObject - */ - public static Optional findClosestDecorativeObject() { - return findClosestByType(Rs2ObjectModel.ObjectType.DECORATIVE_OBJECT); - } - - /** - * Finds DecorativeObjects within distance. - * - * @param distance Maximum distance in tiles - * @return Stream of DecorativeObjects within distance - */ - public static Stream findDecorativeObjectsWithinDistance(int distance) { - return findByTypeWithinDistance(Rs2ObjectModel.ObjectType.DECORATIVE_OBJECT, distance); - } - - // ============================================ - // Action-Based Finding Methods - // ============================================ - - /** - * Finds objects that have a specific action. - * - * @param action The action to look for - * @return Stream of objects with the specified action - */ - public static Stream findByAction(String action) { - return findAll(obj -> obj.hasAction(action)); - } - - /** - * Finds the closest object that has a specific action. - * - * @param action The action to look for - * @return Optional containing the closest object with the specified action - */ - public static Optional findClosestByAction(String action) { - return findClosest(obj -> obj.hasAction(action)); - } - - /** - * Finds objects with a specific action within distance. - * - * @param action The action to look for - * @param distance Maximum distance in tiles - * @return Stream of objects with the specified action within distance - */ - public static Stream findByActionWithinDistance(String action, int distance) { - return findAll(obj -> obj.hasAction(action) && obj.isWithinDistanceFromPlayer(distance)); - } - - /** - * Finds the closest object with a specific action within distance. - * - * @param action The action to look for - * @param distance Maximum distance in tiles - * @return Optional containing the closest object with the specified action within distance - */ - public static Optional findClosestByActionWithinDistance(String action, int distance) { - return findClosest(obj -> obj.hasAction(action) && obj.isWithinDistanceFromPlayer(distance)); - } - - // ============================================ - // Size-Based Finding Methods - // ============================================ - - /** - * Finds objects with a specific width. - * - * @param width The width to match - * @return Stream of objects with the specified width - */ - public static Stream findByWidth(int width) { - return findAll(obj -> obj.getWidth() == width); - } - - /** - * Finds objects with a specific height. - * - * @param height The height to match - * @return Stream of objects with the specified height - */ - public static Stream findByHeight(int height) { - return findAll(obj -> obj.getHeight() == height); - } - - /** - * Finds objects with specific dimensions. - * - * @param width The width to match - * @param height The height to match - * @return Stream of objects with the specified dimensions - */ - public static Stream findBySize(int width, int height) { - return findAll(obj -> obj.getWidth() == width && obj.getHeight() == height); - } - - /** - * Finds large objects (width or height > 1). - * - * @return Stream of large objects - */ - public static Stream findLargeObjects() { - return findAll(obj -> obj.getWidth() > 1 || obj.getHeight() > 1); - } - - /** - * Finds single-tile objects (width and height = 1). - * - * @return Stream of single-tile objects - */ - public static Stream findSingleTileObjects() { - return findAll(obj -> obj.getWidth() == 1 && obj.getHeight() == 1); - } - - // ============================================ - // Property-Based Finding Methods - // ============================================ - - /** - * Finds solid objects (objects that block movement). - * - * @return Stream of solid objects - */ - public static Stream findSolidObjects() { - return findAll(Rs2ObjectModel::isSolid); - } - - /** - * Finds non-solid objects (objects that don't block movement). - * - * @return Stream of non-solid objects - */ - public static Stream findNonSolidObjects() { - return findAll(obj -> !obj.isSolid()); - } - - // ============================================ - // Age-Based Finding Methods - // ============================================ - - /** - * Finds objects that have been cached for at least the specified number of ticks. - * - * @param minTicks Minimum ticks since cache creation - * @return Stream of objects aged at least minTicks - */ - public static Stream findByMinAge(int minTicks) { - return findAll(obj -> obj.getTicksSinceCreation() >= minTicks); - } - - /** - * Finds objects that have been cached for less than the specified number of ticks. - * - * @param maxTicks Maximum ticks since cache creation - * @return Stream of fresh objects - */ - public static Stream findFresh(int maxTicks) { - return findAll(obj -> obj.getTicksSinceCreation() <= maxTicks); - } - - /** - * Finds the oldest cached object. - * - * @return Optional containing the oldest cached object - */ - public static Optional findOldest() { - return getAll().max(Comparator.comparingInt(Rs2ObjectModel::getTicksSinceCreation)); - } - - /** - * Finds the newest cached object. - * - * @return Optional containing the newest cached object - */ - public static Optional findNewest() { - return getAll().min(Comparator.comparingInt(Rs2ObjectModel::getTicksSinceCreation)); - } - - - - - // ============================================ - // Scene and Viewport Extraction Methods - // ============================================ - - /** - * Gets all objects currently in the scene (all cached objects). - * This includes objects that may not be visible in the current viewport. - * - * @return Stream of all objects in the scene - */ - public static Stream getAllInScene() { - return getAll(); - } - - /** - * Gets all objects currently visible in the viewport (on screen). - * Only includes objects that have a convex hull and are rendered. - * - * @return Stream of objects visible in viewport - */ - public static Stream getAllInViewport() { - return filterVisibleInViewport(getAll()); - } - - /** - * Gets all objects by ID that are currently visible in the viewport. - * - * @param objectId The object ID to filter by - * @return Stream of objects with the specified ID that are visible in viewport - */ - public static Stream getAllInViewport(int objectId) { - return filterVisibleInViewport(getByGameId(objectId)); - } - - /** - * Gets the closest object in the viewport by ID. - * - * @param objectId The object ID - * @return Optional containing the closest object in viewport - */ - public static Optional getClosestInViewport(int objectId) { - return getAllInViewport(objectId) - .min(Comparator.comparingInt(Rs2ObjectModel::getDistanceFromPlayer)); - } - - /** - * Gets all objects in the viewport that are interactable (within reasonable distance). - * - * @param maxDistance Maximum distance for interaction - * @return Stream of interactable objects in viewport - */ - public static Stream getAllInteractable(int maxDistance) { - return getAllInViewport() - .filter(obj -> isInteractable(obj, maxDistance)); - } - - /** - * Gets all objects by ID in the viewport that are interactable. - * - * @param objectId The object ID - * @param maxDistance Maximum distance for interaction - * @return Stream of interactable objects with the specified ID - */ - public static Stream getAllInteractable(int objectId, int maxDistance) { - return getAllInViewport(objectId) - .filter(obj -> isInteractable(obj, maxDistance)); - } - - /** - * Gets the closest interactable object by ID. - * - * @param objectId The object ID - * @param maxDistance Maximum distance for interaction - * @return Optional containing the closest interactable object - */ - public static Optional getClosestInteractable(int objectId, int maxDistance) { - return getAllInteractable(objectId, maxDistance) - .min(Comparator.comparingInt(Rs2ObjectModel::getDistanceFromPlayer)); - } - - // ============================================ - // Line of Sight Methods - // ============================================ - - - /** - * Finds objects that intersect the line of sight between two points. - * This is useful for determining what objects are blocking visibility. - * - * @param from Starting world point - * @param to Destination world point - * @return Stream of objects that might block line of sight - */ - public static Stream getObjectsInLineOfSight(WorldPoint from, WorldPoint to) { - if (from == null || to == null || from.getPlane() != to.getPlane()) { - return Stream.empty(); - } - - // Calculate the bounding box that contains both points - int minX = Math.min(from.getX(), to.getX()); - int maxX = Math.max(from.getX(), to.getX()); - int minY = Math.min(from.getY(), to.getY()); - int maxY = Math.max(from.getY(), to.getY()); - int plane = from.getPlane(); - - // Add a small buffer to ensure we catch all relevant objects - int buffer = 3; - - // Get all objects in the bounding box region - return getAll() - .filter(obj -> { - WorldPoint objPoint = obj.getWorldLocation(); - if (objPoint == null) return false; - - // Check if in bounding box with buffer - return objPoint.getPlane() == plane && - objPoint.getX() >= (minX - buffer) && objPoint.getX() <= (maxX + buffer) && - objPoint.getY() >= (minY - buffer) && objPoint.getY() <= (maxY + buffer); - }) - // Filter to objects that actually intersect the line - .filter(obj -> intersectsLine(obj, from, to)); - } - - /** - * Determines if an object intersects the line between two points. - * Uses the object's size and position to check for intersection. - * - * @param obj The object to check - * @param from Starting world point - * @param to Destination world point - * @return true if the object intersects the line - */ - private static boolean intersectsLine(Rs2ObjectModel obj, WorldPoint from, WorldPoint to) { - // For simplicity, consider the object as a rectangle/square - // and check if the line intersects this rectangle - WorldPoint objLocation = obj.getLocation(); - if (objLocation == null) return false; - - // Get object dimensions (default to 1x1) - int sizeX = 1; - int sizeY = 1; - - // Try to get actual dimensions if it's a GameObject - if (obj.getObjectType() == Rs2ObjectModel.ObjectType.GAME_OBJECT && obj.getTileObject() instanceof GameObject) { - GameObject gameObject = (GameObject) obj.getTileObject(); - sizeX = gameObject.getSceneMinLocation().distanceTo(gameObject.getSceneMaxLocation()) + 1; - sizeY = sizeX; // Assume square for simplicity - } - - // Create a WorldArea representing the object - net.runelite.api.coords.WorldArea objArea = new net.runelite.api.coords.WorldArea(objLocation, sizeX, sizeY); - - // Use the line-of-sight method to check if this area blocks the line - // First check if from point has line of sight to object - boolean fromToObjLOS = new net.runelite.api.coords.WorldArea(from, 1, 1) - .hasLineOfSightTo(net.runelite.client.plugins.microbot.Microbot.getClient().getTopLevelWorldView(), objArea); - - // Then check if object has line of sight to destination - boolean objToToLOS = objArea - .hasLineOfSightTo(net.runelite.client.plugins.microbot.Microbot.getClient().getTopLevelWorldView(), - new net.runelite.api.coords.WorldArea(to, 1, 1)); - - // Object intersects line if both checks pass - return fromToObjLOS && objToToLOS; - } - - // ============================================ - // Line of Sight Utilities - // ============================================ - - /** - * Checks if there is a line of sight between the player and a game object. - * Uses RuneLite's WorldArea.hasLineOfSightTo for accurate scene collision detection. - * - * @param object The object to check - * @return True if line of sight exists, false otherwise - */ - public static boolean hasLineOfSight(Rs2ObjectModel object) { - if (object == null) return false; - - try { - // Get player's current world location and create a small area (1x1) - WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - WorldPoint objectLocation = object.getLocation(); - - // Check same plane - if (playerLocation.getPlane() != objectLocation.getPlane()) { - return false; - } - - // For GameObjects, use the actual size of the object - if (object.getObjectType() == Rs2ObjectModel.ObjectType.GAME_OBJECT && - object.getTileObject() instanceof GameObject) { - GameObject gameObject = (GameObject) object.getTileObject(); - - return new WorldArea( - objectLocation, - gameObject.sizeX(), - gameObject.sizeY()) - .hasLineOfSightTo( - Microbot.getClient().getTopLevelWorldView(), - new WorldArea(playerLocation, 1, 1)); - } else { - // For other objects, use 1x1 area as default - return new WorldArea(objectLocation, 1, 1) - .hasLineOfSightTo( - Microbot.getClient().getTopLevelWorldView(), - new WorldArea(playerLocation, 1, 1)); - } - } catch (Exception e) { - return false; - } - } - - /** - * Checks if there is a line of sight between a specific point and a game object. - * - * @param point The world point to check from - * @param object The object to check against - * @return True if line of sight exists, false otherwise - */ - public static boolean hasLineOfSight(WorldPoint point, Rs2ObjectModel object) { - if (object == null || point == null) return false; - - try { - WorldPoint objectLocation = object.getLocation(); - - // Check same plane - if (point.getPlane() != objectLocation.getPlane()) { - return false; - } - - // For GameObjects, use the actual size of the object - if (object.getObjectType() == Rs2ObjectModel.ObjectType.GAME_OBJECT && - object.getTileObject() instanceof GameObject) { - GameObject gameObject = (GameObject) object.getTileObject(); - - return new WorldArea( - objectLocation, - gameObject.sizeX(), - gameObject.sizeY()) - .hasLineOfSightTo( - Microbot.getClient().getTopLevelWorldView(), - new WorldArea(point, 1, 1)); - } else { - // For other objects, use 1x1 area as default - return new WorldArea(objectLocation, 1, 1) - .hasLineOfSightTo( - Microbot.getClient().getTopLevelWorldView(), - new WorldArea(point, 1, 1)); - } - } catch (Exception e) { - return false; - } - } - - /** - * Gets all objects that have line of sight to the player. - * Useful for identifying interactive game objects. - * - * @return Stream of objects with line of sight to player - */ - public static Stream getObjectsWithLineOfSightToPlayer() { - return getAll().filter(Rs2ObjectCacheUtils::hasLineOfSight); - } - - /** - * Gets all objects that have line of sight to a specific world point. - * - * @param point The world point to check from - * @return Stream of objects with line of sight to the point - */ - public static Stream getObjectsWithLineOfSightTo(WorldPoint point) { - return getAll().filter(object -> hasLineOfSight(point, object)); - } - - /** - * Gets all objects at a location that have line of sight to the player. - * - * @param worldPoint The world point to check at - * @param maxDistance Maximum distance from the world point - * @return Stream of objects at the location with line of sight - */ - public static Stream getObjectsAtLocationWithLineOfSight(WorldPoint worldPoint, int maxDistance) { - return getAll() - .filter(object -> object.getLocation().distanceTo(worldPoint) <= maxDistance) - .filter(Rs2ObjectCacheUtils::hasLineOfSight); - } - - // ============================================ - // Viewport Visibility and Interactability Utilities - // ============================================ - - /** - * Checks if any type of game object is visible in the current viewport using enhanced detection. - * Supports all TileObject subtypes: GameObject, WallObject, GroundObject, DecorativeObject. - * Uses client thread for safe access to viewport bounds, canvas coordinates, and object rendering data. - * Includes staleness detection to prevent NPEs from invalid cached objects. - * - * @param objectModel The object model to check (supports all object types) - * @return true if the object is visible on screen - */ - public static boolean isVisibleInViewport(Rs2ObjectModel objectModel) { - try { - if (objectModel == null || objectModel.getTileObject() == null) { - return false; - } - - TileObject tileObject = objectModel.getTileObject(); - - - // Use client thread for safe access to viewport and canvas coordinates - Boolean result = Microbot.getClientThread().runOnClientThreadOptional(() -> { - Client client = Microbot.getClient(); - if (client == null) { - return false; - } - - try { - // Get canvas location (screen coordinates) for the object - Point canvasLocation = tileObject.getCanvasLocation(); - if (canvasLocation == null) { - //return false; // Object is not visible (behind camera, too far, etc.) - } - - // Check if canvas coordinates are within viewport bounds - if (!isPointInViewport(client, canvasLocation)) { - //return false; // Object is outside visible screen area - } - - // Enhanced visibility checks for better accuracy - // First check convex hull (fast geometric check) - Shape hull = getObjectConvexHull(tileObject); - if (hull == null) { - // If no hull available, the basic canvas location check is sufficient - return true; - } - - // Check if convex hull intersects with viewport - Rectangle viewportBounds = getViewportBounds(client); - if (!hull.intersects(viewportBounds)) { - //return false; // Hull doesn't intersect viewport - } - - // For maximum accuracy, check model visibility (optional enhanced check) - return isObjectModelVisible(tileObject); - } catch (NullPointerException | IllegalStateException e) { - // Object became stale during processing - return false; - } catch (Exception e) { - // Other errors should default to not visible - return false; - } - }).orElse(false); - - return result; - - } catch (Exception e) { - // Log error for debugging but don't spam logs - if (Math.random() < 0.001) { // Log ~0.1% of errors to avoid spam - log.info("Error checking viewport visibility for object {}: {}", - objectModel.getId(), e.getMessage()); - } - return false; - } - } - - /** - * Checks if a canvas point is within the current viewport bounds. - * - * @param client The game client - * @param point The canvas point to check - * @return true if the point is within viewport bounds - */ - private static boolean isPointInViewport(Client client, Point point) { - if (point == null) { - return false; - } - - int viewportX = client.getViewportXOffset(); - int viewportY = client.getViewportYOffset(); - int viewportWidth = client.getViewportWidth(); - int viewportHeight = client.getViewportHeight(); - - return point.getX() >= viewportX && - point.getX() <= (viewportX + viewportWidth) && - point.getY() >= viewportY && - point.getY() <= (viewportY + viewportHeight); - } - - /** - * Gets the viewport bounds as a Rectangle. - * - * @param client The game client - * @return Rectangle representing the viewport bounds - */ - private static Rectangle getViewportBounds(Client client) { - return new Rectangle( - client.getViewportXOffset(), - client.getViewportYOffset(), - client.getViewportWidth(), - client.getViewportHeight() - ); - } - - /** - * Gets the convex hull for any type of tile object. - * Based on ObjectIndicatorsOverlay and VisibilityHelper patterns. - * Includes null safety checks to prevent stale object references. - * - * @param tileObject The tile object - * @return The convex hull shape, or null if not visible or object is stale - */ - public static Shape getObjectConvexHull(Object tileObject) { - if(tileObject == null) { - return null; // No object to check - } - - try { - if (tileObject instanceof GameObject) { - GameObject gameObject = (GameObject) tileObject; - // Check if the object is still valid before accessing convex hull - - return gameObject.getConvexHull(); - } else if (tileObject instanceof GroundObject) { - GroundObject groundObject = (GroundObject) tileObject; - - return groundObject.getConvexHull(); - } else if (tileObject instanceof DecorativeObject) { - DecorativeObject decorativeObject = (DecorativeObject) tileObject; - - return decorativeObject.getConvexHull(); - } else if (tileObject instanceof WallObject) { - WallObject wallObject = (WallObject) tileObject; - - return wallObject.getConvexHull(); - } else if (tileObject instanceof TileObject) { - TileObject tileObj = (TileObject) tileObject; - - return tileObj.getCanvasTilePoly(); - } - } catch (NullPointerException | IllegalStateException e) { - // Object has become stale/invalid - this is expected behavior in a cache system - log.info("Stale object detected in convex hull calculation: {}", e.getMessage()); - return null; - } catch (Exception e) { - // Log unexpected errors for debugging - log.warn("Unexpected error getting convex hull: {}", e.getMessage()); - return null; - } - return null; - } - - /** - * Checks if an object's model has visible triangles (enhanced visibility check). - * Based on VisibilityHelper approach - checks model triangle transparency. - * Includes safety checks for stale objects. - * - * @param tileObject The tile object to check - * @return true if the object has visible model triangles - */ - public static boolean isObjectModelVisible(Object tileObject) { - try { - if (tileObject instanceof TileObject) { - return false; - } - return checkObjectModelVisibility(tileObject); - } catch (NullPointerException | IllegalStateException e) { - // Object is stale - return false; - } catch (Exception e) { - return true; // Default to visible on unexpected error - } - } - - /** - * Performs the actual model visibility check for different object types. - * Includes defensive checks for stale objects. - * - * @param tileObject The tile object to check - * @return true if visible triangles are found - */ - private static boolean checkObjectModelVisibility(Object tileObject) { - try { - if (tileObject instanceof GameObject) { - GameObject gameObject = (GameObject) tileObject; - Model model = extractModel(gameObject.getRenderable()); - return modelHasVisibleTriangles(model); - } else if (tileObject instanceof GroundObject) { - GroundObject groundObject = (GroundObject) tileObject; - Model model = extractModel(groundObject.getRenderable()); - return modelHasVisibleTriangles(model); - } else if (tileObject instanceof DecorativeObject) { - DecorativeObject decoObj = (DecorativeObject) tileObject; - Model model1 = extractModel(decoObj.getRenderable()); - Model model2 = extractModel(decoObj.getRenderable2()); - return modelHasVisibleTriangles(model1) || modelHasVisibleTriangles(model2); - } else if (tileObject instanceof WallObject) { - WallObject wallObj = (WallObject) tileObject; - Model model1 = extractModel(wallObj.getRenderable1()); - Model model2 = extractModel(wallObj.getRenderable2()); - return modelHasVisibleTriangles(model1) || modelHasVisibleTriangles(model2); - } - } catch (NullPointerException | IllegalStateException e) { - // Object is stale, not visible - return false; - } catch (Exception e) { - // For other exceptions, log and assume visible - log.debug("Error checking model visibility: {}", e.getMessage()); - } - return true; // Default to visible for unknown types - } - - /** - * Extracts a Model from a Renderable object. - * - * @param renderable The renderable object - * @return The model, or null if not available - */ - private static Model extractModel(Renderable renderable) { - if (renderable == null) { - return null; - } - return renderable instanceof Model ? (Model) renderable : renderable.getModel(); - } - - /** - * Checks if a model has visible triangles by examining transparency. - * - * @param model The model to check - * @return true if visible triangles are found - */ - private static boolean modelHasVisibleTriangles(Model model) { - if (model == null) { - return false; - } - - byte[] triangleTransparencies = model.getFaceTransparencies(); - int triangleCount = model.getFaceCount(); - - if (triangleTransparencies == null) { - return true; // No transparency data means visible - } - - // Check if any triangle is not fully transparent (255 = fully transparent) - for (int i = 0; i < triangleCount; i++) { - if ((triangleTransparencies[i] & 255) < 254) { - return true; - } - } - return false; - } - - /** - * Checks if any entity with a location is within the viewport by checking canvas conversion. - * This is a generic method that can work with any entity that has a world location. - * Uses client thread for safe access to client state. - * - * @param worldPoint The world point to check - * @return true if the location is visible on screen - */ - public static boolean isLocationVisibleInViewport(net.runelite.api.coords.WorldPoint worldPoint) { - try { - if (worldPoint == null) { - return false; - } - - // Use client thread for safe access to client state - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - Client client = Microbot.getClient(); - if (client == null) { - return false; - } - - LocalPoint localPoint = LocalPoint.fromWorld(client.getTopLevelWorldView(), worldPoint); - if (localPoint == null) { - return false; - } - - net.runelite.api.Point canvasPoint = Perspective.localToCanvas(client, localPoint, client.getTopLevelWorldView().getPlane()); - return canvasPoint != null; - }).orElse(false); - } catch (Exception e) { - return false; - } - } - - /** - * Filters a stream of objects to only include those visible in viewport. - * - * @param objectStream Stream of objects to filter - * @return Stream of objects visible in viewport - */ - public static Stream filterVisibleInViewport(Stream objectStream) { - return objectStream.filter(Rs2ObjectCacheUtils::isVisibleInViewport); - } - - /** - * Checks if an object is interactable (visible and within reasonable distance). - * - * @param objectModel The object to check - * @param maxDistance Maximum distance in tiles for interaction - * @return true if the object is interactable - */ - public static boolean isInteractable(Rs2ObjectModel objectModel, int maxDistance) { - try { - if (objectModel == null) { - return false; - } - - // Check if visible in viewport first - if (!isVisibleInViewport(objectModel)) { - return false; - } - - // Check distance from player - return objectModel.getDistanceFromPlayer() <= maxDistance; - } catch (Exception e) { - return false; - } - } - - /** - * Checks if an entity at a world point is interactable (within reasonable distance and visible). - * Uses client thread for safe access to player location. - * - * @param worldPoint The world point to check - * @param maxDistance Maximum distance in tiles for interaction - * @return true if the location is potentially interactable - */ - public static boolean isInteractable(net.runelite.api.coords.WorldPoint worldPoint, int maxDistance) { - try { - if (worldPoint == null) { - return false; - } - - // Use client thread for safe access to player location - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - net.runelite.api.coords.WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - if (playerLocation.distanceTo(worldPoint) > maxDistance) { - return false; - } - - // Check if visible in viewport (already uses client thread internally) - return isLocationVisibleInViewport(worldPoint); - }).orElse(false); - } catch (Exception e) { - return false; - } - } - - // ============================================ - // Updated Existing Methods to Use Local Functions - // ============================================ - - /** - * Gets all objects visible in the viewport. - * - * @return Stream of objects visible in viewport - */ - public static Stream getVisibleInViewport() { - return filterVisibleInViewport(getAll()); - } - - /** - * Gets objects by ID that are visible in the viewport. - * - * @param objectId The object ID - * @return Stream of objects with the specified ID visible in viewport - */ - public static Stream getVisibleInViewportById(int objectId) { - return filterVisibleInViewport(getByGameId(objectId)); - } - - /** - * Finds interactable objects by ID within distance from player. - * - * @param objectId The object ID - * @param maxDistance Maximum distance in tiles - * @return Stream of interactable objects with the specified ID - */ - public static Stream findInteractableById(int objectId, int maxDistance) { - return getByGameId(objectId) - .filter(obj -> isInteractable(obj, maxDistance)); - } - - /** - * Finds interactable objects by name within distance from player. - * - * @param name The object name - * @param maxDistance Maximum distance in tiles - * @return Stream of interactable objects with the specified name - */ - public static Stream findInteractableByName(String name, int maxDistance) { - - return findAll(nameMatches(name, false)) - .filter(obj -> isInteractable(obj, maxDistance)); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/farming/SpiritTree.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/farming/SpiritTree.java index 02cc212d506..e0c5a94c22a 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/farming/SpiritTree.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/farming/SpiritTree.java @@ -14,7 +14,6 @@ import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.questhelper.helpers.mischelpers.farmruns.CropState; import net.runelite.client.plugins.microbot.questhelper.helpers.mischelpers.farmruns.FarmingPatch; -import net.runelite.client.plugins.microbot.util.cache.Rs2SpiritTreeCache; import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; import net.runelite.client.plugins.microbot.util.misc.Rs2UiHelper; import net.runelite.client.plugins.microbot.util.player.Rs2Player; @@ -311,10 +310,6 @@ public boolean isPOHTreeAvailable() { // Check if we are in the player's house // Check if player is in their own house (POH) if (!PohTeleports.isInHouse()) { - // Check if this POH spirit tree is present in the spirit tree cache - if (Rs2SpiritTreeCache.getInstance().containsKey(this)) { - return Rs2SpiritTreeCache.getInstance().get(this).isAvailableForTravel(); - } return false; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/farming/SpiritTreeHelper.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/farming/SpiritTreeHelper.java deleted file mode 100644 index 7306d6b70b3..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/farming/SpiritTreeHelper.java +++ /dev/null @@ -1,248 +0,0 @@ -package net.runelite.client.plugins.microbot.util.farming; - -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.questhelper.helpers.mischelpers.farmruns.CropState; -import net.runelite.client.plugins.microbot.shortestpath.Transport; -import net.runelite.client.plugins.microbot.util.cache.Rs2SpiritTreeCache; -import net.runelite.client.plugins.microbot.util.cache.model.SpiritTreeData; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; - -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Helper class for Spirit Tree operations and pathfinder integration. - * Provides a high-level convenience API that delegates to the Rs2SpiritTreeCache - * for consistent and performant access to spirit tree state information. - */ -@Slf4j -public class SpiritTreeHelper { - - /** - * Check if a spirit tree transport is available. - * This method integrates with the pathfinder to determine transport availability. - * - * @param transport The transport object to check (validates origin availability) - * @return true if the spirit tree at the origin is available for travel - */ - public static boolean isSpiritTreeTransportAvailable(Transport transport) { - return Rs2SpiritTreeCache.isSpiritTreeTransportAvailable(transport); - } - - /** - * Get all available spirit tree origins as world points. - * Used by pathfinder to determine valid transport starting points. - * - * @return Set of world points where spirit trees are available for use - */ - public static Set getAvailableOrigins() { - return Rs2SpiritTreeCache.getAvailableOrigins(); - } - - /** - * Get all available spirit tree destinations as world points. - * Used by pathfinder to determine valid transport destinations. - * Note: This is an alias for getAvailableOrigins() since available origins can serve as destinations. - * - * @return Set of world points where spirit trees are available for travel - */ - public static Set getAvailableDestinations() { - return Rs2SpiritTreeCache.getAvailableDestinations(); - } - - /** - * Check if a specific world point has an available spirit tree (origin check). - * - * @param origin The origin point to check (where a spirit tree should be standing) - * @return true if a spirit tree is available at this location - */ - public static boolean isOriginAvailable(WorldPoint origin) { - return Rs2SpiritTreeCache.isOriginAvailable(origin); - } - - /** - * Check if a specific world point has an available spirit tree (destination check). - * This is an alias for isOriginAvailable() for backward compatibility. - * - * @param destination The destination to check - * @return true if a spirit tree is available at this location - */ - public static boolean isDestinationAvailable(WorldPoint destination) { - return Rs2SpiritTreeCache.isDestinationAvailable(destination); - } - - /** - * Get the closest available spirit tree to the player. - * - * @return Optional containing the closest available spirit tree - */ - public static Optional getClosestAvailableTree() { - WorldPoint playerLocation = Rs2Player.getWorldLocation(); - if (playerLocation == null) { - return Optional.empty(); - } - - return Rs2SpiritTreeCache.getClosestAvailableTree(playerLocation) - .map(SpiritTreeData::getSpiritTree); - } - - /** - * Check spirit tree patches that need farming attention. - * Useful for farming scripts to prioritize patch management. - * - * @return List of patches requiring attention (diseased, dead, or ready for harvest) - */ - public static List getPatchesRequiringAttention() { - return Rs2SpiritTreeCache.getPatchesRequiringAttention().stream() - .map(SpiritTreeData::getSpiritTree) - .collect(Collectors.toList()); - } - - /** - * Get priority patches for planting. - * Returns empty patches sorted by farming level requirement and strategic value. - * - * @return List of patches prioritized for planting - */ - public static List getPriorityPlantingPatches() { - return Rs2SpiritTreeCache.getEmptyPatches().stream() - .map(SpiritTreeData::getSpiritTree) - .filter(SpiritTree::hasLevelRequirement) - .filter(SpiritTree::hasQuestRequirements) - .sorted((patch1, patch2) -> { - // Sort by strategic value: lower farming requirement first, then by convenience - int levelDiff = Integer.compare(patch1.getRequiredSkillLevel(), patch2.getRequiredSkillLevel()); - if (levelDiff != 0) { - return levelDiff; - } - - // Prioritize by strategic locations (Grand Exchange area, commonly used locations) - return Integer.compare(getStrategicValue(patch1), getStrategicValue(patch2)); - }) - .collect(Collectors.toList()); - } - - /** - * Calculate strategic value of a spirit tree location - * Higher values indicate more strategically valuable locations - * - * @param patch The spirit tree patch to evaluate - * @return Strategic value score - */ - private static int getStrategicValue(SpiritTree patch) { - switch (patch) { - case PORT_SARIM: - return 10; // High value - good access to boats, farming - case FARMING_GUILD: - return 9; // High value - farming hub - case HOSIDIUS: - return 8; // Good value - Zeah access - case BRIMHAVEN: - return 7; // Medium value - fruit tree nearby - case ETCETERIA: - return 6; // Lower value - remote location - default: - return 5; // Default value - } - } - - /** - * Check if spirit tree patches should be refreshed in the pathfinder. - * This method can be called periodically to update transport availability. - * - * @return true if any farmable spirit tree states have changed significantly - */ - public static boolean shouldRefreshSpiritTreeStates() { - // Check if any farmable patches have recently changed state using cached data - List farmableStates = Rs2SpiritTreeCache.getFarmableTreeStates(); - - // For now, we rely on the cache's automatic update system - // This method can be enhanced with more sophisticated change detection - long staleDataCount = farmableStates.stream() - .filter(data -> data.isStale(10 * 60 * 1000L)) // 10 minutes - .count(); - - if (staleDataCount > 0) { - log.debug("Found {} stale spirit tree entries, refresh recommended", staleDataCount); - return true; - } - - return false; - } - - /** - * Get a summary of spirit tree farming status. - * Useful for reporting to users or logging. - * - * @return Formatted string with farming status summary - */ - public static String getFarmingStatusSummary() { - return Rs2SpiritTreeCache.getFarmingStatusSummary(); - } - - /** - * Force refresh of farming patch states. - * This can be called when the player visits a farming patch to update state. - */ - public static void refreshPatchStates() { - log.debug("Refreshing spirit tree patch states via cache"); - Rs2SpiritTreeCache.refreshFarmableStates(); - } - - /** - * Check if player can use spirit tree transportation. - * Validates basic requirements for spirit tree travel. - * - * @return true if player can use spirit trees - */ - public static boolean canUseSpirituTrees() { - // Check if player has completed the basic quest requirement - return SpiritTree.TREE_GNOME_VILLAGE.hasQuestRequirements(); - } - - /** - * Get recommended next action for spirit tree farming. - * Provides guidance for farming scripts. - * - * @return String describing the recommended action - */ - public static String getRecommendedFarmingAction() { - if (!Rs2SpiritTreeCache.isInitialized()) { - return "Initialize spirit tree cache system"; - } - - List needsAttention = Rs2SpiritTreeCache.getPatchesRequiringAttention(); - if (!needsAttention.isEmpty()) { - SpiritTreeData data = needsAttention.get(0); - SpiritTree patch = data.getSpiritTree(); - CropState state = data.getCropState(); - - if (state != null) { - switch (state) { - case HARVESTABLE: - return "Harvest " + patch.getName() + " spirit tree"; - case UNCHECKED: - return "Check health of " + patch.getName() + " spirit tree"; - case DISEASED: - return "Cure diseased spirit tree at " + patch.getName(); - case DEAD: - return "Clear dead spirit tree at " + patch.getName(); - default: - break; - } - } - } - - List empty = getPriorityPlantingPatches(); - if (!empty.isEmpty()) { - SpiritTree patch = empty.get(0); - return "Plant spirit tree at " + patch.getName() + " (requires level " + patch.getRequiredSkillLevel() + ")"; - } - - return "All spirit tree patches are being maintained"; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java index 4a8d4568c73..b4238bc3f30 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java @@ -14,7 +14,6 @@ import net.runelite.api.widgets.WidgetInfo; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.globval.enums.InterfaceTab; -import net.runelite.client.plugins.microbot.util.cache.Rs2QuestCache; import net.runelite.client.plugins.microbot.util.coords.Rs2WorldPoint; import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; @@ -1423,11 +1422,7 @@ public static int getPoseAnimation() { * @return The {@link QuestState} representing the player's progress in the quest. */ public static QuestState getQuestState(Quest quest) { - if (Microbot.isRs2CacheEnabled) { - return Rs2QuestCache.getQuestState(quest); - } else { - return Microbot.getRs2PlayerCache().getQuestState(quest); - } + return Microbot.getRs2PlayerCache().getQuestState(quest); } /** diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/world/WorldHoppingConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/world/WorldHoppingConfig.java index dc99bb1a12c..7b11eb89c77 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/world/WorldHoppingConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/world/WorldHoppingConfig.java @@ -1,12 +1,7 @@ package net.runelite.client.plugins.microbot.util.world; -import java.util.Map; - import lombok.Builder; import lombok.Getter; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.pluginscheduler.tasks.requirements.requirement.shop.ShopItemRequirement; -import net.runelite.client.plugins.microbot.util.shop.models.Rs2ShopItem; /** * Configuration for world hopping behavior in shop operations. @@ -77,37 +72,6 @@ public int getHopDelay(int attemptNumber) { } - - public static int estimateWorldHopsNeeded(Map shopItemRequirements) { - try { - int maxWorldHopsNeeded = 0; - - for (ShopItemRequirement itemReq : shopItemRequirements.values()) { - if (itemReq.isCompleted()) { - continue; // Skip completed items - } - - if (itemReq.getShopItem().getBaseStock() <= 0) { - return -1; // Cannot estimate for items with no base stock - } - - int remainingToBuy = itemReq.getRemainingAmount(); - int availablePerWorld = Math.max(itemReq.getShopItem().getBaseStock() - itemReq.getMinimumStockForBuying(), 0); - - if (availablePerWorld <= 0) { - return -1; // Insufficient stock per world for this item - } - - int worldHopsForThisItem = (int) Math.ceil((double) remainingToBuy /(availablePerWorld)); - maxWorldHopsNeeded = Math.max(maxWorldHopsNeeded, worldHopsForThisItem); - } - - return maxWorldHopsNeeded; - } catch (Exception e) { - Microbot.logStackTrace("ShopRequirement.estimateWorldHopsNeeded", e); - return -1; - } - } /** * Creates default configuration for shop operations */ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/area_map.png b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/area_map.png deleted file mode 100644 index 95ed799ceac5eb77f4740cd8bcbbfafd8fbb0e55..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17509 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 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/chronometer.png b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/chronometer.png deleted file mode 100644 index ec50807c0deaba6e26414eccdc66852d354f2c06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23542 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 HvkCeib_d2= diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/clock.png b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/clock.png deleted file mode 100644 index 4a16b83fecd3bd09432356825407d6fa87b4a729..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53520 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�zEL81>rwiYI@e^mTXn}2_Mn>pFu%Z)n)T4JYD*MgZHm%gjBp?)DB$P)2 zVpMPVl*z{)JMtl0>QzhH)qUBNG3A2n=d07vIi|R@@P3mV)g2ilBmL0fFx!yVNaZ zx_?_+qFqXaT=Z!9_2Vwi3R^><`Sf&$hJQ=kTV}s5OpL)%-UVy+e}v>NA)Dwwe^c0y zl16CNICRj~rYi0xS=WDybh)X8Z6VD!n45P%yQOT>6M_)=+?9x%@q?tf#vA^RPp!=U zkE})@=}L63?SihtsENH+aEs^#naL?%?RXe4H|Vyd$TH(CyRblR2s#eZ(RL;Us0TKO zG{Wgs2+ia+E0DarheG+|>Ww!uAdP%layg|lP$9PPoJaJq$TZm~wHp!I#JuAv#MwI? z9kw)9zTfC_uP-}=T`6G?t4&era`ehTpL9zog*kbM&=D%aKR0DVZ$8&3> zcz5=U1pr7!b#))o40>NW;2w2Ks+nXn@_({857_i?9=e#TPp;~ZW-xpI_GGu%A`Ve@ z!BDfi`lAMliH}||PM;V6%hrIu@M&$5Ug_c118yW@zq0Qwsev9?G)LE9B3b0?C8t{i zqFBqCqeBc2^UR!AsKrym+>X3&|LDO~_iv>>KY`qξ1?c+#`E?wNFLvoVGKajIl4 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 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/delete.png b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/delete.png deleted file mode 100644 index 2993cffb29dabd4bd44c45286ba1f66a366a24ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7842 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 v@;vP(R_q0%s^(>F6B;(G`WmWF^2ZhI#n3F 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 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/loot_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/loot_icon.png deleted file mode 100644 index ac88ed20fe7fd8c4e749947ac31755be8b2e82c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 892 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 BXodg) diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/old-watch.png b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/old-watch.png deleted file mode 100644 index 01b82d9a553a45bbd6096d002b6f9ec7c82d6642..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5617 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 aL%L^HJxh-_9u5G_8o~DPXO%CCP5%!zg)|KS diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/padlock.png b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/padlock.png deleted file mode 100644 index 3b6c462c73c5059f80b47b8847460bda9e3379e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15064 zcmd73X*`r|_%M9USVNYQrEFzn$(j_CwG=}!w#rUI_QFuMnYf7-G`6zKShFM~WGN!s z$izt2l4XoF`#R4#_x*pK5AT=veZIZ@ell}i$9W$6dY)p?MtXS!` ztBTOtIk?yP?i^;OV%K4hk(eeUe`E~X0406*`snE~#g%}$eE1y4Tt1Pwe}3k0m0M2f z(aBdw&~K*>j7-H6$G)BK=un%f3&SEFp@UNXq_5TzF%6kBvl6Oqkvvg;ZP+nd7T0>PAEGDhPoLgdV(mAzn*qe6biZ6;ZFs_o7vJCd zd6y>)pDB>c7?D?7@b$g;r)sprE9HBx(c(0*ELPIv;4uW5{gHwS`L`Pn_)rCln@&Z9 zNr{Kbq5EEOo@o?AeSFBc!ggcR_tlS&HD*SCsc)W{8Oc9p+vQXAl?lp*OF_|zA@Z#2c))l!(q`HY>>N4 zNa=wrY9^NUpooy@R)~^iZ+=AhPSKFoT0haddGOtD?83!*WPqhBoLiI^GQZwft|dot z4hm}aNl8>WjA1o710|@-*Lr*AF*1Uyt5XpWx=I+n0R-9%Kod%o4u zbNaq}JwGR3tdq$BORQDQF!I?9)mjQZXi}RwXsCRXdnbckdp{d;xcWDAzfz*Xj^rVM z(NA?vySp=21kLxZepE~{(7+qyh5Zgbn98sfjv&8+bWborX6Fx#n#@Sni2WCYtHZiU zCcR7!XJ^vSzUmGwGN1FvTy1RbhHwly&_wjq?oL*Dg%0gHo=>p7=)(tITt`T+LV+^H zK5wsI@@9$6ffH37dL04BUGCgmzhD@Y+s1rwV^Y=PSUX(NQYPuRLBV&A&~aKlP}$O3 zB11DD6V!`eqV^gDRktzkuO7>>UN4qjWO8_S?eyv`nWUZArR|&d{FUOb@j~hnWNeYw0rB37LF7ICXY-iH=h0Zg! z@-vA!wheC5u8~+IGVZ3GwvvK+|LlfIr*(vlZ|)BO2?m$0LWQzy+LxYbIWJ}453H0v zMwTCuf>@@5!_*Rb_5wSN8*(e(I6cOQKuJ2V=-Lo_nhlW&O!aK$OytnD06&?eIsyO;1AzID z;oQmqa1ybfMCd~FoQ*ZRDFvBLrn6TLd1@vZG(3-p#O+*$MaQh@Ej1dG^eps@C!QW* zYhF{G`D`Pd@hJXzF4AWNm3H<^2ffE%DjXXW&*;~{AKowfs??fvQV31eE%aLR%^tl zv|II-PZaXp*|UE?hhE+lv8vf;_|n(@CwalifK@7wxkcPB2X z{|tpqX-ho+vts}9@^a_ryT2;d8`6v_hztlGCb3I^tr^tLbHZv#B~-15LFLslQ#sj;u!-kS%cthqp?}OH_L)3NuQS zho%wl(9+V95qarXA00^z5srW{#O45w*-$kGR^CEP(f{=Qo(<2*$r%nB^a{xWye!^= zE5?JkArWdr8jn80CnyWkHN0Hr=p>UgcN+y2ax!Nf2lA-VuQPF~*iCJh zERPJP=t|StL}8KRjc)sO*ZG0H&hap1hXa+BC$1@@*a2fGG-v0*x8-lxO~7dYz|oKc znd<`_D#wGLE!ltvct)LxsHmue+Q5ajF0f+#A2=EY{LmXkGd3k6+2eH`1>DEKHJ%}t zHmn?{?~o03qrncB@r@@t=o%aa7UAqs-t_A+Sq&G|jKY0ST%rcz8^vM@lD{xY!rJuY zQ+B7A_ygWs&p@xA3F6!YME9DcnXzK{t5>hug^d~y`cx2Mx`Ip&L5*URQu0Yi$R-D^ zy0Ak%zh()RJb^1-ne;gLxXLou{PpYCu0cC&3zq&O8m`C$NUcHbBw{!rJ?{q(wyLS* zv8`iNA?apa8lXDFG{Fk2pQh~;w>EV{E1NTtP34@r zzw=VvbDv5gT$hPyLJ^3uH)YKUb(m}kU9%U6IxZq|(6)m~BLbdvVN4U;06
Wmw( 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 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/position.png b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/position.png deleted file mode 100644 index 2c1ce182574d442a96c0f8ff2e05e24d67e1c3c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30852 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�YxsKvW1$=Dybc4C?N07 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 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/ungroup.png b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/pluginscheduler/ungroup.png deleted file mode 100644 index 14966d8b5cfe8474b71c1d889c74b03947df2627..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2553 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&zE~)R&4Yzkn1=v6E*A2N2Y7q;vrJoCO|{ z#S9F5M?jcysy3fAP*AeOHKHUqKdq!Zu_%?Hyu4g5GcUV1Ik6yBFTW^#_B$IXpvE#! z7srr_Id5-n>L&=a_D>C(N)dbP{Wwl3kYNSN=ne8Qc^ zsgr^(Xt}Tzu}3W_T7H7HqHA&XvpwgF8SUdYKTKV32(+0#z-ih|X1lh>m&^CBIbBu$ ztuJVqn?;?&j-?VP<(~kn}K5iv<{vM-G;kG|E+cvu&u=~kO zu5VC`Co4P#iwPIfzA@bDyT5;~ZKC@3>B}EK{B-$^WI;gQ+`8Q4)ox3+D=I*(9 z8}t8BoCGkuY_^&Gz|w-Zza-r4&%_v(3|8!$@h4!RlO+^4zEiB$QsjjL^{k@W+?C`mZgM85h#tnL}?{4g4|GlTuuJ8Cyi+j&c{@49&50rZZj2qI+MVxVT z>x|q1i`ljh9>o0ob57!M#y6gZZXX68aM@)pGwbWsUiIW2_RZxCF)1`CGG~M83v>1G z?VHUR4!Az!C$+f8=`Erwj6r7tuol^J*Q$2+xqBPBf5-o`l)LvY_ZyFe#_{uY^KBE& zPg_(M7nNF9AKOQRY6X}W7^WWt6-5%se@nmdSs3)E^J@q*Y-s*mzyDYI&v*Umo68R< z?ku&ccATwFO1(@ln1K~HL*Z#)N=y_#y~}1ku+7l-UY Date: Sat, 15 Nov 2025 07:52:03 +0100 Subject: [PATCH 19/42] remove caching and scheduler --- .../client/plugins/microbot/shortestpath/transports.tsv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv index 530e838b97f..1e18df02492 100644 --- a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv +++ b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv @@ -5193,4 +5193,4 @@ 2763 2952 1 2763 2951 0 Climb-down;Ladder;16679 # woodcutting guild 1574 3483 1 1575 3483 0 Climb-down;Rope ladder;28858 -1575 3483 0 1574 3483 1 Climb-up;Rope ladder;28857 "" "" \ No newline at end of file +1575 3483 0 1574 3483 1 Climb-up;Rope ladder;28857 \ No newline at end of file From 61b1db664f3139cb1adf6c667b2d6cf14530e532 Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 15 Nov 2025 07:54:24 +0100 Subject: [PATCH 20/42] feat(api): add new api for ground item, NPC, and player querying --- .../api/grounditem/GroundItemApiExample.java | 141 ++++++++++++++++++ .../api/grounditem/Rs2GroundItemCache.java | 65 ++++++++ .../grounditem/Rs2GroundItemQueryable.java | 97 ++++++++++++ .../microbot/api/npc/NpcApiExample.java | 71 +++++++++ .../plugins/microbot/api/npc/Rs2NpcCache.java | 41 +++++ .../microbot/api/npc/Rs2NpcQueryable.java | 97 ++++++++++++ .../microbot/api/player/PlayerApiExample.java | 116 ++++++++++++++ .../microbot/api/player/Rs2PlayerCache.java | 47 ++++++ .../api/player/Rs2PlayerQueryable.java | 108 ++++++++++++++ 9 files changed, 783 insertions(+) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/GroundItemApiExample.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemCache.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemQueryable.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/NpcApiExample.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcCache.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcQueryable.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerCache.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerQueryable.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/GroundItemApiExample.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/GroundItemApiExample.java new file mode 100644 index 00000000000..82016a0e52c --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/GroundItemApiExample.java @@ -0,0 +1,141 @@ +package net.runelite.client.plugins.microbot.api.grounditem; + +import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; + +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 GroundItemApiExample { + + public static void examples() { + // Create a new cache instance + Rs2GroundItemCache cache = new Rs2GroundItemCache(); + + // Example 1: Get the nearest ground item + Rs2GroundItemModel nearestItem = cache.query().nearest(); + + // Example 2: Get the nearest ground item within 10 tiles + Rs2GroundItemModel nearestItemWithinRange = cache.query().nearest(10); + + // Example 3: Find a ground item by name + Rs2GroundItemModel coins = cache.query().withName("Coins"); + + // Example 4: Find a ground item by multiple names + Rs2GroundItemModel loot = cache.query().withNames("Dragon bones", "Dragon scale", "Dragon dagger"); + + // Example 5: Find a ground item by ID + Rs2GroundItemModel itemById = cache.query().withId(995); // Coins + + // Example 6: Find a ground item by multiple IDs + Rs2GroundItemModel itemByIds = cache.query().withIds(995, 526, 537); // 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 + Rs2GroundItemModel lootableItem = cache.query() + .where(Rs2GroundItemModel::isLootAble) + .nearest(); + + // Example 9: Find items owned by player + List ownedItems = cache.query() + .where(Rs2GroundItemModel::isOwned) + .toList(); + + // Example 10: Find stackable items + List stackableItems = cache.query() + .where(Rs2GroundItemModel::isStackable) + .toList(); + + // Example 11: Find noted items + List notedItems = cache.query() + .where(Rs2GroundItemModel::isNoted) + .toList(); + + // Example 12: Find items worth high alching + List alchableItems = cache.query() + .where(item -> item.isProfitableToHighAlch(100)) + .toList(); + + // Example 13: Find items about to despawn + List despawningItems = cache.query() + .where(item -> item.willDespawnWithin(30)) + .toList(); + + // Example 14: Find common loot items + List commonLoot = cache.query() + .where(Rs2GroundItemModel::isCommonLoot) + .toList(); + + // Example 15: Find priority items (high value or about to despawn) + List priorityItems = cache.query() + .where(Rs2GroundItemModel::shouldPrioritize) + .toList(); + + // Example 16: Complex query - Find nearest valuable lootable item within 15 tiles + Rs2GroundItemModel target = cache.query() + .where(Rs2GroundItemModel::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(Rs2GroundItemModel::isTradeable) + .toList(); + + // Example 19: Find members items + List membersItems = cache.query() + .where(Rs2GroundItemModel::isMembers) + .toList(); + + // Example 20: Static method to get stream directly + Rs2GroundItemModel firstItem = Rs2GroundItemCache.getGroundItemsStream() + .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(); + + // Example 22: Find items by distance from specific point + List itemsNearBank = cache.query() + .where(item -> item.isWithinDistanceFromPlayer(5)) + .toList(); + + // Example 23: Find items that are clickable (visible in viewport) + List clickableItems = cache.query() + .where(Rs2GroundItemModel::isClickable) + .toList(); + + // Example 24: Find best value item to loot + Rs2GroundItemModel bestValue = cache.query() + .where(Rs2GroundItemModel::isLootAble) + .where(item -> !item.isDespawned()) + .where(item -> item.isWithinDistanceFromPlayer(10)) + .toList() + .stream() + .max((a, b) -> Integer.compare(a.getTotalGeValue(), b.getTotalGeValue())) + .orElse(null); + + // Example 25: Find items worth looting based on minimum value + List worthLooting = cache.query() + .where(item -> item.isWorthLootingGe(10000)) + .toList(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemCache.java new file mode 100644 index 00000000000..4f50aa11aee --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemCache.java @@ -0,0 +1,65 @@ +package net.runelite.client.plugins.microbot.api.grounditem; + +import net.runelite.api.events.ItemDespawned; +import net.runelite.api.events.ItemSpawned; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +public class Rs2GroundItemCache { + + private static int lastUpdateGroundItems = 0; + private static List groundItems = new ArrayList<>(); + + public Rs2GroundItemQueryable query() { + return new Rs2GroundItemQueryable(); + } + + public static void registerEventBus() { + Microbot.getEventBus().register(Rs2GroundItemCache.class); + } + + /** + * Get all ground items in the current scene + * Uses the existing ground item cache from util.cache package + * @return Stream of Rs2GroundItemModel + */ + public static Stream getGroundItemsStream() { + + if (lastUpdateGroundItems >= Microbot.getClient().getTickCount()) { + return groundItems.stream(); + } + + // Use the existing ground item cache + List result = new ArrayList<>(); + + groundItems = result; + lastUpdateGroundItems = Microbot.getClient().getTickCount(); + return result.stream(); + } + + @Subscribe + public void onItemSpawned(ItemSpawned event) + { + /*groundItems.add( + new TileItemEx( + event.getItem(), + WorldPoint.fromLocal(Static.getClient(), event.getTile().getLocalLocation()), + event.getTile().getLocalLocation() + ) + );*/ + } + + @Subscribe + public void onItemDespawned(ItemDespawned event) + { + /*groundItems.removeIf(ex -> ex.getItem().equals(event.getItem()) && + ex.getWorldPoint().equals(WorldPoint.fromLocal(Static.getClient(), event.getTile().getLocalLocation())) && + ex.getLocalPoint().equals(event.getTile().getLocalLocation()) + );*/ + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemQueryable.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemQueryable.java new file mode 100644 index 00000000000..74bb8bde72b --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemQueryable.java @@ -0,0 +1,97 @@ +package net.runelite.client.plugins.microbot.api.grounditem; + +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.api.IEntityQueryable; +import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; + +import java.util.Arrays; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +public final class Rs2GroundItemQueryable + implements IEntityQueryable { + + private Stream source; + + public Rs2GroundItemQueryable() { + this.source = Rs2GroundItemCache.getGroundItemsStream(); + } + + @Override + public Rs2GroundItemQueryable where(java.util.function.Predicate predicate) { + source = source.filter(predicate); + return this; + } + + @Override + public Rs2GroundItemModel first() { + return source.findFirst().orElse(null); + } + + @Override + public Rs2GroundItemModel nearest() { + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc == null) { + return null; + } + + return source + .min(java.util.Comparator.comparingInt( + o -> o.getLocation().distanceTo(playerLoc))) + .orElse(null); + } + + @Override + public Rs2GroundItemModel nearest(int maxDistance) { + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc == null) { + return null; + } + + return source + .filter(x -> x.getLocation().distanceTo(playerLoc) <= maxDistance) + .min(java.util.Comparator.comparingInt( + o -> o.getLocation().distanceTo(playerLoc))) + .orElse(null); + } + + @Override + public Rs2GroundItemModel withName(String name) { + return source.filter(x -> x.getName() != null && x.getName().equalsIgnoreCase(name)) + .findFirst() + .orElse(null); + } + + @Override + public Rs2GroundItemModel withNames(String... names) { + return source.filter(x -> { + if (x.getName() == null) return false; + return Arrays.stream(names) + .anyMatch(name -> x.getName().equalsIgnoreCase(name)); + }).findFirst().orElse(null); + } + + @Override + public Rs2GroundItemModel withId(int id) { + return source.filter(x -> x.getId() == id) + .findFirst() + .orElse(null); + } + + @Override + public Rs2GroundItemModel withIds(int... ids) { + return source.filter(x -> { + for (int id : ids) { + if (x.getId() == id) return true; + } + return false; + }).findFirst().orElse(null); + } + + @Override + public java.util.List toList() { + return source.collect(Collectors.toList()); + } +} 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..6b26777e720 --- /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.util.npc.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"); + + // Example 4: Find an NPC by multiple names + Rs2NpcModel enemy = cache.query().withNames("Goblin", "Guard", "Dark wizard"); + + // Example 5: Find an NPC by ID + Rs2NpcModel npcById = cache.query().withId(1234); + + // Example 6: Find an NPC by multiple IDs + Rs2NpcModel npcByIds = cache.query().withIds(1234, 5678, 9012); + + // 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..a566b135cdd --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcCache.java @@ -0,0 +1,41 @@ +package net.runelite.client.plugins.microbot.api.npc; + +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; + +import java.util.ArrayList; +import java.util.List; +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 + * + * @return Stream of Rs2NpcModel + */ + public static Stream getNpcsStream() { + + if (lastUpdateNpcs >= Microbot.getClient().getTickCount()) { + return npcs.stream(); + } + + List result = Microbot.getClientThread().invoke(() -> Microbot + .getClient() + .getTopLevelWorldView() + .npcs().stream().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..2c8cb8110b8 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/Rs2NpcQueryable.java @@ -0,0 +1,97 @@ +package net.runelite.client.plugins.microbot.api.npc; + +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.api.IEntityQueryable; +import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; + +import java.util.Arrays; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +public final class Rs2NpcQueryable + implements IEntityQueryable { + + private Stream source; + + public Rs2NpcQueryable() { + this.source = Rs2NpcCache.getNpcsStream(); + } + + @Override + public Rs2NpcQueryable where(java.util.function.Predicate predicate) { + source = source.filter(predicate); + return this; + } + + @Override + public Rs2NpcModel first() { + return source.findFirst().orElse(null); + } + + @Override + public Rs2NpcModel nearest() { + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc == null) { + return null; + } + + return source + .min(java.util.Comparator.comparingInt( + o -> o.getWorldLocation().distanceTo(playerLoc))) + .orElse(null); + } + + @Override + public Rs2NpcModel nearest(int maxDistance) { + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc == null) { + return null; + } + + return source + .filter(x -> x.getWorldLocation().distanceTo(playerLoc) <= maxDistance) + .min(java.util.Comparator.comparingInt( + o -> o.getWorldLocation().distanceTo(playerLoc))) + .orElse(null); + } + + @Override + public Rs2NpcModel withName(String name) { + return source.filter(x -> x.getName() != null && x.getName().equalsIgnoreCase(name)) + .findFirst() + .orElse(null); + } + + @Override + public Rs2NpcModel withNames(String... names) { + return source.filter(x -> { + if (x.getName() == null) return false; + return Arrays.stream(names) + .anyMatch(name -> x.getName().equalsIgnoreCase(name)); + }).findFirst().orElse(null); + } + + @Override + public Rs2NpcModel withId(int id) { + return source.filter(x -> x.getId() == id) + .findFirst() + .orElse(null); + } + + @Override + public Rs2NpcModel withIds(int... ids) { + return source.filter(x -> { + for (int id : ids) { + if (x.getId() == id) return true; + } + return false; + }).findFirst().orElse(null); + } + + @Override + public java.util.List toList() { + return source.collect(Collectors.toList()); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java new file mode 100644 index 00000000000..51197deec6a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java @@ -0,0 +1,116 @@ +package net.runelite.client.plugins.microbot.api.player; + +import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Example usage of the Player API + * + * This demonstrates how to query players using the new API structure: + * - Rs2PlayerCache: Caches players for efficient querying + * - Rs2PlayerQueryable: Provides a fluent interface for filtering and querying players + */ +public class PlayerApiExample { + + public static void examples() { + // Create a new cache instance + Rs2PlayerCache cache = new Rs2PlayerCache(); + + // Example 1: Get the nearest player (excluding local player) + Rs2PlayerModel nearestPlayer = cache.query().nearest(); + + // Example 2: Get the nearest player within 10 tiles + Rs2PlayerModel nearestPlayerWithinRange = cache.query().nearest(10); + + // Example 3: Find a player by name + Rs2PlayerModel playerByName = cache.query().withName("PlayerName"); + + // Example 4: Find a player by multiple names + Rs2PlayerModel anyOfThesePlayers = cache.query().withNames("Player1", "Player2", "Player3"); + + // Example 5: Find a player by ID + Rs2PlayerModel playerById = cache.query().withId(12345); + + // Example 6: Find a player by multiple IDs + Rs2PlayerModel playerByIds = cache.query().withIds(12345, 67890, 11111); + + // Example 7: Include local player in query + Rs2PlayerModel localPlayer = cache.query() + .includeLocalPlayer() + .where(player -> player.getPlayer() != null) + .first(); + + // Example 8: Find all friends + List friends = cache.query() + .where(Rs2PlayerModel::isFriend) + .toList(); + + // Example 9: Find all clan members + List clanMembers = cache.query() + .where(Rs2PlayerModel::isClanMember) + .toList(); + + // Example 10: Find all friends chat members + List fcMembers = cache.query() + .where(Rs2PlayerModel::isFriendsChatMember) + .toList(); + + // Example 11: Find players in combat (with health bar visible) + List playersInCombat = cache.query() + .where(player -> player.getHealthRatio() != -1) + .toList(); + + // Example 12: Find nearest player who is not in your clan + Rs2PlayerModel nearestNonClan = cache.query() + .where(player -> !player.isClanMember()) + .where(player -> !player.isFriend()) + .nearest(); + + // Example 13: Find players with skull (PvP) + List skulledPlayers = cache.query() + .where(player -> player.getSkullIcon() != -1) + .toList(); + + // Example 14: Find players with prayer active (overhead icon) + List playersWithPrayer = cache.query() + .where(player -> player.getOverheadIcon() != null) + .toList(); + + // Example 15: Find nearest player that is animating (doing something) + Rs2PlayerModel animatingPlayer = cache.query() + .where(player -> player.getAnimation() != -1) + .nearest(); + + // Example 16: Complex query - Find nearest low health enemy player within 5 tiles + Rs2PlayerModel target = cache.query() + .where(player -> !player.isFriend()) + .where(player -> !player.isClanMember()) + .where(player -> player.getHealthRatio() > 0) + .where(player -> player.getHealthRatio() < player.getHealthScale() / 2) + .nearest(5); + + // Example 17: Find all players on the same team (Castle Wars, etc.) + int myTeam = 1; // Example team ID + List teammates = cache.query() + .where(player -> player.getTeam() == myTeam) + .toList(); + + // Example 18: Static method to get stream directly + Rs2PlayerModel firstPlayer = Rs2PlayerCache.getPlayersStream() + .filter(player -> player.getName() != null) + .findFirst() + .orElse(null); + + // Example 19: Get all players including local player + List allPlayersIncludingMe = Rs2PlayerCache.getPlayersStream(true) + .collect(Collectors.toList()); + + // Example 20: Find players by partial name match + Rs2PlayerModel playerContainingName = cache.query() + .where(player -> player.getName() != null && + player.getName().toLowerCase().contains("iron")) + .first(); + } +} 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..be02189541e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerCache.java @@ -0,0 +1,47 @@ +package net.runelite.client.plugins.microbot.api.player; + +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; + +import java.util.ArrayList; +import java.util.List; +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 (excluding local player) + * @return Stream of Rs2PlayerModel + */ + public static Stream getPlayersStream() { + return getPlayersStream(false); + } + + /** + * Get all players in the current scene + * @param includeLocalPlayer whether to include the local player in the results + * @return Stream of Rs2PlayerModel + */ + public static Stream getPlayersStream(boolean includeLocalPlayer) { + + if (lastUpdatePlayers >= Microbot.getClient().getTickCount()) { + return players.stream(); + } + + // Get all players using the existing Rs2Player utility + List result = Rs2Player.getPlayers(player -> true, includeLocalPlayer).collect(Collectors.toList()); + + players = result; + lastUpdatePlayers = Microbot.getClient().getTickCount(); + return result.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..f41339efdb8 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/Rs2PlayerQueryable.java @@ -0,0 +1,108 @@ +package net.runelite.client.plugins.microbot.api.player; + +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.api.IEntityQueryable; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; + +import java.util.Arrays; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +public final class Rs2PlayerQueryable + implements IEntityQueryable { + + private Stream source; + private boolean includeLocalPlayer = false; + + public Rs2PlayerQueryable() { + this.source = Rs2PlayerCache.getPlayersStream(); + } + + /** + * Include the local player in the query results + * @return this queryable for chaining + */ + public Rs2PlayerQueryable includeLocalPlayer() { + this.includeLocalPlayer = true; + this.source = Rs2PlayerCache.getPlayersStream(true); + return this; + } + + @Override + public Rs2PlayerQueryable where(java.util.function.Predicate predicate) { + source = source.filter(predicate); + return this; + } + + @Override + public Rs2PlayerModel first() { + return source.findFirst().orElse(null); + } + + @Override + public Rs2PlayerModel nearest() { + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc == null) { + return null; + } + + return source + .min(java.util.Comparator.comparingInt( + o -> o.getWorldLocation().distanceTo(playerLoc))) + .orElse(null); + } + + @Override + public Rs2PlayerModel nearest(int maxDistance) { + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc == null) { + return null; + } + + return source + .filter(x -> x.getWorldLocation().distanceTo(playerLoc) <= maxDistance) + .min(java.util.Comparator.comparingInt( + o -> o.getWorldLocation().distanceTo(playerLoc))) + .orElse(null); + } + + @Override + public Rs2PlayerModel withName(String name) { + return source.filter(x -> x.getName() != null && x.getName().equalsIgnoreCase(name)) + .findFirst() + .orElse(null); + } + + @Override + public Rs2PlayerModel withNames(String... names) { + return source.filter(x -> { + if (x.getName() == null) return false; + return Arrays.stream(names) + .anyMatch(name -> x.getName().equalsIgnoreCase(name)); + }).findFirst().orElse(null); + } + + @Override + public Rs2PlayerModel withId(int id) { + return source.filter(x -> x.getId() == id) + .findFirst() + .orElse(null); + } + + @Override + public Rs2PlayerModel withIds(int... ids) { + return source.filter(x -> { + for (int id : ids) { + if (x.getId() == id) return true; + } + return false; + }).findFirst().orElse(null); + } + + @Override + public java.util.List toList() { + return source.collect(Collectors.toList()); + } +} From 6af8f1c5c429dd7ee038a1437eed6f22bef1c393 Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 15 Nov 2025 07:55:30 +0100 Subject: [PATCH 21/42] chore(pom): update microbot version to 2.0.42 --- runelite-client/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runelite-client/pom.xml b/runelite-client/pom.xml index a89f0fe91eb..4047ff04003 100644 --- a/runelite-client/pom.xml +++ b/runelite-client/pom.xml @@ -41,7 +41,7 @@ nogit false false - 2.0.41 + 2.0.42 nogit From 1badf00fa4c312784eb559a0f2ccc823e5659e9c Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 15 Nov 2025 14:55:47 +0100 Subject: [PATCH 22/42] refactor(api): rename ground item classes to tile item for clarity --- runelite-client/pom.xml | 2 +- .../plugins/microbot/MicrobotPlugin.java | 5 - .../api/grounditem/GroundItemApiExample.java | 141 --- .../api/grounditem/Rs2GroundItemCache.java | 65 -- .../api/tileitem/Rs2TileItemCache.java | 63 ++ .../Rs2TileItemQueryable.java} | 38 +- .../api/tileitem/TileItemApiExample.java | 104 +++ .../api/tileitem/models/Rs2TileItemModel.java | 169 ++++ .../util/grounditem/Rs2GroundItemModel.java | 874 ------------------ 9 files changed, 356 insertions(+), 1105 deletions(-) delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/GroundItemApiExample.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemCache.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/Rs2TileItemCache.java rename runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/{grounditem/Rs2GroundItemQueryable.java => tileitem/Rs2TileItemQueryable.java} (61%) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/TileItemApiExample.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/models/Rs2TileItemModel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItemModel.java diff --git a/runelite-client/pom.xml b/runelite-client/pom.xml index 4047ff04003..f976393de74 100644 --- a/runelite-client/pom.xml +++ b/runelite-client/pom.xml @@ -41,7 +41,7 @@ nogit false false - 2.0.42 + 2.1.0 nogit diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java index 06341e932b5..b3703d6b5fd 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java @@ -16,12 +16,10 @@ import net.runelite.client.events.RuneScapeProfileChanged; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; -import net.runelite.client.plugins.microbot.api.grounditem.Rs2GroundItemCache; import net.runelite.client.plugins.microbot.pouch.PouchOverlay; import net.runelite.client.plugins.microbot.ui.MicrobotPluginConfigurationDescriptor; import net.runelite.client.plugins.microbot.ui.MicrobotPluginListPanel; import net.runelite.client.plugins.microbot.ui.MicrobotTopLevelConfigPanel; -import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; import net.runelite.client.plugins.microbot.util.inventory.Rs2Gembag; import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; @@ -49,7 +47,6 @@ import java.awt.image.BufferedImage; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; -import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -174,8 +171,6 @@ protected void startUp() throws AWTException Microbot.getPouchScript().startUp(); - Rs2GroundItemCache.registerEventBus(); - if (overlayManager != null) { overlayManager.add(microbotOverlay); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/GroundItemApiExample.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/GroundItemApiExample.java deleted file mode 100644 index 82016a0e52c..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/GroundItemApiExample.java +++ /dev/null @@ -1,141 +0,0 @@ -package net.runelite.client.plugins.microbot.api.grounditem; - -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; - -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 GroundItemApiExample { - - public static void examples() { - // Create a new cache instance - Rs2GroundItemCache cache = new Rs2GroundItemCache(); - - // Example 1: Get the nearest ground item - Rs2GroundItemModel nearestItem = cache.query().nearest(); - - // Example 2: Get the nearest ground item within 10 tiles - Rs2GroundItemModel nearestItemWithinRange = cache.query().nearest(10); - - // Example 3: Find a ground item by name - Rs2GroundItemModel coins = cache.query().withName("Coins"); - - // Example 4: Find a ground item by multiple names - Rs2GroundItemModel loot = cache.query().withNames("Dragon bones", "Dragon scale", "Dragon dagger"); - - // Example 5: Find a ground item by ID - Rs2GroundItemModel itemById = cache.query().withId(995); // Coins - - // Example 6: Find a ground item by multiple IDs - Rs2GroundItemModel itemByIds = cache.query().withIds(995, 526, 537); // 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 - Rs2GroundItemModel lootableItem = cache.query() - .where(Rs2GroundItemModel::isLootAble) - .nearest(); - - // Example 9: Find items owned by player - List ownedItems = cache.query() - .where(Rs2GroundItemModel::isOwned) - .toList(); - - // Example 10: Find stackable items - List stackableItems = cache.query() - .where(Rs2GroundItemModel::isStackable) - .toList(); - - // Example 11: Find noted items - List notedItems = cache.query() - .where(Rs2GroundItemModel::isNoted) - .toList(); - - // Example 12: Find items worth high alching - List alchableItems = cache.query() - .where(item -> item.isProfitableToHighAlch(100)) - .toList(); - - // Example 13: Find items about to despawn - List despawningItems = cache.query() - .where(item -> item.willDespawnWithin(30)) - .toList(); - - // Example 14: Find common loot items - List commonLoot = cache.query() - .where(Rs2GroundItemModel::isCommonLoot) - .toList(); - - // Example 15: Find priority items (high value or about to despawn) - List priorityItems = cache.query() - .where(Rs2GroundItemModel::shouldPrioritize) - .toList(); - - // Example 16: Complex query - Find nearest valuable lootable item within 15 tiles - Rs2GroundItemModel target = cache.query() - .where(Rs2GroundItemModel::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(Rs2GroundItemModel::isTradeable) - .toList(); - - // Example 19: Find members items - List membersItems = cache.query() - .where(Rs2GroundItemModel::isMembers) - .toList(); - - // Example 20: Static method to get stream directly - Rs2GroundItemModel firstItem = Rs2GroundItemCache.getGroundItemsStream() - .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(); - - // Example 22: Find items by distance from specific point - List itemsNearBank = cache.query() - .where(item -> item.isWithinDistanceFromPlayer(5)) - .toList(); - - // Example 23: Find items that are clickable (visible in viewport) - List clickableItems = cache.query() - .where(Rs2GroundItemModel::isClickable) - .toList(); - - // Example 24: Find best value item to loot - Rs2GroundItemModel bestValue = cache.query() - .where(Rs2GroundItemModel::isLootAble) - .where(item -> !item.isDespawned()) - .where(item -> item.isWithinDistanceFromPlayer(10)) - .toList() - .stream() - .max((a, b) -> Integer.compare(a.getTotalGeValue(), b.getTotalGeValue())) - .orElse(null); - - // Example 25: Find items worth looting based on minimum value - List worthLooting = cache.query() - .where(item -> item.isWorthLootingGe(10000)) - .toList(); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemCache.java deleted file mode 100644 index 4f50aa11aee..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemCache.java +++ /dev/null @@ -1,65 +0,0 @@ -package net.runelite.client.plugins.microbot.api.grounditem; - -import net.runelite.api.events.ItemDespawned; -import net.runelite.api.events.ItemSpawned; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; - -public class Rs2GroundItemCache { - - private static int lastUpdateGroundItems = 0; - private static List groundItems = new ArrayList<>(); - - public Rs2GroundItemQueryable query() { - return new Rs2GroundItemQueryable(); - } - - public static void registerEventBus() { - Microbot.getEventBus().register(Rs2GroundItemCache.class); - } - - /** - * Get all ground items in the current scene - * Uses the existing ground item cache from util.cache package - * @return Stream of Rs2GroundItemModel - */ - public static Stream getGroundItemsStream() { - - if (lastUpdateGroundItems >= Microbot.getClient().getTickCount()) { - return groundItems.stream(); - } - - // Use the existing ground item cache - List result = new ArrayList<>(); - - groundItems = result; - lastUpdateGroundItems = Microbot.getClient().getTickCount(); - return result.stream(); - } - - @Subscribe - public void onItemSpawned(ItemSpawned event) - { - /*groundItems.add( - new TileItemEx( - event.getItem(), - WorldPoint.fromLocal(Static.getClient(), event.getTile().getLocalLocation()), - event.getTile().getLocalLocation() - ) - );*/ - } - - @Subscribe - public void onItemDespawned(ItemDespawned event) - { - /*groundItems.removeIf(ex -> ex.getItem().equals(event.getItem()) && - ex.getWorldPoint().equals(WorldPoint.fromLocal(Static.getClient(), event.getTile().getLocalLocation())) && - ex.getLocalPoint().equals(event.getTile().getLocalLocation()) - );*/ - } -} 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..3f1db345106 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/Rs2TileItemCache.java @@ -0,0 +1,63 @@ +package net.runelite.client.plugins.microbot.api.tileitem; + +import net.runelite.api.events.ItemDespawned; +import net.runelite.api.events.ItemSpawned; +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.api.tileitem.models.Rs2TileItemModel; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +@Singleton +public class Rs2TileItemCache { + + private static int lastUpdateGroundItems = 0; + private static List groundItems = new ArrayList<>(); + + @Inject + public Rs2TileItemCache(EventBus eventBus) { + eventBus.register(this); + } + + public Rs2TileItemQueryable query() { + return new Rs2TileItemQueryable(); + } + + /** + * Get all ground items in the current scene + * Uses the existing ground item cache from util.cache package + * + * @return Stream of Rs2GroundItemModel + */ + public static Stream getGroundItemsStream() { + + if (lastUpdateGroundItems >= Microbot.getClient().getTickCount()) { + return groundItems.stream(); + } + + // Use the existing ground item cache + List result = new ArrayList<>(); + + groundItems = result; + lastUpdateGroundItems = Microbot.getClient().getTickCount(); + return result.stream(); + } + + @Subscribe + public void onItemSpawned(ItemSpawned event) { + groundItems.add(new Rs2TileItemModel(event.getTile(), event.getItem())); + } + + @Subscribe + public void onItemDespawned(ItemDespawned event) { + groundItems.removeIf(groundItem -> groundItem.getId() == event.getItem().getId() && + groundItem.getWorldLocation().equals(event.getTile().getWorldLocation()) + && groundItem.getLocalLocation().equals(event.getTile().getLocalLocation()) + ); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemQueryable.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/Rs2TileItemQueryable.java similarity index 61% rename from runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemQueryable.java rename to runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/Rs2TileItemQueryable.java index 74bb8bde72b..f8461d6bfaf 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/grounditem/Rs2GroundItemQueryable.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/Rs2TileItemQueryable.java @@ -1,8 +1,8 @@ -package net.runelite.client.plugins.microbot.api.grounditem; +package net.runelite.client.plugins.microbot.api.tileitem; import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.api.IEntityQueryable; -import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItemModel; +import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import java.util.Arrays; @@ -10,28 +10,28 @@ import java.util.stream.Stream; -public final class Rs2GroundItemQueryable - implements IEntityQueryable { +public final class Rs2TileItemQueryable + implements IEntityQueryable { - private Stream source; + private Stream source; - public Rs2GroundItemQueryable() { - this.source = Rs2GroundItemCache.getGroundItemsStream(); + public Rs2TileItemQueryable() { + this.source = Rs2TileItemCache.getGroundItemsStream(); } @Override - public Rs2GroundItemQueryable where(java.util.function.Predicate predicate) { + public Rs2TileItemQueryable where(java.util.function.Predicate predicate) { source = source.filter(predicate); return this; } @Override - public Rs2GroundItemModel first() { + public Rs2TileItemModel first() { return source.findFirst().orElse(null); } @Override - public Rs2GroundItemModel nearest() { + public Rs2TileItemModel nearest() { WorldPoint playerLoc = Rs2Player.getWorldLocation(); if (playerLoc == null) { return null; @@ -39,33 +39,33 @@ public Rs2GroundItemModel nearest() { return source .min(java.util.Comparator.comparingInt( - o -> o.getLocation().distanceTo(playerLoc))) + o -> o.getWorldLocation().distanceTo(playerLoc))) .orElse(null); } @Override - public Rs2GroundItemModel nearest(int maxDistance) { + public Rs2TileItemModel nearest(int maxDistance) { WorldPoint playerLoc = Rs2Player.getWorldLocation(); if (playerLoc == null) { return null; } return source - .filter(x -> x.getLocation().distanceTo(playerLoc) <= maxDistance) + .filter(x -> x.getWorldLocation().distanceTo(playerLoc) <= maxDistance) .min(java.util.Comparator.comparingInt( - o -> o.getLocation().distanceTo(playerLoc))) + o -> o.getWorldLocation().distanceTo(playerLoc))) .orElse(null); } @Override - public Rs2GroundItemModel withName(String name) { + public Rs2TileItemModel withName(String name) { return source.filter(x -> x.getName() != null && x.getName().equalsIgnoreCase(name)) .findFirst() .orElse(null); } @Override - public Rs2GroundItemModel withNames(String... names) { + public Rs2TileItemModel withNames(String... names) { return source.filter(x -> { if (x.getName() == null) return false; return Arrays.stream(names) @@ -74,14 +74,14 @@ public Rs2GroundItemModel withNames(String... names) { } @Override - public Rs2GroundItemModel withId(int id) { + public Rs2TileItemModel withId(int id) { return source.filter(x -> x.getId() == id) .findFirst() .orElse(null); } @Override - public Rs2GroundItemModel withIds(int... ids) { + public Rs2TileItemModel withIds(int... ids) { return source.filter(x -> { for (int id : ids) { if (x.getId() == id) return true; @@ -91,7 +91,7 @@ public Rs2GroundItemModel withIds(int... ids) { } @Override - public java.util.List toList() { + public java.util.List toList() { return source.collect(Collectors.toList()); } } 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..6b579be3b97 --- /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"); + + // Example 4: Find a ground item by multiple names + Rs2TileItemModel loot = cache.query().withNames("Dragon bones", "Dragon scale", "Dragon dagger"); + + // Example 5: Find a ground item by ID + Rs2TileItemModel itemById = cache.query().withId(995); // Coins + + // Example 6: Find a ground item by multiple IDs + Rs2TileItemModel itemByIds = cache.query().withIds(995, 526, 537); // 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.getGroundItemsStream() + .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..1d9512cb6c6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/models/Rs2TileItemModel.java @@ -0,0 +1,169 @@ +package net.runelite.client.plugins.microbot.api.tileitem.models; + +import net.runelite.api.*; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; + +import java.util.function.Supplier; + +public class Rs2TileItemModel implements TileItem { + + private final Tile tile; + 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(); + } + + 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(); + }); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItemModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItemModel.java deleted file mode 100644 index 4c011502e45..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItemModel.java +++ /dev/null @@ -1,874 +0,0 @@ -package net.runelite.client.plugins.microbot.util.grounditem; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Constants; -import net.runelite.api.ItemComposition; -import net.runelite.api.Perspective; -import net.runelite.api.Point; -import net.runelite.api.Scene; -import net.runelite.api.Tile; -import net.runelite.api.TileItem; -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.util.grandexchange.Rs2GrandExchange; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import net.runelite.client.util.RSTimeUnit; - -import java.awt.Rectangle; - -import java.time.Duration; -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; - -/** - * Enhanced model for ground items with caching, tick tracking, and despawn utilities. - * Provides a comprehensive replacement for the deprecated RS2Item class. - * Includes looting utility methods and value-based filtering for automation. - * - * @author Vox - * @version 2.0 - */ -@Data -@Getter -@EqualsAndHashCode -@Slf4j -public class Rs2GroundItemModel { - - private final TileItem tileItem; - private final Tile tile; - private ItemComposition itemComposition; - private final WorldPoint location; - private final int id; - private final int quantity; - private String name; - private final boolean isOwned; - private final boolean isLootAble; - private final long creationTime; - private final int creationTick; - - // Despawn tracking fields - following GroundItemsOverlay pattern - private final Instant spawnTime; - private final Duration despawnDuration; - private final Duration visibleDuration; - - /** - * Creates a new Rs2GroundItemModel from a TileItem and Tile. - * - * @param tileItem The TileItem from the game - * @param tile The tile the item is on - */ - public Rs2GroundItemModel(TileItem tileItem, Tile tile) { - this.tileItem = tileItem; - this.tile = tile; - this.location = tile.getWorldLocation(); - this.id = tileItem.getId(); - this.quantity = tileItem.getQuantity(); - this.isOwned = tileItem.getOwnership() == TileItem.OWNERSHIP_SELF; - this.isLootAble = !(tileItem.getOwnership() == TileItem.OWNERSHIP_OTHER); - this.creationTime = System.currentTimeMillis(); - this.creationTick = Microbot.getClient().getTickCount(); - - - // Initialize despawn tracking following GroundItemsPlugin.buildGroundItem() pattern - this.spawnTime = Instant.now(); - - // Calculate despawn time exactly like GroundItemsPlugin.buildGroundItem() - // final int despawnTime = item.getDespawnTime() - client.getTickCount(); - // .despawnTime(Duration.of(despawnTime, RSTimeUnit.GAME_TICKS)) - int despawnTime = 0; - int visibleTime = 0; - if (tileItem.getDespawnTime() > this.creationTick){ - despawnTime = tileItem.getDespawnTime() - this.creationTick; - - } - if (tileItem.getVisibleTime() > this.creationTick) { - visibleTime = tileItem.getVisibleTime() - this.creationTick; - } - // Use the exact same pattern as official RuneLite GroundItemsPlugin - this.despawnDuration = Duration.of(despawnTime, RSTimeUnit.GAME_TICKS); - this.visibleDuration = Duration.of(visibleTime, RSTimeUnit.GAME_TICKS); - - // Initialize composition and name as null for lazy loading - this.itemComposition = null; - this.name = null; - log.debug("Created Rs2GroundItemModel: {} x{} at {} | Spawn: {} | Despawn: {} (Local) | tick despawn: {} | current tick: {}", - getName(), quantity, location, - spawnTime.atZone(ZoneOffset.systemDefault()).format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), - getDespawnTime().atZone(ZoneOffset.systemDefault()).format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), - tileItem.getDespawnTime(), - this.creationTick - ); - } - - /** - * Lazy loads the ItemComposition if not already loaded. - * This ensures we can work with ground items while minimizing performance impact. - */ - private void ensureCompositionLoaded() { - if (itemComposition == null && id > 0) { - try { - this.itemComposition = Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getClient().getItemDefinition(id) - ).orElse(null); - - if (itemComposition != null) { - this.name = itemComposition.getName(); - } else { - log.warn("Failed to load ItemComposition for ground item id: {}, setting default name", id); - this.name = "Unknown Item"; - } - } catch (Exception e) { - log.warn("Error loading ItemComposition for ground item id: {}, using defaults: {}", id, e.getMessage()); - this.name = "Unknown Item"; - this.itemComposition = null; - } - } - } - - /** - * Gets the item name, loading composition if needed. - * - * @return The item name or "Unknown Item" if composition fails to load - */ - public String getName() { - if (name == null) { - ensureCompositionLoaded(); - } - return name != null ? name : "Unknown Item"; - } - - /** - * Gets the item composition, loading it if needed. - * - * @return The ItemComposition or null if it fails to load - */ - public ItemComposition getItemComposition() { - if (itemComposition == null) { - ensureCompositionLoaded(); - } - return itemComposition; - } - - // ============================================ - // Time and Tick Tracking Methods - // ============================================ - - /** - * Gets the number of ticks since this item was created. - * - * @return The number of ticks since creation - */ - public int getTicksSinceCreation() { - return Microbot.getClient().getTickCount() - creationTick; - } - - /** - * Gets the number of ticks since this item spawned (alias for getTicksSinceCreation). - * - * @return The number of ticks since spawn - */ - public int getTicksSinceSpawn() { - return getTicksSinceCreation(); - } - - /** - * Gets the time in milliseconds since this item was created. - * - * @return Milliseconds since creation - */ - public long getTimeSinceCreation() { - return System.currentTimeMillis() - creationTime; - } - - // ============================================ - // Despawn Tracking Methods - // ============================================ - - /** - * Gets the spawn time as an Instant (following GroundItemsOverlay pattern). - * - * @return Instant when the item spawned - */ - public Instant getSpawnTime() { - return spawnTime; - } - - /** - * Gets the despawn duration for this item. - * - * @return Duration until despawn from spawn time - */ - public Duration getDespawnDuration() { - return despawnDuration; - } - - /** - * Gets the absolute time when this item will despawn (following GroundItemsOverlay pattern). - * - * @return Instant when the item despawns - */ - public Instant getDespawnTime() { - return spawnTime.plus(despawnDuration); - } - - /** - * Gets the UTC timestamp when this item will despawn. - * - * @return UTC timestamp in milliseconds when item despawns - */ - public long getDespawnTimestampUtc() { - return getDespawnTime().toEpochMilli(); - } - - /** - * Gets the duration remaining until this item despawns. - * - * @return Duration until despawn, or Duration.ZERO if already despawned - */ - public Duration getTimeUntilDespawn() { - Instant despawnTime = getDespawnTime(); - Instant now = Instant.now(); - - if (now.isAfter(despawnTime)) { - return Duration.ZERO; - } - - return Duration.between(now, despawnTime); - } - - /** - * Gets the number of ticks remaining until this item despawns. - * - * @return Ticks until despawn, or 0 if already despawned - */ - public int getTicksUntilDespawn() { - // Convert despawnDuration back to ticks using RSTimeUnit pattern - long despawnTicks = despawnDuration.toMillis() / Constants.GAME_TICK_LENGTH; - - int currentTick = Microbot.getClientThread().runOnClientThreadOptional( - () -> Microbot.getClient().getTickCount() - ).orElse((int)(creationTick + despawnTicks + 1)); // Fallback assumes despawned - - int ticksSinceSpawn = currentTick - creationTick; - long ticksRemaining = despawnTicks - ticksSinceSpawn; - return Math.max(0, (int)ticksRemaining); - } - - // ============================================ - // UTC Timestamp Getter Methods - // ============================================ - - /** - * Gets the UTC spawn time as ZonedDateTime. - * - * @return Spawn time in UTC - */ - public ZonedDateTime getSpawnTimeUtc() { - return spawnTime.atZone(ZoneOffset.UTC); - } - - /** - * Gets the UTC spawn timestamp in milliseconds. - * - * @return Spawn timestamp in UTC milliseconds - */ - public long getSpawnTimestampUtc() { - return spawnTime.toEpochMilli(); - } - - /** - * Gets the total number of ticks this item should exist before despawning. - * - * @return Total despawn ticks - */ - public int getDespawnTicks() { - return (int)(despawnDuration.toMillis() / Constants.GAME_TICK_LENGTH); - } - - /** - * Gets the number of seconds remaining until this item despawns. - * - * @return Seconds until despawn, or 0 if already despawned - */ - public long getSecondsUntilDespawn() { - return getTimeUntilDespawn().getSeconds(); - } - - /** - * Checks if this item has despawned based on game ticks. - * This is more accurate than real-time checking since OSRS is tick-based. - * - * @return true if the item should have despawned - */ - public boolean isDespawned() { - if (isPersistened()) { - return false; // Persisted items never despawn - } - - return Instant.now().isAfter(getDespawnTime()); - // Use tick-based calculation for more accurate game timing - //long despawnTicks = despawnDuration.toMillis() / Constants.GAME_TICK_LENGTH; - //int currentTick = Microbot.getClient().getTickCount(); - //int ticksSinceSpawn = currentTick - creationTick; - //return ticksSinceSpawn >= despawnTicks; - } - public boolean isPersistened(){ - // Check if the despawn duration is negative or very large - return despawnDuration.isNegative() ||despawnDuration.isZero() || despawnDuration.toMillis() > 24 * 60 * 60 * 1000; // More than 24 hours - } - - /** - * Checks if this item has despawned based on UTC timestamp (fallback method). - * Less accurate than tick-based method but useful when client is unavailable. - * - * @return true if the item should have despawned based on time - */ - public boolean isDespawnedByTime() { - return ZonedDateTime.now(ZoneOffset.UTC).isAfter(getDespawnTime().atZone(ZoneOffset.UTC)); - } /** - * Checks if this item will despawn within the specified number of seconds. - * - * @param seconds The time threshold in seconds - * @return true if the item will despawn within the given time - */ - public boolean willDespawnWithin(long seconds) { - return getSecondsUntilDespawn() <= seconds; - } - - /** - * Checks if this item will despawn within the specified number of ticks. - * - * @param ticks The time threshold in ticks - * @return true if the item will despawn within the given ticks - */ - public boolean willDespawnWithinTicks(int ticks) { - return getTicksUntilDespawn() <= ticks; - } - - // ============================================ - // Item Property Methods - // ============================================ - - /** - * Checks if this item is stackable. - * - * @return true if stackable, false otherwise - */ - public boolean isStackable() { - try { - ItemComposition composition = getItemComposition(); - return composition != null && composition.isStackable(); - } catch (Exception e) { - log.warn("Error checking if item is stackable for id: {}: {}", id, e.getMessage()); - return false; - } - } - - /** - * Checks if this item is noted. - * - * @return true if noted, false otherwise - */ - public boolean isNoted() { - try { - ItemComposition composition = getItemComposition(); - return composition != null && composition.getNote() != -1; - } catch (Exception e) { - log.warn("Error checking if item is noted for id: {}: {}", id, e.getMessage()); - return false; - } - } - - /** - * Gets the item's store value. - * - * @return The item's store value - */ - public int getValue() { - try { - ItemComposition composition = getItemComposition(); - return composition != null ? composition.getPrice() : 0; - } catch (Exception e) { - log.warn("Error getting value for item id: {}: {}", id, e.getMessage()); - return 0; - } - } - - /** - * Gets the item's Grand Exchange price. - * - * @return The item's GE price - */ - public int getPrice() { - return Rs2GrandExchange.getPrice(this.id); - } - - /** - * Gets the total value of this item stack (quantity * unit value). - * - * @return The total stack value - */ - public int getTotalValue() { - return getValue() * quantity; - } - - /** - * Gets the total Grand Exchange value of this item stack. - * - * @return The total stack GE value - */ - public int getTotalGeValue() { - return getPrice() * quantity; - } - - /** - * Gets the item's high alchemy value. - * - * @return The high alchemy value - */ - public int getHaPrice() { - try { - ItemComposition composition = getItemComposition(); - return composition != null ? composition.getHaPrice() : 0; - } catch (Exception e) { - log.warn("Error getting high alchemy price for item id: {}: {}", id, e.getMessage()); - return 0; - } - } - - /** - * Gets the total high alchemy value of this item stack. - * - * @return The total stack high alchemy value - */ - public int getTotalHaValue() { - return getHaPrice() * quantity; - } - - /** - * Gets the item's low alchemy value. - * This is calculated as 40% of the store price. - * - * @return The low alchemy value - */ - public int getLaValue() { - try { - ItemComposition composition = getItemComposition(); - return composition != null ? (int)(composition.getPrice() * 0.4) : 0; - } catch (Exception e) { - log.warn("Error getting low alchemy value for item id: {}: {}", id, e.getMessage()); - return 0; - } - } - - /** - * Gets the total low alchemy value of this item stack. - * - * @return The total stack low alchemy value - */ - public int getTotalLaValue() { - return getLaValue() * quantity; - } - - /** - * Checks if this item is members-only. - * - * @return true if members-only, false otherwise - */ - public boolean isMembers() { - try { - ItemComposition composition = getItemComposition(); - return composition != null && composition.isMembers(); - } catch (Exception e) { - log.warn("Error checking if item is members-only for id: {}: {}", id, e.getMessage()); - return false; - } - } - - /** - * Checks if this item is tradeable. - * - * @return true if tradeable, false otherwise - */ - public boolean isTradeable() { - try { - ItemComposition composition = getItemComposition(); - return composition != null && composition.isTradeable(); - } catch (Exception e) { - log.warn("Error checking if item is tradeable for id: {}: {}", id, e.getMessage()); - return false; - } - } - - /** - * Gets the item's inventory actions. - * - * @return Array of inventory actions - */ - public String[] getInventoryActions() { - try { - ItemComposition composition = getItemComposition(); - return composition != null ? composition.getInventoryActions() : new String[0]; - } catch (Exception e) { - log.warn("Error getting inventory actions for item id: {}: {}", id, e.getMessage()); - return new String[0]; - } - } - - // ============================================ - // Looting Utility Methods - // ============================================ - - /** - * Checks if this item is worth looting based on minimum value. - * - * @param minValue The minimum value threshold - * @return true if the item's total value meets the threshold - */ - public boolean isWorthLooting(int minValue) { - return getTotalValue() >= minValue; - } - - /** - * Checks if this item is worth looting based on Grand Exchange value. - * - * @param minGeValue The minimum GE value threshold - * @return true if the item's total GE value meets the threshold - */ - public boolean isWorthLootingGe(int minGeValue) { - return getTotalGeValue() >= minGeValue; - } - - /** - * Checks if this item is worth high alching based on profit margin. - * - * @param minProfit The minimum profit threshold - * @return true if high alching would be profitable - */ - public boolean isProfitableToHighAlch(int minProfit) { - // High alch value minus nature rune cost (estimated) - int profit = getTotalHaValue() - (quantity * 200); // Assuming 200gp nature rune cost - return profit >= minProfit; - } - - /** - * Checks if this item is a commonly desired loot type. - * Includes coins, gems, ores, logs, herbs, and high-value items. - * - * @return true if the item is commonly looted - */ - public boolean isCommonLoot() { - if (name == null) return false; - - String lowerName = name.toLowerCase(); - - // Always loot coins - if (lowerName.contains("coins")) return true; - - // High value items - if (getTotalValue() >= 1000) return true; - - // Common valuable items - return lowerName.contains("gem") || - lowerName.contains("ore") || - lowerName.contains("bar") || - lowerName.contains("log") || - lowerName.contains("herb") || - lowerName.contains("seed") || - lowerName.contains("rune") || - lowerName.contains("arrow") || - lowerName.contains("bolt"); - } - - /** - * Checks if this item should be prioritized for urgent looting. - * Based on high value and short despawn time. - * - * @return true if the item should be prioritized - */ - public boolean shouldPrioritize() { - // High value items or items about to despawn - return getTotalValue() >= 5000 || willDespawnWithin(30); - } - - // ============================================ - // Distance and Position Methods - // ============================================ - - /** - * Gets the distance to this item from the player. - * - * @return The distance in tiles - */ - public int getDistanceFromPlayer() { - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - WorldPoint playerLocation = - Rs2Player.getWorldLocation(); - return playerLocation.distanceTo(location); - }).orElse(Integer.MAX_VALUE); - } - - /** - * Checks if this item is within a certain distance from the player. - * - * @param maxDistance The maximum distance in tiles - * @return true if within distance, false otherwise - */ - public boolean isWithinDistanceFromPlayer(int maxDistance) { - return getDistanceFromPlayer() <= maxDistance; - } - - /** - * Gets the distance to this item from a specific point. - * - * @param point The world point to measure from - * @return The distance in tiles - */ - public int getDistanceFrom(WorldPoint point) { - return location.distanceTo(point); - } - - /** - * Checks if this item is within a certain distance from a specific point. - * - * @param point The world point to measure from - * @param maxDistance The maximum distance in tiles - * @return true if within distance, false otherwise - */ - public boolean isWithinDistanceFrom(WorldPoint point, int maxDistance) { - return getDistanceFrom(point) <= maxDistance; - } - - // ============================================ - // Scene and Viewport Detection Methods - // ============================================ - - /** - * Checks if this ground item is still present in the current scene. - * This verifies that the TileItem still exists on its tile in the scene. - * - * @return true if the item is still in the current scene, false otherwise - */ - public boolean isInCurrentScene() { - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - try { - Scene scene = Microbot.getClient().getTopLevelWorldView().getScene(); - - // Check if the world point is within current scene bounds using WorldPoint.isInScene - if (!WorldPoint.isInScene(scene, location.getX(), location.getY())) { - return false; - } - - // Convert world point to local coordinates for scene tile access - LocalPoint localPoint = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), location); - if (localPoint == null) { - return false; - } - - // Get the tile from the scene using local coordinates - Tile[][][] sceneTiles = scene.getTiles(); - int plane = location.getPlane(); - int sceneX = localPoint.getSceneX(); - int sceneY = localPoint.getSceneY(); - - // Validate scene coordinates - if (plane < 0 || plane >= sceneTiles.length || - sceneX < 0 || sceneX >= Constants.SCENE_SIZE || - sceneY < 0 || sceneY >= Constants.SCENE_SIZE) { - return false; - } - - Tile sceneTile = sceneTiles[plane][sceneX][sceneY]; - if (sceneTile == null) { - return false; - } - - // Check if our TileItem is still on this tile - if (sceneTile.getGroundItems() != null) { - for (TileItem item : sceneTile.getGroundItems()) { - if (item.getId() == this.id && - item.getQuantity() == this.quantity && - item.equals(this.tileItem)) { - return true; - } - } - } - - return false; - } catch (Exception e) { - log.warn("Error checking if ground item is in current scene: {}", e.getMessage()); - return false; - } - }).orElse(false); - } - - /** - * Checks if this ground item is visible and clickable in the current viewport. - * This combines scene presence checking with viewport visibility detection. - * - * @return true if the item is visible and clickable in the viewport, false otherwise - */ - public boolean isVisibleInViewport() { - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - try { - // First check if item is still in scene - if (!isInCurrentScene()) { - return false; - } - - // Convert world location to canvas point using Perspective.localToCanvas - LocalPoint localPoint = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), location); - if (localPoint == null) { - return false; - } - - Point canvasPoint = Perspective.localToCanvas(Microbot.getClient(), localPoint, location.getPlane()); - if (canvasPoint == null) { - return false; - } - - // Check if the point is within the viewport bounds - // Following the pattern from Rs2ObjectCacheUtils.isPointInViewport - int viewportX = Microbot.getClient().getViewportXOffset(); - int viewportY = Microbot.getClient().getViewportYOffset(); - int viewportWidth = Microbot.getClient().getViewportWidth(); - int viewportHeight = Microbot.getClient().getViewportHeight(); - - return canvasPoint.getX() >= viewportX && - canvasPoint.getX() <= viewportX + viewportWidth && - canvasPoint.getY() >= viewportY && - canvasPoint.getY() <= viewportY + viewportHeight; - - } catch (Exception e) { - log.warn("Error checking if ground item is visible in viewport: {}", e.getMessage()); - return false; - } - }).orElse(false); - } - - /** - * Gets the canvas point for this ground item if it's visible. - * Useful for click operations and overlay rendering. - * - * @return the Point on the canvas, or null if not visible - */ - public Point getCanvasPoint() { - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - try { - if (!isInCurrentScene()) { - return null; - } - - LocalPoint localPoint = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), location); - if (localPoint == null) { - return null; - } - - return Perspective.localToCanvas(Microbot.getClient(), localPoint, location.getPlane()); - } catch (Exception e) { - log.warn("Error getting canvas point for ground item: {}", e.getMessage()); - return null; - } - }).orElse(null); - } - - /** - * Checks if this ground item is clickable (in scene and in viewport). - * Convenience method that combines isInCurrentScene() and isVisibleInViewport(). - * - * @return true if the item can be clicked, false otherwise - */ - public boolean isClickable() { - return isInCurrentScene() && isVisibleInViewport(); - } - - /** - * Checks if this ground item is currently clickable by the player within a specific distance. - * This combines scene presence, viewport visibility, and distance checks. - * - * @param maxDistance The maximum interaction distance in tiles - * @return true if the item is clickable within the specified distance, false otherwise - */ - public boolean isClickable(int maxDistance) { - // Check if item is lootable first - if (!isLootAble) { - return false; - } - - // Check if item has despawned - if (isDespawned()) { - return false; - } - - // Check distance from player - if (!isWithinDistanceFromPlayer(maxDistance)) { - return false; - } - - // Check if visible in viewport (includes scene presence check) - return isVisibleInViewport(); - } - - /** - * Gets the viewport bounds as a Rectangle for utility calculations. - * Following the pattern from Rs2ObjectCacheUtils.getViewportBounds. - * - * @return Rectangle representing the current viewport bounds, or null if unavailable - */ - public Rectangle getViewportBounds() { - return Microbot.getClientThread().runOnClientThreadOptional(() -> { - try { - int viewportX = Microbot.getClient().getViewportXOffset(); - int viewportY = Microbot.getClient().getViewportYOffset(); - int viewportWidth = Microbot.getClient().getViewportWidth(); - int viewportHeight = Microbot.getClient().getViewportHeight(); - - return new Rectangle(viewportX, viewportY, viewportWidth, viewportHeight); - } catch (Exception e) { - log.warn("Error getting viewport bounds: {}", e.getMessage()); - return null; - } - }).orElse(null); - } - - // ============================================ - // Utility Methods - // ============================================ - - /** - * Gets a string representation of this item with UTC timing information. - * - * @return String representation - */ - @Override - public String toString() { - return String.format("Rs2GroundItemModel{id=%d, name='%s', quantity=%d, location=%s, owned=%s, lootable=%s, value=%d, despawnTicksLeft=%d, spawnTimeUtc='%s', despawnTimeUtc='%s'}", - id, name, quantity, location, isOwned, isLootAble, getTotalValue(), getTicksUntilDespawn(), - getSpawnTimeUtc().toString(), getDespawnTime().atZone(ZoneOffset.UTC).toString()); - } - - /** - * Gets a detailed string representation including all properties. - * - * @return Detailed string representation - */ - public String toDetailedString() { - return String.format( - "Rs2GroundItemModel{" + - "id=%d, name='%s', quantity=%d, location=%s, " + - "owned=%s, lootable=%s, stackable=%s, noted=%s, tradeable=%s, " + - "value=%d, geValue=%d, haValue=%d, totalValue=%d, " + - "ticksSinceSpawn=%d, ticksUntilDespawn=%d, secondsUntilDespawn=%d" + - "}", - id, name, quantity, location, - isOwned, isLootAble, isStackable(), isNoted(), isTradeable(), - getValue(), getPrice(), getHaPrice(), getTotalValue(), - getTicksSinceSpawn(), getTicksUntilDespawn(), getSecondsUntilDespawn() - ); - } -} From cb34059ef846eadc3fc7193b3fd65cd69d8a8f45 Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 15 Nov 2025 15:59:29 +0100 Subject: [PATCH 23/42] refactor(api): simplify ground items retrieval and add line of sight check --- .../api/tileitem/Rs2TileItemCache.java | 16 +--- .../api/tileitem/models/Rs2TileItemModel.java | 90 +++++++++++++++++++ 2 files changed, 93 insertions(+), 13 deletions(-) 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 index 3f1db345106..697fd7a0878 100644 --- 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 @@ -6,6 +6,7 @@ import net.runelite.client.eventbus.Subscribe; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; import javax.inject.Inject; import javax.inject.Singleton; @@ -16,8 +17,7 @@ @Singleton public class Rs2TileItemCache { - private static int lastUpdateGroundItems = 0; - private static List groundItems = new ArrayList<>(); + private static final List groundItems = new ArrayList<>(); @Inject public Rs2TileItemCache(EventBus eventBus) { @@ -35,17 +35,7 @@ public Rs2TileItemQueryable query() { * @return Stream of Rs2GroundItemModel */ public static Stream getGroundItemsStream() { - - if (lastUpdateGroundItems >= Microbot.getClient().getTickCount()) { - return groundItems.stream(); - } - - // Use the existing ground item cache - List result = new ArrayList<>(); - - groundItems = result; - lastUpdateGroundItems = Microbot.getClient().getTickCount(); - return result.stream(); + return groundItems.stream().filter(x -> x.getWorldLocation().getPlane() == Rs2Player.getWorldLocation().getPlane()); } @Subscribe 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 index 1d9512cb6c6..5802e5d84b4 100644 --- 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 @@ -1,15 +1,24 @@ 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.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 { + @Getter private final Tile tile; + @Getter private final TileItem tileItem; public Rs2TileItemModel(Tile tileObject, TileItem tileItem) { @@ -166,4 +175,85 @@ public int getTotalValue() { return price * tileItem.getQuantity(); }); } + + + public boolean hasLineOfSight() { + WorldPoint worldPoint = Rs2Player.getWorldLocation(); + if (worldPoint == null) { + return false; + } + return Microbot.getClientThread().invoke((Supplier) () -> + tile.getWorldLocation() + .toWorldArea() + .hasLineOfSightTo(Microbot.getClient().getTopLevelWorldView(), worldPoint.toWorldArea())); + } + + 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(action, param0, param1, menuAction.getId(), identifier, -1, target), + canvas.getBounds()); + } + } else { + Microbot.doInvoke(new NewMenuEntry(action, param0, param1, menuAction.getId(), identifier, -1, target), + new Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); + + } + } catch (Exception ex) { + Microbot.log(ex.getMessage()); + ex.printStackTrace(); + } + return true; + } } From 239b187a923a25cc1de0f032e5312cbf17e1203a Mon Sep 17 00:00:00 2001 From: chsami Date: Sun, 16 Nov 2025 12:30:45 +0100 Subject: [PATCH 24/42] refactor(api): introduce IEntity interface and update related models --- .../client/plugins/microbot/Microbot.java | 8 +- .../microbot/api/AbstractEntityQueryable.java | 170 ++++ .../client/plugins/microbot/api/IEntity.java | 13 + .../microbot/api/IEntityQueryable.java | 5 + .../microbot/api/npc/Rs2NpcQueryable.java | 86 +- .../microbot/api/player/PlayerApiExample.java | 10 +- .../microbot/api/player/Rs2PlayerCache.java | 18 +- .../api/player/Rs2PlayerQueryable.java | 98 +-- .../api/playerstate/Rs2PlayerStateCache.java | 177 ++++ .../api/tileitem/Rs2TileItemCache.java | 70 +- .../api/tileitem/Rs2TileItemQueryable.java | 88 +- .../api/tileitem/models/Rs2TileItemModel.java | 3 +- .../tileobject/Rs2TileObjectQueryable.java | 85 +- .../tileobject/models/Rs2TileObjectModel.java | 7 +- .../microbot/example/ExampleScript.java | 77 +- .../plugins/microbot/util/ActorModel.java | 2 +- .../microbot/util/npc/Rs2NpcModel.java | 17 +- .../microbot/util/player/Rs2Player.java | 2 +- .../microbot/util/player/Rs2PlayerCache.java | 176 ---- .../microbot/util/player/Rs2PlayerModel.java | 17 +- .../microbot/util/security/LoginManager.java | 766 ++++++++++-------- 21 files changed, 923 insertions(+), 972 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/IEntity.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/playerstate/Rs2PlayerStateCache.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerCache.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java index 25132fbb46a..cf978ff1e9d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java @@ -42,7 +42,7 @@ import net.runelite.client.plugins.microbot.util.mouse.Mouse; import net.runelite.client.plugins.microbot.util.mouse.VirtualMouse; import net.runelite.client.plugins.microbot.util.mouse.naturalmouse.NaturalMouse; -import net.runelite.client.plugins.microbot.util.player.Rs2PlayerCache; +import net.runelite.client.plugins.microbot.api.playerstate.Rs2PlayerStateCache; import net.runelite.client.plugins.microbot.util.security.LoginManager; import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; import net.runelite.client.ui.overlay.infobox.InfoBoxManager; @@ -190,7 +190,7 @@ public class Microbot { @Inject @Getter - private static Rs2PlayerCache rs2PlayerCache; + private static Rs2PlayerStateCache rs2PlayerStateCache; /** * Get the total runtime of the script @@ -215,11 +215,11 @@ public static boolean isDebug() { } public static int getVarbitValue(@Varbit int varbit) { - return rs2PlayerCache.getVarbitValue(varbit); + return rs2PlayerStateCache.getVarbitValue(varbit); } public static int getVarbitPlayerValue(@Varp int varpId) { - return rs2PlayerCache.getVarpValue(varpId); + return rs2PlayerStateCache.getVarpValue(varpId); } public static EnumComposition getEnum(int id) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java new file mode 100644 index 00000000000..d3550c283ee --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java @@ -0,0 +1,170 @@ +package net.runelite.client.plugins.microbot.api; + +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Generic abstract implementation of {@link IEntityQueryable} to reduce duplication. + * + * @param concrete queryable type (self type) + * @param entity model type + */ +public abstract class AbstractEntityQueryable< + Q extends IEntityQueryable, + E extends IEntity + > + implements IEntityQueryable { + + protected Stream source; + + protected AbstractEntityQueryable() { + this.source = initialSource(); + } + + /** + * Provide the initial stream to query against. + */ + protected abstract Stream initialSource(); + + + /** + * Player location used for proximity queries. + */ + protected WorldPoint getPlayerLocation() { + return Rs2Player.getWorldLocation(); + } + + @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 = getPlayerLocation(); + if (playerLoc == null) { + return null; + } + + 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) { + return null; + } + + this.source = this.source + .filter(o -> o.getWorldLocation().distanceTo(anchor) <= distance); + + return (Q) this; + } + + @Override + public E first() { + return source.findFirst().orElse(null); + } + + @Override + public E nearest() { + WorldPoint playerLoc = getPlayerLocation(); + if (playerLoc == null) { + return null; + } + return source + .min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(playerLoc))) + .orElse(null); + } + + @Override + public E nearest(int maxDistance) { + WorldPoint playerLoc = getPlayerLocation(); + if (playerLoc == null) { + return null; + } + return source + .filter(x -> { + WorldPoint loc = x.getWorldLocation(); + return loc != null && loc.distanceTo(playerLoc) <= maxDistance; + }) + .min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(playerLoc))) + .orElse(null); + } + + @Override + public E nearest(WorldPoint anchor, int maxDistance) { + if (anchor == null) { + return null; + } + return source + .filter(x -> { + WorldPoint loc = x.getWorldLocation(); + return loc != null && loc.distanceTo(anchor) <= maxDistance; + }) + .min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(anchor))) + .orElse(null); + } + + @Override + public E withName(String name) { + if (name == null) return null; + return Microbot.getClientThread().invoke(() -> { + return source.filter(x -> { + String n = x.getName(); + return n != null && n.equalsIgnoreCase(name); + }) + .min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(Rs2Player.getWorldLocation()))) + .orElse(null); + }); + } + + @Override + public E withNames(String... names) { + if (names == null || names.length == 0) return null; + return source.filter(x -> { + String n = x.getName(); + if (n == null) return false; + return Arrays.stream(names).anyMatch(n::equalsIgnoreCase); + }).min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(Rs2Player.getWorldLocation()))) + .orElse(null); + } + + @Override + public E withId(int id) { + return source.filter(x -> x.getId() == id).min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(Rs2Player.getWorldLocation()))) + .orElse(null); + } + + @Override + public E withIds(int... ids) { + if (ids == null || ids.length == 0) return null; + return source.filter(x -> { + int entityId = x.getId(); + for (int id : ids) { + if (entityId == id) return true; + } + return false; + }).min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(Rs2Player.getWorldLocation()))) + .orElse(null); + } + + @Override + public List toList() { + return source.collect(Collectors.toList()); + } +} 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..46a0cfe25af --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/IEntity.java @@ -0,0 +1,13 @@ +package net.runelite.client.plugins.microbot.api; + +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; + +public interface IEntity { + int getId(); + String getName(); + WorldPoint getWorldLocation(); + LocalPoint getLocalLocation(); + boolean click(); + boolean click(String action); +} 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 index 94dc3117940..c07ac959d58 100644 --- 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 @@ -1,10 +1,15 @@ package net.runelite.client.plugins.microbot.api; +import net.runelite.api.coords.WorldPoint; + public interface IEntityQueryable, E> { Q where(java.util.function.Predicate predicate); + Q within(int distance); + Q within(WorldPoint anchor, int distance); E first(); E nearest(); E nearest(int maxDistance); + E nearest(WorldPoint anchor, int maxDistance); E withName(String name); E withNames(String...names); E withId(int id); 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 index 2c8cb8110b8..2f54573104c 100644 --- 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 @@ -1,97 +1,21 @@ package net.runelite.client.plugins.microbot.api.npc; import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.api.AbstractEntityQueryable; import net.runelite.client.plugins.microbot.api.IEntityQueryable; import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import java.util.Arrays; -import java.util.stream.Collectors; import java.util.stream.Stream; - -public final class Rs2NpcQueryable +public final class Rs2NpcQueryable extends AbstractEntityQueryable implements IEntityQueryable { - private Stream source; - public Rs2NpcQueryable() { - this.source = Rs2NpcCache.getNpcsStream(); - } - - @Override - public Rs2NpcQueryable where(java.util.function.Predicate predicate) { - source = source.filter(predicate); - return this; - } - - @Override - public Rs2NpcModel first() { - return source.findFirst().orElse(null); - } - - @Override - public Rs2NpcModel nearest() { - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - if (playerLoc == null) { - return null; - } - - return source - .min(java.util.Comparator.comparingInt( - o -> o.getWorldLocation().distanceTo(playerLoc))) - .orElse(null); - } - - @Override - public Rs2NpcModel nearest(int maxDistance) { - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - if (playerLoc == null) { - return null; - } - - return source - .filter(x -> x.getWorldLocation().distanceTo(playerLoc) <= maxDistance) - .min(java.util.Comparator.comparingInt( - o -> o.getWorldLocation().distanceTo(playerLoc))) - .orElse(null); - } - - @Override - public Rs2NpcModel withName(String name) { - return source.filter(x -> x.getName() != null && x.getName().equalsIgnoreCase(name)) - .findFirst() - .orElse(null); - } - - @Override - public Rs2NpcModel withNames(String... names) { - return source.filter(x -> { - if (x.getName() == null) return false; - return Arrays.stream(names) - .anyMatch(name -> x.getName().equalsIgnoreCase(name)); - }).findFirst().orElse(null); - } - - @Override - public Rs2NpcModel withId(int id) { - return source.filter(x -> x.getId() == id) - .findFirst() - .orElse(null); - } - - @Override - public Rs2NpcModel withIds(int... ids) { - return source.filter(x -> { - for (int id : ids) { - if (x.getId() == id) return true; - } - return false; - }).findFirst().orElse(null); + super(); } @Override - public java.util.List toList() { - return source.collect(Collectors.toList()); + protected Stream initialSource() { + return Rs2NpcCache.getNpcsStream(); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java index 51197deec6a..a56ea75dca0 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java @@ -36,12 +36,6 @@ public static void examples() { // Example 6: Find a player by multiple IDs Rs2PlayerModel playerByIds = cache.query().withIds(12345, 67890, 11111); - // Example 7: Include local player in query - Rs2PlayerModel localPlayer = cache.query() - .includeLocalPlayer() - .where(player -> player.getPlayer() != null) - .first(); - // Example 8: Find all friends List friends = cache.query() .where(Rs2PlayerModel::isFriend) @@ -103,8 +97,8 @@ public static void examples() { .findFirst() .orElse(null); - // Example 19: Get all players including local player - List allPlayersIncludingMe = Rs2PlayerCache.getPlayersStream(true) + // Example 19: Get all players + List allPlayersIncludingMe = Rs2PlayerCache.getPlayersStream() .collect(Collectors.toList()); // Example 20: Find players by partial name match 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 index be02189541e..0bbcffeb37b 100644 --- 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 @@ -6,6 +6,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -18,27 +19,22 @@ public Rs2PlayerQueryable query() { return new Rs2PlayerQueryable(); } - /** - * Get all players in the current scene (excluding local player) - * @return Stream of Rs2PlayerModel - */ - public static Stream getPlayersStream() { - return getPlayersStream(false); - } - /** * Get all players in the current scene - * @param includeLocalPlayer whether to include the local player in the results * @return Stream of Rs2PlayerModel */ - public static Stream getPlayersStream(boolean includeLocalPlayer) { + public static Stream getPlayersStream() { if (lastUpdatePlayers >= Microbot.getClient().getTickCount()) { return players.stream(); } // Get all players using the existing Rs2Player utility - List result = Rs2Player.getPlayers(player -> true, includeLocalPlayer).collect(Collectors.toList()); + List result = Microbot.getClient().getTopLevelWorldView().players() + .stream() + .filter(Objects::nonNull) + .map(Rs2PlayerModel::new) + .collect(Collectors.toList()); players = result; lastUpdatePlayers = Microbot.getClient().getTickCount(); 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 index f41339efdb8..f4ab6ef1186 100644 --- 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 @@ -1,108 +1,20 @@ package net.runelite.client.plugins.microbot.api.player; -import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.api.AbstractEntityQueryable; import net.runelite.client.plugins.microbot.api.IEntityQueryable; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; -import java.util.Arrays; -import java.util.stream.Collectors; import java.util.stream.Stream; - -public final class Rs2PlayerQueryable +public final class Rs2PlayerQueryable extends AbstractEntityQueryable implements IEntityQueryable { - private Stream source; - private boolean includeLocalPlayer = false; - public Rs2PlayerQueryable() { - this.source = Rs2PlayerCache.getPlayersStream(); - } - - /** - * Include the local player in the query results - * @return this queryable for chaining - */ - public Rs2PlayerQueryable includeLocalPlayer() { - this.includeLocalPlayer = true; - this.source = Rs2PlayerCache.getPlayersStream(true); - return this; - } - - @Override - public Rs2PlayerQueryable where(java.util.function.Predicate predicate) { - source = source.filter(predicate); - return this; - } - - @Override - public Rs2PlayerModel first() { - return source.findFirst().orElse(null); - } - - @Override - public Rs2PlayerModel nearest() { - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - if (playerLoc == null) { - return null; - } - - return source - .min(java.util.Comparator.comparingInt( - o -> o.getWorldLocation().distanceTo(playerLoc))) - .orElse(null); - } - - @Override - public Rs2PlayerModel nearest(int maxDistance) { - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - if (playerLoc == null) { - return null; - } - - return source - .filter(x -> x.getWorldLocation().distanceTo(playerLoc) <= maxDistance) - .min(java.util.Comparator.comparingInt( - o -> o.getWorldLocation().distanceTo(playerLoc))) - .orElse(null); - } - - @Override - public Rs2PlayerModel withName(String name) { - return source.filter(x -> x.getName() != null && x.getName().equalsIgnoreCase(name)) - .findFirst() - .orElse(null); - } - - @Override - public Rs2PlayerModel withNames(String... names) { - return source.filter(x -> { - if (x.getName() == null) return false; - return Arrays.stream(names) - .anyMatch(name -> x.getName().equalsIgnoreCase(name)); - }).findFirst().orElse(null); - } - - @Override - public Rs2PlayerModel withId(int id) { - return source.filter(x -> x.getId() == id) - .findFirst() - .orElse(null); - } - - @Override - public Rs2PlayerModel withIds(int... ids) { - return source.filter(x -> { - for (int id : ids) { - if (x.getId() == id) return true; - } - return false; - }).findFirst().orElse(null); + super(); } @Override - public java.util.List toList() { - return source.collect(Collectors.toList()); + protected Stream initialSource() { + return Rs2PlayerCache.getPlayersStream(); } } 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 index 697fd7a0878..54de5e425b6 100644 --- 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 @@ -1,53 +1,71 @@ package net.runelite.client.plugins.microbot.api.tileitem; -import net.runelite.api.events.ItemDespawned; -import net.runelite.api.events.ItemSpawned; -import net.runelite.client.eventbus.EventBus; -import net.runelite.client.eventbus.Subscribe; +import net.runelite.api.Player; +import net.runelite.api.Tile; +import net.runelite.api.TileItem; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import javax.inject.Inject; import javax.inject.Singleton; import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; +/** + * Cache for ground items (tile items) in the current scene. + * Uses polling-based approach to ensure reliability, as ItemSpawned/ItemDespawned events + * are not always triggered consistently. + */ @Singleton public class Rs2TileItemCache { - private static final List groundItems = new ArrayList<>(); - - @Inject - public Rs2TileItemCache(EventBus eventBus) { - eventBus.register(this); - } + private static int lastUpdateTick = 0; + private static List groundItems = new ArrayList<>(); public Rs2TileItemQueryable query() { return new Rs2TileItemQueryable(); } /** - * Get all ground items in the current scene - * Uses the existing ground item cache from util.cache package + * Get all ground items in the current scene. + * 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 Rs2GroundItemModel + * @return Stream of Rs2TileItemModel */ public static Stream getGroundItemsStream() { - return groundItems.stream().filter(x -> x.getWorldLocation().getPlane() == Rs2Player.getWorldLocation().getPlane()); - } + // Only refresh once per tick to avoid unnecessary scanning + if (lastUpdateTick >= Microbot.getClient().getTickCount()) { + return groundItems.stream(); + } - @Subscribe - public void onItemSpawned(ItemSpawned event) { - groundItems.add(new Rs2TileItemModel(event.getTile(), event.getItem())); - } + Player player = Microbot.getClient().getLocalPlayer(); + if (player == null) return Stream.empty(); + + List result = new ArrayList<>(); + + // Get all tiles in current plane + var tileValues = Microbot.getClient().getTopLevelWorldView().getScene().getTiles()[Microbot.getClient().getTopLevelWorldView().getPlane()]; + + for (Tile[] tileRow : tileValues) { + for (Tile tile : tileRow) { + if (tile == null) continue; + + List items = tile.getGroundItems(); + if (items == null || items.isEmpty()) continue; + + // Add all items from this tile to the result + for (TileItem item : items) { + if (item != null) { + result.add(new Rs2TileItemModel(tile, item)); + } + } + } + } - @Subscribe - public void onItemDespawned(ItemDespawned event) { - groundItems.removeIf(groundItem -> groundItem.getId() == event.getItem().getId() && - groundItem.getWorldLocation().equals(event.getTile().getWorldLocation()) - && groundItem.getLocalLocation().equals(event.getTile().getLocalLocation()) - ); + groundItems = 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 index f8461d6bfaf..1fce214b829 100644 --- 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 @@ -1,97 +1,21 @@ package net.runelite.client.plugins.microbot.api.tileitem; import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.api.IEntityQueryable; +import net.runelite.client.plugins.microbot.api.AbstractEntityQueryable; +import net.runelite.client.plugins.microbot.api.IEntityQueryable; // optional import net.runelite.client.plugins.microbot.api.tileitem.models.Rs2TileItemModel; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import java.util.Arrays; -import java.util.stream.Collectors; import java.util.stream.Stream; - -public final class Rs2TileItemQueryable +public final class Rs2TileItemQueryable extends AbstractEntityQueryable implements IEntityQueryable { - private Stream source; - public Rs2TileItemQueryable() { - this.source = Rs2TileItemCache.getGroundItemsStream(); - } - - @Override - public Rs2TileItemQueryable where(java.util.function.Predicate predicate) { - source = source.filter(predicate); - return this; - } - - @Override - public Rs2TileItemModel first() { - return source.findFirst().orElse(null); - } - - @Override - public Rs2TileItemModel nearest() { - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - if (playerLoc == null) { - return null; - } - - return source - .min(java.util.Comparator.comparingInt( - o -> o.getWorldLocation().distanceTo(playerLoc))) - .orElse(null); - } - - @Override - public Rs2TileItemModel nearest(int maxDistance) { - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - if (playerLoc == null) { - return null; - } - - return source - .filter(x -> x.getWorldLocation().distanceTo(playerLoc) <= maxDistance) - .min(java.util.Comparator.comparingInt( - o -> o.getWorldLocation().distanceTo(playerLoc))) - .orElse(null); - } - - @Override - public Rs2TileItemModel withName(String name) { - return source.filter(x -> x.getName() != null && x.getName().equalsIgnoreCase(name)) - .findFirst() - .orElse(null); - } - - @Override - public Rs2TileItemModel withNames(String... names) { - return source.filter(x -> { - if (x.getName() == null) return false; - return Arrays.stream(names) - .anyMatch(name -> x.getName().equalsIgnoreCase(name)); - }).findFirst().orElse(null); - } - - @Override - public Rs2TileItemModel withId(int id) { - return source.filter(x -> x.getId() == id) - .findFirst() - .orElse(null); - } - - @Override - public Rs2TileItemModel withIds(int... ids) { - return source.filter(x -> { - for (int id : ids) { - if (x.getId() == id) return true; - } - return false; - }).findFirst().orElse(null); + super(); } @Override - public java.util.List toList() { - return source.collect(Collectors.toList()); + protected Stream initialSource() { + return Rs2TileItemCache.getGroundItemsStream(); } } 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 index 5802e5d84b4..83d850454d5 100644 --- 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 @@ -6,6 +6,7 @@ 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; @@ -14,7 +15,7 @@ import java.util.function.Supplier; @Slf4j -public class Rs2TileItemModel implements TileItem { +public class Rs2TileItemModel implements TileItem, IEntity { @Getter private final Tile tile; 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 index 875460f3996..93bfaf65f4a 100644 --- 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 @@ -2,97 +2,22 @@ import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.Microbot; +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 net.runelite.client.plugins.microbot.util.player.Rs2Player; import java.util.stream.Collectors; import java.util.stream.Stream; - -public final class Rs2TileObjectQueryable +public final class Rs2TileObjectQueryable extends AbstractEntityQueryable implements IEntityQueryable { - private Stream source; - public Rs2TileObjectQueryable() { - this.source = Rs2TileObjectCache.getObjectsStream(); - } - - @Override - public Rs2TileObjectQueryable where(java.util.function.Predicate predicate) { - source = source.filter(predicate); - return this; - } - - @Override - public Rs2TileObjectModel first() { - return source.findFirst().orElse(null); - } - - @Override - public Rs2TileObjectModel nearest() { - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - if (playerLoc == null) { - return null; - } - - return source - .min(java.util.Comparator.comparingInt( - o -> o.getWorldLocation().distanceTo(playerLoc))) - .orElse(null); - } - - @Override - public Rs2TileObjectModel nearest(int maxDistance) { - WorldPoint playerLoc = Rs2Player.getWorldLocation(); - if (playerLoc == null) { - return null; - } - - return source - .filter(x -> x.getWorldLocation().distanceTo(playerLoc) <= maxDistance) - .min(java.util.Comparator.comparingInt( - o -> o.getWorldLocation().distanceTo(playerLoc))) - .orElse(null); - } - - @Override - public Rs2TileObjectModel withName(String name) { - return source.filter(x -> x.getName().equalsIgnoreCase(name)).findFirst().orElse(null); - } - - @Override - public Rs2TileObjectModel withNames(String... names) { - return Microbot.getClientThread().invoke(() -> source.filter(x -> { - for (String name : names) { - if (x.getName().equalsIgnoreCase(name)) { - return true; - } - } - return false; - }).findFirst().orElse(null)); - } - - @Override - public Rs2TileObjectModel withId(int id) { - return source.filter(x -> x.getId() == id).findFirst().orElse(null); - } - - @Override - public Rs2TileObjectModel withIds(int... ids) { - return source.filter(x -> { - for (int id : ids) { - if (x.getId() == id) { - return true; - } - } - return false; - }).findFirst().orElse(null); + super(); } @Override - public java.util.List toList() { - return source.collect(Collectors.toList()); + 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 index 218ba4ffe5c..5b500d72a5d 100644 --- 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 @@ -6,6 +6,7 @@ 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.camera.Rs2Camera; import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; import net.runelite.client.plugins.microbot.util.menu.NewMenuEntry; @@ -19,7 +20,7 @@ import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; -public class Rs2TileObjectModel implements TileObject { +public class Rs2TileObjectModel implements TileObject, IEntity { public Rs2TileObjectModel(GameObject gameObject) { this.tileObject = gameObject; @@ -150,6 +151,10 @@ public ObjectComposition getObjectComposition() { }); } + public boolean click() { + return click(""); + } + /** * Clicks on the specified tile object with no specific action. * Delegates to Rs2GameObject.clickObject. 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 ca0e5d2ff74..266b92a3b41 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 @@ -3,28 +3,37 @@ 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.util.tileobject.Rs2TileObjectApi; -import net.runelite.client.plugins.microbot.util.tileobject.Rs2TileObjectModel; +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.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.api.player.Rs2PlayerCache; -import java.util.List; +import javax.inject.Inject; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; -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. */ @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. */ @@ -33,46 +42,28 @@ public boolean run() { try { if (!Microbot.isLoggedIn()) return; - // Performance test: Loop over game objects and get compositions long startTime = System.currentTimeMillis(); - AtomicLong endTime = new AtomicLong(); + var groundItems = rs2TileItemCache.query().toList(); + var objects = rs2TileObjectCache.query().toList(); + var rs2Players = rs2PlayerCache.query().toList(); + var rs2Npcs = rs2NpcCache.query().toList(); - var tileObjects = Microbot.getClientThread().invoke(() -> { - // List _tileObjects = Rs2TileObjectApi.getObjectsStream().filter(x -> x.getName() != null && !x.getName().isEmpty() && x.getName() != "null").collect(Collectors.toList()); - Rs2TileObjectModel test = Rs2TileObjectApi.getNearest(tile -> tile.getName() != null && tile.getName().toLowerCase().contains("tree")); - endTime.set(System.currentTimeMillis()); - System.out.println("Retrieved " + test.getName() + " game objects in " + (endTime.get() - startTime) + " ms"); + //groundItems.get(0).click("Take"); + long endTime = System.currentTimeMillis(); + long totalTime = endTime - startTime; + System.out.println("fetched " + rs2Players.size() + " players and " + rs2Npcs.size() + " npcs."); + System.out.println("fetched " + objects.size() + " objects."); + System.out.println("fetched " + groundItems.size() + " ground items."); + System.out.println("Player location: " + Rs2Player.getWorldLocation()); + System.out.println("fetched " + groundItems.size() + " ground items."); + System.out.println("all in time: " + totalTime + " ms"); + /*var tree = rs2TileObjectCache.query().within(Rs2Player.getWorldLocation(), 20).withName("Tree"); - /*for (Rs2TileObjectModel rs2TileObjectModel: _tileObjects) { - var name = rs2TileObjectModel.getName(); // Access name to simulate some processing - System.out.println("Object Name: " + name); - } -*/ - return Rs2TileObjectApi.getObjectsStream().collect(Collectors.toList()); - }); + tree.click(); - - int compositionCount = 0; - - /*for (Rs2TileObjectModel tileObject : tileObjects) { - var name = tileObject.getName(); // Access name to simulate some processing - if (name != null) { - compositionCount++; - System.out.println("composition " + compositionCount + ": " + name); - } - }*/ - - endTime.set(System.currentTimeMillis()); - long durationMs = (endTime.get() - startTime); - -/* - log.info("Performance Test Results:"); - log.info(" Total GameObjects: {}", tileObjects.size()); - log.info(" Compositions retrieved: {}", compositionCount); - log.info(" Time taken: {} ms", durationMs); - log.info(" Average time per object: {} Ξs", - tileObjects.size() > 0 ? (endTime.get() - startTime) / 1000 / tileObjects.size() : 0); -*/ + System.out.println(tree.getId()); + System.out.println(tree.getName()); + */ } catch (Exception ex) { log.error("Error in performance test loop", ex); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/ActorModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/ActorModel.java index ddfc8d29cdc..e5942c52182 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/ActorModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/ActorModel.java @@ -37,7 +37,7 @@ public int getCombatLevel() return Microbot.getClientThread().runOnClientThreadOptional(actor::getCombatLevel).orElse(0); } - @Override + @Override public @Nullable String getName() { return Microbot.getClientThread().runOnClientThreadOptional(actor::getName).orElse(null); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcModel.java index 0f3245c9767..188df4e96e0 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcModel.java @@ -8,7 +8,9 @@ import net.runelite.api.NpcOverrides; 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.ActorModel; +import org.apache.commons.lang3.NotImplementedException; import org.jetbrains.annotations.Nullable; import java.util.Arrays; @@ -16,7 +18,7 @@ @Getter @EqualsAndHashCode(callSuper = true) // Ensure equality checks include ActorModel fields -public class Rs2NpcModel extends ActorModel implements NPC +public class Rs2NpcModel extends ActorModel implements NPC, IEntity { private final NPC runeliteNpc; @@ -33,7 +35,8 @@ public int getId() return runeliteNpc.getId(); } - @Override + + @Override public int getIndex() { return runeliteNpc.getIndex(); @@ -190,4 +193,14 @@ public HeadIcon getHeadIcon() { return null; } + + @Override + public boolean click() { + throw new NotImplementedException("click() not implemented yet for Rs2NpcModel"); + } + + @Override + public boolean click(String action) { + throw new NotImplementedException("click() not implemented yet for Rs2NpcModel"); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java index b4238bc3f30..2eca00bac5e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java @@ -1422,7 +1422,7 @@ public static int getPoseAnimation() { * @return The {@link QuestState} representing the player's progress in the quest. */ public static QuestState getQuestState(Quest quest) { - return Microbot.getRs2PlayerCache().getQuestState(quest); + return Microbot.getRs2PlayerStateCache().getQuestState(quest); } /** diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerCache.java deleted file mode 100644 index 828f247d0e1..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerCache.java +++ /dev/null @@ -1,176 +0,0 @@ -package net.runelite.client.plugins.microbot.util.player; - -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-related data such as quest states, varbits, and varps. - */ -@Singleton -@Slf4j -public final class Rs2PlayerCache { - @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 Rs2PlayerCache(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; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerModel.java index 849a0f49c6d..762d5c56c81 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerModel.java @@ -5,11 +5,12 @@ import net.runelite.api.HeadIcon; import net.runelite.api.Player; import net.runelite.api.PlayerComposition; +import net.runelite.client.plugins.microbot.api.IEntity; import net.runelite.client.plugins.microbot.util.ActorModel; +import org.apache.commons.lang3.NotImplementedException; @Getter -public class Rs2PlayerModel extends ActorModel implements Player -{ +public class Rs2PlayerModel extends ActorModel implements Player, IEntity { private final Player player; @@ -25,7 +26,7 @@ public int getId() return player.getId(); } - @Override + @Override public PlayerComposition getPlayerComposition() { return player.getPlayerComposition(); @@ -84,4 +85,14 @@ public int getFootprintSize() { return 0; } + + @Override + public boolean click() { + throw new NotImplementedException("click() not implemented yet for Rs2PlayerModel"); + } + + @Override + public boolean click(String action) { + throw new NotImplementedException("click() not implemented yet for Rs2PlayerModel"); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/security/LoginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/security/LoginManager.java index 865e1247d1c..34a7ad772f1 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/security/LoginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/security/LoginManager.java @@ -35,373 +35,421 @@ @Slf4j public final class LoginManager { - private static final int MAX_PLAYER_COUNT = 1950; - private static final Object LOGIN_LOCK = new Object(); - private static final AtomicBoolean LOGIN_ATTEMPT_ACTIVE = new AtomicBoolean(false); - private static final AtomicReference LAST_LOGIN_ATTEMPT = new AtomicReference<>(null); + private static final int MAX_PLAYER_COUNT = 1950; + private static final Object LOGIN_LOCK = new Object(); + private static final AtomicBoolean LOGIN_ATTEMPT_ACTIVE = new AtomicBoolean(false); + private static final AtomicReference LAST_LOGIN_ATTEMPT = new AtomicReference<>(null); - @Getter - private static Instant lastLoginTimestamp = null; + @Getter + private static Instant lastLoginTimestamp = null; @Setter - public static ConfigProfile activeProfile = null; + public static ConfigProfile activeProfile = null; - public static ConfigProfile getActiveProfile() { + public static ConfigProfile getActiveProfile() { return Microbot.getConfigManager().getProfile(); - } + } - private LoginManager() { - // Utility class - } + private LoginManager() { + // Utility class + } /** - * Returns the current RuneLite client GameState or UNKNOWN if client not available. - */ - public static GameState getGameState() { - Client client = Microbot.getClient(); - return client != null ? client.getGameState() : GameState.UNKNOWN; - } - - /** - * Returns true if the client is currently considered logged in. - */ - public static boolean isLoggedIn() { - Client client = Microbot.getClient(); - return client != null && client.getGameState() == GameState.LOGGED_IN; - } - - /** - * Returns true if a login attempt is currently being processed. - */ - public static boolean isLoginAttemptActive() { - return LOGIN_ATTEMPT_ACTIVE.get(); - } - - /** - * Marks the client as logged in by updating timestamps. This should be called when GameState transitions. - */ - public static void markLoggedIn() { - // Only set timestamp if client reports logged in. - if (isLoggedIn()) { - LOGIN_ATTEMPT_ACTIVE.set(false); - lastLoginTimestamp = Instant.now(); - } - } - - /** - * Marks the client as logged out. Should be triggered whenever the game enters the login screen. - */ - public static void markLoggedOut() { - LOGIN_ATTEMPT_ACTIVE.set(false); - } - - /** - * Returns the duration the account has been logged in for. Equivalent to Microbot.getLoginTime(). - */ - public static Duration getLoginDuration() { - if (lastLoginTimestamp == null || !isLoggedIn()) { - return Duration.of(0, ChronoUnit.MILLIS); - } - return Duration.between(lastLoginTimestamp, Instant.now()); - } - - /** - * Attempts a login using the active profile and an intelligent world selection. - */ - public static boolean login() { - if (getActiveProfile() == null) { - log.warn("No active profile available for login"); - return false; - } - System.out.println(getActiveProfile()); - Client client = Microbot.getClient(); - if (client == null) { - log.warn("Cannot login - client is not initialised"); - return false; - } - int targetWorld = getRandomWorld(getActiveProfile().isMember()); - return login(getActiveProfile().getName(), getActiveProfile().getPassword(), targetWorld); - } - - /** - * Attempts a login using the active profile into a specific world. - */ - public static boolean login(int worldId) { - if (getActiveProfile() == null) { - log.warn("No active profile available for world specific login"); - return false; - } - return login(getActiveProfile().getName(), getActiveProfile().getPassword(), worldId); - } - - /** - * Attempts a login with explicit credentials and world target. - */ - public static boolean login(String username, String encryptedPassword, int worldId) { - if (username == null || username.isBlank()) { - log.warn("Cannot login without username"); - return false; - } - if (isLoggedIn()) { - return true; - } - if (LOGIN_ATTEMPT_ACTIVE.get()) { - log.debug("Login attempt already active - skipping duplicate request"); - return false; - } - final Client client = Microbot.getClient(); - if (client == null) { - log.warn("Cannot login - client is not initialised"); - return false; - } - synchronized (LOGIN_LOCK) { - Instant lastAttempt = LAST_LOGIN_ATTEMPT.get(); - Instant now = Instant.now(); - if (lastAttempt != null && Duration.between(lastAttempt, now).toMillis() < 1500) { - log.debug("Login throttled - last attempt {}ms ago", Duration.between(lastAttempt, now).toMillis()); - return false; - } - LAST_LOGIN_ATTEMPT.set(now); - LOGIN_ATTEMPT_ACTIVE.set(true); - } - - try { - handleDisconnectDialogs(client); - triggerLoginScreen(); - trySetWorld(worldId); - setCredentials(client, username, encryptedPassword); - submitLogin(); - handleBlockingDialogs(client); - return true; - } catch (Exception ex) { - log.error("Error during login attempt", ex); - return false; - } finally { - // Keep attempt active until either logged in state observed or logout recorded externally - LOGIN_ATTEMPT_ACTIVE.set(false); - } - } - - private static void handleDisconnectDialogs(Client client) { - if (client == null) { - return; - } - int loginIndex = client.getLoginIndex(); - if (loginIndex == 3 || loginIndex == 24) { - int loginScreenWidth = 804; - int startingWidth = (client.getCanvasWidth() / 2) - (loginScreenWidth / 2); - Microbot.getMouse().click(365 + startingWidth, 308); - sleep(600); - } - } - - private static void triggerLoginScreen() { - Rs2Keyboard.keyPress(KeyEvent.VK_ENTER); - sleep(600); - } - - private static void trySetWorld(int worldId) { - if (worldId <= 0) { - return; - } - try { - setWorld(worldId); - } catch (Exception e) { - log.warn("Changing world failed for {}", worldId, e); - } - } - - private static void setCredentials(Client client, String username, String encryptedPassword) { - client.setUsername(username); - if (encryptedPassword == null || encryptedPassword.isBlank()) { - return; - } - try { - client.setPassword(Encryption.decrypt(encryptedPassword)); - } catch (Exception e) { - log.warn("Unable to decrypt stored password", e); - } - sleep(300); - } - - private static void submitLogin() { - Rs2Keyboard.keyPress(KeyEvent.VK_ENTER); - sleep(300); - Rs2Keyboard.keyPress(KeyEvent.VK_ENTER); - } - - private static void handleBlockingDialogs(Client client) { - if (client == null) { - return; - } - int loginIndex = client.getLoginIndex(); - int loginScreenWidth = 804; - int startingWidth = (client.getCanvasWidth() / 2) - (loginScreenWidth / 2); - - if (loginIndex == 10) { - Microbot.getMouse().click(365 + startingWidth, 250); - } else if (loginIndex == 9) { - Microbot.getMouse().click(365 + startingWidth, 300); - } - } - - public static int getRandomWorld(boolean isMembers, WorldRegion region) { - WorldResult worldResult = Microbot.getWorldService().getWorlds(); - if (worldResult == null) { - return isMembers ? 360 : 383; - } - List worlds = worldResult.getWorlds(); - boolean isInSeasonalWorld; - if (Microbot.getClient() != null && Microbot.getClient().getWorldType() != null) { - isInSeasonalWorld = Microbot.getClient().getWorldType().contains(net.runelite.api.WorldType.SEASONAL); - } else { - isInSeasonalWorld = false; - } - - List filteredWorlds = worlds.stream() - .filter(x -> !x.getTypes().contains(WorldType.PVP) && - !x.getTypes().contains(WorldType.HIGH_RISK) && - !x.getTypes().contains(WorldType.BOUNTY) && - !x.getTypes().contains(WorldType.SKILL_TOTAL) && - !x.getTypes().contains(WorldType.LAST_MAN_STANDING) && - !x.getTypes().contains(WorldType.QUEST_SPEEDRUNNING) && - !x.getTypes().contains(WorldType.BETA_WORLD) && - !x.getTypes().contains(WorldType.DEADMAN) && - !x.getTypes().contains(WorldType.PVP_ARENA) && - !x.getTypes().contains(WorldType.TOURNAMENT) && - !x.getTypes().contains(WorldType.NOSAVE_MODE) && - !x.getTypes().contains(WorldType.LEGACY_ONLY) && - !x.getTypes().contains(WorldType.EOC_ONLY) && - !x.getTypes().contains(WorldType.FRESH_START_WORLD) && - x.getPlayers() < MAX_PLAYER_COUNT && - x.getPlayers() >= 0) - .filter(x -> isInSeasonalWorld == x.getTypes().contains(WorldType.SEASONAL)) - .collect(Collectors.toList()); - - filteredWorlds = isMembers - ? filteredWorlds.stream().filter(x -> x.getTypes().contains(WorldType.MEMBERS)).collect(Collectors.toList()) - : filteredWorlds.stream().filter(x -> !x.getTypes().contains(WorldType.MEMBERS)).collect(Collectors.toList()); - - if (region != null) { - filteredWorlds = filteredWorlds.stream() - .filter(x -> x.getRegion() == region) - .collect(Collectors.toList()); - } - - if (filteredWorlds.isEmpty()) { - return isMembers ? 360 : 383; - } - - Random random = new Random(); - World world = filteredWorlds.get(random.nextInt(filteredWorlds.size())); - - return (world != null) ? world.getId() : (isMembers ? 360 : 383); - } - - public static int getRandomWorld(boolean isMembers) { - return getRandomWorld(isMembers, null); - } - - public static int getNextWorld(boolean isMembers) { - return getNextWorld(isMembers, null); - } - - public static int getNextWorld(boolean isMembers, WorldRegion region) { - WorldResult worldResult = Microbot.getWorldService().getWorlds(); - if (worldResult == null) { - return isMembers ? 360 : 383; - } - - List worlds = worldResult.getWorlds(); - boolean isInSeasonalWorld; - if (Microbot.getClient() != null && Microbot.getClient().getWorldType() != null) { - isInSeasonalWorld = Microbot.getClient().getWorldType().contains(net.runelite.api.WorldType.SEASONAL); - } else { - isInSeasonalWorld = false; - } - - List filteredWorlds = worlds.stream() - .filter(x -> !x.getTypes().contains(WorldType.PVP) && - !x.getTypes().contains(WorldType.HIGH_RISK) && - !x.getTypes().contains(WorldType.BOUNTY) && - !x.getTypes().contains(WorldType.SKILL_TOTAL) && - !x.getTypes().contains(WorldType.LAST_MAN_STANDING) && - !x.getTypes().contains(WorldType.QUEST_SPEEDRUNNING) && - !x.getTypes().contains(WorldType.BETA_WORLD) && - !x.getTypes().contains(WorldType.DEADMAN) && - !x.getTypes().contains(WorldType.PVP_ARENA) && - !x.getTypes().contains(WorldType.TOURNAMENT) && - !x.getTypes().contains(WorldType.NOSAVE_MODE) && - !x.getTypes().contains(WorldType.LEGACY_ONLY) && - !x.getTypes().contains(WorldType.EOC_ONLY) && - !x.getTypes().contains(WorldType.FRESH_START_WORLD) && - x.getPlayers() < MAX_PLAYER_COUNT && - x.getPlayers() >= 0) - .filter(x -> isInSeasonalWorld == x.getTypes().contains(WorldType.SEASONAL)) - .collect(Collectors.toList()); - - filteredWorlds = isMembers - ? filteredWorlds.stream().filter(x -> x.getTypes().contains(WorldType.MEMBERS)).collect(Collectors.toList()) - : filteredWorlds.stream().filter(x -> !x.getTypes().contains(WorldType.MEMBERS)).collect(Collectors.toList()); - - if (region != null) { - filteredWorlds = filteredWorlds.stream() - .filter(x -> x.getRegion() == region) - .collect(Collectors.toList()); - } - - int currentWorldId = Microbot.getClient() != null ? Microbot.getClient().getWorld() : -1; - int currentIndex = -1; - - for (int i = 0; i < filteredWorlds.size(); i++) { - if (filteredWorlds.get(i).getId() == currentWorldId) { - currentIndex = i; - break; - } - } - - if (currentIndex != -1) { - int nextIndex = (currentIndex + 1) % filteredWorlds.size(); - return filteredWorlds.get(nextIndex).getId(); - } else if (!filteredWorlds.isEmpty()) { - return filteredWorlds.get(0).getId(); - } - - return isMembers ? 360 : 383; - } - - public static void setWorld(int worldNumber) { - try { - if (Microbot.getWorldService() == null || Microbot.getClient() == null) { - log.warn("Cannot change world - client or world service unavailable"); - return; - } - WorldResult worldResult = Microbot.getWorldService().getWorlds(); - if (worldResult == null) { - log.warn("Cannot change world - world service returned no data"); - return; - } - net.runelite.http.api.worlds.World world = worldResult.findWorld(worldNumber); - if (world == null) { - log.warn("Failed to find world {}", worldNumber); - return; - } - final net.runelite.api.World rsWorld = Microbot.getClient().createWorld(); - if (rsWorld == null) { - log.warn("Failed to create world instance for {}", worldNumber); - return; - } - rsWorld.setActivity(world.getActivity()); - rsWorld.setAddress(world.getAddress()); - rsWorld.setId(world.getId()); - rsWorld.setPlayerCount(world.getPlayers()); - rsWorld.setLocation(world.getLocation()); - rsWorld.setTypes(WorldUtil.toWorldTypes(world.getTypes())); - Microbot.getClient().changeWorld(rsWorld); - } catch (Exception ex) { - log.warn("Failed to set target world {}", worldNumber, ex); - } - } + * Returns the current RuneLite client GameState or UNKNOWN if client not available. + */ + public static GameState getGameState() { + Client client = Microbot.getClient(); + return client != null ? client.getGameState() : GameState.UNKNOWN; + } + + /** + * Returns true if the client is currently considered logged in. + */ + public static boolean isLoggedIn() { + Client client = Microbot.getClient(); + return client != null && client.getGameState() == GameState.LOGGED_IN; + } + + /** + * Returns true if a login attempt is currently being processed. + */ + public static boolean isLoginAttemptActive() { + return LOGIN_ATTEMPT_ACTIVE.get(); + } + + /** + * Marks the client as logged in by updating timestamps. This should be called when GameState transitions. + */ + public static void markLoggedIn() { + // Only set timestamp if client reports logged in. + if (isLoggedIn()) { + LOGIN_ATTEMPT_ACTIVE.set(false); + lastLoginTimestamp = Instant.now(); + } + } + + /** + * Marks the client as logged out. Should be triggered whenever the game enters the login screen. + */ + public static void markLoggedOut() { + LOGIN_ATTEMPT_ACTIVE.set(false); + } + + /** + * Returns the duration the account has been logged in for. Equivalent to Microbot.getLoginTime(). + */ + public static Duration getLoginDuration() { + if (lastLoginTimestamp == null || !isLoggedIn()) { + return Duration.of(0, ChronoUnit.MILLIS); + } + return Duration.between(lastLoginTimestamp, Instant.now()); + } + + /** + * Attempts a login using the active profile and an intelligent world selection. + */ + public static boolean login() { + if (getActiveProfile() == null) { + log.warn("No active profile available for login"); + return false; + } + System.out.println(getActiveProfile()); + Client client = Microbot.getClient(); + if (client == null) { + log.warn("Cannot login - client is not initialised"); + return false; + } + + if (getActiveProfile().isMember() && !isCurrentWorldMembers() || + !getActiveProfile().isMember() && isCurrentWorldMembers()) { + int targetWorld = getRandomWorld(getActiveProfile().isMember()); + return login(getActiveProfile().getName(), getActiveProfile().getPassword(), targetWorld); + } + + return login(getActiveProfile().getName(), getActiveProfile().getPassword(), Microbot.getClient().getWorld()); + + } + + /** + * Attempts a login using the active profile into a specific world. + */ + public static boolean login(int worldId) { + if (getActiveProfile() == null) { + log.warn("No active profile available for world specific login"); + return false; + } + return login(getActiveProfile().getName(), getActiveProfile().getPassword(), worldId); + } + + /** + * Attempts a login with explicit credentials and world target. + */ + public static boolean login(String username, String encryptedPassword, int worldId) { + if (username == null || username.isBlank()) { + log.warn("Cannot login without username"); + return false; + } + if (isLoggedIn()) { + return true; + } + if (LOGIN_ATTEMPT_ACTIVE.get()) { + log.debug("Login attempt already active - skipping duplicate request"); + return false; + } + final Client client = Microbot.getClient(); + if (client == null) { + log.warn("Cannot login - client is not initialised"); + return false; + } + synchronized (LOGIN_LOCK) { + Instant lastAttempt = LAST_LOGIN_ATTEMPT.get(); + Instant now = Instant.now(); + if (lastAttempt != null && Duration.between(lastAttempt, now).toMillis() < 1500) { + log.debug("Login throttled - last attempt {}ms ago", Duration.between(lastAttempt, now).toMillis()); + return false; + } + LAST_LOGIN_ATTEMPT.set(now); + LOGIN_ATTEMPT_ACTIVE.set(true); + } + + try { + handleDisconnectDialogs(client); + triggerLoginScreen(); + trySetWorld(worldId); + setCredentials(client, username, encryptedPassword); + submitLogin(); + handleBlockingDialogs(client); + return true; + } catch (Exception ex) { + log.error("Error during login attempt", ex); + return false; + } finally { + // Keep attempt active until either logged in state observed or logout recorded externally + LOGIN_ATTEMPT_ACTIVE.set(false); + } + } + + private static void handleDisconnectDialogs(Client client) { + if (client == null) { + return; + } + int loginIndex = client.getLoginIndex(); + if (loginIndex == 3 || loginIndex == 24) { + int loginScreenWidth = 804; + int startingWidth = (client.getCanvasWidth() / 2) - (loginScreenWidth / 2); + Microbot.getMouse().click(365 + startingWidth, 308); + sleep(600); + } + } + + private static void triggerLoginScreen() { + Rs2Keyboard.keyPress(KeyEvent.VK_ENTER); + sleep(600); + } + + private static void trySetWorld(int worldId) { + if (worldId <= 0) { + return; + } + try { + setWorld(worldId); + } catch (Exception e) { + log.warn("Changing world failed for {}", worldId, e); + } + } + + private static void setCredentials(Client client, String username, String encryptedPassword) { + client.setUsername(username); + if (encryptedPassword == null || encryptedPassword.isBlank()) { + return; + } + try { + client.setPassword(Encryption.decrypt(encryptedPassword)); + } catch (Exception e) { + log.warn("Unable to decrypt stored password", e); + } + sleep(300); + } + + private static void submitLogin() { + Rs2Keyboard.keyPress(KeyEvent.VK_ENTER); + sleep(300); + Rs2Keyboard.keyPress(KeyEvent.VK_ENTER); + } + + private static void handleBlockingDialogs(Client client) { + if (client == null) { + return; + } + int loginIndex = client.getLoginIndex(); + int loginScreenWidth = 804; + int startingWidth = (client.getCanvasWidth() / 2) - (loginScreenWidth / 2); + + if (loginIndex == 10) { + Microbot.getMouse().click(365 + startingWidth, 250); + } else if (loginIndex == 9) { + Microbot.getMouse().click(365 + startingWidth, 300); + } + } + + public static int getRandomWorld(boolean isMembers, WorldRegion region) { + WorldResult worldResult = Microbot.getWorldService().getWorlds(); + if (worldResult == null) { + return isMembers ? 360 : 383; + } + List worlds = worldResult.getWorlds(); + boolean isInSeasonalWorld; + if (Microbot.getClient() != null && Microbot.getClient().getWorldType() != null) { + isInSeasonalWorld = Microbot.getClient().getWorldType().contains(net.runelite.api.WorldType.SEASONAL); + } else { + isInSeasonalWorld = false; + } + + List filteredWorlds = worlds.stream() + .filter(x -> !x.getTypes().contains(WorldType.PVP) && + !x.getTypes().contains(WorldType.HIGH_RISK) && + !x.getTypes().contains(WorldType.BOUNTY) && + !x.getTypes().contains(WorldType.SKILL_TOTAL) && + !x.getTypes().contains(WorldType.LAST_MAN_STANDING) && + !x.getTypes().contains(WorldType.QUEST_SPEEDRUNNING) && + !x.getTypes().contains(WorldType.BETA_WORLD) && + !x.getTypes().contains(WorldType.DEADMAN) && + !x.getTypes().contains(WorldType.PVP_ARENA) && + !x.getTypes().contains(WorldType.TOURNAMENT) && + !x.getTypes().contains(WorldType.NOSAVE_MODE) && + !x.getTypes().contains(WorldType.LEGACY_ONLY) && + !x.getTypes().contains(WorldType.EOC_ONLY) && + !x.getTypes().contains(WorldType.FRESH_START_WORLD) && + x.getPlayers() < MAX_PLAYER_COUNT && + x.getPlayers() >= 0) + .filter(x -> isInSeasonalWorld == x.getTypes().contains(WorldType.SEASONAL)) + .collect(Collectors.toList()); + + filteredWorlds = isMembers + ? filteredWorlds.stream().filter(x -> x.getTypes().contains(WorldType.MEMBERS)).collect(Collectors.toList()) + : filteredWorlds.stream().filter(x -> !x.getTypes().contains(WorldType.MEMBERS)).collect(Collectors.toList()); + + if (region != null) { + filteredWorlds = filteredWorlds.stream() + .filter(x -> x.getRegion() == region) + .collect(Collectors.toList()); + } + + if (filteredWorlds.isEmpty()) { + return isMembers ? 360 : 383; + } + + Random random = new Random(); + World world = filteredWorlds.get(random.nextInt(filteredWorlds.size())); + + return (world != null) ? world.getId() : (isMembers ? 360 : 383); + } + + public static int getRandomWorld(boolean isMembers) { + return getRandomWorld(isMembers, null); + } + + public static int getNextWorld(boolean isMembers) { + return getNextWorld(isMembers, null); + } + + public static int getNextWorld(boolean isMembers, WorldRegion region) { + WorldResult worldResult = Microbot.getWorldService().getWorlds(); + if (worldResult == null) { + return isMembers ? 360 : 383; + } + + List worlds = worldResult.getWorlds(); + boolean isInSeasonalWorld; + if (Microbot.getClient() != null && Microbot.getClient().getWorldType() != null) { + isInSeasonalWorld = Microbot.getClient().getWorldType().contains(net.runelite.api.WorldType.SEASONAL); + } else { + isInSeasonalWorld = false; + } + + List filteredWorlds = worlds.stream() + .filter(x -> !x.getTypes().contains(WorldType.PVP) && + !x.getTypes().contains(WorldType.HIGH_RISK) && + !x.getTypes().contains(WorldType.BOUNTY) && + !x.getTypes().contains(WorldType.SKILL_TOTAL) && + !x.getTypes().contains(WorldType.LAST_MAN_STANDING) && + !x.getTypes().contains(WorldType.QUEST_SPEEDRUNNING) && + !x.getTypes().contains(WorldType.BETA_WORLD) && + !x.getTypes().contains(WorldType.DEADMAN) && + !x.getTypes().contains(WorldType.PVP_ARENA) && + !x.getTypes().contains(WorldType.TOURNAMENT) && + !x.getTypes().contains(WorldType.NOSAVE_MODE) && + !x.getTypes().contains(WorldType.LEGACY_ONLY) && + !x.getTypes().contains(WorldType.EOC_ONLY) && + !x.getTypes().contains(WorldType.FRESH_START_WORLD) && + x.getPlayers() < MAX_PLAYER_COUNT && + x.getPlayers() >= 0) + .filter(x -> isInSeasonalWorld == x.getTypes().contains(WorldType.SEASONAL)) + .collect(Collectors.toList()); + + filteredWorlds = isMembers + ? filteredWorlds.stream().filter(x -> x.getTypes().contains(WorldType.MEMBERS)).collect(Collectors.toList()) + : filteredWorlds.stream().filter(x -> !x.getTypes().contains(WorldType.MEMBERS)).collect(Collectors.toList()); + + if (region != null) { + filteredWorlds = filteredWorlds.stream() + .filter(x -> x.getRegion() == region) + .collect(Collectors.toList()); + } + + int currentWorldId = Microbot.getClient() != null ? Microbot.getClient().getWorld() : -1; + int currentIndex = -1; + + for (int i = 0; i < filteredWorlds.size(); i++) { + if (filteredWorlds.get(i).getId() == currentWorldId) { + currentIndex = i; + break; + } + } + + if (currentIndex != -1) { + int nextIndex = (currentIndex + 1) % filteredWorlds.size(); + return filteredWorlds.get(nextIndex).getId(); + } else if (!filteredWorlds.isEmpty()) { + return filteredWorlds.get(0).getId(); + } + + return isMembers ? 360 : 383; + } + + /** + * Determine if the provided world id corresponds to a members world. + * + * @param worldId target world id (e.g. 301, 302, etc.) + * @return true if world exists and has the MEMBERS type; false otherwise or if data unavailable + */ + public static boolean isMemberWorld(int worldId) { + if (worldId <= 0) { + return false; + } + if (Microbot.getWorldService() == null) { + return false; + } + try { + WorldResult result = Microbot.getWorldService().getWorlds(); + if (result == null) { + return false; + } + World world = result.findWorld(worldId); + if (world == null || world.getTypes() == null) { + return false; + } + return world.getTypes().contains(WorldType.MEMBERS); + } catch (Exception e) { + log.debug("Failed to determine membership for world {}", worldId, e); + return false; + } + } + + /** + * Convenience method to check if the current client world is a members world. + * @return true if client available and current world is members, false otherwise. + */ + public static boolean isCurrentWorldMembers() { + Client client = Microbot.getClient(); + if (client == null) { + return false; + } + return isMemberWorld(client.getWorld()); + } + + public static void setWorld(int worldNumber) { + try { + if (Microbot.getWorldService() == null || Microbot.getClient() == null) { + log.warn("Cannot change world - client or world service unavailable"); + return; + } + WorldResult worldResult = Microbot.getWorldService().getWorlds(); + if (worldResult == null) { + log.warn("Cannot change world - world service returned no data"); + return; + } + net.runelite.http.api.worlds.World world = worldResult.findWorld(worldNumber); + if (world == null) { + log.warn("Failed to find world {}", worldNumber); + return; + } + final net.runelite.api.World rsWorld = Microbot.getClient().createWorld(); + if (rsWorld == null) { + log.warn("Failed to create world instance for {}", worldNumber); + return; + } + rsWorld.setActivity(world.getActivity()); + rsWorld.setAddress(world.getAddress()); + rsWorld.setId(world.getId()); + rsWorld.setPlayerCount(world.getPlayers()); + rsWorld.setLocation(world.getLocation()); + rsWorld.setTypes(WorldUtil.toWorldTypes(world.getTypes())); + Microbot.getClient().changeWorld(rsWorld); + } catch (Exception ex) { + log.warn("Failed to set target world {}", worldNumber, ex); + } + } } From d82f3b9bce1483160ed42f11ffaf8caee0a619e4 Mon Sep 17 00:00:00 2001 From: chsami Date: Sun, 16 Nov 2025 12:40:52 +0100 Subject: [PATCH 25/42] refactor(api): reorganize player and NPC model imports and structure --- .../microbot/api/actor/Rs2ActorModel.java | 435 ++++++++++++++++++ .../plugins/microbot/api/npc/Rs2NpcCache.java | 2 +- .../microbot/api/npc/Rs2NpcQueryable.java | 3 +- .../microbot/api/npc/models/Rs2NpcModel.java | 206 +++++++++ .../microbot/api/player/PlayerApiExample.java | 2 +- .../microbot/api/player/Rs2PlayerCache.java | 3 +- .../api/player/Rs2PlayerQueryable.java | 2 +- .../api/player/models/Rs2PlayerModel.java | 98 ++++ .../microbot/util/npc/Rs2NpcModel.java | 12 +- .../microbot/util/player/Rs2PlayerModel.java | 12 +- 10 files changed, 746 insertions(+), 29 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/actor/Rs2ActorModel.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/models/Rs2NpcModel.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/models/Rs2PlayerModel.java 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..bb11972cecc --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/actor/Rs2ActorModel.java @@ -0,0 +1,435 @@ +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 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 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() + { + 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(); + } +} 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 index a566b135cdd..2f52c65c088 100644 --- 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 @@ -1,7 +1,7 @@ package net.runelite.client.plugins.microbot.api.npc; import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; +import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; import java.util.ArrayList; import java.util.List; 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 index 2f54573104c..052fddee7d1 100644 --- 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 @@ -1,9 +1,8 @@ package net.runelite.client.plugins.microbot.api.npc; -import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.api.AbstractEntityQueryable; import net.runelite.client.plugins.microbot.api.IEntityQueryable; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; +import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; import java.util.stream.Stream; 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..bc2b2071686 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/models/Rs2NpcModel.java @@ -0,0 +1,206 @@ +package net.runelite.client.plugins.microbot.api.npc.models; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import net.runelite.api.HeadIcon; +import net.runelite.api.NPC; +import net.runelite.api.NPCComposition; +import net.runelite.api.NpcOverrides; +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.ActorModel; +import org.apache.commons.lang3.NotImplementedException; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.function.Predicate; + +@Getter +@EqualsAndHashCode(callSuper = true) // Ensure equality checks include ActorModel fields +public class Rs2NpcModel extends ActorModel implements NPC, IEntity +{ + + private final NPC runeliteNpc; + + public Rs2NpcModel(final NPC npc) + { + super(npc); + this.runeliteNpc = npc; + } + + @Override + public int getId() + { + return runeliteNpc.getId(); + } + + + @Override + public int getIndex() + { + return runeliteNpc.getIndex(); + } + + @Override + public NPCComposition getComposition() + { + return runeliteNpc.getComposition(); + } + + @Override + public @Nullable NPCComposition getTransformedComposition() + { + return runeliteNpc.getTransformedComposition(); + } + + @Override + public @Nullable NpcOverrides getModelOverrides() + { + return runeliteNpc.getModelOverrides(); + } + + @Override + public @Nullable NpcOverrides getChatheadOverrides() + { + return runeliteNpc.getChatheadOverrides(); + } + + @Override + public int @Nullable [] getOverheadArchiveIds() + { + return runeliteNpc.getOverheadArchiveIds(); + } + + @Override + public short @Nullable [] getOverheadSpriteIds() + { + return runeliteNpc.getOverheadSpriteIds(); + } + + // 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 this.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 (runeliteNpc == null) { + return null; + } + + if (runeliteNpc.getOverheadSpriteIds() == null) { + Microbot.log("Failed to find the correct overhead prayer."); + return null; + } + + for (int i = 0; i < runeliteNpc.getOverheadSpriteIds().length; i++) { + int overheadSpriteId = runeliteNpc.getOverheadSpriteIds()[i]; + + if (overheadSpriteId == -1) continue; + + return HeadIcon.values()[overheadSpriteId]; + } + + Microbot.log("Found overheadSpriteIds: " + Arrays.toString(runeliteNpc.getOverheadSpriteIds()) + " but failed to find valid overhead prayer."); + + return null; + } + + @Override + public boolean click() { + throw new NotImplementedException("click() not implemented yet for Rs2NpcModel"); + } + + @Override + public boolean click(String action) { + throw new NotImplementedException("click() not implemented yet for Rs2NpcModel"); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java index a56ea75dca0..4fa997916b5 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java @@ -1,6 +1,6 @@ package net.runelite.client.plugins.microbot.api.player; -import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; +import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; import java.util.List; import java.util.stream.Collectors; 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 index 0bbcffeb37b..bd34518e572 100644 --- 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 @@ -1,8 +1,7 @@ package net.runelite.client.plugins.microbot.api.player; import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.player.Rs2Player; -import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; +import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; import java.util.ArrayList; import java.util.List; 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 index f4ab6ef1186..78f8f7a16c8 100644 --- 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 @@ -2,7 +2,7 @@ import net.runelite.client.plugins.microbot.api.AbstractEntityQueryable; import net.runelite.client.plugins.microbot.api.IEntityQueryable; -import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; +import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; import java.util.stream.Stream; 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..ffb81a67c60 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/models/Rs2PlayerModel.java @@ -0,0 +1,98 @@ +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.client.plugins.microbot.api.IEntity; +import net.runelite.client.plugins.microbot.util.ActorModel; +import org.apache.commons.lang3.NotImplementedException; + +@Getter +public class Rs2PlayerModel extends ActorModel implements Player, IEntity { + + private final Player player; + + public Rs2PlayerModel(final Player player) + { + super(player); + this.player = player; + } + + @Override + public int getId() + { + return player.getId(); + } + + @Override + public PlayerComposition getPlayerComposition() + { + return player.getPlayerComposition(); + } + + @Override + public Polygon[] getPolygons() + { + return player.getPolygons(); + } + + @Override + public int getTeam() + { + return player.getTeam(); + } + + @Override + public boolean isFriendsChatMember() + { + return player.isFriendsChatMember(); + } + + @Override + public boolean isFriend() + { + return player.isFriend(); + } + + @Override + public boolean isClanMember() + { + return player.isClanMember(); + } + + @Override + public HeadIcon getOverheadIcon() + { + return player.getOverheadIcon(); + } + + @Override + public int getSkullIcon() + { + return player.getSkullIcon(); + } + + @Override + public void setSkullIcon(int skullIcon) + { + player.setSkullIcon(skullIcon); + } + + @Override + public int getFootprintSize() + { + return 0; + } + + @Override + public boolean click() { + throw new NotImplementedException("click() not implemented yet for Rs2PlayerModel"); + } + + @Override + public boolean click(String action) { + throw new NotImplementedException("click() not implemented yet for Rs2PlayerModel"); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcModel.java index 188df4e96e0..8f04373be24 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2NpcModel.java @@ -18,7 +18,7 @@ @Getter @EqualsAndHashCode(callSuper = true) // Ensure equality checks include ActorModel fields -public class Rs2NpcModel extends ActorModel implements NPC, IEntity +public class Rs2NpcModel extends ActorModel implements NPC { private final NPC runeliteNpc; @@ -193,14 +193,4 @@ public HeadIcon getHeadIcon() { return null; } - - @Override - public boolean click() { - throw new NotImplementedException("click() not implemented yet for Rs2NpcModel"); - } - - @Override - public boolean click(String action) { - throw new NotImplementedException("click() not implemented yet for Rs2NpcModel"); - } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerModel.java index 762d5c56c81..96dbd69894f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerModel.java @@ -10,7 +10,7 @@ import org.apache.commons.lang3.NotImplementedException; @Getter -public class Rs2PlayerModel extends ActorModel implements Player, IEntity { +public class Rs2PlayerModel extends ActorModel implements Player { private final Player player; @@ -85,14 +85,4 @@ public int getFootprintSize() { return 0; } - - @Override - public boolean click() { - throw new NotImplementedException("click() not implemented yet for Rs2PlayerModel"); - } - - @Override - public boolean click(String action) { - throw new NotImplementedException("click() not implemented yet for Rs2PlayerModel"); - } } From 42f2ffbcd5a8723ca2b4e45229e32dc118fd8782 Mon Sep 17 00:00:00 2001 From: chsami Date: Sun, 16 Nov 2025 12:50:54 +0100 Subject: [PATCH 26/42] refactor(api): mark legacy game object classes as deprecated --- .../client/plugins/microbot/util/gameobject/Rs2GameObject.java | 1 + .../client/plugins/microbot/util/grounditem/Rs2GroundItem.java | 1 + .../net/runelite/client/plugins/microbot/util/npc/Rs2Npc.java | 1 + .../runelite/client/plugins/microbot/util/player/Rs2Player.java | 1 + 4 files changed, 4 insertions(+) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java index 1d4b4dee52a..8f177cf5f2b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java @@ -33,6 +33,7 @@ /** * TODO: This class should be cleaned up, less methods by passing filters instead of multiple parameters */ +@Deprecated(since = "2.1.0 - Use Rs2TileObjectQueryable instead", forRemoval = true) public class Rs2GameObject { /** * Extracts all {@link GameObject}s located on a given {@link Tile}. diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItem.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItem.java index 10f66852f6e..953722f1d17 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItem.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItem.java @@ -31,6 +31,7 @@ * Todo: rework this class to not be dependant on the grounditem plugin */ @Slf4j +@Deprecated(since = "2.1.0 - Use Rs2TileItemCache/Rs2TileItemQuery instead", forRemoval = true) public class Rs2GroundItem { private static final int DESPAWN_DELAY_THRESHOLD_TICKS = 150; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2Npc.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2Npc.java index aad28be8d7a..0c7938227b7 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2Npc.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2Npc.java @@ -28,6 +28,7 @@ import static net.runelite.api.Perspective.LOCAL_TILE_SIZE; @Slf4j +@Deprecated(since = "2.1.0 - Use Rs2NpcCache/Rs2NpcQuery instead", forRemoval = true) public class Rs2Npc { /** * Retrieves an NPC by its index, returning an {@link Rs2NpcModel}. diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java index 2eca00bac5e..8319a30dfa9 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java @@ -49,6 +49,7 @@ import static net.runelite.api.MenuAction.CC_OP; import static net.runelite.client.plugins.microbot.util.Global.*; +@Deprecated(since = "2.1.0 - Use Rs2PlayerCache/Rs2PlayerQueryable", forRemoval = true) public class Rs2Player { static int VENOM_VALUE_CUTOFF = -38; private static int antiFireTime = -1; From 78e024c41f2643d2875ff3529f6a01ac6c186d1a Mon Sep 17 00:00:00 2001 From: chsami Date: Sun, 16 Nov 2025 13:04:41 +0100 Subject: [PATCH 27/42] refactor(api): update Rs2NpcModel import path --- .../runelite/client/plugins/microbot/api/npc/NpcApiExample.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 6b26777e720..71814e0d46e 100644 --- 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 @@ -1,6 +1,6 @@ package net.runelite.client.plugins.microbot.api.npc; -import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; +import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; import java.util.List; From 1da0633a7334e2d8db10b7234c6aba60414e5eb2 Mon Sep 17 00:00:00 2001 From: chsami Date: Sun, 16 Nov 2025 14:32:49 +0100 Subject: [PATCH 28/42] feat(login): auto-fill credentials from active profile on login screen --- .../plugins/microbot/MicrobotPlugin.java | 61 ++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java index b3703d6b5fd..57df8890df9 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java @@ -291,14 +291,71 @@ public void onGameStateChanged(GameStateChanged gameStateChanged) } if (gameStateChanged.getGameState() == GameState.HOPPING || gameStateChanged.getGameState() == GameState.LOGIN_SCREEN || gameStateChanged.getGameState() == GameState.CONNECTION_LOST) { - // Clear all cache states when logging out through Rs2CacheManager - //Rs2CacheManager.emptyCacheState(); // should not be nessary here, handled in ClientShutdown event, + // Clear all cache states when logging out through Rs2CacheManager + //Rs2CacheManager.emptyCacheState(); // should not be nessary here, handled in ClientShutdown event, // and we also handle correct cache loading in onRuneScapeProfileChanged event LoginManager.markLoggedOut(); Microbot.setLastKnownRegions(null); + + // Auto-fill credentials from active profile on login screen + if (gameStateChanged.getGameState() == GameState.LOGIN_SCREEN) + { + prefillCredentialsFromActiveProfile(); + } } } + /** + * Pre-fills username and password from the active profile when reaching the login screen. + * This automatically populates the login fields, saving the user from manually entering credentials. + */ + private void prefillCredentialsFromActiveProfile() + { + try + { + final Client client = Microbot.getClient(); + if (client == null) + { + return; + } + + net.runelite.client.config.ConfigProfile activeProfile = LoginManager.getActiveProfile(); + if (activeProfile == null) + { + log.debug("No active profile available for credential auto-fill"); + return; + } + + // Set username + String username = activeProfile.getName(); + if (username != null && !username.isBlank()) + { + client.setUsername(username); + log.debug("Auto-filled username from active profile: {}", username); + } + + // Set password (decrypt first) + String encryptedPassword = activeProfile.getPassword(); + if (encryptedPassword != null && !encryptedPassword.isBlank()) + { + try + { + String decryptedPassword = net.runelite.client.plugins.microbot.util.security.Encryption.decrypt(encryptedPassword); + client.setPassword(decryptedPassword); + log.debug("Auto-filled password from active profile"); + } + catch (Exception e) + { + log.warn("Unable to decrypt stored password for auto-fill", e); + } + } + } + catch (Exception e) + { + log.error("Error during credential auto-fill", e); + } + } + @Subscribe public void onVarbitChanged(VarbitChanged event) { From 6c4c8190321d0a8371175546a4b16a574f93ec81 Mon Sep 17 00:00:00 2001 From: chsami Date: Mon, 24 Nov 2025 20:14:05 +0100 Subject: [PATCH 29/42] feat(config): add world selection feature to user profiles --- .../runelite/client/config/ConfigManager.java | 19 + .../runelite/client/config/ConfigProfile.java | 3 + .../microbot/ui/MicrobotProfilePanel.java | 77 +++- .../microbot/ui/WorldSelectorDialog.java | 355 ++++++++++++++++++ .../microbot/util/security/LoginManager.java | 34 +- 5 files changed, 481 insertions(+), 7 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/WorldSelectorDialog.java diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java index 36a3eef4c99..505b44611ab 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java @@ -360,6 +360,25 @@ public void setDiscordWebhookUrl(ConfigProfile profile, String discordWebhookUrl } } + public void setSelectedWorld(ConfigProfile profile, Integer selectedWorld) { + // Flush pending config changes first in case the profile being + // synced is the active profile. + sendConfig(); + + try (ProfileManager.Lock lock = profileManager.lock()) { + profile = lock.findProfile(profile.getId()); + if (profile == null) { + return; + } + + // Update the selectedWorld only if it's changed + if (!Objects.equals(profile.getSelectedWorld(), selectedWorld)) { + profile.setSelectedWorld(selectedWorld); + lock.dirty(); + } + } + } + public void setMemberExpireDays(ConfigProfile profile, long memberExpireDays) { diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigProfile.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigProfile.java index d6444535d56..0877f01e3c8 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigProfile.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigProfile.java @@ -53,6 +53,9 @@ public class ConfigProfile @Getter @Setter public boolean isMember; + @Getter + @Setter + private Integer selectedWorld; // null = not set, -1 = Random Members, -2 = Random F2P, positive = specific world ID @Getter @Setter private String discordWebhookUrl; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotProfilePanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotProfilePanel.java index dfd0d155b46..61b80a6a309 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotProfilePanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotProfilePanel.java @@ -616,7 +616,7 @@ public void keyPressed(KeyEvent e) { gbc.gridy = 2; detailsPanel.add(createLabeledField("Webhook:", discordWebhookUrl), gbc); - // Member checkbox + // Member checkbox (kept for backward compatibility) member = new JCheckBox("Is Member"); member.setSelected(profile.isMember()); member.setOpaque(false); @@ -627,6 +627,22 @@ public void keyPressed(KeyEvent e) { gbc.gridy = 3; detailsPanel.add(member, gbc); + // World selector button + JPanel worldPanel = new JPanel(new BorderLayout(4, 0)); + worldPanel.setOpaque(false); + + JLabel worldLabel = new JLabel("World:"); + worldLabel.setForeground(new Color(180, 180, 180)); + worldLabel.setFont(new Font("Roboto", Font.PLAIN, 11)); + worldLabel.setPreferredSize(new Dimension(80, 20)); + + JButton worldSelector = createWorldSelectorButton(profile); + worldPanel.add(worldLabel, BorderLayout.WEST); + worldPanel.add(worldSelector, BorderLayout.CENTER); + + gbc.gridy = 4; + detailsPanel.add(worldPanel, gbc); + add(detailsPanel, BorderLayout.CENTER); // Button Panel @@ -836,6 +852,65 @@ private JTextField createDetailField(String placeholder, String value) { return field; } + private JButton createWorldSelectorButton(net.runelite.client.config.ConfigProfile profile) { + JButton button = new JButton(getWorldDisplayText(profile.getSelectedWorld())); + button.setBackground(new Color(60, 60, 60)); + button.setForeground(TEXT_COLOR); + button.setFocusPainted(false); + button.setBorder(new CompoundBorder( + new LineBorder(new Color(60, 60, 60), 1), + new EmptyBorder(4, 8, 4, 8) + )); + button.setFont(new Font("Roboto", Font.PLAIN, 11)); + button.setCursor(new Cursor(Cursor.HAND_CURSOR)); + button.setHorizontalAlignment(SwingConstants.LEFT); + + button.addActionListener(e -> { + WorldSelectorDialog dialog = new WorldSelectorDialog((JFrame) SwingUtilities.getWindowAncestor(this)); + dialog.setVisible(true); + + Integer selectedWorld = dialog.getSelectedWorld(); + if (selectedWorld != null) { + configManager.setSelectedWorld(profile, selectedWorld); + button.setText(getWorldDisplayText(selectedWorld)); + // Update member status based on world selection if needed + if (selectedWorld == -1) { + configManager.setMember(profile, true); + member.setSelected(true); + } else if (selectedWorld == -2) { + configManager.setMember(profile, false); + member.setSelected(false); + } + } + }); + + button.addMouseListener(new MouseAdapter() { + @Override + public void mouseEntered(MouseEvent e) { + button.setBackground(new Color(80, 80, 80)); + } + + @Override + public void mouseExited(MouseEvent e) { + button.setBackground(new Color(60, 60, 60)); + } + }); + + return button; + } + + private String getWorldDisplayText(Integer worldId) { + if (worldId == null) { + return "Click to select world..."; + } else if (worldId == -1) { + return "[Random] Members World"; + } else if (worldId == -2) { + return "[Random] F2P World"; + } else { + return "World " + worldId; + } + } + void setActive(boolean active) { this.active = active; if (active) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/WorldSelectorDialog.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/WorldSelectorDialog.java new file mode 100644 index 00000000000..aa0fc354198 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/WorldSelectorDialog.java @@ -0,0 +1,355 @@ +package net.runelite.client.plugins.microbot.ui; + +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.ui.ColorScheme; +import net.runelite.http.api.worlds.World; +import net.runelite.http.api.worlds.WorldRegion; +import net.runelite.http.api.worlds.WorldResult; +import net.runelite.http.api.worlds.WorldType; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.DefaultTableModel; +import javax.swing.table.TableRowSorter; +import java.awt.*; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +@Slf4j +public class WorldSelectorDialog extends JDialog { + private static final Color BACKGROUND_COLOR = new Color(40, 40, 40); + private static final Color TEXT_COLOR = new Color(220, 220, 220); + private static final Color ACCENT_COLOR = new Color(255, 140, 0); + private static final Color MEMBERS_COLOR = new Color(255, 200, 50); + private static final Color F2P_COLOR = new Color(150, 150, 150); + + private Integer selectedWorld = null; + private JTable worldTable; + private DefaultTableModel tableModel; + private JTextField searchField; + + public WorldSelectorDialog(JFrame parent) { + super(parent, "Select World", true); + initializeUI(); + loadWorlds(); + } + + private void initializeUI() { + setLayout(new BorderLayout(10, 10)); + getContentPane().setBackground(BACKGROUND_COLOR); + + // Top panel with search and special options + JPanel topPanel = new JPanel(new BorderLayout(10, 10)); + topPanel.setOpaque(false); + topPanel.setBorder(new EmptyBorder(15, 15, 5, 15)); + + // Title + JLabel titleLabel = new JLabel("Select World for Profile"); + titleLabel.setFont(new Font("Roboto", Font.BOLD, 16)); + titleLabel.setForeground(TEXT_COLOR); + topPanel.add(titleLabel, BorderLayout.NORTH); + + // Search panel + JPanel searchPanel = new JPanel(new BorderLayout(5, 0)); + searchPanel.setOpaque(false); + + JLabel searchLabel = new JLabel("Search:"); + searchLabel.setForeground(TEXT_COLOR); + searchPanel.add(searchLabel, BorderLayout.WEST); + + searchField = new JTextField(); + searchField.setBackground(new Color(60, 60, 60)); + searchField.setForeground(TEXT_COLOR); + searchField.setCaretColor(TEXT_COLOR); + searchField.setBorder(new EmptyBorder(5, 10, 5, 10)); + searchField.addKeyListener(new java.awt.event.KeyAdapter() { + public void keyReleased(java.awt.event.KeyEvent evt) { + filterWorlds(); + } + }); + searchPanel.add(searchField, BorderLayout.CENTER); + + topPanel.add(searchPanel, BorderLayout.CENTER); + + // Special selection buttons + JPanel specialPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 5)); + specialPanel.setOpaque(false); + + JButton randomMembersBtn = createButton("Random Members World"); + randomMembersBtn.addActionListener(e -> { + selectedWorld = -1; + dispose(); + }); + specialPanel.add(randomMembersBtn); + + JButton randomF2PBtn = createButton("Random F2P World"); + randomF2PBtn.addActionListener(e -> { + selectedWorld = -2; + dispose(); + }); + specialPanel.add(randomF2PBtn); + + topPanel.add(specialPanel, BorderLayout.SOUTH); + + add(topPanel, BorderLayout.NORTH); + + // Center panel with world table + JPanel centerPanel = new JPanel(new BorderLayout()); + centerPanel.setOpaque(false); + centerPanel.setBorder(new EmptyBorder(5, 15, 5, 15)); + + String[] columnNames = {"World", "Players", "Type", "Region", "Activity"}; + tableModel = new DefaultTableModel(columnNames, 0) { + @Override + public boolean isCellEditable(int row, int column) { + return false; + } + }; + + worldTable = new JTable(tableModel); + worldTable.setBackground(new Color(50, 50, 50)); + worldTable.setForeground(TEXT_COLOR); + worldTable.setSelectionBackground(ACCENT_COLOR); + worldTable.setSelectionForeground(Color.BLACK); + worldTable.setRowHeight(25); + worldTable.setGridColor(new Color(70, 70, 70)); + worldTable.setShowGrid(true); + worldTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + // Custom cell renderer + DefaultTableCellRenderer centerRenderer = new DefaultTableCellRenderer(); + centerRenderer.setHorizontalAlignment(JLabel.CENTER); + for (int i = 0; i < worldTable.getColumnCount(); i++) { + worldTable.getColumnModel().getColumn(i).setCellRenderer(new WorldTableCellRenderer()); + } + + // Set column widths + worldTable.getColumnModel().getColumn(0).setPreferredWidth(80); + worldTable.getColumnModel().getColumn(1).setPreferredWidth(80); + worldTable.getColumnModel().getColumn(2).setPreferredWidth(100); + worldTable.getColumnModel().getColumn(3).setPreferredWidth(100); + worldTable.getColumnModel().getColumn(4).setPreferredWidth(200); + + // Enable sorting + TableRowSorter sorter = new TableRowSorter<>(tableModel); + worldTable.setRowSorter(sorter); + + // Sort by world ID by default + sorter.setComparator(0, Comparator.comparingInt(o -> Integer.parseInt(o.toString()))); + sorter.setComparator(1, Comparator.comparingInt(o -> Integer.parseInt(o.toString()))); + + // Double-click to select + worldTable.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + if (evt.getClickCount() == 2) { + selectWorld(); + } + } + }); + + JScrollPane scrollPane = new JScrollPane(worldTable); + scrollPane.setBackground(BACKGROUND_COLOR); + scrollPane.getViewport().setBackground(new Color(50, 50, 50)); + scrollPane.setBorder(BorderFactory.createLineBorder(new Color(80, 80, 80))); + + centerPanel.add(scrollPane, BorderLayout.CENTER); + add(centerPanel, BorderLayout.CENTER); + + // Bottom panel with buttons + JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 10)); + bottomPanel.setOpaque(false); + bottomPanel.setBorder(new EmptyBorder(5, 15, 15, 15)); + + JButton selectBtn = createButton("Select"); + selectBtn.addActionListener(e -> selectWorld()); + bottomPanel.add(selectBtn); + + JButton cancelBtn = createButton("Cancel"); + cancelBtn.addActionListener(e -> { + selectedWorld = null; + dispose(); + }); + bottomPanel.add(cancelBtn); + + add(bottomPanel, BorderLayout.SOUTH); + + setSize(700, 500); + setLocationRelativeTo(getParent()); + } + + private JButton createButton(String text) { + JButton button = new JButton(text); + button.setBackground(new Color(60, 60, 60)); + button.setForeground(TEXT_COLOR); + button.setFocusPainted(false); + button.setBorder(new EmptyBorder(8, 16, 8, 16)); + button.setCursor(new Cursor(Cursor.HAND_CURSOR)); + + button.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseEntered(java.awt.event.MouseEvent evt) { + button.setBackground(new Color(80, 80, 80)); + } + + public void mouseExited(java.awt.event.MouseEvent evt) { + button.setBackground(new Color(60, 60, 60)); + } + }); + + return button; + } + + private void loadWorlds() { + try { + WorldResult worldResult = Microbot.getWorldService().getWorlds(); + if (worldResult == null) { + JOptionPane.showMessageDialog(this, + "Failed to fetch world list. Please try again.", + "Error", JOptionPane.ERROR_MESSAGE); + return; + } + + List worlds = worldResult.getWorlds(); + // Sort by world ID + worlds.sort(Comparator.comparingInt(World::getId)); + + for (World world : worlds) { + // Skip restricted worlds + if (world.getTypes().contains(WorldType.PVP) || + world.getTypes().contains(WorldType.HIGH_RISK) || + world.getTypes().contains(WorldType.BOUNTY) || + world.getTypes().contains(WorldType.SKILL_TOTAL) || + world.getTypes().contains(WorldType.LAST_MAN_STANDING) || + world.getTypes().contains(WorldType.QUEST_SPEEDRUNNING) || + world.getTypes().contains(WorldType.BETA_WORLD) || + world.getTypes().contains(WorldType.DEADMAN) || + world.getTypes().contains(WorldType.PVP_ARENA) || + world.getTypes().contains(WorldType.TOURNAMENT) || + world.getTypes().contains(WorldType.NOSAVE_MODE) || + world.getTypes().contains(WorldType.LEGACY_ONLY) || + world.getTypes().contains(WorldType.EOC_ONLY) || + world.getTypes().contains(WorldType.FRESH_START_WORLD)) { + continue; + } + + String worldType = world.getTypes().contains(WorldType.MEMBERS) ? "Members" : "F2P"; + String region = getRegionDisplay(world.getRegion()); + String activity = world.getActivity() != null ? world.getActivity() : "-"; + + Object[] row = { + world.getId(), + world.getPlayers(), + worldType, + region, + activity + }; + tableModel.addRow(row); + } + + } catch (Exception e) { + log.error("Error loading worlds", e); + JOptionPane.showMessageDialog(this, + "Error loading worlds: " + e.getMessage(), + "Error", JOptionPane.ERROR_MESSAGE); + } + } + + private String getRegionDisplay(WorldRegion region) { + if (region == null) { + return "Unknown"; + } + String regionName = region.toString(); + switch (regionName) { + case "UNITED_STATES": + return "USA"; + case "UNITED_KINGDOM": + return "UK"; + case "AUSTRALIA": + return "AUS"; + case "GERMANY": + return "GER"; + default: + // Format other regions nicely (e.g., "SOUTH_AMERICA" -> "South America") + return formatRegionName(regionName); + } + } + + private String formatRegionName(String regionName) { + if (regionName == null || regionName.isEmpty()) { + return "Unknown"; + } + // Replace underscores with spaces and capitalize each word + String[] words = regionName.toLowerCase().split("_"); + StringBuilder formatted = new StringBuilder(); + for (String word : words) { + if (word.length() > 0) { + formatted.append(Character.toUpperCase(word.charAt(0))) + .append(word.substring(1)) + .append(" "); + } + } + return formatted.toString().trim(); + } + + private void filterWorlds() { + String searchText = searchField.getText().toLowerCase(); + TableRowSorter sorter = (TableRowSorter) worldTable.getRowSorter(); + + if (searchText.isEmpty()) { + sorter.setRowFilter(null); + } else { + sorter.setRowFilter(RowFilter.regexFilter("(?i)" + searchText)); + } + } + + private void selectWorld() { + int selectedRow = worldTable.getSelectedRow(); + if (selectedRow >= 0) { + int modelRow = worldTable.convertRowIndexToModel(selectedRow); + selectedWorld = (Integer) tableModel.getValueAt(modelRow, 0); + dispose(); + } else { + JOptionPane.showMessageDialog(this, + "Please select a world from the table.", + "No Selection", JOptionPane.WARNING_MESSAGE); + } + } + + public Integer getSelectedWorld() { + return selectedWorld; + } + + // Custom cell renderer for the world table + private class WorldTableCellRenderer extends DefaultTableCellRenderer { + @Override + public Component getTableCellRendererComponent(JTable table, Object value, + boolean isSelected, boolean hasFocus, + int row, int column) { + Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + + setHorizontalAlignment(JLabel.CENTER); + + if (!isSelected) { + c.setBackground(new Color(50, 50, 50)); + c.setForeground(TEXT_COLOR); + + // Color code based on world type (column 2) + if (column == 2) { + String type = (String) value; + if ("Members".equals(type)) { + c.setForeground(MEMBERS_COLOR); + } else { + c.setForeground(F2P_COLOR); + } + } + } else { + c.setBackground(ACCENT_COLOR); + c.setForeground(Color.BLACK); + } + + return c; + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/security/LoginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/security/LoginManager.java index 34a7ad772f1..39809d54eb4 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/security/LoginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/security/LoginManager.java @@ -122,14 +122,36 @@ public static boolean login() { return false; } - if (getActiveProfile().isMember() && !isCurrentWorldMembers() || - !getActiveProfile().isMember() && isCurrentWorldMembers()) { - int targetWorld = getRandomWorld(getActiveProfile().isMember()); - return login(getActiveProfile().getName(), getActiveProfile().getPassword(), targetWorld); + // Get the selected world from profile + Integer selectedWorld = getActiveProfile().getSelectedWorld(); + int targetWorld; + + if (selectedWorld != null) { + if (selectedWorld == -1) { + // Random Members World + targetWorld = getRandomWorld(true); + log.info("Using random members world for login: {}", targetWorld); + } else if (selectedWorld == -2) { + // Random F2P World + targetWorld = getRandomWorld(false); + log.info("Using random F2P world for login: {}", targetWorld); + } else { + // Specific world selected + targetWorld = selectedWorld; + log.info("Using profile-selected world for login: {}", targetWorld); + } + } else { + // Fallback to old behavior if no world is selected + if (getActiveProfile().isMember() && !isCurrentWorldMembers() || + !getActiveProfile().isMember() && isCurrentWorldMembers()) { + targetWorld = getRandomWorld(getActiveProfile().isMember()); + } else { + targetWorld = Microbot.getClient().getWorld(); + } + log.info("Using fallback world selection for login: {}", targetWorld); } - return login(getActiveProfile().getName(), getActiveProfile().getPassword(), Microbot.getClient().getWorld()); - + return login(getActiveProfile().getName(), getActiveProfile().getPassword(), targetWorld); } /** From ffa8f9fb3a3bc51c2f3bb512ab9b0fb6a461ed25 Mon Sep 17 00:00:00 2001 From: chsami Date: Wed, 26 Nov 2025 06:42:45 +0100 Subject: [PATCH 30/42] feat(microbot): implement world view handling for NPCs and players --- .../client/plugins/microbot/Microbot.java | 8 ++-- .../plugins/microbot/MicrobotPlugin.java | 16 +++++-- .../plugins/microbot/api/npc/Rs2NpcCache.java | 23 +++++++--- .../microbot/api/player/Rs2PlayerCache.java | 24 +++++++--- .../api/tileitem/Rs2TileItemCache.java | 30 ++++++------ .../api/tileobject/Rs2TileObjectCache.java | 46 +++++++++++-------- 6 files changed, 93 insertions(+), 54 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java index cf978ff1e9d..f2d3991c2a0 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java @@ -66,10 +66,7 @@ import java.time.Instant; import java.util.List; import java.util.*; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; @@ -191,7 +188,8 @@ public class Microbot { @Inject @Getter private static Rs2PlayerStateCache rs2PlayerStateCache; - + @Getter + private static final Set worldViewIds = ConcurrentHashMap.newKeySet(); /** * Get the total runtime of the script * diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java index c37a01fb59d..3a08a6aaf20 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java @@ -48,10 +48,8 @@ import java.awt.image.BufferedImage; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.Arrays; +import java.util.*; import java.util.List; -import java.util.Objects; @PluginDescriptor( name = PluginDescriptor.Default + "Microbot", @@ -599,4 +597,16 @@ public boolean hasWidgetOverlapWithBounds(Rectangle overlayBoundsCanvas) { return result; } + + @Subscribe + public void onWorldViewLoaded(WorldViewLoaded event) + { + Microbot.getWorldViewIds().add(event.getWorldView().getId()); + } + + @Subscribe + public void onWorldViewUnloaded(WorldViewUnloaded event) + { + Microbot.getWorldViewIds().remove(event.getWorldView().getId()); + } } 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 index 2f52c65c088..40e4c65b959 100644 --- 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 @@ -1,10 +1,12 @@ 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; @@ -18,7 +20,7 @@ public Rs2NpcQueryable query() { } /** - * Get all NPCs in the current scene + * Get all NPCs in the current scene across all world views * * @return Stream of Rs2NpcModel */ @@ -28,11 +30,20 @@ public static Stream getNpcsStream() { return npcs.stream(); } - List result = Microbot.getClientThread().invoke(() -> Microbot - .getClient() - .getTopLevelWorldView() - .npcs().stream().map(Rs2NpcModel::new) - .collect(Collectors.toList())); + 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(); 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 index bd34518e572..6eeb720cf1f 100644 --- 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 @@ -1,5 +1,6 @@ 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; @@ -28,15 +29,24 @@ public static Stream getPlayersStream() { return players.stream(); } - // Get all players using the existing Rs2Player utility - List result = Microbot.getClient().getTopLevelWorldView().players() - .stream() - .filter(Objects::nonNull) - .map(Rs2PlayerModel::new) - .collect(Collectors.toList()); + List result = new ArrayList<>(); + + for (var id : Microbot.getWorldViewIds()) { + WorldView worldView = Microbot.getClient().getWorldView(id); + if (worldView == null) { + continue; + } + // Get all players using the existing Rs2Player utility + players.addAll(worldView.players() + .stream() + .filter(Objects::nonNull) + .map(Rs2PlayerModel::new) + .collect(Collectors.toList())); + } + players = result; lastUpdatePlayers = Microbot.getClient().getTickCount(); - return result.stream(); + return players.stream(); } } 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 index 54de5e425b6..120fc9359b4 100644 --- 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 @@ -3,9 +3,9 @@ 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 net.runelite.client.plugins.microbot.util.player.Rs2Player; import javax.inject.Singleton; import java.util.ArrayList; @@ -28,7 +28,7 @@ public Rs2TileItemQueryable query() { } /** - * Get all ground items in the current scene. + * Get all ground 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. * @@ -45,20 +45,24 @@ public static Stream getGroundItemsStream() { List result = new ArrayList<>(); - // Get all tiles in current plane - var tileValues = Microbot.getClient().getTopLevelWorldView().getScene().getTiles()[Microbot.getClient().getTopLevelWorldView().getPlane()]; + for (var id : Microbot.getWorldViewIds()) { + WorldView worldView = Microbot.getClient().getWorldView(id); + if (worldView == null) { + continue; + } - for (Tile[] tileRow : tileValues) { - for (Tile tile : tileRow) { - if (tile == 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; + List items = tile.getGroundItems(); + if (items == null || items.isEmpty()) continue; - // Add all items from this tile to the result - for (TileItem item : items) { - if (item != null) { - result.add(new Rs2TileItemModel(tile, item)); + for (TileItem item : items) { + if (item != null) { + result.add(new Rs2TileItemModel(tile, item)); + } } } } 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 index e68e411224f..598d31f57da 100644 --- 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 @@ -3,6 +3,7 @@ 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; @@ -21,6 +22,7 @@ public Rs2TileObjectQueryable query() { /** * Get all tile objects in the current scene + * * @return Stream of Rs2TileObjectModel */ public static Stream getObjectsStream() { @@ -34,32 +36,36 @@ public static Stream getObjectsStream() { List result = new ArrayList<>(); - var tileValues = Microbot.getClient().getTopLevelWorldView().getScene().getTiles()[Microbot.getClient().getTopLevelWorldView().getPlane()]; - - for (Tile[] tileValue : tileValues) { - for (Tile tile : tileValue) { - if (tile == null) continue; + for (var id : Microbot.getWorldViewIds()) { + WorldView worldView = Microbot.getClient().getWorldView(id); + if (worldView == null) { + continue; + } + var tileValues = Microbot.getClient().getTopLevelWorldView().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.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())); + 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(); From 2f5673654680cac6257ce52aa1d16f41b99ceb5a Mon Sep 17 00:00:00 2001 From: chsami Date: Wed, 26 Nov 2025 20:54:49 +0100 Subject: [PATCH 31/42] feat(query): enhance entity querying with additional filters and methods --- .../microbot/api/AbstractEntityQueryable.java | 171 +++++++++++------- .../microbot/api/IEntityQueryable.java | 19 +- .../microbot/api/npc/NpcApiExample.java | 8 +- .../microbot/api/player/PlayerApiExample.java | 8 +- .../api/tileitem/TileItemApiExample.java | 8 +- .../microbot/util/player/Rs2Player.java | 6 +- 6 files changed, 131 insertions(+), 89 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java index d3550c283ee..047d9e895a2 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java @@ -5,17 +5,12 @@ import net.runelite.client.plugins.microbot.util.player.Rs2Player; import java.util.Arrays; +import java.util.Comparator; import java.util.List; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; -/** - * Generic abstract implementation of {@link IEntityQueryable} to reduce duplication. - * - * @param concrete queryable type (self type) - * @param entity model type - */ public abstract class AbstractEntityQueryable< Q extends IEntityQueryable, E extends IEntity @@ -28,15 +23,8 @@ protected AbstractEntityQueryable() { this.source = initialSource(); } - /** - * Provide the initial stream to query against. - */ protected abstract Stream initialSource(); - - /** - * Player location used for proximity queries. - */ protected WorldPoint getPlayerLocation() { return Rs2Player.getWorldLocation(); } @@ -53,7 +41,8 @@ public Q where(Predicate predicate) { public Q within(int distance) { WorldPoint playerLoc = getPlayerLocation(); if (playerLoc == null) { - return null; + this.source = Stream.empty(); + return (Q) this; } this.source = this.source @@ -66,7 +55,8 @@ public Q within(int distance) { @Override public Q within(WorldPoint anchor, int distance) { if (anchor == null) { - return null; + this.source = Stream.empty(); + return (Q) this; } this.source = this.source @@ -75,6 +65,65 @@ public Q within(WorldPoint anchor, int 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); @@ -82,13 +131,7 @@ public E first() { @Override public E nearest() { - WorldPoint playerLoc = getPlayerLocation(); - if (playerLoc == null) { - return null; - } - return source - .min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(playerLoc))) - .orElse(null); + return nearest(Integer.MAX_VALUE); } @Override @@ -97,13 +140,8 @@ public E nearest(int maxDistance) { if (playerLoc == null) { return null; } - return source - .filter(x -> { - WorldPoint loc = x.getWorldLocation(); - return loc != null && loc.distanceTo(playerLoc) <= maxDistance; - }) - .min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(playerLoc))) - .orElse(null); + + return nearest(playerLoc, maxDistance); } @Override @@ -111,60 +149,53 @@ public E nearest(WorldPoint anchor, int maxDistance) { if (anchor == null) { return null; } + return source - .filter(x -> { - WorldPoint loc = x.getWorldLocation(); - return loc != null && loc.distanceTo(anchor) <= maxDistance; + .map(entity -> { + WorldPoint loc = entity.getWorldLocation(); + int distance = (loc != null) ? loc.distanceTo(anchor) : Integer.MAX_VALUE; + return new EntityDistance<>(entity, distance); }) - .min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(anchor))) + .filter(pair -> pair.distance <= maxDistance) + .min(Comparator.comparingInt(pair -> pair.distance)) + .map(pair -> pair.entity) .orElse(null); } @Override - public E withName(String name) { - if (name == null) return null; - return Microbot.getClientThread().invoke(() -> { - return source.filter(x -> { - String n = x.getName(); - return n != null && n.equalsIgnoreCase(name); - }) - .min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(Rs2Player.getWorldLocation()))) - .orElse(null); - }); + public List toList() { + return source.collect(Collectors.toList()); } - @Override - public E withNames(String... names) { - if (names == null || names.length == 0) return null; - return source.filter(x -> { - String n = x.getName(); - if (n == null) return false; - return Arrays.stream(names).anyMatch(n::equalsIgnoreCase); - }).min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(Rs2Player.getWorldLocation()))) - .orElse(null); + public E firstOnClientThread() { + return Microbot.getClientThread().invoke(() -> first()); } - @Override - public E withId(int id) { - return source.filter(x -> x.getId() == id).min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(Rs2Player.getWorldLocation()))) - .orElse(null); + public E nearestOnClientThread() { + return Microbot.getClientThread().invoke(() -> nearest()); } - @Override - public E withIds(int... ids) { - if (ids == null || ids.length == 0) return null; - return source.filter(x -> { - int entityId = x.getId(); - for (int id : ids) { - if (entityId == id) return true; - } - return false; - }).min(java.util.Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(Rs2Player.getWorldLocation()))) - .orElse(null); + public E nearestOnClientThread(int maxDistance) { + return Microbot.getClientThread().invoke(() -> nearest(maxDistance)); } - @Override - public List toList() { - return source.collect(Collectors.toList()); + public E nearestOnClientThread(WorldPoint anchor, int maxDistance) { + return Microbot.getClientThread().invoke(() -> nearest(anchor, maxDistance)); + } + + public List toListOnClientThread() { + return Microbot.getClientThread().invoke(() -> toList()); } } + + + +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/IEntityQueryable.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/IEntityQueryable.java index c07ac959d58..38719791e69 100644 --- 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 @@ -1,18 +1,25 @@ package net.runelite.client.plugins.microbot.api; import net.runelite.api.coords.WorldPoint; +import java.util.List; +import java.util.function.Predicate; public interface IEntityQueryable, E> { - Q where(java.util.function.Predicate predicate); + Q where(Predicate predicate); 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 nearest(); E nearest(int maxDistance); E nearest(WorldPoint anchor, int maxDistance); - E withName(String name); - E withNames(String...names); - E withId(int id); - E withIds(int...ids); - java.util.List toList(); + List toList(); + E firstOnClientThread(); + E nearestOnClientThread(); + E nearestOnClientThread(int maxDistance); + E nearestOnClientThread(WorldPoint anchor, int maxDistance); + List toListOnClientThread(); } 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 index 71814e0d46e..fea7bdad501 100644 --- 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 @@ -24,16 +24,16 @@ public static void examples() { Rs2NpcModel nearestNpcWithinRange = cache.query().nearest(10); // Example 3: Find an NPC by name - Rs2NpcModel goblin = cache.query().withName("Goblin"); + Rs2NpcModel goblin = cache.query().withName("Goblin").nearest(); // Example 4: Find an NPC by multiple names - Rs2NpcModel enemy = cache.query().withNames("Goblin", "Guard", "Dark wizard"); + Rs2NpcModel enemy = cache.query().withNames("Goblin", "Guard", "Dark wizard").nearest(); // Example 5: Find an NPC by ID - Rs2NpcModel npcById = cache.query().withId(1234); + Rs2NpcModel npcById = cache.query().withId(1234).nearest(); // Example 6: Find an NPC by multiple IDs - Rs2NpcModel npcByIds = cache.query().withIds(1234, 5678, 9012); + Rs2NpcModel npcByIds = cache.query().withIds(1234, 5678, 9012).nearest(); // Example 7: Get all NPCs with a custom filter Rs2NpcModel attackingNpc = cache.query() diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java index 4fa997916b5..a372e8a90c0 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java @@ -25,16 +25,16 @@ public static void examples() { Rs2PlayerModel nearestPlayerWithinRange = cache.query().nearest(10); // Example 3: Find a player by name - Rs2PlayerModel playerByName = cache.query().withName("PlayerName"); + Rs2PlayerModel playerByName = cache.query().withName("PlayerName").nearest(); // Example 4: Find a player by multiple names - Rs2PlayerModel anyOfThesePlayers = cache.query().withNames("Player1", "Player2", "Player3"); + Rs2PlayerModel anyOfThesePlayers = cache.query().withNames("Player1", "Player2", "Player3").nearest(); // Example 5: Find a player by ID - Rs2PlayerModel playerById = cache.query().withId(12345); + Rs2PlayerModel playerById = cache.query().withId(12345).nearest(); // Example 6: Find a player by multiple IDs - Rs2PlayerModel playerByIds = cache.query().withIds(12345, 67890, 11111); + Rs2PlayerModel playerByIds = cache.query().withIds(12345, 67890, 11111).nearest(); // Example 8: Find all friends List friends = cache.query() 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 index 6b579be3b97..755e674ade0 100644 --- 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 @@ -26,16 +26,16 @@ public void examples() { Rs2TileItemModel nearestItemWithinRange = cache.query().nearest(10); // Example 3: Find a ground item by name - Rs2TileItemModel coins = cache.query().withName("Coins"); + 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"); + 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); // Coins + 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); // Coins, Bones, Dragon bones + 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() diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java index ca82206b1f1..e8bc38a6886 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java @@ -50,7 +50,6 @@ import static net.runelite.api.MenuAction.CC_OP; import static net.runelite.client.plugins.microbot.util.Global.*; -@Deprecated(since = "2.1.0 - Use Rs2PlayerCache/Rs2PlayerQueryable", forRemoval = true) public class Rs2Player { static int VENOM_VALUE_CUTOFF = -38; private static int antiFireTime = -1; @@ -672,6 +671,7 @@ public static double getHealthPercentage() { * @param predicate A condition to filter players (optional). * @return A stream of Rs2PlayerModel objects representing nearby players. */ + @Deprecated(since = "2.1.0 - Use Rs2PlayerCache/Rs2PlayerQueryable", forRemoval = true) public static Stream getPlayers(Predicate predicate) { return getPlayers(predicate, false); } @@ -683,6 +683,7 @@ public static Stream getPlayers(Predicate predic * @param includeLocalPlayer a flag on whether to include the local player within the stream * @return A stream of Rs2PlayerModel objects representing nearby players. */ + @Deprecated(since = "2.1.0 - Use Rs2PlayerCache/Rs2PlayerQueryable", forRemoval = true) public static Stream getPlayers(Predicate predicate, boolean includeLocalPlayer) { List players = Optional.of(Microbot.getClient().getTopLevelWorldView().players() .stream() @@ -704,6 +705,7 @@ public static Stream getPlayers(Predicate predic * If {@code false}, checks if the player name contains the given string. * @return The first matching {@code Rs2PlayerModel}, or {@code null} if no player is found. */ + @Deprecated(since = "2.1.0 - Use Rs2PlayerCache/Rs2PlayerQueryable", forRemoval = true) public static Rs2PlayerModel getPlayer(String playerName, boolean exact) { return getPlayers(player -> { String name = player.getName(); @@ -719,6 +721,7 @@ public static Rs2PlayerModel getPlayer(String playerName, boolean exact) { * @return The first matching {@code Rs2PlayerModel}, or {@code null} if no player is found. * Uses {@code getPlayer(playerName, false)} to perform a case-insensitive partial match. */ + @Deprecated(since = "2.1.0 - Use Rs2PlayerCache/Rs2PlayerQueryable", forRemoval = true) public static Rs2PlayerModel getPlayer(String playerName) { return getPlayer(playerName, false); } @@ -728,6 +731,7 @@ public static Rs2PlayerModel getPlayer(String playerName) { * * @return a list of players that are in combat */ + @Deprecated(since = "2.1.0 - Use Rs2PlayerCache/Rs2PlayerQueryable", forRemoval = true) public static List getPlayersInCombat() { return getPlayers(player -> player.getHealthRatio() != -1).collect(Collectors.toList()); } From 736861f933e8b0698d2da6604393bba56b79984e Mon Sep 17 00:00:00 2001 From: chsami Date: Sun, 30 Nov 2025 15:42:18 +0100 Subject: [PATCH 32/42] refactor(npc): update NewMenuEntry usage for improved readability --- .run/Runelite.run.xml | 24 +- .../microbot/api/IEntityQueryable.java | 38 +- .../microbot/api/npc/models/Rs2NpcModel.java | 4 +- .../api/player/models/Rs2PlayerModel.java | 4 +- .../api/tileitem/models/Rs2TileItemModel.java | 24 +- .../tileobject/models/Rs2TileObjectModel.java | 12 +- .../microbot/questhelper/QuestScript.java | 10 +- .../questhelper/logic/PiratesTreasure.java | 19 +- .../plugins/microbot/util/bank/Rs2Bank.java | 8 +- .../util/depositbox/Rs2DepositBox.java | 10 +- .../microbot/util/equipment/Rs2Equipment.java | 10 +- .../util/gameobject/Rs2GameObject.java | 13 +- .../util/grandexchange/Rs2GrandExchange.java | 72 ++- .../util/grounditem/Rs2GroundItem.java | 24 +- .../microbot/util/inventory/Rs2Inventory.java | 11 +- .../microbot/util/inventory/Rs2RunePouch.java | 9 +- .../plugins/microbot/util/magic/Rs2Magic.java | 11 +- .../microbot/util/menu/NewMenuEntry.java | 96 +-- .../plugins/microbot/util/npc/Rs2Npc.java | 13 +- .../microbot/util/player/Rs2Player.java | 25 +- .../microbot/util/prayer/Rs2Prayer.java | 57 +- .../microbot/util/sailing/Rs2Sailing.java | 9 +- .../microbot/util/settings/Rs2Settings.java | 60 +- .../plugins/microbot/util/shop/Rs2Shop.java | 10 +- .../util/tileobject/Rs2TileObjectModel.java | 12 +- .../microbot/util/walker/Rs2Walker.java | 581 ++++++++++-------- .../microbot/util/widget/Rs2Widget.java | 10 +- 27 files changed, 778 insertions(+), 398 deletions(-) diff --git a/.run/Runelite.run.xml b/.run/Runelite.run.xml index ebfc8daf0ae..0b28a112a9e 100644 --- a/.run/Runelite.run.xml +++ b/.run/Runelite.run.xml @@ -1,13 +1,13 @@ - - - + + + \ No newline at end of file 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 index 38719791e69..ed9d4ae1d38 100644 --- 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 @@ -4,7 +4,7 @@ import java.util.List; import java.util.function.Predicate; -public interface IEntityQueryable, E> { +public interface IEntityQueryable, E extends IEntity> { Q where(Predicate predicate); Q within(int distance); Q within(WorldPoint anchor, int distance); @@ -22,4 +22,40 @@ public interface IEntityQueryable, E> { E nearestOnClientThread(int maxDistance); E nearestOnClientThread(WorldPoint anchor, int maxDistance); List toListOnClientThread(); + + default boolean interact() { + E entity = nearest(); + if (entity == null) return false; + return entity.click(); + } + + default boolean interact(String action) { + E entity = nearest(); + if (entity == null) return false; + return entity.click(action); + } + + default boolean interact(String action, int maxDistance) { + E entity = nearest(maxDistance); + if (entity == null) return false; + return entity.click(action); + } + + default boolean interact(int id) { + E entity = ((Q) this).withId(id).nearest(); + if (entity == null) return false; + return entity.click(); + } + + default boolean interact(int id, String action) { + E entity = ((Q) this).withId(id).nearest(); + if (entity == null) return false; + return entity.click(action); + } + + default boolean interact(int id, String action, int maxDistance) { + E entity = ((Q) this).withId(id).nearest(maxDistance); + if (entity == null) return false; + return entity.click(action); + } } 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 index bc2b2071686..505c0f9f75a 100644 --- 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 @@ -196,11 +196,11 @@ public HeadIcon getHeadIcon() { @Override public boolean click() { - throw new NotImplementedException("click() not implemented yet for Rs2NpcModel"); + return net.runelite.client.plugins.microbot.util.npc.Rs2Npc.interact(this); } @Override public boolean click(String action) { - throw new NotImplementedException("click() not implemented yet for Rs2NpcModel"); + return net.runelite.client.plugins.microbot.util.npc.Rs2Npc.interact(this, action); } } 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 index ffb81a67c60..32decf15459 100644 --- 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 @@ -88,11 +88,11 @@ public int getFootprintSize() @Override public boolean click() { - throw new NotImplementedException("click() not implemented yet for Rs2PlayerModel"); + throw new NotImplementedException("click() not implemented yet for Rs2PlayerModel - player interactions are not well-defined in the current codebase"); } @Override public boolean click(String action) { - throw new NotImplementedException("click() not implemented yet for Rs2PlayerModel"); + throw new NotImplementedException("click(String action) not implemented yet for Rs2PlayerModel - player interactions are not well-defined in the current codebase"); } } 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 index 83d850454d5..44fc06cd7ab 100644 --- 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 @@ -243,12 +243,28 @@ public boolean click(String action) { if (localPoint1 != null) { Polygon canvas = Perspective.getCanvasTilePoly(Microbot.getClient(), localPoint1); if (canvas != null) { - Microbot.doInvoke(new NewMenuEntry(action, param0, param1, menuAction.getId(), identifier, -1, target), - canvas.getBounds()); + 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(action, param0, param1, menuAction.getId(), identifier, -1, target), - new Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); + 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) { 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 index 5b500d72a5d..3ed71d32db4 100644 --- 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 @@ -255,7 +255,17 @@ public boolean click(String action) { } - Microbot.doInvoke(new NewMenuEntry(param0, param1, menuAction.getId(), getId(), -1, action, objName, tileObject), Rs2UiHelper.getObjectClickbox(tileObject)); + Microbot.doInvoke(new NewMenuEntry() + .param0(param0) + .param1(param1) + .opcode(menuAction.getId()) + .identifier(getId()) + .itemId(-1) + .option(action) + .target(objName) + .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); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/QuestScript.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/QuestScript.java index 73c82b9e1f4..9a99d0bd817 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/QuestScript.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/QuestScript.java @@ -311,7 +311,15 @@ public boolean applyNpcStep(NpcStep step) { if (emoteWidget.getSpriteId() == emoteStep.getEmote().getSpriteId()) { var id = emoteWidget.getOriginalX() / 42 + ((emoteWidget.getOriginalY() - 6) / 49) * 4; - Microbot.doInvoke(new NewMenuEntry("Perform", emoteWidget.getText(), 1, MenuAction.CC_OP, id, ComponentID.EMOTES_EMOTE_CONTAINER, false), new Rectangle(0, 0, 1, 1)); + Microbot.doInvoke(new NewMenuEntry() + .option("Perform") + .target(emoteWidget.getText()) + .identifier(1) + .type(MenuAction.CC_OP) + .param0(id) + .param1(ComponentID.EMOTES_EMOTE_CONTAINER) + , new Rectangle(0, 0, 1, 1)); + Rs2Player.waitForAnimation(); if (Rs2Dialogue.isInDialogue()) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/logic/PiratesTreasure.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/logic/PiratesTreasure.java index 9ca7ad5ff54..26802677ee1 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/logic/PiratesTreasure.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/questhelper/logic/PiratesTreasure.java @@ -201,16 +201,15 @@ private boolean openSearchWidget() { Microbot.log("Clicking search icon at " + clickPoint.getX() + ", " + clickPoint.getY()); - // Create a direct menu entry for clicking the search - NewMenuEntry entry = new NewMenuEntry( - "Open", - "Search", - 2, - MenuAction.CC_OP, - 0, - 26148864, - false - ); + // Create a direct menu entry for clicking the search using the builder-style API + NewMenuEntry entry = new NewMenuEntry() + .option("Open") + .target("Search") + .identifier(2) + .type(MenuAction.CC_OP) + .param0(0) + .param1(26148864) + .forceLeftClick(false); // Use mouse to click at the exact point Microbot.getMouse().click(clickPoint, entry); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/bank/Rs2Bank.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/bank/Rs2Bank.java index 82492fa44de..b8cfcaf29f4 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/bank/Rs2Bank.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/bank/Rs2Bank.java @@ -127,7 +127,13 @@ public static void invokeMenu(final int identifier, Rs2ItemModel rs2Item) { itemBoundingBox = itemBounds(rs2Item); } - Microbot.doInvoke(new NewMenuEntry(rs2Item.getSlot(), container, MenuAction.CC_OP.getId(), identifier, rs2Item.getId(), rs2Item.getName()), (itemBoundingBox == null) ? new Rectangle(1, 1) : itemBoundingBox); + Microbot.doInvoke(new NewMenuEntry() + .param0(rs2Item.getSlot()) + .param1(container) + .opcode(MenuAction.CC_OP.getId()) + .identifier(identifier) + .itemId(rs2Item.getId()) + .target(rs2Item.getName()), (itemBoundingBox == null) ? new Rectangle(1, 1) : itemBoundingBox); // MenuEntryImpl(getOption=Wear, getTarget=Amulet of glory(4), getIdentifier=9, getType=CC_OP_LOW_PRIORITY, getParam0=1, getParam1=983043, getItemId=1712, isForceLeftClick=false, isDeprioritized=false) // Rs2Reflection.invokeMenu(rs2Item.slot, container, MenuAction.CC_OP.getId(), identifier, rs2Item.id, "Withdraw-1", rs2Item.name, -1, -1); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/depositbox/Rs2DepositBox.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/depositbox/Rs2DepositBox.java index 284852e36a3..b7a93c2868b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/depositbox/Rs2DepositBox.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/depositbox/Rs2DepositBox.java @@ -360,7 +360,15 @@ private static void invokeMenu(int entryIndex, Rs2ItemModel item) { Rectangle itemBoundingBox = itemBounds(item); - Microbot.doInvoke(new NewMenuEntry(item.getSlot(), DEPOSITBOX_INVENTORY_ITEM_CONTAINER_COMPONENT_ID, action.getId(), identifier, item.getId(), option), (itemBoundingBox == null) ? new Rectangle(1, 1) : itemBoundingBox); + Microbot.doInvoke(new NewMenuEntry() + .param0(item.getSlot()) + .param1(DEPOSITBOX_INVENTORY_ITEM_CONTAINER_COMPONENT_ID) + .opcode(action.getId()) + .identifier(identifier) + .itemId(item.getId()) + .option(option) + , + (itemBoundingBox == null) ? new Rectangle(1, 1) : itemBoundingBox); } /** diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/equipment/Rs2Equipment.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/equipment/Rs2Equipment.java index bbea927887f..98315b8b868 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/equipment/Rs2Equipment.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/equipment/Rs2Equipment.java @@ -376,7 +376,15 @@ public static void invokeMenu(Rs2ItemModel rs2Item, String action) { rectangle = getSafeBounds(InterfaceID.WORNITEMS,24); } - Microbot.doInvoke(new NewMenuEntry(param0, param1, menuAction.getId(), identifier, -1, rs2Item.getName()), rectangle); + Microbot.doInvoke(new NewMenuEntry() + .param0(param0) + .param1(param1) + .opcode(menuAction.getId()) + .identifier(identifier) + .itemId(-1) + .target(rs2Item.getName()) + , + rectangle); //Rs2Reflection.invokeMenu(param0, param1, menuAction.getId(), identifier, rs2Item.id, action, target, -1, -1); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java index 177d630dda2..50f2888258c 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java @@ -1880,7 +1880,18 @@ public static boolean clickObject(TileObject object, String action) { } - Microbot.doInvoke(new NewMenuEntry(param0, param1, menuAction.getId(), object.getId(), -1, action, objComp.getName(), object, worldViewId), Rs2UiHelper.getObjectClickbox(object)); + Microbot.doInvoke(new NewMenuEntry() + .param0(param0) + .param1(param1) + .opcode(menuAction.getId()) + .identifier(object.getId()) + .itemId(-1) + .option(action) + .target(objComp.getName()) + .gameObject(object) + .worldViewId(worldViewId) + , + Rs2UiHelper.getObjectClickbox(object)); // 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); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grandexchange/Rs2GrandExchange.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grandexchange/Rs2GrandExchange.java index 1c526378a47..6ac2f27259b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grandexchange/Rs2GrandExchange.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grandexchange/Rs2GrandExchange.java @@ -365,7 +365,15 @@ private static void viewOffer(Widget widget) { return; } // MenuEntryImpl(getOption=View offer, getTarget=, getIdentifier=1, getType=CC_OP, getParam0=2, getParam1=30474247, getItemId=-1, isForceLeftClick=false, getWorldViewId=-1, isDeprioritized=false) - NewMenuEntry menuEntry = new NewMenuEntry("View offer", "", 1, MenuAction.CC_OP, 2, widget.getId(), false); + NewMenuEntry menuEntry = new NewMenuEntry() + .option("View offer") + .target("") + .identifier(1) + .type(MenuAction.CC_OP) + .param0(2) + .param1(widget.getId()) + .itemId(-1) + .forceLeftClick(false); Rectangle bounds = widget.getBounds(); Microbot.doInvoke(menuEntry, bounds); } @@ -425,7 +433,17 @@ private static boolean collectOffer(boolean toBank) { desiredAction = toBank ? "Bank" : "Collect"; } param0 = i == 0 ? 2 : 3; - NewMenuEntry menuEntry = new NewMenuEntry(desiredAction, "", identifier, MenuAction.CC_OP, param0, child.getId(), false); + + NewMenuEntry menuEntry = new NewMenuEntry() + .option(desiredAction) + .target("") + .identifier(identifier) + .type(MenuAction.CC_OP) + .param0(param0) + .param1(child.getId()) + .itemId(child.getItemId()) + .forceLeftClick(false); + Rectangle bounds = child.getBounds() != null && Rs2UiHelper.isRectangleWithinCanvas(child.getBounds()) ? child.getBounds() : Rs2UiHelper.getDefaultRectangle(); Microbot.doInvoke(menuEntry, bounds); if (!Rs2AntibanSettings.naturalMouse) { @@ -491,7 +509,15 @@ private static void adjustPriceByPercent(int percent) { } else { // MenuEntryImpl(getOption=Customise, getTarget=, getIdentifier=2, getType=CC_OP, getParam0=14, getParam1=30474266, getItemId=-1, isForceLeftClick=false, getWorldViewId=-1, isDeprioritized=false) // MenuEntryImpl(getOption=Customise, getTarget=, getIdentifier=2, getType=CC_OP, getParam0=15, getParam1=30474266, getItemId=-1, isForceLeftClick=false, getWorldViewId=-1, isDeprioritized=false) - NewMenuEntry menuEntry = new NewMenuEntry("Customise", "", 2, MenuAction.CC_OP, isIncrease ? 15 : 14, adjustXWidget.getId(), false); + NewMenuEntry menuEntry = new NewMenuEntry() + .option("Customise") + .target("") + .identifier(2) + .type(MenuAction.CC_OP) + .param0(isIncrease ? 15 : 14) + .param1(adjustXWidget.getId()) + .itemId(-1) + .forceLeftClick(false); Rectangle bounds = adjustXWidget.getBounds() != null && Rs2UiHelper.isRectangleWithinCanvas(adjustXWidget.getBounds()) ? adjustXWidget.getBounds() : Rs2UiHelper.getDefaultRectangle(); Microbot.doInvoke(menuEntry, bounds); } @@ -665,7 +691,15 @@ public static boolean collectAll(boolean collectToBank) { } // MenuEntryImpl(getOption=Collect to bank, getTarget=, getIdentifier=2, getType=CC_OP, getParam0=0, getParam1=30474246, getItemId=-1, isForceLeftClick=false, getWorldViewId=-1, isDeprioritized=false) // MenuEntryImpl(getOption=Collect to inventory, getTarget=, getIdentifier=1, getType=CC_OP, getParam0=0, getParam1=30474246, getItemId=-1, isForceLeftClick=false, getWorldViewId=-1, isDeprioritized=false) - NewMenuEntry entry = new NewMenuEntry(collectToBank ? "Collect to bank" : "Collect to inventory", "", collectToBank ? 2 : 1, MenuAction.CC_OP, 0, collectButton.getId(), false); + NewMenuEntry entry = new NewMenuEntry() + .option(collectToBank ? "Collect to bank" : "Collect to inventory") + .target("") + .identifier(collectToBank ? 2 : 1) + .type(MenuAction.CC_OP) + .param0(0) + .param1(collectButton.getId()) + .itemId(-1) + .forceLeftClick(false); Rectangle bounds = collectButton.getBounds() != null && Rs2UiHelper.isRectangleWithinCanvas(collectButton.getBounds()) ? collectButton.getBounds() : Rs2UiHelper.getDefaultRectangle(); @@ -787,7 +821,15 @@ public static boolean abortOffer(String name, boolean collectToBank) { } Widget parent = GrandExchangeWidget.getSlot(matchingSlot.get()); - NewMenuEntry menuEntry = new NewMenuEntry("Abort offer", "", 2, MenuAction.CC_OP, 2, parent.getId(), false); + NewMenuEntry menuEntry = new NewMenuEntry() + .option("Abort offer") + .target("") + .identifier(2) + .type(MenuAction.CC_OP) + .param0(2) + .param1(parent.getId()) + .itemId(-1) + .forceLeftClick(false); Rectangle bounds = parent.getBounds() != null && Rs2UiHelper.isRectangleWithinCanvas(parent.getBounds()) ? parent.getBounds() : Rs2UiHelper.getDefaultRectangle(); @@ -816,7 +858,15 @@ public static boolean abortAllOffers(boolean collectToBank) { }) .forEach(slot -> { Widget parent = GrandExchangeWidget.getSlot(slot); - NewMenuEntry menuEntry = new NewMenuEntry("Abort offer", "", 2, MenuAction.CC_OP, 2, parent.getId(), false); + NewMenuEntry menuEntry = new NewMenuEntry() + .option("Abort offer") + .target("") + .identifier(2) + .type(MenuAction.CC_OP) + .param0(2) + .param1(parent.getId()) + .itemId(-1) + .forceLeftClick(false); Rectangle bounds = parent.getBounds() != null && Rs2UiHelper.isRectangleWithinCanvas(parent.getBounds()) ? parent.getBounds() : Rs2UiHelper.getDefaultRectangle(); @@ -1891,7 +1941,15 @@ public static List> cancelSpecificOffers(List Rs2Widget.isWidgetVisible(584, 0)); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/inventory/Rs2RunePouch.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/inventory/Rs2RunePouch.java index b83d41e389d..5544a1fdf42 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/inventory/Rs2RunePouch.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/inventory/Rs2RunePouch.java @@ -457,7 +457,14 @@ private static boolean coreLoad(Map requiredRunes) break; } Rectangle loadBounds = loadWidget.getBounds(); - NewMenuEntry menuEntry = new NewMenuEntry("Load", "", 1, MenuAction.CC_OP, -1, loadWidget.getId(), false); + NewMenuEntry menuEntry = new NewMenuEntry() + .option("Load") + .target("") + .identifier(1) + .type(MenuAction.CC_OP) + .itemId(-1) + .param1(loadWidget.getId()) + .forceLeftClick(false); Microbot.doInvoke(menuEntry, loadBounds != null && Rs2UiHelper.isRectangleWithinCanvas(loadBounds) ? loadBounds : Rs2UiHelper.getDefaultRectangle()); Global.sleepUntil(() -> getRunes().entrySet().stream().allMatch(e -> requiredRunes.getOrDefault(e.getKey(), 0) <= e.getValue())); return closeRunePouch(); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/magic/Rs2Magic.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/magic/Rs2Magic.java index ceb407750a0..b09075e2764 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/magic/Rs2Magic.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/magic/Rs2Magic.java @@ -170,7 +170,16 @@ public static boolean cast(MagicAction magicSpell, String option, int identifier if (magicSpell.getWidgetId() == -1) throw new NotImplementedException("This spell has not been configured yet in the MagicAction.java class"); - Microbot.doInvoke(new NewMenuEntry(option, -1, magicSpell.getWidgetId(), menuAction.getId(), identifier, -1, magicSpell.getName()), new Rectangle(Rs2Widget.getWidget(magicSpell.getWidgetId()).getBounds())); + Microbot.doInvoke(new NewMenuEntry() + .option(option) + .param0(-1) + .param1(magicSpell.getWidgetId()) + .opcode(menuAction.getId()) + .identifier(identifier) + .itemId(-1) + .target(magicSpell.getName()) + , + new Rectangle(Rs2Widget.getWidget(magicSpell.getWidgetId()).getBounds())); //Rs2Reflection.invokeMenu(-1, magicSpell.getWidgetId(), menuAction.getId(), 1, -1, "Cast", "" + magicSpell.getName() + "", -1, -1); return true; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/menu/NewMenuEntry.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/menu/NewMenuEntry.java index aa85d17a6fc..d41d929f63f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/menu/NewMenuEntry.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/menu/NewMenuEntry.java @@ -30,74 +30,70 @@ public class NewMenuEntry implements MenuEntry { private Widget widget; private int worldViewId = -1; - private NewMenuEntry(int param0, int param1, MenuAction type, int identifier) { - this.param0 = param0; - this.param1 = param1; - this.type = type; - this.identifier = identifier; + public NewMenuEntry() { + this.option = ""; + this.target = ""; + this.itemId = -1; } - private NewMenuEntry(int param0, int param1, int opcode, int identifier) { - this(param0, param1, MenuAction.of(opcode), identifier); + public NewMenuEntry option(String option) { + this.option = option; + return this; } - public NewMenuEntry(int param0, int param1, int opcode, int identifier, int itemId, String target) { - this(param0, param1, opcode, identifier); - this.option = target; - this.target = ""; - this.forceLeftClick = false; - this.itemId = itemId; + public NewMenuEntry target(String target) { + this.target = target; + return this; } - public NewMenuEntry(int param0, int param1, int opcode, int identifier, int itemId, String target, int worldViewId) { - this(param0, param1, opcode, identifier, itemId, target); - this.setWorldViewId(worldViewId); + public NewMenuEntry identifier(int identifier) { + this.identifier = identifier; + return this; } - public NewMenuEntry(int param0, int param1, int opcode, int identifier, int itemId, String target, Actor actor, String option) { - this(param0, param1, opcode, identifier); - this.option = option; - this.target = target; - this.forceLeftClick = false; - this.itemId = itemId; - this.actor = actor; + public NewMenuEntry type(MenuAction type) { + this.type = type; + return this; } - public NewMenuEntry(int param0, int param1, int opcode, int identifier, int itemId, String target, Actor actor) { - this(param0, param1, opcode, identifier, itemId, target, actor, "Use"); + public NewMenuEntry opcode(int opcode) { + this.type = MenuAction.of(opcode); + return this; } - public NewMenuEntry(int param0, int param1, int opcode, int identifier, int itemId, String option, String target, TileObject gameObject) { - this(param0, param1, opcode, identifier); - this.option = option; - this.target = target; - this.forceLeftClick = false; - this.itemId = itemId; - this.gameObject = gameObject; + public NewMenuEntry param0(int param0) { + this.param0 = param0; + return this; } - public NewMenuEntry(int param0, int param1, int opcode, int identifier, int itemId, String option, String target, TileObject gameObject, int worldViewId) { - this(param0, param1, opcode, identifier, itemId, option, target, gameObject); - this.forceLeftClick = false; - this.setWorldViewId(worldViewId); + public NewMenuEntry param1(int param1) { + this.param1 = param1; + return this; } - public NewMenuEntry(String option, String target, int identifier, MenuAction type, int param0, int param1, boolean forceLeftClick) { - this(param0, param1, type, identifier); - this.option = option; - this.target = target; + public NewMenuEntry forceLeftClick(boolean forceLeftClick) { this.forceLeftClick = forceLeftClick; + return this; } - public NewMenuEntry(String option, int param0, int param1, int opcode, int identifier, int itemId, String target) { - this(param0, param1, opcode, identifier); - this.option = option; - this.target = target; - this.forceLeftClick = false; - this.itemId = itemId; + public NewMenuEntry actor(Actor actor) { + this.actor = actor; + return this; } - public NewMenuEntry() { + public NewMenuEntry gameObject(TileObject gameObject) { + this.gameObject = gameObject; + return this; + } + + public NewMenuEntry widget(Widget widget) { + this.widget = widget; + return this; + } + + public NewMenuEntry worldViewId(int worldViewId) { + this.worldViewId = worldViewId; + return this; } public MenuEntry setOption(String option) { @@ -175,13 +171,17 @@ public int getItemOp() { return 0; } + public NewMenuEntry itemId(int itemId) { + this.itemId = itemId; + return this; + } + @Override public MenuEntry setItemId(int itemId) { this.itemId = itemId; return this; } - public MenuEntry setWidget(Widget widget) { this.widget = widget; return this; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2Npc.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2Npc.java index 0c7938227b7..481fbb44cbf 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2Npc.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/npc/Rs2Npc.java @@ -714,8 +714,17 @@ public static boolean interact(Rs2NpcModel npc, String action) { Rs2Camera.turnTo(npc); } - Microbot.doInvoke(new NewMenuEntry(0, 0, menuAction.getId(), npc.getIndex(), -1, npc.getName(), npc, action), - Rs2UiHelper.getActorClickbox(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) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java index e8bc38a6886..c9017900fbd 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java @@ -450,10 +450,22 @@ public static void logout() { Widget currentWorldWidget = Rs2Widget.getWidget(69, 3); if (currentWorldWidget != null) { // From World Switcher - Microbot.doInvoke(new NewMenuEntry(-1, 4522009, CC_OP.getId(), 1, -1, "Logout"), new Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); + Microbot.doInvoke(new NewMenuEntry() + .param0(-1) + .param1(4522009) + .opcode(CC_OP.getId()) + .identifier(1) + .itemId(-1) + .option("Logout"), new Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); } else { // From red logout button - Microbot.doInvoke(new NewMenuEntry(-1, 11927560, CC_OP.getId(), 1, -1, "Logout"), new Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); + Microbot.doInvoke(new NewMenuEntry() + .param0(-1) + .param1(11927560) + .opcode(CC_OP.getId()) + .identifier(1) + .itemId(-1) + .option("Logout"), new Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); } } @@ -1754,7 +1766,14 @@ private static boolean invokeMenu(Rs2PlayerModel rs2Player, String action) { // Invoke the menu entry using the selected action Microbot.doInvoke( - new NewMenuEntry(0, 0, menuAction.getId(), rs2Player.getId(), -1, rs2Player.getName(), rs2Player), + new NewMenuEntry() + .param0(0) + .param1(0) + .opcode(menuAction.getId()) + .identifier(rs2Player.getId()) + .itemId(-1) + .target(rs2Player.getName()) + .actor(rs2Player), Rs2UiHelper.getActorClickbox(rs2Player) ); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/prayer/Rs2Prayer.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/prayer/Rs2Prayer.java index 5d560ff4414..6782472dabf 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/prayer/Rs2Prayer.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/prayer/Rs2Prayer.java @@ -82,14 +82,13 @@ public static boolean toggle(Rs2PrayerEnum prayer, boolean on, boolean withMouse * @param withMouse true to use mouse clicks with prayer bounds */ private static void invokePrayer(Rs2PrayerEnum prayer, boolean withMouse) { - NewMenuEntry menuEntry = new NewMenuEntry( - -1, - prayer.getIndex(), - MenuAction.CC_OP.getId(), - 1, - -1, - "Activate" - ); + NewMenuEntry menuEntry = new NewMenuEntry() + .param0(-1) + .param1(prayer.getIndex()) + .opcode(MenuAction.CC_OP.getId()) + .identifier(1) + .itemId(-1) + .option("Activate"); Rectangle prayerBounds = withMouse ? getPrayerBounds(prayer) : Rs2UiHelper.getDefaultRectangle(); @@ -165,16 +164,37 @@ public static boolean setQuickPrayers(Rs2PrayerEnum[] prayers) { if (Rs2Widget.isHidden(QUICK_PRAYER_ORB_COMPONENT_ID)) return false; // Open the menu - Microbot.doInvoke(new NewMenuEntry("Setup",-1, QUICK_PRAYER_ORB_COMPONENT_ID, MenuAction.CC_OP.getId(), 2, -1, "Quick-prayers"), new Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); + Microbot.doInvoke(new NewMenuEntry() + .option("Setup") + .param0(-1) + .param1(QUICK_PRAYER_ORB_COMPONENT_ID) + .opcode(MenuAction.CC_OP.getId()) + .identifier(2) + .itemId(-1) + .target("Quick-prayers"), new Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); sleepUntil(() -> !Rs2Widget.isHidden(QUICK_PRAYER_SELECT_COMPONENT_ID)); for (Rs2PrayerEnum prayer : prayers) { if(isQuickPrayerSet(prayer)) continue; - Microbot.doInvoke(new NewMenuEntry(prayer.getName(),prayer.getQuickPrayerIndex(), QUICK_PRAYER_SELECT_COMPONENT_ID, MenuAction.CC_OP.getId(), 1, -1, "Toggle"), new Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); + Microbot.doInvoke(new NewMenuEntry() + .option(prayer.getName()) + .param0(prayer.getQuickPrayerIndex()) + .param1(QUICK_PRAYER_SELECT_COMPONENT_ID) + .opcode(MenuAction.CC_OP.getId()) + .identifier(1) + .itemId(-1) + .target("Toggle"), new Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); } - Microbot.doInvoke(new NewMenuEntry("Done",-1, QUICK_PRAYER_DONE_BUTTON_COMPONENT_ID, MenuAction.CC_OP.getId(), 1, -1, ""), new Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); + Microbot.doInvoke(new NewMenuEntry() + .option("Done") + .param0(-1) + .param1(QUICK_PRAYER_DONE_BUTTON_COMPONENT_ID) + .opcode(MenuAction.CC_OP.getId()) + .identifier(1) + .itemId(-1) + .target(""), new Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); return true; } @@ -224,14 +244,13 @@ public static boolean toggleQuickPrayer(boolean on, boolean withMouse) { * @param withMouse true to use mouse with orb bounds */ private static void invokeQuickPrayer(boolean withMouse) { - NewMenuEntry entry = new NewMenuEntry( - -1, - QUICK_PRAYER_ORB_COMPONENT_ID, - MenuAction.CC_OP.getId(), - 1, - -1, - "Quick-prayers" - ); + NewMenuEntry entry = new NewMenuEntry() + .param0(-1) + .param1(QUICK_PRAYER_ORB_COMPONENT_ID) + .opcode(MenuAction.CC_OP.getId()) + .identifier(1) + .itemId(-1) + .option("Quick-prayers"); Microbot.doInvoke(entry, withMouse ? getQuickPrayerOrbBounds() : Rs2UiHelper.getDefaultRectangle()); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/Rs2Sailing.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/Rs2Sailing.java index 651e9117cf7..260e8e06594 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/Rs2Sailing.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/Rs2Sailing.java @@ -432,7 +432,14 @@ public static void setHeading(Heading heading) { return; } currentHeading = heading; - var menuEntry = new NewMenuEntry("Set-Heading", "", heading.getValue(), MenuAction.SET_HEADING, 0, 0, false); + 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) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/settings/Rs2Settings.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/settings/Rs2Settings.java index 534c34662a5..6abe42464eb 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/settings/Rs2Settings.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/settings/Rs2Settings.java @@ -82,7 +82,15 @@ public static boolean enableDropShiftSetting(boolean closeInterface) } // MenuEntryImpl(getOption=Toggle, getTarget=, getIdentifier=1, getType=CC_OP, getParam0=8, getParam1=8781844, getItemId=-1, isForceLeftClick=false, getWorldViewId=-1, isDeprioritized=false) - NewMenuEntry menuEntry = new NewMenuEntry("Toggle", "", 1, MenuAction.CC_OP, 8, widget.getId(), false); + NewMenuEntry menuEntry = new NewMenuEntry() + .option("Toggle") + .target("") + .identifier(1) + .type(MenuAction.CC_OP) + .param0(8) + .param1(widget.getId()) + .forceLeftClick(false) + ; Microbot.doInvoke(menuEntry, Rs2UiHelper.getDefaultRectangle()); boolean success = sleepUntil(Rs2Settings::isDropShiftSettingEnabled); @@ -127,7 +135,15 @@ public static boolean hideRoofs(boolean closeInterface) } // MenuEntryImpl(getOption=Toggle, getTarget=, getIdentifier=1, getType=CC_OP, getParam0=4, getParam1=8781843, getItemId=-1, isForceLeftClick=false, getWorldViewId=-1, isDeprioritized=false) - NewMenuEntry menuEntry = new NewMenuEntry("Toggle", "", 1, MenuAction.CC_OP, 4, widget.getId(), false); + NewMenuEntry menuEntry = new NewMenuEntry() + .option("Toggle") + .target("") + .identifier(1) + .type(MenuAction.CC_OP) + .param0(4) + .param1(widget.getId()) + .forceLeftClick(false) + ; Microbot.doInvoke(menuEntry, Rs2UiHelper.getDefaultRectangle()); boolean success = sleepUntil(Rs2Settings::isHideRoofsEnabled); @@ -171,7 +187,15 @@ public static boolean disableLevelUpNotifications(boolean closeInterface) } // MenuEntryImpl(getOption=Toggle, getTarget=, getIdentifier=1, getType=CC_OP, getParam0=14, getParam1=8781843, getItemId=-1, isForceLeftClick=false, getWorldViewId=-1, isDeprioritized=false) - NewMenuEntry menuEntry = new NewMenuEntry("Toggle", "", 1, MenuAction.CC_OP, 14, widget.getId(), false); + NewMenuEntry menuEntry = new NewMenuEntry() + .option("Toggle") + .target("") + .identifier(1) + .type(MenuAction.CC_OP) + .param0(14) + .param1(widget.getId()) + .forceLeftClick(false) + ; Microbot.doInvoke(menuEntry, Rs2UiHelper.getDefaultRectangle()); boolean success = sleepUntil(() -> !isLevelUpNotificationsEnabled()); @@ -245,7 +269,15 @@ public static boolean disableWorldSwitcherConfirmation(boolean closeInterface) { if (widget == null) return false; // MenuEntryImpl(getOption=Toggle, getTarget=, getIdentifier=1, getType=CC_OP, getParam0=35, getParam1=8781844, getItemId=-1, isForceLeftClick=false, getWorldViewId=-1, isDeprioritized=false) - NewMenuEntry menuEntry = new NewMenuEntry("Toggle", "", 1, MenuAction.CC_OP, 35, widget.getId(), false); + NewMenuEntry menuEntry = new NewMenuEntry() + .option("Toggle") + .target("") + .identifier(1) + .type(MenuAction.CC_OP) + .param0(35) + .param1(widget.getId()) + .forceLeftClick(false) + ; Microbot.doInvoke(menuEntry, Rs2UiHelper.getDefaultRectangle()); boolean success = sleepUntil(() -> !isWorldSwitcherConfirmationEnabled()); @@ -301,7 +333,15 @@ public static void enableSpellFiltering() } Rectangle spellbookBounds = spellbookInterfaceWidget.getBounds(); - NewMenuEntry spellFilterEntry = new NewMenuEntry("Enable spell filtering", "", 2, MenuAction.CC_OP, -1, spellbookInterfaceWidget.getId(), false); + NewMenuEntry spellFilterEntry = new NewMenuEntry() + .option("Enable spell filtering") + .target("") + .identifier(2) + .type(MenuAction.CC_OP) + .param0(-1) + .param1(spellbookInterfaceWidget.getId()) + .forceLeftClick(false) + ; Microbot.doInvoke(spellFilterEntry, spellbookBounds != null && Rs2UiHelper.isRectangleWithinCanvas(spellbookBounds) ? spellbookBounds : Rs2UiHelper.getDefaultRectangle()); sleepUntil(Rs2Settings::isSpellFilteringEnabled, 2000); } @@ -345,7 +385,15 @@ private static boolean switchToSettingsTab(String tabName) return false; } - NewMenuEntry menuEntry = new NewMenuEntry("Select " + tabName, "", 1, MenuAction.CC_OP, index, widget.getId(), false); + NewMenuEntry menuEntry = new NewMenuEntry() + .option("Select " + tabName) + .target("") + .identifier(1) + .type(MenuAction.CC_OP) + .param0(index) + .param1(widget.getId()) + .forceLeftClick(false) + ; Microbot.doInvoke(menuEntry, Rs2UiHelper.getDefaultRectangle()); return true; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/shop/Rs2Shop.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/shop/Rs2Shop.java index 9aa567e43bf..ac7a84d5ac7 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/shop/Rs2Shop.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/shop/Rs2Shop.java @@ -415,7 +415,15 @@ private static void invokeMenu(Rs2ItemModel rs2Item, String action) { } - Microbot.doInvoke(new NewMenuEntry(param0, param1, menuAction.getId(), identifier, rs2Item.getId(), rs2Item.getName()), (itemBounds(rs2Item) == null) ? new Rectangle(1, 1) : itemBounds(rs2Item)); + Microbot.doInvoke(new NewMenuEntry() + .param0(param0) + .param1(param1) + .opcode(menuAction.getId()) + .identifier(identifier) + .itemId(rs2Item.getId()) + .target(rs2Item.getName()) + , + (itemBounds(rs2Item) == null) ? new Rectangle(1, 1) : itemBounds(rs2Item)); //Rs2Reflection.invokeMenu(param0, param1, menuAction.getId(), identifier, rs2Item.id, action, target, -1, -1); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java index f67e90dc374..21be674b3d7 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java @@ -251,7 +251,17 @@ public boolean click(String action) { } - Microbot.doInvoke(new NewMenuEntry(param0, param1, menuAction.getId(), getId(), -1, action, objName, tileObject), Rs2UiHelper.getObjectClickbox(tileObject)); + Microbot.doInvoke(new NewMenuEntry() + .param0(param0) + .param1(param1) + .opcode(menuAction.getId()) + .identifier(getId()) + .itemId(-1) + .option(action) + .target(objName) + .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); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2Walker.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2Walker.java index c709dbb2a4f..7a0b6944c48 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2Walker.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/walker/Rs2Walker.java @@ -529,7 +529,16 @@ public static void walkFastLocal(LocalPoint localPoint) { int canvasX = canv != null ? canv.getX() : -1; int canvasY = canv != null ? canv.getY() : -1; - Microbot.doInvoke(new NewMenuEntry(canvasX, canvasY, MenuAction.WALK.getId(), 0, -1, "Walk here"), new Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); + NewMenuEntry entry = new NewMenuEntry() + .param0(canvasX) + .param1(canvasY) + .type(MenuAction.WALK) + .identifier(0) + .itemId(-1) + .option("Walk here"); + + Microbot.doInvoke(entry, + new Rectangle(1, 1, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); //Rs2Reflection.invokeMenu(canvasX, canvasY, MenuAction.WALK.getId(), 0, -1, "Walk here", "", -1, -1); } @@ -562,7 +571,16 @@ public static boolean walkFastCanvas(WorldPoint worldPoint, boolean toggleRun) { return Rs2Walker.walkMiniMap(worldPoint); } - Microbot.doInvoke(new NewMenuEntry(canvasX, canvasY, MenuAction.WALK.getId(), 0, 0, "Walk here"), new Rectangle(canvasX, canvasY, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); + NewMenuEntry entry = new NewMenuEntry() + .param0(canvasX) + .param1(canvasY) + .type(MenuAction.WALK) + .identifier(0) + .itemId(0) + .option("Walk here"); + + Microbot.doInvoke(entry, + new Rectangle(canvasX, canvasY, Microbot.getClient().getCanvasWidth(), Microbot.getClient().getCanvasHeight())); return true; } @@ -597,15 +615,15 @@ public static int getTotalTiles(WorldPoint start, WorldPoint destination) { if (path.isEmpty() || path.get(path.size() - 1).getPlane() != destination.getPlane()) return Integer.MAX_VALUE; // Create a WorldArea centered on the worldPoint by calculating the south-west corner WorldPoint pathPoint_SW = new WorldPoint( - path.get(path.size() - 1).getX() - 2, - path.get(path.size() - 1).getY() - 2, - path.get(path.size() - 1).getPlane() + path.get(path.size() - 1).getX() - 2, + path.get(path.size() - 1).getY() - 2, + path.get(path.size() - 1).getPlane() ); // Create a WorldArea centered on the worldPoint by calculating the south-west corner WorldPoint objectPoint_SW = new WorldPoint( - destination.getX() - 2, - destination.getY() - 2, - destination.getPlane() + destination.getX() - 2, + destination.getY() - 2, + destination.getPlane() ); WorldArea pathArea = new WorldArea(pathPoint_SW, 5, 5); WorldArea objectArea = new WorldArea(objectPoint_SW, 5, 5); @@ -632,16 +650,16 @@ public static int getTotalTilesFromPath(List path, WorldPoint destin // Create centered WorldAreas instead of corner-based WorldPoint pathEndpoint = path.get(path.size() - 1); WorldPoint pathSouthWest = new WorldPoint( - pathEndpoint.getX() - 4, - pathEndpoint.getY() - 4, - pathEndpoint.getPlane() + pathEndpoint.getX() - 4, + pathEndpoint.getY() - 4, + pathEndpoint.getPlane() ); WorldArea pathArea = new WorldArea(pathSouthWest, 8, 8); WorldPoint destSouthWest = new WorldPoint( - destination.getX() - 4, - destination.getY() - 4, - destination.getPlane() + destination.getX() - 4, + destination.getY() - 4, + destination.getPlane() ); WorldArea objectArea = new WorldArea(destSouthWest, 8, 8); @@ -668,9 +686,9 @@ public static boolean canReach(WorldPoint worldPoint, int sizeX, int sizeY, int // Create centered WorldArea for the object instead of corner-based WorldPoint objectSouthWest = new WorldPoint( - worldPoint.getX() - (sizeX + 2) / 2, - worldPoint.getY() - (sizeY + 2) / 2, - worldPoint.getPlane() + worldPoint.getX() - (sizeX + 2) / 2, + worldPoint.getY() - (sizeY + 2) / 2, + worldPoint.getPlane() ); WorldArea objectArea = new WorldArea(objectSouthWest, sizeX + 2, sizeY + 2); @@ -686,9 +704,9 @@ public static boolean canReach(WorldPoint worldPoint, int sizeX, int sizeY, int // Create centered WorldArea for the path endpoint instead of corner-based WorldPoint pathEndpoint = pathfinder.getPath().get(pathfinder.getPath().size() - 1); WorldPoint pathSouthWest = new WorldPoint( - pathEndpoint.getX() - pathSizeX / 2, - pathEndpoint.getY() - pathSizeY / 2, - pathEndpoint.getPlane() + pathEndpoint.getX() - pathSizeX / 2, + pathEndpoint.getY() - pathSizeY / 2, + pathEndpoint.getPlane() ); pathArea = new WorldArea(pathSouthWest, pathSizeX, pathSizeY); } catch (Exception e) { @@ -927,29 +945,29 @@ public static List getTransportsForPath(List path, int in private static List applyTransportFiltering(List transports) { return transports.stream() .filter(t -> t.getType() == TransportType.TELEPORTATION_ITEM || t.getType() == TransportType.FAIRY_RING || - t.getType() == TransportType.TELEPORTATION_SPELL || t.getType() == TransportType.CANOE || - t.getType() == TransportType.BOAT || t.getType() == TransportType.CHARTER_SHIP || - t.getType() == TransportType.SHIP || t.getType() == TransportType.MINECART || - t.getType() == TransportType.MAGIC_CARPET) + t.getType() == TransportType.TELEPORTATION_SPELL || t.getType() == TransportType.CANOE || + t.getType() == TransportType.BOAT || t.getType() == TransportType.CHARTER_SHIP || + t.getType() == TransportType.SHIP || t.getType() == TransportType.MINECART || + t.getType() == TransportType.MAGIC_CARPET) .peek(t -> { // Set fairy ring requirements if not already set if (t.getType() == TransportType.FAIRY_RING && - ((t.getItemIdRequirements() == null || t.getItemIdRequirements().isEmpty()) ) && Microbot.getVarbitValue(VarbitID.LUMBRIDGE_DIARY_ELITE_COMPLETE) != 1) { + ((t.getItemIdRequirements() == null || t.getItemIdRequirements().isEmpty()) ) && Microbot.getVarbitValue(VarbitID.LUMBRIDGE_DIARY_ELITE_COMPLETE) != 1) { t.setItemIdRequirements(Set.of(Set.of( - ItemID.DRAMEN_STAFF, - ItemID.LUNAR_MOONCLAN_LIMINAL_STAFF + ItemID.DRAMEN_STAFF, + ItemID.LUNAR_MOONCLAN_LIMINAL_STAFF ))); } // Set currency requirements for currency-based transports if (isCurrencyBasedTransport(t.getType()) && - (t.getItemIdRequirements() == null || t.getItemIdRequirements().isEmpty()) && - t.getCurrencyName() != null && !t.getCurrencyName().isEmpty() && t.getCurrencyAmount() > 0) { + (t.getItemIdRequirements() == null || t.getItemIdRequirements().isEmpty()) && + t.getCurrencyName() != null && !t.getCurrencyName().isEmpty() && t.getCurrencyAmount() > 0) { int currencyItemId = getCurrencyItemId(t.getCurrencyName()); if (currencyItemId != -1) { t.setItemIdRequirements(Set.of(Set.of(currencyItemId))); log.debug("Set currency requirement for {}: {} x{} (ID: {})", - t.getType(), t.getCurrencyName(), t.getCurrencyAmount(), currencyItemId); + t.getType(), t.getCurrencyName(), t.getCurrencyAmount(), currencyItemId); } } }) @@ -1064,8 +1082,8 @@ private static boolean handleDoors(List path, int index) { } for (WorldPoint probe : probes) { - boolean adjacentToPath = probe.distanceTo(fromWp) <= 1 || probe.distanceTo(toWp) <= 1; - if (!adjacentToPath || !Objects.equals(probe.getPlane(), Microbot.getClient().getLocalPlayer().getWorldLocation().getPlane())) continue; + boolean adjacentToPath = probe.distanceTo(fromWp) <= 1 || probe.distanceTo(toWp) <= 1; + if (!adjacentToPath || !Objects.equals(probe.getPlane(), Microbot.getClient().getLocalPlayer().getWorldLocation().getPlane())) continue; WallObject wall = Rs2GameObject.getWallObject(o -> o.getWorldLocation().equals(probe), probe, 3); @@ -1075,31 +1093,31 @@ private static boolean handleDoors(List path, int index) { if (object == null) continue; ObjectComposition comp = Rs2GameObject.convertToObjectComposition(object); - // Ignore imposter objects + // Ignore imposter objects if (comp == null || comp.getImpostorIds() != null || comp.getName().equals("null")) continue; String action = Arrays.stream(comp.getActions()) - .filter(Objects::nonNull) - .filter(act -> doorActions.stream().anyMatch(dact -> act.toLowerCase().startsWith(dact.toLowerCase()))) - .min(Comparator.comparing(act -> doorActions.indexOf(doorActions.stream().filter(dact -> act.toLowerCase().startsWith(dact)).findFirst().orElse("")))) - .orElse(null); + .filter(Objects::nonNull) + .filter(act -> doorActions.stream().anyMatch(dact -> act.toLowerCase().startsWith(dact.toLowerCase()))) + .min(Comparator.comparing(act -> doorActions.indexOf(doorActions.stream().filter(dact -> act.toLowerCase().startsWith(dact)).findFirst().orElse("")))) + .orElse(null); if (action == null) continue; boolean found = false; - final String name = comp.getName(); + final String name = comp.getName(); if (object instanceof WallObject) { int orientation = ((WallObject) object).getOrientationA(); if (searchNeighborPoint(orientation, probe, fromWp) || searchNeighborPoint(orientation, probe, toWp)) { - log.info("Found WallObject door - name {} with action {} at {} - from {} to {}", name, action, probe, fromWp, toWp); - found = true; - } + log.info("Found WallObject door - name {} with action {} at {} - from {} to {}", name, action, probe, fromWp, toWp); + found = true; + } } else { if (name != null && name.toLowerCase().contains("door")) { - log.info("Found GameObject door - name {} with action {} at {} - from {} to {}", name, action, probe, fromWp, toWp); + log.info("Found GameObject door - name {} with action {} at {} - from {} to {}", name, action, probe, fromWp, toWp); found = true; } } @@ -1137,11 +1155,11 @@ private static boolean handleStrongholdOfSecurityAnswer(TileObject object, Strin if (!isInDialogue) return true; // Skip over first door dialogue & don't forget to set up two-factor warning - if (Rs2Dialogue.getDialogueText().toLowerCase().contains("two-factor authentication options") || Rs2Dialogue.getDialogueText().toLowerCase().contains("hopefully you will learn
much from us.")) { - Rs2Dialogue.sleepUntilHasContinue(); - sleepUntil(() -> !Rs2Dialogue.hasContinue() || Rs2Dialogue.getDialogueText().toLowerCase().contains("to pass you must answer me"), Rs2Dialogue::clickContinue, 5000, Rs2Random.between(600, 800)); - if (!Rs2Dialogue.isInDialogue()) return true; - } + if (Rs2Dialogue.getDialogueText().toLowerCase().contains("two-factor authentication options") || Rs2Dialogue.getDialogueText().toLowerCase().contains("hopefully you will learn
much from us.")) { + Rs2Dialogue.sleepUntilHasContinue(); + sleepUntil(() -> !Rs2Dialogue.hasContinue() || Rs2Dialogue.getDialogueText().toLowerCase().contains("to pass you must answer me"), Rs2Dialogue::clickContinue, 5000, Rs2Random.between(600, 800)); + if (!Rs2Dialogue.isInDialogue()) return true; + } String dialogueAnswer = null; int attempts = 0; @@ -1265,7 +1283,7 @@ public static int getClosestTileIndex(List path) { * Force the walker to recalculate path */ public static void recalculatePath() { - WorldPoint _currentTarget = currentTarget; + WorldPoint _currentTarget = currentTarget; Rs2Walker.setTarget(null); Rs2Walker.setTarget(_currentTarget); } @@ -1434,32 +1452,32 @@ private static boolean handleTransports(List path, int indexOfStartP if (path.get(i).equals(origin)) { if (transport.getType() == TransportType.SHIP || transport.getType() == TransportType.NPC || transport.getType() == TransportType.BOAT) { - Rs2NpcModel npc = Rs2Npc.getNpc(transport.getName()); + Rs2NpcModel npc = Rs2Npc.getNpc(transport.getName()); if (Rs2Npc.canWalkTo(npc, 20) && Rs2Npc.interact(npc, transport.getAction())) { Rs2Player.waitForWalking(); sleepUntil(Rs2Dialogue::isInDialogue,600*2); - if (Objects.equals(transport.getName(), "Veos") && Objects.equals(transport.getAction(), "Talk-to")) { - sleepUntil(() -> !Rs2Dialogue.hasContinue(), Rs2Dialogue::clickContinue, 5000, Rs2Random.between(600, 800)); - Rs2Dialogue.clickOption("Can you take me somewhere?"); - sleepUntil(() -> !Rs2Dialogue.hasContinue() && !Rs2Dialogue.hasSelectAnOption(), Rs2Dialogue::clickContinue, 5000, Rs2Random.between(600, 800)); - Rs2Dialogue.clickOption(transport.getDisplayInfo()); - sleepUntil(() -> !Rs2Dialogue.hasContinue() && !Rs2Dialogue.hasSelectAnOption(), Rs2Dialogue::clickContinue, 5000, Rs2Random.between(600, 800)); - } - - if (Objects.equals(transport.getName(), "Captain Magoro") && Objects.equals(transport.getAction(), "Talk-to")) { - sleepUntil(() -> !Rs2Dialogue.hasContinue(), Rs2Dialogue::clickContinue, 5000, Rs2Random.between(600, 800)); - Rs2Dialogue.clickOption(transport.getDisplayInfo()); - sleepUntil(() -> !Rs2Dialogue.hasContinue() && !Rs2Dialogue.hasSelectAnOption(), Rs2Dialogue::clickContinue, 5000, Rs2Random.between(600, 800)); - } - - if (Rs2Dialogue.clickOption("I'm just going to Pirates' cove")){ - sleep(600 * 2); - Rs2Dialogue.clickContinue(); + if (Objects.equals(transport.getName(), "Veos") && Objects.equals(transport.getAction(), "Talk-to")) { + sleepUntil(() -> !Rs2Dialogue.hasContinue(), Rs2Dialogue::clickContinue, 5000, Rs2Random.between(600, 800)); + Rs2Dialogue.clickOption("Can you take me somewhere?"); + sleepUntil(() -> !Rs2Dialogue.hasContinue() && !Rs2Dialogue.hasSelectAnOption(), Rs2Dialogue::clickContinue, 5000, Rs2Random.between(600, 800)); + Rs2Dialogue.clickOption(transport.getDisplayInfo()); + sleepUntil(() -> !Rs2Dialogue.hasContinue() && !Rs2Dialogue.hasSelectAnOption(), Rs2Dialogue::clickContinue, 5000, Rs2Random.between(600, 800)); + } + + if (Objects.equals(transport.getName(), "Captain Magoro") && Objects.equals(transport.getAction(), "Talk-to")) { + sleepUntil(() -> !Rs2Dialogue.hasContinue(), Rs2Dialogue::clickContinue, 5000, Rs2Random.between(600, 800)); + Rs2Dialogue.clickOption(transport.getDisplayInfo()); + sleepUntil(() -> !Rs2Dialogue.hasContinue() && !Rs2Dialogue.hasSelectAnOption(), Rs2Dialogue::clickContinue, 5000, Rs2Random.between(600, 800)); + } + + if (Rs2Dialogue.clickOption("I'm just going to Pirates' cove")){ + sleep(600 * 2); + Rs2Dialogue.clickContinue(); } else if (Objects.equals(transport.getName(), "Mountain Guide")) { - Rs2Dialogue.clickOption(transport.getDisplayInfo()); - } + Rs2Dialogue.clickOption(transport.getDisplayInfo()); + } sleepUntil(() -> !Rs2Player.isAnimating()); sleepUntil(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) < 10); sleep(600 * 6); @@ -1534,9 +1552,9 @@ private static boolean handleTransports(List path, int indexOfStartP if (transport.getType() == TransportType.FAIRY_RING && !Rs2Player.getWorldLocation().equals(transport.getDestination())) { if (handleFairyRing(transport)) { - sleepUntilTrue(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) < OFFSET); - break; - } + sleepUntilTrue(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) < OFFSET); + break; + } } if (transport.getType() == TransportType.TELEPORTATION_MINIGAME) { @@ -1563,42 +1581,42 @@ private static boolean handleTransports(List path, int indexOfStartP } } - if (transport.getObjectId() <= 0) break; - - // Use class-level constants for object ID mapping - List objectIdsToSearch = new ArrayList<>(); - objectIdsToSearch.add(transport.getObjectId()); - - // If this transport is for an open trapdoor, also search for the closed version - if (OPEN_TO_CLOSED_MAPPINGS.containsKey(transport.getObjectId())) { - objectIdsToSearch.add(OPEN_TO_CLOSED_MAPPINGS.get(transport.getObjectId())); - } - - List objects = Rs2GameObject.getAll(o -> objectIdsToSearch.contains(o.getId()), transport.getOrigin(), 10).stream() - .sorted(Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(transport.getOrigin()))) - .collect(Collectors.toList()); - - TileObject object = objects.stream().findFirst().orElse(null); - if (object instanceof GroundObject) { - object = objects.stream() - .filter(o -> !Objects.equals(o.getWorldLocation(), Microbot.getClient().getLocalPlayer().getWorldLocation())) - .min(Comparator.comparing(o -> ((TileObject) o).getWorldLocation().distanceTo(transport.getOrigin())) - .thenComparing(o -> ((TileObject) o).getWorldLocation().distanceTo(transport.getDestination()))).orElse(null); - } - - if (object != null) { - System.out.println("Object Type: " + Rs2GameObject.getObjectType(object)); - - if (!(object instanceof GroundObject)) { - if (!Rs2Tile.isTileReachable(transport.getOrigin())) { - break; - } - } - - handleObject(transport, object); - sleepUntil(() -> !Rs2Player.isAnimating()); - return sleepUntilTrue(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) < OFFSET); - } + if (transport.getObjectId() <= 0) break; + + // Use class-level constants for object ID mapping + List objectIdsToSearch = new ArrayList<>(); + objectIdsToSearch.add(transport.getObjectId()); + + // If this transport is for an open trapdoor, also search for the closed version + if (OPEN_TO_CLOSED_MAPPINGS.containsKey(transport.getObjectId())) { + objectIdsToSearch.add(OPEN_TO_CLOSED_MAPPINGS.get(transport.getObjectId())); + } + + List objects = Rs2GameObject.getAll(o -> objectIdsToSearch.contains(o.getId()), transport.getOrigin(), 10).stream() + .sorted(Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(transport.getOrigin()))) + .collect(Collectors.toList()); + + TileObject object = objects.stream().findFirst().orElse(null); + if (object instanceof GroundObject) { + object = objects.stream() + .filter(o -> !Objects.equals(o.getWorldLocation(), Microbot.getClient().getLocalPlayer().getWorldLocation())) + .min(Comparator.comparing(o -> ((TileObject) o).getWorldLocation().distanceTo(transport.getOrigin())) + .thenComparing(o -> ((TileObject) o).getWorldLocation().distanceTo(transport.getDestination()))).orElse(null); + } + + if (object != null) { + System.out.println("Object Type: " + Rs2GameObject.getObjectType(object)); + + if (!(object instanceof GroundObject)) { + if (!Rs2Tile.isTileReachable(transport.getOrigin())) { + break; + } + } + + handleObject(transport, object); + sleepUntil(() -> !Rs2Player.isAnimating()); + return sleepUntilTrue(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) < OFFSET); + } } } } @@ -1634,7 +1652,7 @@ private static void handleObject(Transport transport, TileObject tileObject) { sleepUntil(() -> Rs2Player.getPoseAnimation() != 2148, 10000); } } else if (transport.getType() == TransportType.TELEPORTATION_PORTAL) { - sleep(600 * 2); // wait extra 2 game ticks before moving + sleep(600 * 2); // wait extra 2 game ticks before moving } else { Rs2Player.waitForWalking(); Rs2Dialogue.clickOption("Yes please"); //shillo village cart @@ -1649,22 +1667,22 @@ private static void handleObject(Transport transport, TileObject tileObject) { private static boolean handleObjectExceptions(Transport transport, TileObject tileObject) { for (Map.Entry entry : OPEN_TO_CLOSED_MAPPINGS.entrySet()) { final int closedTrapdoorId = entry.getKey(); - final int openTrapdoorId = entry.getValue(); + final int openTrapdoorId = entry.getValue(); if (transport.getObjectId() == openTrapdoorId) { - if (tileObject.getId() == closedTrapdoorId) { - Rs2GameObject.interact(tileObject, "Open"); - sleepUntil(() -> Rs2GameObject.exists(openTrapdoorId)); - TileObject openTrapdoor = Rs2GameObject.getAll(o -> o.getId() == openTrapdoorId, tileObject.getWorldLocation(), 10).stream().findFirst().orElse(null); - if (openTrapdoor != null) { - Rs2GameObject.interact(openTrapdoor, transport.getAction()); - } - } else if (tileObject.getId() == openTrapdoorId) { - Rs2GameObject.interact(tileObject, transport.getAction()); - } - sleepUntil(() -> !Rs2Player.isAnimating()); - sleepUntilTrue(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) < OFFSET); - return true; + if (tileObject.getId() == closedTrapdoorId) { + Rs2GameObject.interact(tileObject, "Open"); + sleepUntil(() -> Rs2GameObject.exists(openTrapdoorId)); + TileObject openTrapdoor = Rs2GameObject.getAll(o -> o.getId() == openTrapdoorId, tileObject.getWorldLocation(), 10).stream().findFirst().orElse(null); + if (openTrapdoor != null) { + Rs2GameObject.interact(openTrapdoor, transport.getAction()); + } + } else if (tileObject.getId() == openTrapdoorId) { + Rs2GameObject.interact(tileObject, transport.getAction()); + } + sleepUntil(() -> !Rs2Player.isAnimating()); + sleepUntilTrue(() -> Rs2Player.getWorldLocation().distanceTo(transport.getDestination()) < OFFSET); + return true; } } @@ -1700,17 +1718,17 @@ private static boolean handleObjectExceptions(Transport transport, TileObject ti // Handle Cobwebs blocking path if (tileObject.getId() == ObjectID.BIGWEB_SLASHABLE && !Rs2Equipment.isWearing(ItemID.ARANEA_BOOTS)) { sleepUntil(() -> !Rs2Player.isMoving() && !Rs2Player.isAnimating(1200)); - final WorldPoint webLocation = tileObject.getWorldLocation(); - final WorldPoint currentPlayerPoint = Microbot.getClient().getLocalPlayer().getWorldLocation(); - boolean doesWebStillExist = Rs2GameObject.getAll(o -> Objects.equals(webLocation, o.getWorldLocation()) && o.getId() == ObjectID.BIGWEB_SLASHABLE).stream().findFirst().isPresent(); - if (doesWebStillExist) { - sleepUntil(() -> Rs2GameObject.getAll(o -> Objects.equals(webLocation, o.getWorldLocation()) && o.getId() == ObjectID.BIGWEB_SLASHABLE).stream().findFirst().isEmpty(), - () -> { - Rs2GameObject.interact(tileObject, "slash"); - Rs2Player.waitForAnimation(); - }, 8000, 1200); - } - Rs2Walker.walkFastCanvas(transport.getDestination()); + final WorldPoint webLocation = tileObject.getWorldLocation(); + final WorldPoint currentPlayerPoint = Microbot.getClient().getLocalPlayer().getWorldLocation(); + boolean doesWebStillExist = Rs2GameObject.getAll(o -> Objects.equals(webLocation, o.getWorldLocation()) && o.getId() == ObjectID.BIGWEB_SLASHABLE).stream().findFirst().isPresent(); + if (doesWebStillExist) { + sleepUntil(() -> Rs2GameObject.getAll(o -> Objects.equals(webLocation, o.getWorldLocation()) && o.getId() == ObjectID.BIGWEB_SLASHABLE).stream().findFirst().isEmpty(), + () -> { + Rs2GameObject.interact(tileObject, "slash"); + Rs2Player.waitForAnimation(); + }, 8000, 1200); + } + Rs2Walker.walkFastCanvas(transport.getDestination()); return sleepUntil(() -> !Objects.equals(currentPlayerPoint, Microbot.getClient().getLocalPlayer().getWorldLocation())); } @@ -1771,28 +1789,28 @@ private static boolean handleObjectExceptions(Transport transport, TileObject ti return true; } - if (Rs2GameObject.getObjectIdsByName("Fossil_Rowboat").contains(tileObject.getId())) { - if (transport.getDisplayInfo() == null || transport.getDisplayInfo().isEmpty()) return false; - - char option = transport.getDisplayInfo().charAt(0); - Rs2Dialogue.sleepUntilSelectAnOption(); - Rs2Keyboard.keyPress(option); - sleepUntil(() -> Rs2Player.getWorldLocation().distanceTo2D(transport.getDestination()) < OFFSET, 10000); - return true; - } - - // Handle door/gate near wilderness agility course - if (tileObject.getId() == ObjectID.BALANCEGATE52A || tileObject.getId() == ObjectID.BALANCEGATE52B_RIGHT || tileObject.getId() == ObjectID.BALANCEGATE52B_LEFT) { - Rs2Player.waitForAnimation(600 * 4); - return true; - } - - if (tileObject.getId() == ObjectID.AERIAL_FISHING_BOAT) { - Rs2Dialogue.sleepUntilSelectAnOption(); - Rs2Dialogue.clickOption(transport.getDisplayInfo(), true); - sleepUntil(() -> Rs2Player.getWorldLocation().distanceTo2D(transport.getDestination()) < OFFSET, 10000); - return true; - } + if (Rs2GameObject.getObjectIdsByName("Fossil_Rowboat").contains(tileObject.getId())) { + if (transport.getDisplayInfo() == null || transport.getDisplayInfo().isEmpty()) return false; + + char option = transport.getDisplayInfo().charAt(0); + Rs2Dialogue.sleepUntilSelectAnOption(); + Rs2Keyboard.keyPress(option); + sleepUntil(() -> Rs2Player.getWorldLocation().distanceTo2D(transport.getDestination()) < OFFSET, 10000); + return true; + } + + // Handle door/gate near wilderness agility course + if (tileObject.getId() == ObjectID.BALANCEGATE52A || tileObject.getId() == ObjectID.BALANCEGATE52B_RIGHT || tileObject.getId() == ObjectID.BALANCEGATE52B_LEFT) { + Rs2Player.waitForAnimation(600 * 4); + return true; + } + + if (tileObject.getId() == ObjectID.AERIAL_FISHING_BOAT) { + Rs2Dialogue.sleepUntilSelectAnOption(); + Rs2Dialogue.clickOption(transport.getDisplayInfo(), true); + sleepUntil(() -> Rs2Player.getWorldLocation().distanceTo2D(transport.getDestination()) < OFFSET, 10000); + return true; + } return false; } @@ -2143,7 +2161,15 @@ private static boolean handleMinigameTeleport(Transport transport) { Widget destinationWidget = Rs2Widget.findWidget(destination, minigameWidgetList); if (destinationWidget == null) return false; - NewMenuEntry destinationMenuEntry = new NewMenuEntry("Select", "", 1, MenuAction.CC_OP, destinationWidget.getIndex(), minigameWidgetParent.getId(), false); + NewMenuEntry destinationMenuEntry = new NewMenuEntry() + .option("Select") + .target("") + .identifier(1) + .type(MenuAction.CC_OP) + .param0(destinationWidget.getIndex()) + .param1(minigameWidgetParent.getId()) + .forceLeftClick(false); + Microbot.doInvoke(destinationMenuEntry, new Rectangle(1, 1)); sleepUntil(() -> Rs2Widget.getWidget(SELECTED_MINIGAME).getText().equalsIgnoreCase(destination)); } @@ -2257,10 +2283,10 @@ private static boolean handleCanoe(Transport transport) { } private static boolean handleQuetzal(Transport transport) { - @Component + @Component int VARLAMORE_QUETZAL_MAP = InterfaceID.QuetzalMenu.CONTENTS; - @Component - int VARLAMORE_QUETZAL_OPTIONS = InterfaceID.QuetzalMenu.ICONS; + @Component + int VARLAMORE_QUETZAL_OPTIONS = InterfaceID.QuetzalMenu.ICONS; String displayInfo = transport.getDisplayInfo(); if (displayInfo == null || displayInfo.isEmpty()) return false; @@ -2275,15 +2301,15 @@ private static boolean handleQuetzal(Transport transport) { return false; } - Widget quetzalMapWidget = Rs2Widget.getWidget(VARLAMORE_QUETZAL_OPTIONS); - List quetzalMapChildren = Arrays.stream(quetzalMapWidget.getDynamicChildren()) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + Widget quetzalMapWidget = Rs2Widget.getWidget(VARLAMORE_QUETZAL_OPTIONS); + List quetzalMapChildren = Arrays.stream(quetzalMapWidget.getDynamicChildren()) + .filter(Objects::nonNull) + .collect(Collectors.toList()); - Widget actionWidget = Rs2Widget.findWidget(displayInfo, quetzalMapChildren, false); + Widget actionWidget = Rs2Widget.findWidget(displayInfo, quetzalMapChildren, false); if (actionWidget != null) { Rs2Widget.clickWidget(actionWidget); - log.info("Traveling to {} - ({})", transport.getDisplayInfo(), transport.getDestination()); + log.info("Traveling to {} - ({})", transport.getDisplayInfo(), transport.getDestination()); return sleepUntilTrue(() -> Rs2Player.getWorldLocation().distanceTo2D(transport.getDestination()) < OFFSET, 100, 5000); } } @@ -2345,8 +2371,16 @@ private static boolean handleCharterShip(Transport transport) { boolean isWidgetVisible = Microbot.getClientThread().runOnClientThreadOptional(() -> !destinationWidget.isHidden()).orElse(false); - NewMenuEntry destinationMenuEntry = new NewMenuEntry(destinationText, "", 1, MenuAction.CC_OP, destinationWidget.getIndex(), destinationWidget.getId(), false); - Microbot.doInvoke(destinationMenuEntry, (Rs2UiHelper.isRectangleWithinCanvas(destinationWidget.getBounds()) && isWidgetVisible) ? destinationWidget.getBounds() : new Rectangle(1,1)); + NewMenuEntry destinationMenuEntry = new NewMenuEntry() + .option(destinationText) + .target("") + .identifier(1) + .type(MenuAction.CC_OP) + .param0(destinationWidget.getIndex()) + .param1(destinationWidget.getId()) + .forceLeftClick(false); + + Microbot.doInvoke(destinationMenuEntry, new Rectangle(1, 1)); return true; } return false; @@ -2372,7 +2406,7 @@ private static boolean interactWithAdventureLog(Transport transport) { if (destinationWidget == null) return false; Rs2Widget.clickWidget(destinationWidget); - log.info("Traveling to {} - ({})", transport.getDisplayInfo(), transport.getDestination()); + log.info("Traveling to {} - ({})", transport.getDisplayInfo(), transport.getDestination()); return sleepUntilTrue(() -> Rs2Player.getWorldLocation().distanceTo2D(transport.getDestination()) < OFFSET, 100, 5000); } @@ -2529,36 +2563,36 @@ private static boolean handleFairyRing(Transport transport) { * @param slotAcwRotationId The widget ID for anticlockwise rotation button * @param slotCwRotationId The widget ID for clockwise rotation button */ - private static void rotateSlotToDesiredRotation(int slotId, int currentRotation, int desiredRotation, int slotAcwRotationId, int slotCwRotationId) { - int anticlockwiseTurns = (desiredRotation - currentRotation + 2048) % 2048; - int clockwiseTurns = (currentRotation - desiredRotation + 2048) % 2048; - - int turns = Math.min(clockwiseTurns, anticlockwiseTurns) / 512; - boolean rotateCW = clockwiseTurns <= anticlockwiseTurns; - int rotationWidget = rotateCW ? slotCwRotationId : slotAcwRotationId; - - for (int i = 0; i < turns; i++) { - final int previousRotation = currentRotation; - Rs2Widget.clickWidget(rotationWidget); - - sleepUntil(() -> { - Widget slotWidget = Rs2Widget.getWidget(slotId); - return slotWidget != null && slotWidget.getRotationY() != previousRotation; - }, 2000); - - Widget slotWidget = Rs2Widget.getWidget(slotId); - if (slotWidget != null) { - currentRotation = slotWidget.getRotationY(); - } else { - break; - } - } - - sleepUntil(() -> { - Widget slotWidget = Rs2Widget.getWidget(slotId); - return slotWidget != null && slotWidget.getRotationY() == desiredRotation; - }, 3000); - } + private static void rotateSlotToDesiredRotation(int slotId, int currentRotation, int desiredRotation, int slotAcwRotationId, int slotCwRotationId) { + int anticlockwiseTurns = (desiredRotation - currentRotation + 2048) % 2048; + int clockwiseTurns = (currentRotation - desiredRotation + 2048) % 2048; + + int turns = Math.min(clockwiseTurns, anticlockwiseTurns) / 512; + boolean rotateCW = clockwiseTurns <= anticlockwiseTurns; + int rotationWidget = rotateCW ? slotCwRotationId : slotAcwRotationId; + + for (int i = 0; i < turns; i++) { + final int previousRotation = currentRotation; + Rs2Widget.clickWidget(rotationWidget); + + sleepUntil(() -> { + Widget slotWidget = Rs2Widget.getWidget(slotId); + return slotWidget != null && slotWidget.getRotationY() != previousRotation; + }, 2000); + + Widget slotWidget = Rs2Widget.getWidget(slotId); + if (slotWidget != null) { + currentRotation = slotWidget.getRotationY(); + } else { + break; + } + } + + sleepUntil(() -> { + Widget slotWidget = Rs2Widget.getWidget(slotId); + return slotWidget != null && slotWidget.getRotationY() == desiredRotation; + }, 3000); + } /** * Maps fairy ring letters to their corresponding rotation values. @@ -2598,26 +2632,26 @@ private static int getDesiredRotation(char letter) { * @param itemId The item ID to check for teleportation capabilities * @return true if the item is a teleportation item, false otherwise */ - public static boolean isTeleportItem(int itemId) { - if (ShortestPathPlugin.getPathfinderConfig().getAllTransports().isEmpty()) { - ShortestPathPlugin.getPathfinderConfig().refresh(); - } + public static boolean isTeleportItem(int itemId) { + if (ShortestPathPlugin.getPathfinderConfig().getAllTransports().isEmpty()) { + ShortestPathPlugin.getPathfinderConfig().refresh(); + } - Set teleportItemIds = ShortestPathPlugin.getPathfinderConfig().getAllTransports().values() - .stream() - .flatMap(Set::stream) - .filter(t -> TransportType.isTeleport(t.getType())) - .map(Transport::getItemIdRequirements) - .flatMap(Set::stream) - .flatMap(Set::stream) - .collect(Collectors.toSet()); + Set teleportItemIds = ShortestPathPlugin.getPathfinderConfig().getAllTransports().values() + .stream() + .flatMap(Set::stream) + .filter(t -> TransportType.isTeleport(t.getType())) + .map(Transport::getItemIdRequirements) + .flatMap(Set::stream) + .flatMap(Set::stream) + .collect(Collectors.toSet()); - // Items that are not included in transports - teleportItemIds.add(ItemID.DRAMEN_STAFF); - teleportItemIds.add(ItemID.LUNAR_MOONCLAN_LIMINAL_STAFF); + // Items that are not included in transports + teleportItemIds.add(ItemID.DRAMEN_STAFF); + teleportItemIds.add(ItemID.LUNAR_MOONCLAN_LIMINAL_STAFF); - return teleportItemIds.contains(itemId); - } + return teleportItemIds.contains(itemId); + } /** @@ -2650,7 +2684,7 @@ public static int findNearestAccessibleTarget(WorldPoint startPoint, List getTransportsForDestination(WorldPoint destination try { // Store and configure pathfinder settings ShortestPathPlugin.getPathfinderConfig().setUseBankItems(useBankItems); - ShortestPathPlugin.getPathfinderConfig().refresh(destination); // Use target-based refresh - List path = getWalkPath(destination); + ShortestPathPlugin.getPathfinderConfig().refresh(); + // Run pathfinder + Pathfinder pf = new Pathfinder(ShortestPathPlugin.getPathfinderConfig(), Rs2Player.getWorldLocation(), destination); + pf.run(); + + List path = pf.getPath(); + if (path.isEmpty()) { + log.debug("Unable to find path to destination: " + destination); + return new ArrayList<>(); + } - // Get path and extract relevant transports with filtering applied + // Get transports along the path List transports = getTransportsForPath(path, 0, prefTransportType, true); // Log found transports for debugging @@ -2798,28 +2840,28 @@ public static boolean hasRequiredTransportItems(Transport transport) { Rs2Inventory.hasItem(ItemID.LUNAR_MOONCLAN_LIMINAL_STAFF) || Rs2Equipment.isWearing(ItemID.LUNAR_MOONCLAN_LIMINAL_STAFF) || Microbot.getVarbitValue(VarbitID.LUMBRIDGE_DIARY_ELITE_COMPLETE) == 1; } else if (transport.getType() == TransportType.TELEPORTATION_ITEM || - transport.getType() == TransportType.TELEPORTATION_SPELL || transport.getType() == TransportType.CANOE || - transport.getType() == TransportType.BOAT || transport.getType() == TransportType.CHARTER_SHIP || - transport.getType() == TransportType.SHIP || transport.getType() == TransportType.MINECART || - transport.getType() == TransportType.MAGIC_CARPET + transport.getType() == TransportType.TELEPORTATION_SPELL || transport.getType() == TransportType.CANOE || + transport.getType() == TransportType.BOAT || transport.getType() == TransportType.CHARTER_SHIP || + transport.getType() == TransportType.SHIP || transport.getType() == TransportType.MINECART || + transport.getType() == TransportType.MAGIC_CARPET ) { if (transport.getType() == TransportType.TELEPORTATION_SPELL && transport.getDisplayInfo() != null) { // Extract spell name from displayInfo (handle potential format "spellname:option") String spellName = transport.getDisplayInfo().contains(":") - ? transport.getDisplayInfo().split(":")[0].trim() - : transport.getDisplayInfo().trim(); + ? transport.getDisplayInfo().split(":")[0].trim() + : transport.getDisplayInfo().trim(); // Find matching Rs2Spells enum by name (case-insensitive partial match) boolean hasMultipleDestination = transport.getDisplayInfo().contains(":"); String displayInfo = hasMultipleDestination - ? transport.getDisplayInfo().split(":")[0].trim().toLowerCase() - : transport.getDisplayInfo(); + ? transport.getDisplayInfo().split(":")[0].trim().toLowerCase() + : transport.getDisplayInfo(); log.debug("Looking for spell rune requirements for: '{}' - display info {}", spellName, displayInfo); Rs2Spells rs2Spell = Rs2Magic.getRs2Spell(displayInfo); return Rs2Magic.hasRequiredRunes(rs2Spell); } if (isCurrencyBasedTransport(transport.getType()) && - (transport.getItemIdRequirements() == null || transport.getItemIdRequirements().isEmpty()) && - transport.getCurrencyName() != null && !transport.getCurrencyName().isEmpty() && transport.getCurrencyAmount() > 0) { + (transport.getItemIdRequirements() == null || transport.getItemIdRequirements().isEmpty()) && + transport.getCurrencyName() != null && !transport.getCurrencyName().isEmpty() && transport.getCurrencyAmount() > 0) { int currencyItemId = getCurrencyItemId(transport.getCurrencyName()); return Rs2Inventory.count(currencyItemId) >= transport.getCurrencyAmount(); } @@ -2881,7 +2923,7 @@ public static Map getMissingTransportItemIdsWithQuantities(Lis int currentQuantity = itemQuantityMap.getOrDefault(runeItemId, 0); itemQuantityMap.put(runeItemId, currentQuantity + requiredQuantity); log.debug("Added teleportation spell rune requirement: {} (ID: {}) x{} (bank has: {})", - runeItemId, runeItemId, requiredQuantity, bankQuantity); + runeItemId, runeItemId, requiredQuantity, bankQuantity); } } catch (Exception e) { log.debug("Could not check bank for rune " + runeItemId + ": " + e.getMessage()); @@ -2937,7 +2979,7 @@ public static Map getMissingTransportItemIdsWithQuantities(Lis // For currency-based transports, use the actual currency amount requiredQuantity = transport.getCurrencyAmount(); log.debug("Currency-based transport {} requires {} x{}", - transport.getType(), transport.getCurrencyName(), requiredQuantity); + transport.getType(), transport.getCurrencyName(), requiredQuantity); } else { // For regular items (teleportation items, fairy rings, etc.), assume 1 is needed requiredQuantity = 1; @@ -2970,13 +3012,13 @@ private static Map getSpellRuneRequirements(Transport transpor try { // Extract spell name from displayInfo (handle potential format "spellname:option") String spellName = transport.getDisplayInfo().contains(":") - ? transport.getDisplayInfo().split(":")[0].trim() - : transport.getDisplayInfo().trim(); + ? transport.getDisplayInfo().split(":")[0].trim() + : transport.getDisplayInfo().trim(); // Find matching Rs2Spells enum by name (case-insensitive partial match) boolean hasMultipleDestination = transport.getDisplayInfo().contains(":"); String displayInfo = hasMultipleDestination - ? transport.getDisplayInfo().split(":")[0].trim().toLowerCase() - : transport.getDisplayInfo(); + ? transport.getDisplayInfo().split(":")[0].trim().toLowerCase() + : transport.getDisplayInfo(); log.debug("Looking for spell rune requirements for: '{}' - display info {}", spellName, displayInfo); Rs2Spells rs2Spell = Rs2Magic.getRs2Spell(displayInfo); if (rs2Spell == null) return runeRequirements; @@ -2984,18 +3026,18 @@ private static Map getSpellRuneRequirements(Transport transpor Map requiredRunes = Rs2Magic.getRequiredRunes(rs2Spell,1,true); List elementalRunes = rs2Spell.getElementalRunes(); log.debug("Spell '{}' requires {} runes, including {} elemental runes", - spellName, requiredRunes.size(), elementalRunes.size()); + spellName, requiredRunes.size(), elementalRunes.size()); // Convert rune requirements to item IDs with quantities requiredRunes.forEach((rune, quantity) -> { - int runeItemId = rune.getItemId(); - runeRequirements.put(runeItemId, quantity); - log.debug("Spell '{}' requires {} x {} (ID: {})", - spellName, quantity, rune.name(), runeItemId); + int runeItemId = rune.getItemId(); + runeRequirements.put(runeItemId, quantity); + log.debug("Spell '{}' requires {} x {} (ID: {})", + spellName, quantity, rune.name(), runeItemId); }); } catch (Exception e) { log.warn("Error getting spell rune requirements for transport '{}': {}", - transport.getDisplayInfo(), e.getMessage()); + transport.getDisplayInfo(), e.getMessage()); } return runeRequirements; @@ -3009,10 +3051,10 @@ private static Map getSpellRuneRequirements(Transport transpor */ private static boolean isCurrencyBasedTransport(TransportType transportType) { return transportType == TransportType.BOAT || - transportType == TransportType.CHARTER_SHIP || - transportType == TransportType.SHIP || - transportType == TransportType.MINECART || - transportType == TransportType.MAGIC_CARPET; + transportType == TransportType.CHARTER_SHIP || + transportType == TransportType.SHIP || + transportType == TransportType.MINECART || + transportType == TransportType.MAGIC_CARPET; } /** @@ -3160,19 +3202,19 @@ public static TransportRouteAnalysis compareRoutes(WorldPoint startPoint,WorldPo performanceLog.append("\tResult: Direct route only (banking route unavailable)\n"); log.info(performanceLog.toString()); return new TransportRouteAnalysis(directPath, null, null, new ArrayList<>(),new ArrayList<>(), - "Direct route only (banking route unavailable)"); + "Direct route only (banking route unavailable)"); } boolean directIsFaster = directDistance <= bankingRouteDistance; String recommendation = directIsFaster ? - String.format("\tDirect route is faster (%d vs %d tiles)", directDistance, bankingRouteDistance) : - String.format("\tBanking route is faster (%d vs %d tiles)", bankingRouteDistance, directDistance); + String.format("\tDirect route is faster (%d vs %d tiles)", directDistance, bankingRouteDistance) : + String.format("\tBanking route is faster (%d vs %d tiles)", bankingRouteDistance, directDistance); performanceLog.append("\tResult:\n\t\t ").append(recommendation).append("\n"); log.info(performanceLog.toString()); return new TransportRouteAnalysis(directPath, - nearestBank, nearestBank != null ? nearestBank.getWorldPoint() : null,pathToBank,pathWithBankedItemsToTarget, recommendation); + nearestBank, nearestBank != null ? nearestBank.getWorldPoint() : null,pathToBank,pathWithBankedItemsToTarget, recommendation); } catch (Exception e) { long totalEndTime = System.nanoTime(); @@ -3245,7 +3287,7 @@ public static WalkerState walkWithBankedTransportsAndState(WorldPoint target, in Map missingItemsWithQuantities = getMissingTransportItemIdsWithQuantities(missingTransports); if(!missingTransports.isEmpty()){ log.info("\n\tFor {} transports to destination in the bank to target {} we found {} missing items", - missingTransports.size(), target, missingItemsWithQuantities.size()); + missingTransports.size(), target, missingItemsWithQuantities.size()); } // If no missing transport items, go directly if (missingItemsWithQuantities.isEmpty() && !forceBanking) { @@ -3303,7 +3345,7 @@ public static WalkerState walkWithBankedTransportsAndState(WorldPoint target, in * @return true if the banking workflow was successful, false otherwise */ private static boolean walkWithBanking(WorldPoint bankLocation, Map missingItemsWithQuantities, WorldPoint finalTarget) { - return walkWithBankingState(bankLocation, missingItemsWithQuantities, finalTarget, 10)== WalkerState.ARRIVED; + return walkWithBankingState(bankLocation, missingItemsWithQuantities, finalTarget, 10)== WalkerState.ARRIVED; } /** @@ -3322,8 +3364,8 @@ private static WalkerState walkWithBankingState(WorldPoint bankLocation, log.warn("Cannot perform banking workflow with null locations"); return WalkerState.EXIT; } - // Step 1: Walk to bank - setTarget(null); // Clear current target to avoid conflicts + // Step 1: Walk to bank + setTarget(null); // Clear current target to avoid conflicts WalkerState bankWalkResult = walkWithStateInternal(bankLocation, distance); if (bankWalkResult != WalkerState.ARRIVED) { log.warn("Failed to arrive at bank at: " + bankLocation + ", state: " + bankWalkResult); @@ -3358,7 +3400,7 @@ private static WalkerState walkWithBankingState(WorldPoint bankLocation, sleepUntil(() -> Rs2Inventory.count(itemId) >= currentCount + amountToWithdraw, 3000); } else { log.warn("Required transport item {} not found in bank (need {} but bank has less)", - itemId, amountToWithdraw); + itemId, amountToWithdraw); } } else { log.debug("Already have enough of item {}: {} (need {})", itemId, currentCount, amountNeeded); @@ -3380,7 +3422,7 @@ private static WalkerState walkWithBankingState(WorldPoint bankLocation, ShortestPathPlugin.getPathfinderConfig().refresh(finalTarget); // Step 5: Continue to final target log.debug("Banking complete, continuing to final target: " + finalTarget); - setTarget(null); // Clear current target to avoid conflicts + setTarget(null); // Clear current target to avoid conflicts return walkWithStateInternal(finalTarget, distance); } catch (Exception e) { @@ -3389,14 +3431,23 @@ private static WalkerState walkWithBankingState(WorldPoint bankLocation, } } - public static boolean closeWorldMap() { - if (!Rs2Widget.isWidgetVisible(InterfaceID.Worldmap.CLOSE)) return false; - Widget closeButton = Rs2Widget.getWidget(InterfaceID.Worldmap.CLOSE); - if (closeButton != null) { - Rectangle closeButtonBounds = closeButton.getBounds(); - NewMenuEntry closeEntry = new NewMenuEntry("Close", "", 1, MenuAction.CC_OP, -1, InterfaceID.Worldmap.CLOSE, false); - Microbot.doInvoke(closeEntry, closeButtonBounds != null && Rs2UiHelper.isRectangleWithinCanvas(closeButtonBounds) ? closeButtonBounds : Rs2UiHelper.getDefaultRectangle()); - } - return sleepUntil(() -> !Rs2Widget.isWidgetVisible(InterfaceID.Worldmap.CLOSE), 3000); - } + public static boolean closeWorldMap() { + if (!Rs2Widget.isWidgetVisible(InterfaceID.Worldmap.CLOSE)) return false; + Widget closeButton = Rs2Widget.getWidget(InterfaceID.Worldmap.CLOSE); + if (closeButton != null) { + Rectangle closeButtonBounds = closeButton.getBounds(); + NewMenuEntry closeEntry = new NewMenuEntry() + .option("Close") + .target("") + .identifier(1) + .type(MenuAction.CC_OP) + .param0(-1) + .param1(InterfaceID.Worldmap.CLOSE) + .forceLeftClick(false); + + Microbot.doInvoke(closeEntry, closeButtonBounds != null && Rs2UiHelper.isRectangleWithinCanvas(closeButtonBounds) ? closeButtonBounds : Rs2UiHelper.getDefaultRectangle()); + } + return sleepUntil(() -> !Rs2Widget.isWidgetVisible(InterfaceID.Worldmap.CLOSE), 3000); + } } + diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/widget/Rs2Widget.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/widget/Rs2Widget.java index 37a68d4f9fa..ec9adb41daf 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/widget/Rs2Widget.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/widget/Rs2Widget.java @@ -359,7 +359,15 @@ public static void clickWidgetFast(Widget widget, int param0, int identifier) { int param1 = widget.getId(); String target = ""; MenuAction menuAction = MenuAction.CC_OP; - Microbot.doInvoke(new NewMenuEntry(param0 != -1 ? param0 : widget.getType(), param1, menuAction.getId(), identifier, widget.getItemId(), target), widget.getBounds()); + Microbot.doInvoke(new NewMenuEntry() + .param0(param0 != -1 ? param0 : widget.getType()) + .param1(param1) + .opcode(menuAction.getId()) + .identifier(identifier) + .itemId(widget.getItemId()) + .target(target) + , + widget.getBounds()); } public static void clickWidgetFast(Widget widget, int param0) { From a343bed36b69d20df45324693448293b614d24a7 Mon Sep 17 00:00:00 2001 From: chsami Date: Sun, 30 Nov 2025 15:44:05 +0100 Subject: [PATCH 33/42] feat(tileObject): add operation override methods for enhanced functionality --- .../api/tileobject/models/Rs2TileObjectModel.java | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 index 3ed71d32db4..2342d5f72e6 100644 --- 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 @@ -140,6 +140,16 @@ public String getName() { 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()); From 1b46beb5a5dc9ca92e87727fcc394e4c95dacb8d Mon Sep 17 00:00:00 2001 From: chsami Date: Sun, 30 Nov 2025 15:59:08 +0100 Subject: [PATCH 34/42] feat(tileObject): add operation override methods for tile object model --- .../microbot/util/tileobject/Rs2TileObjectModel.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java index 21be674b3d7..17aff810878 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java @@ -8,7 +8,6 @@ import net.runelite.client.plugins.microbot.Microbot; 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.gameobject.Rs2GameObject; 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.walker.Rs2Walker; @@ -140,6 +139,16 @@ public String getName() { 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()); From 91f06d8105fc41cddbcc0b7deaeaac5a4d72c2e7 Mon Sep 17 00:00:00 2001 From: chsami Date: Sun, 30 Nov 2025 15:59:50 +0100 Subject: [PATCH 35/42] refactor(plugin): update log message for plugin removal --- .../plugins/microbot/externalplugins/MicrobotPluginManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0b36e40f318..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 @@ -1078,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()); } From 0fbe025460e39ac5175b8cfc6ada0f8c1bba830b Mon Sep 17 00:00:00 2001 From: chsami Date: Fri, 5 Dec 2025 06:32:18 +0100 Subject: [PATCH 36/42] feat: reachable check added to every entity --- runelite-client/pom.xml | 5 + .../client/plugins/microbot/Microbot.java | 1 + .../microbot/api/AbstractEntityQueryable.java | 54 +++++++ .../client/plugins/microbot/api/IEntity.java | 4 + .../microbot/api/IEntityQueryable.java | 66 ++++---- .../api/player/models/Rs2PlayerModel.java | 7 + .../tileobject/models/Rs2TileObjectModel.java | 26 ++- .../microbot/example/ExampleScript.java | 30 ++-- .../example/ExampleScriptOverlay.java | 72 ++++++++- .../microbot/util/reachable/Rs2Reachable.java | 153 ++++++++++++++++++ .../util/tileobject/Rs2TileObjectApi.java | 102 ------------ .../util/tileobject/Rs2TileObjectModel.java | 2 - 12 files changed, 345 insertions(+), 177 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/reachable/Rs2Reachable.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectApi.java diff --git a/runelite-client/pom.xml b/runelite-client/pom.xml index f4904aa3fd3..e4b7e7c181b 100644 --- a/runelite-client/pom.xml +++ b/runelite-client/pom.xml @@ -53,6 +53,11 @@ + + it.unimi.dsi + fastutil + 8.5.18 + org.json json diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java index f2d3991c2a0..fd041e64cdd 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/Microbot.java @@ -1,6 +1,7 @@ package net.runelite.client.plugins.microbot; import com.google.inject.Injector; +import it.unimi.dsi.fastutil.ints.IntSet; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java index 047d9e895a2..bfaa1a89d04 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java @@ -129,11 +129,28 @@ public E first() { return source.findFirst().orElse(null); } + @Override + public E firstReachable() { + return source.filter(x -> !x.isReachable()).findFirst().orElse(null); + } + @Override public E nearest() { return nearest(Integer.MAX_VALUE); } + @Override + public E nearestReachable() { + source = source.filter(x -> !x.isReachable()); + return nearest(Integer.MAX_VALUE); + } + + @Override + public E nearestReachable(int maxDistance) { + source = source.filter(x -> !x.isReachable()); + return nearest(maxDistance); + } + @Override public E nearest(int maxDistance) { WorldPoint playerLoc = getPlayerLocation(); @@ -186,6 +203,43 @@ public E nearestOnClientThread(WorldPoint anchor, int 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); + } } 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 index 46a0cfe25af..95b6f67fab9 100644 --- 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 @@ -2,6 +2,7 @@ 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(); @@ -10,4 +11,7 @@ public interface IEntity { LocalPoint getLocalLocation(); 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 index ed9d4ae1d38..211afc8566a 100644 --- 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 @@ -1,61 +1,57 @@ package net.runelite.client.plugins.microbot.api; import net.runelite.api.coords.WorldPoint; + import java.util.List; import java.util.function.Predicate; public interface IEntityQueryable, E extends IEntity> { Q where(Predicate predicate); + 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(); - default boolean interact() { - E entity = nearest(); - if (entity == null) return false; - return entity.click(); - } - - default boolean interact(String action) { - E entity = nearest(); - if (entity == null) return false; - return entity.click(action); - } - - default boolean interact(String action, int maxDistance) { - E entity = nearest(maxDistance); - if (entity == null) return false; - return entity.click(action); - } - - default boolean interact(int id) { - E entity = ((Q) this).withId(id).nearest(); - if (entity == null) return false; - return entity.click(); - } - - default boolean interact(int id, String action) { - E entity = ((Q) this).withId(id).nearest(); - if (entity == null) return false; - return entity.click(action); - } - - default boolean interact(int id, String action, int maxDistance) { - E entity = ((Q) this).withId(id).nearest(maxDistance); - if (entity == null) return false; - return entity.click(action); - } + 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/player/models/Rs2PlayerModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/models/Rs2PlayerModel.java index 32decf15459..6b67c3ce58c 100644 --- 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 @@ -5,6 +5,7 @@ import net.runelite.api.HeadIcon; import net.runelite.api.Player; import net.runelite.api.PlayerComposition; +import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.api.IEntity; import net.runelite.client.plugins.microbot.util.ActorModel; import org.apache.commons.lang3.NotImplementedException; @@ -14,6 +15,12 @@ public class Rs2PlayerModel extends ActorModel implements Player, IEntity { private final Player player; + public Rs2PlayerModel() + { + super(Microbot.getClient().getLocalPlayer()); + this.player = Microbot.getClient().getLocalPlayer(); + } + public Rs2PlayerModel(final Player player) { super(player); 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 index 2342d5f72e6..4887768d08c 100644 --- 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 @@ -95,11 +95,10 @@ public int getId() { public String getName() { return Microbot.getClientThread().invoke(() -> { ObjectComposition composition = Microbot.getClient().getObjectDefinition(tileObject.getId()); - if(composition.getImpostorIds() != null) - { + if (composition.getImpostorIds() != null) { composition = composition.getImpostor(); } - if(composition == null) + if (composition == null) return null; return Rs2UiHelper.stripColTags(composition.getName()); }); @@ -153,8 +152,7 @@ public boolean isOpShown(int index) { public ObjectComposition getObjectComposition() { return Microbot.getClientThread().invoke(() -> { ObjectComposition composition = Microbot.getClient().getObjectDefinition(tileObject.getId()); - if(composition.getImpostorIds() != null) - { + if (composition.getImpostorIds() != null) { composition = composition.getImpostor(); } return composition; @@ -266,16 +264,16 @@ public boolean click(String action) { Microbot.doInvoke(new NewMenuEntry() - .param0(param0) - .param1(param1) - .opcode(menuAction.getId()) - .identifier(getId()) - .itemId(-1) - .option(action) - .target(objName) - .gameObject(tileObject) + .param0(param0) + .param1(param1) + .opcode(menuAction.getId()) + .identifier(getId()) + .itemId(-1) + .option(action) + .target(objName) + .gameObject(tileObject) , - Rs2UiHelper.getObjectClickbox(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); 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 266b92a3b41..4ff96d9d000 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,13 +1,16 @@ package net.runelite.client.plugins.microbot.example; import lombok.extern.slf4j.Slf4j; +import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.Script; 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.shortestpath.WorldPointUtil; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.api.player.Rs2PlayerCache; +import net.runelite.client.plugins.microbot.util.reachable.Rs2Reachable; import javax.inject.Inject; import java.util.concurrent.TimeUnit; @@ -37,33 +40,22 @@ public class ExampleScript extends Script { /** * Main entry point for the performance test script. */ + private static final WorldPoint HOPPER_DEPOSIT_DOWN = new WorldPoint(3748, 5672, 0); + public boolean run() { mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { try { if (!Microbot.isLoggedIn()) return; - long startTime = System.currentTimeMillis(); - var groundItems = rs2TileItemCache.query().toList(); - var objects = rs2TileObjectCache.query().toList(); - var rs2Players = rs2PlayerCache.query().toList(); - var rs2Npcs = rs2NpcCache.query().toList(); - //groundItems.get(0).click("Take"); - long endTime = System.currentTimeMillis(); - long totalTime = endTime - startTime; - System.out.println("fetched " + rs2Players.size() + " players and " + rs2Npcs.size() + " npcs."); - System.out.println("fetched " + objects.size() + " objects."); - System.out.println("fetched " + groundItems.size() + " ground items."); - System.out.println("Player location: " + Rs2Player.getWorldLocation()); - System.out.println("fetched " + groundItems.size() + " ground items."); - System.out.println("all in time: " + totalTime + " ms"); - /*var tree = rs2TileObjectCache.query().within(Rs2Player.getWorldLocation(), 20).withName("Tree"); + rs2TileObjectCache.query().withIds(26661, 26662, 26663, 26664).interact("Mine"); + + var tiles = Rs2Reachable.getReachableTiles(Rs2Player.getWorldLocation()); + + var pack = WorldPointUtil.packWorldPoint(new WorldPoint(3725, 5651, 0)); - tree.click(); + System.out.println(tiles.contains(pack)); - System.out.println(tree.getId()); - System.out.println(tree.getName()); - */ } catch (Exception ex) { log.error("Error in performance test loop", ex); 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/util/reachable/Rs2Reachable.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/reachable/Rs2Reachable.java new file mode 100644 index 00000000000..feaccbafbec --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/reachable/Rs2Reachable.java @@ -0,0 +1,153 @@ +package net.runelite.client.plugins.microbot.util.reachable; + +import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import lombok.Getter; +import net.runelite.api.Client; +import net.runelite.api.CollisionData; +import net.runelite.api.CollisionDataFlag; +import net.runelite.api.WorldView; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.shortestpath.WorldPointUtil; + +public class Rs2Reachable { + private static int lastUpdateTick = 0; + @Getter + private static IntSet reachableTiles; + + private static final int REGION_SIZE = 104; + + public static boolean isReachable(WorldPoint target) { + IntSet reachableTiles = getReachableTiles(target); + int packedPoint = WorldPointUtil.packWorldPoint( + target.getX(), + target.getY(), + target.getPlane() + ); + return reachableTiles.contains(packedPoint); + } + + public static IntSet getReachableTiles(WorldPoint start) { + Client client = Microbot.getClient(); + int tick = client.getTickCount(); + + // Reuse cache for this tick + if (reachableTiles != null && lastUpdateTick == tick) { + return reachableTiles; + } + + return Microbot.getClientThread().invoke(() -> { + Client c = Microbot.getClient(); + WorldView worldView = c.getTopLevelWorldView(); + if (worldView == null) { + reachableTiles = new IntOpenHashSet(); + lastUpdateTick = tick; + return reachableTiles; + } + + CollisionData[] collisionMaps = worldView.getCollisionMaps(); + if (collisionMaps == null) { + reachableTiles = new IntOpenHashSet(); + lastUpdateTick = tick; + return reachableTiles; + } + + int plane = worldView.getPlane(); + int[][] collisionFlags = collisionMaps[plane].getFlags(); + + boolean[][] visited = new boolean[REGION_SIZE][REGION_SIZE]; + IntArrayFIFOQueue openQueue = new IntArrayFIFOQueue(); + + int worldBaseX = worldView.getBaseX(); + int worldBaseY = worldView.getBaseY(); + + if (start == null || start.getPlane() != plane) { + reachableTiles = new IntOpenHashSet(); + lastUpdateTick = tick; + return reachableTiles; + } + + int localStartX = start.getX() - worldBaseX; + int localStartY = start.getY() - worldBaseY; + + if (localStartX < 0 || localStartY < 0 + || localStartX >= REGION_SIZE || localStartY >= REGION_SIZE) { + reachableTiles = new IntOpenHashSet(); + lastUpdateTick = tick; + return reachableTiles; + } + + int startKey = (localStartX << 16) | localStartY; + openQueue.enqueue(startKey); + visited[localStartX][localStartY] = true; + + while (!openQueue.isEmpty()) { + int tileKey = openQueue.dequeueInt(); + int localX = tileKey >> 16; + int localY = tileKey & 0xFFFF; + + int tileFlags = collisionFlags[localX][localY]; + + // South + int southY = localY - 1; + if (southY >= 0 + && (tileFlags & CollisionDataFlag.BLOCK_MOVEMENT_SOUTH) == 0 + && (collisionFlags[localX][southY] & CollisionDataFlag.BLOCK_MOVEMENT_FULL) == 0 + && !visited[localX][southY]) { + openQueue.enqueue((localX << 16) | southY); + visited[localX][southY] = true; + } + + // North + int northY = localY + 1; + if (northY < REGION_SIZE + && (tileFlags & CollisionDataFlag.BLOCK_MOVEMENT_NORTH) == 0 + && (collisionFlags[localX][northY] & CollisionDataFlag.BLOCK_MOVEMENT_FULL) == 0 + && !visited[localX][northY]) { + openQueue.enqueue((localX << 16) | northY); + visited[localX][northY] = true; + } + + // West + int westX = localX - 1; + if (westX >= 0 + && (tileFlags & CollisionDataFlag.BLOCK_MOVEMENT_WEST) == 0 + && (collisionFlags[westX][localY] & CollisionDataFlag.BLOCK_MOVEMENT_FULL) == 0 + && !visited[westX][localY]) { + openQueue.enqueue((westX << 16) | localY); + visited[westX][localY] = true; + } + + // East + int eastX = localX + 1; + if (eastX < REGION_SIZE + && (tileFlags & CollisionDataFlag.BLOCK_MOVEMENT_EAST) == 0 + && (collisionFlags[eastX][localY] & CollisionDataFlag.BLOCK_MOVEMENT_FULL) == 0 + && !visited[eastX][localY]) { + openQueue.enqueue((eastX << 16) | localY); + visited[eastX][localY] = true; + } + } + + IntSet reachablePackedPoints = new IntOpenHashSet(); + + for (int x = 0; x < REGION_SIZE; x++) { + for (int y = 0; y < REGION_SIZE; y++) { + if (visited[x][y]) { + int worldX = worldBaseX + x; + int worldY = worldBaseY + y; + reachablePackedPoints.add( + WorldPointUtil.packWorldPoint(worldX, worldY, plane) + ); + } + } + } + + reachableTiles = reachablePackedPoints; + lastUpdateTick = tick; + return reachableTiles; + }); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectApi.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectApi.java deleted file mode 100644 index be23e4a5b4c..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectApi.java +++ /dev/null @@ -1,102 +0,0 @@ -package net.runelite.client.plugins.microbot.util.tileobject; - -import net.runelite.api.GameObject; -import net.runelite.api.Player; -import net.runelite.api.Tile; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.function.Predicate; -import java.util.stream.Stream; - -/** - * API for interacting with tile objects in the game world. - */ -public class Rs2TileObjectApi { - - private static int lastUpdateObjects = 0; - private static List tileObjects = new ArrayList<>(); - - /** - * 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<>(); - - var tileValues = Microbot.getClient().getTopLevelWorldView().getScene().getTiles()[Microbot.getClient().getTopLevelWorldView().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(); - } - - /** - * Returns the nearest tile object matching the supplied predicate, or null if none match. - * Distance is based on straight-line world tile distance from the local player. - * - * @return nearest matching Rs2TileObjectModel or null - */ - public static Rs2TileObjectModel getNearest() { - return getNearest(null); - } - - /** - * Returns the nearest tile object matching the supplied predicate, or null if none match. - * Distance is based on straight-line world tile distance from the local player. - * - * @param filter predicate to test objects - * @return nearest matching Rs2TileObjectModel or null - */ - public static Rs2TileObjectModel getNearest(Predicate filter) { - Player player = Microbot.getClient().getLocalPlayer(); - if (player == null) return null; - - WorldPoint playerLoc = player.getWorldLocation(); - - Stream stream = getObjectsStream(); - if (filter != null) { - stream = stream.filter(filter); - } - - return stream - .min(Comparator.comparingInt(o -> o.getWorldLocation().distanceTo(playerLoc))) - .orElse(null); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java index 17aff810878..e3e0b1e2d49 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java @@ -271,8 +271,6 @@ public boolean click(String action) { .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) { Microbot.log("Failed to interact with object " + ex.getMessage()); From e7ae88245149631e5c7414793766c6123df700b7 Mon Sep 17 00:00:00 2001 From: chsami Date: Mon, 8 Dec 2025 03:44:32 +0100 Subject: [PATCH 37/42] feat: boat object interactions --- .../plugins/devtools/DevToolsOverlay.java | 2 +- .../plugins/microbot/MicrobotPlugin.java | 4 +- .../microbot/api/AbstractEntityQueryable.java | 9 +- .../client/plugins/microbot/api/IEntity.java | 1 + .../api/player/models/Rs2PlayerModel.java | 21 + .../api/tileitem/models/Rs2TileItemModel.java | 12 - .../api/tileobject/Rs2TileObjectCache.java | 2 +- .../tileobject/models/Rs2TileObjectModel.java | 38 +- .../microbot/example/ExampleScript.java | 24 +- .../microbot/shortestpath/SailingPanel.java | 26 +- .../plugins/microbot/util/ActorModel.java | 1 + .../microbot/util/camera/Rs2Camera.java | 21 +- .../util/gameobject/Rs2GameObject.java | 18 +- .../microbot/util/menu/NewMenuEntry.java | 2 +- .../microbot/util/player/Rs2Player.java | 8 +- .../microbot/util/sailing/Rs2Sailing.java | 707 ++++-------------- .../util/sailing/data/BoatPathFollower.java | 12 +- 17 files changed, 251 insertions(+), 657 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsOverlay.java index b4a7e7ac53b..0febf164821 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/devtools/DevToolsOverlay.java @@ -500,7 +500,7 @@ private void renderTileObject(Graphics2D graphics, TileObject tileObject, Color { if (tileObject != null) { - OverlayUtil.renderTileOverlay(graphics, tileObject, "ID: " + tileObject.getId(), color); + OverlayUtil.renderTileOverlay(graphics, tileObject, "ID: " + tileObject.getId() + " wv:" + tileObject.getWorldView().getId(), color); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java index 3a08a6aaf20..9efc230ce4f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java @@ -27,7 +27,7 @@ import net.runelite.client.plugins.microbot.util.overlay.GembagOverlay; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.reflection.Rs2Reflection; -import net.runelite.client.plugins.microbot.util.sailing.Rs2Sailing; +import net.runelite.client.plugins.microbot.api.boat.Rs2Boat; import net.runelite.client.plugins.microbot.util.shop.Rs2Shop; import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; import net.runelite.client.plugins.microbot.util.security.LoginManager; @@ -428,7 +428,7 @@ private void onChatMessage(ChatMessage event) } Microbot.getPouchScript().onChatMessage(event); Rs2Gembag.onChatMessage(event); - Rs2Sailing.handleChatMessage(event); + Rs2Boat.handleChatMessage(event); } @Subscribe diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java index bfaa1a89d04..abd2950cdc1 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java @@ -2,6 +2,7 @@ import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import java.util.Arrays; @@ -25,10 +26,6 @@ protected AbstractEntityQueryable() { protected abstract Stream initialSource(); - protected WorldPoint getPlayerLocation() { - return Rs2Player.getWorldLocation(); - } - @SuppressWarnings("unchecked") @Override public Q where(Predicate predicate) { @@ -39,7 +36,7 @@ public Q where(Predicate predicate) { @SuppressWarnings("unchecked") @Override public Q within(int distance) { - WorldPoint playerLoc = getPlayerLocation(); + WorldPoint playerLoc = new Rs2PlayerModel().getWorldLocation(); if (playerLoc == null) { this.source = Stream.empty(); return (Q) this; @@ -153,7 +150,7 @@ public E nearestReachable(int maxDistance) { @Override public E nearest(int maxDistance) { - WorldPoint playerLoc = getPlayerLocation(); + WorldPoint playerLoc = new Rs2PlayerModel().getWorldLocation(); if (playerLoc == null) { return null; } 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 index 95b6f67fab9..46e2c9c641c 100644 --- 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 @@ -1,5 +1,6 @@ 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; 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 index 6b67c3ce58c..74918bfc1fd 100644 --- 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 @@ -5,8 +5,11 @@ import net.runelite.api.HeadIcon; import net.runelite.api.Player; import net.runelite.api.PlayerComposition; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.gameval.AnimationID; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.api.IEntity; +import net.runelite.client.plugins.microbot.api.boat.Rs2Boat; import net.runelite.client.plugins.microbot.util.ActorModel; import org.apache.commons.lang3.NotImplementedException; @@ -27,6 +30,16 @@ public Rs2PlayerModel(final Player player) this.player = player; } + @Override + public WorldPoint getWorldLocation() + { + if (Rs2Boat.isOnBoat()) { + return Rs2Boat.getPlayerBoatLocation(); + } else { + return super.getWorldLocation(); + } + } + @Override public int getId() { @@ -102,4 +115,12 @@ public boolean click() { public boolean click(String action) { throw new NotImplementedException("click(String action) not implemented yet for Rs2PlayerModel - player interactions are not well-defined in the current codebase"); } + + + //TODO: We need a method that will search the name of the animation in the list of ids + // example: "salvage" as animation name and compare it against the player.getAnimation() id + +/* public boolean isSalvaging() { + return player.getAnimation() == + }*/ } 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 index 44fc06cd7ab..bf9b78808ff 100644 --- 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 @@ -177,18 +177,6 @@ public int getTotalValue() { }); } - - public boolean hasLineOfSight() { - WorldPoint worldPoint = Rs2Player.getWorldLocation(); - if (worldPoint == null) { - return false; - } - return Microbot.getClientThread().invoke((Supplier) () -> - tile.getWorldLocation() - .toWorldArea() - .hasLineOfSightTo(Microbot.getClient().getTopLevelWorldView(), worldPoint.toWorldArea())); - } - public boolean click() { return click(""); } 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 index 598d31f57da..ba96145d8d6 100644 --- 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 @@ -41,7 +41,7 @@ public static Stream getObjectsStream() { if (worldView == null) { continue; } - var tileValues = Microbot.getClient().getTopLevelWorldView().getScene().getTiles()[worldView.getPlane()]; + var tileValues = Microbot.getClient().getWorldView(worldView.getId()).getScene().getTiles()[worldView.getPlane()]; for (Tile[] tileValue : tileValues) { for (Tile tile : tileValue) { if (tile == null) continue; 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 index 4887768d08c..0c24cfb3d91 100644 --- 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 @@ -7,6 +7,7 @@ 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.Rs2Boat; 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; @@ -16,6 +17,7 @@ import org.jetbrains.annotations.Nullable; import java.awt.*; +import java.util.Objects; import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; @@ -89,7 +91,34 @@ public int getId() { @Override public @NotNull WorldPoint getWorldLocation() { - return tileObject.getWorldLocation(); + if (Rs2Boat.isOnBoat()) { + return Objects.requireNonNull(Microbot.getClientThread().invoke(() -> { + LocalPoint localPoint = LocalPoint.fromWorld( + getWorldView(), + this.tileObject.getWorldLocation() + ); + + var mainWorldProjection = + getWorldView() + .getMainWorldProjection(); + + if (mainWorldProjection == null) { + return this.tileObject.getWorldLocation(); + } + + 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 + )); + })); + } else { + return this.tileObject.getWorldLocation(); + } } public String getName() { @@ -171,12 +200,6 @@ public boolean click() { * @return true if the interaction was successful, false otherwise */ public boolean click(String action) { - if (Microbot.getClient().getLocalPlayer().getWorldLocation().distanceTo(getWorldLocation()) > 51) { - Microbot.log("Object with id " + getId() + " is not close enough to interact with. Walking to the object...."); - Rs2Walker.walkTo(getWorldLocation()); - return false; - } - try { int param0; @@ -271,6 +294,7 @@ public boolean click(String action) { .itemId(-1) .option(action) .target(objName) + .setWorldViewId(getWorldView().getId()) .gameObject(tileObject) , Rs2UiHelper.getObjectClickbox(tileObject)); 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 4ff96d9d000..bc2b1d8fd27 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,12 +1,14 @@ package net.runelite.client.plugins.microbot.example; import lombok.extern.slf4j.Slf4j; +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.Script; 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.player.Rs2Player; import net.runelite.client.plugins.microbot.api.player.Rs2PlayerCache; @@ -14,6 +16,7 @@ import javax.inject.Inject; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; /** * Performance test script for measuring GameObject composition retrieval speed. @@ -47,20 +50,25 @@ public boolean run() { try { if (!Microbot.isLoggedIn()) return; +/* + 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); + }*/ - rs2TileObjectCache.query().withIds(26661, 26662, 26663, 26664).interact("Mine"); - - var tiles = Rs2Reachable.getReachableTiles(Rs2Player.getWorldLocation()); - - var pack = WorldPointUtil.packWorldPoint(new WorldPoint(3725, 5651, 0)); - - System.out.println(tiles.contains(pack)); + rs2TileObjectCache.query().withIds(60493).nearest().click("Deploy"); } catch (Exception ex) { log.error("Error in performance test loop", ex); } - }, 0, 5000, TimeUnit.MILLISECONDS); + }, 0, 1000, TimeUnit.MILLISECONDS); return true; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/SailingPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/SailingPanel.java index ba3970d719c..e66d0d23eaf 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/SailingPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/SailingPanel.java @@ -19,7 +19,7 @@ import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.util.sailing.Rs2Sailing; +import net.runelite.client.plugins.microbot.api.boat.Rs2Boat; import net.runelite.client.plugins.microbot.util.sailing.data.BoatPathFollower; import net.runelite.client.plugins.microbot.util.sailing.data.PortLocation; import net.runelite.client.plugins.microbot.util.sailing.data.PortPaths; @@ -341,17 +341,17 @@ private void refreshTaskList() @Override protected RefreshResult doInBackground() { - boolean onBoat = Rs2Sailing.isOnBoat(); - boolean navigating = Rs2Sailing.isNavigating(); + boolean onBoat = Rs2Boat.isOnBoat(); + boolean navigating = Rs2Boat.isNavigating(); - Map activeTasks = Rs2Sailing.getPortTasksVarbits(); + Map activeTasks = Rs2Boat.getPortTasksVarbits(); java.util.List taskDataList = new ArrayList<>(); if (activeTasks != null && !activeTasks.isEmpty()) { for (Map.Entry entry : activeTasks.entrySet()) { - PortTaskData taskData = Rs2Sailing.getPortTaskData(entry.getValue()); + PortTaskData taskData = Rs2Boat.getPortTaskData(entry.getValue()); if (taskData != null) { taskDataList.add(taskData); @@ -537,7 +537,7 @@ private void startManualNavigation() return; } - if (!Rs2Sailing.isOnBoat()) + if (!Rs2Boat.isOnBoat()) { Microbot.showMessage("You must be on a boat to navigate!"); return; @@ -557,7 +557,7 @@ private void startManualNavigation() WorldPoint startLocation = start.getNavigationLocation(); // Check if player is close enough to start location - WorldPoint boatLocation = Rs2Sailing.getPlayerBoatLocation(); + WorldPoint boatLocation = Rs2Boat.getPlayerBoatLocation(); if (boatLocation == null) { Microbot.showMessage("Could not determine boat location."); @@ -604,7 +604,7 @@ private void startManualNavigation() sailingFuture = sailingExecutor.scheduleWithFixedDelay(() -> { try { - if (!Rs2Sailing.isOnBoat()) + if (!Rs2Boat.isOnBoat()) { Microbot.log("No longer on boat, stopping navigation."); stopNavigation(); @@ -630,7 +630,7 @@ private void startManualNavigation() private void startNavigation(PortTaskData task) { - if (!Rs2Sailing.isOnBoat()) + if (!Rs2Boat.isOnBoat()) { Microbot.showMessage("You must be on a boat to navigate!"); return; @@ -645,7 +645,7 @@ private void startNavigation(PortTaskData task) : path.getStart().getNavigationLocation(); // Check if player is close enough to start location - WorldPoint boatLocation = Rs2Sailing.getPlayerBoatLocation(); + WorldPoint boatLocation = Rs2Boat.getPlayerBoatLocation(); if (boatLocation == null) { Microbot.showMessage("Could not determine boat location."); @@ -692,7 +692,7 @@ private void startNavigation(PortTaskData task) sailingFuture = sailingExecutor.scheduleWithFixedDelay(() -> { try { - if (!Rs2Sailing.isOnBoat()) + if (!Rs2Boat.isOnBoat()) { Microbot.log("No longer on boat, stopping navigation."); stopNavigation(); @@ -736,9 +736,9 @@ private void stopNavigation() currentPath = null; // Stop the boat sails - if (Rs2Sailing.isOnBoat() && Rs2Sailing.isNavigating()) + if (Rs2Boat.isOnBoat() && Rs2Boat.isNavigating()) { - Rs2Sailing.unsetSails(); + Rs2Boat.unsetSails(); } Microbot.log("Sailing navigation stopped."); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/ActorModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/ActorModel.java index e5942c52182..15c46a96776 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/ActorModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/ActorModel.java @@ -8,6 +8,7 @@ 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.boat.Rs2Boat; import org.jetbrains.annotations.Nullable; import java.awt.*; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/camera/Rs2Camera.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/camera/Rs2Camera.java index cc9a6ad8a27..879a1de2cc7 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/camera/Rs2Camera.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/camera/Rs2Camera.java @@ -8,6 +8,7 @@ import net.runelite.client.config.ConfigManager; import net.runelite.client.plugins.camera.CameraPlugin; import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; import net.runelite.client.plugins.microbot.util.Global; import net.runelite.client.plugins.microbot.util.keyboard.Rs2Keyboard; import net.runelite.client.plugins.microbot.util.player.Rs2Player; @@ -20,26 +21,30 @@ public class Rs2Camera { private static final NpcTracker NPC_TRACKER = new NpcTracker(); public static int angleToTile(Actor t) { - int angle = (int) Math.toDegrees(Math.atan2(t.getWorldLocation().getY() - Microbot.getClient().getLocalPlayer().getWorldLocation().getY(), - t.getWorldLocation().getX() - Microbot.getClient().getLocalPlayer().getWorldLocation().getX())); + var playerLocation = new Rs2PlayerModel().getWorldLocation(); + int angle = (int) Math.toDegrees(Math.atan2(t.getWorldLocation().getY() - playerLocation.getY(), + t.getWorldLocation().getX() - playerLocation.getX())); return angle >= 0 ? angle : 360 + angle; } public static int angleToTile(TileObject t) { - int angle = (int) Math.toDegrees(Math.atan2(t.getWorldLocation().getY() - Microbot.getClient().getLocalPlayer().getWorldLocation().getY(), - t.getWorldLocation().getX() - Microbot.getClient().getLocalPlayer().getWorldLocation().getX())); + var playerLocation = new Rs2PlayerModel().getWorldLocation(); + int angle = (int) Math.toDegrees(Math.atan2(t.getWorldLocation().getY() - playerLocation.getY(), + t.getWorldLocation().getX() - playerLocation.getX())); return angle >= 0 ? angle : 360 + angle; } public static int angleToTile(LocalPoint localPoint) { - int angle = (int) Math.toDegrees(Math.atan2(localPoint.getY() - Microbot.getClient().getLocalPlayer().getLocalLocation().getY(), - localPoint.getX() - Microbot.getClient().getLocalPlayer().getLocalLocation().getX())); + var playerLocation = new Rs2PlayerModel().getWorldLocation(); + int angle = (int) Math.toDegrees(Math.atan2(localPoint.getY() - playerLocation.getY(), + localPoint.getX() - playerLocation.getX())); return angle >= 0 ? angle : 360 + angle; } public static int angleToTile(WorldPoint worldPoint) { - int angle = (int) Math.toDegrees(Math.atan2(worldPoint.getY() - Rs2Player.getWorldLocation().getY(), - worldPoint.getX() - Rs2Player.getWorldLocation().getX())); + var playerLocation = new Rs2PlayerModel().getWorldLocation(); + int angle = (int) Math.toDegrees(Math.atan2(worldPoint.getY() - playerLocation.getY(), + worldPoint.getX() - playerLocation.getX())); return angle >= 0 ? angle : 360 + angle; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java index 50f2888258c..e137c1288f4 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java @@ -17,7 +17,7 @@ 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.player.Rs2Player; -import net.runelite.client.plugins.microbot.util.sailing.Rs2Sailing; +import net.runelite.client.plugins.microbot.api.boat.Rs2Boat; import net.runelite.client.plugins.microbot.util.tile.Rs2Tile; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import org.apache.commons.lang3.tuple.Triple; @@ -529,13 +529,13 @@ public static List getAll(Predicate List getAll(Predicate predicate, int distance) { - Supplier s = Rs2Sailing::isOnBoat; + Supplier s = Rs2Boat::isOnBoat; var isOnBoat = Microbot.getClientThread().invoke(s); WorldPoint worldPoint; if (!isOnBoat) { worldPoint = Microbot.getClient().getLocalPlayer().getWorldLocation(); } else { - worldPoint = Rs2Sailing.getPlayerBoatLocation(); + worldPoint = Rs2Boat.getPlayerBoatLocation(); } return getAll(predicate, worldPoint, distance); } @@ -1532,7 +1532,7 @@ private static Stream getSceneObjects(Function Stream getSceneObjects(Function Stream getSceneObjects(Function List getSceneObjects(Function 51) { + if (!Rs2Boat.isOnBoat() && Microbot.getClient().getLocalPlayer().getWorldLocation().distanceTo(object.getWorldLocation()) > 51) { Microbot.log("Object with id " + object.getId() + " is not close enough to interact with. Walking to the object...."); Rs2Walker.walkTo(object.getWorldLocation()); return false; @@ -2063,4 +2063,4 @@ public static boolean hoverOverObject(TileObject object) { Microbot.getNaturalMouse().moveTo(point.getX(), point.getY()); return true; } -} \ No newline at end of file +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/menu/NewMenuEntry.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/menu/NewMenuEntry.java index d41d929f63f..880df8172ce 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/menu/NewMenuEntry.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/menu/NewMenuEntry.java @@ -137,7 +137,7 @@ public int getWorldViewId() { } @Override - public MenuEntry setWorldViewId(int worldViewId) { + public NewMenuEntry setWorldViewId(int worldViewId) { this.worldViewId = worldViewId; return this; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java index c9017900fbd..5ab1585ab9a 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java @@ -13,6 +13,7 @@ import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.WidgetInfo; import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.api.boat.Rs2Boat; import net.runelite.client.plugins.microbot.globval.enums.InterfaceTab; import net.runelite.client.plugins.microbot.util.coords.Rs2WorldPoint; import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; @@ -27,7 +28,6 @@ import net.runelite.client.plugins.microbot.util.misc.Rs2Potion; import net.runelite.client.plugins.microbot.util.misc.Rs2UiHelper; import net.runelite.client.plugins.microbot.util.npc.Rs2NpcModel; -import net.runelite.client.plugins.microbot.util.sailing.Rs2Sailing; import net.runelite.client.plugins.microbot.util.security.LoginManager; import net.runelite.client.plugins.microbot.util.tabs.Rs2Tab; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; @@ -967,7 +967,11 @@ public static WorldPoint getWorldLocation() { LocalPoint l = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), Microbot.getClient().getLocalPlayer().getWorldLocation()); return WorldPoint.fromLocalInstance(Microbot.getClient(), l); } else { - return Microbot.getClient().getLocalPlayer().getWorldLocation(); + if (Rs2Boat.isOnBoat()) { + return Rs2Boat.getPlayerBoatLocation(); + } else { + return Microbot.getClient().getLocalPlayer().getWorldLocation(); + } } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/Rs2Sailing.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/Rs2Sailing.java index 260e8e06594..564895172fc 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/Rs2Sailing.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/Rs2Sailing.java @@ -1,593 +1,138 @@ package net.runelite.client.plugins.microbot.util.sailing; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.*; -import net.runelite.api.annotations.Varbit; -import net.runelite.api.coords.LocalPoint; +import net.runelite.api.WorldEntity; import net.runelite.api.coords.WorldPoint; import net.runelite.api.events.ChatMessage; -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.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.api.boat.Rs2Boat; import net.runelite.client.plugins.microbot.util.sailing.data.BoatType; import net.runelite.client.plugins.microbot.util.sailing.data.Heading; import net.runelite.client.plugins.microbot.util.sailing.data.PortTaskData; import net.runelite.client.plugins.microbot.util.sailing.data.PortTaskVarbits; -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.Collections; -import java.util.List; import java.util.Map; -import java.util.stream.Collectors; -import static net.runelite.api.gameval.ObjectID.*; -import static net.runelite.client.plugins.microbot.Microbot.log; -import static net.runelite.client.plugins.microbot.util.Global.sleep; -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -@Slf4j -public class Rs2Sailing { - - public static void handleChatMessage(ChatMessage event) { - if (event.getType() == ChatMessageType.GAMEMESSAGE) { - String message = event.getMessage(); - if (message.contains("You feel a gust of wind")) { - Microbot.getClientThread().runOnSeperateThread(Rs2Sailing::trimSails); - } - } - } - - //temp fix for disembark plank - public static final int[] ganpgplank_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 - }; - - public static Heading currentHeading = Heading.SOUTH; - - public static BoatType getBoatType() { - if (Microbot.getVarbitPlayerValue(VarPlayerID.SAILING_SIDEPANEL_BOAT_TYPE) == 8110) { - return BoatType.RAFT; - } - return BoatType.RAFT; - } - - public static 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; - } - } - - /** - * Check if the player is currently on a boat. - * @return - */ - public static boolean isOnBoat() { - return Microbot.getVarbitValue(VarbitID.SAILING_PLAYER_IS_ON_PLAYER_BOAT) == 1; - } - - public static boolean isNavigating() { - return Microbot.getVarbitValue(VarbitID.SAILING_SIDEPANEL_PLAYER_AT_HELM) == 1; - } - - public static boolean navigate() { - if (!isOnBoat()) return false; - if (isNavigating()) return true; - Rs2GameObject.interact(getSteeringForBoatType(), "Navigate"); - sleepUntil(Rs2Sailing::isNavigating, 5000); - return isNavigating(); - } - - /** - * Get the player's location relative to the boat they are on. - * @return - */ - public static WorldPoint getPlayerBoatLocation() { - return Microbot.getClientThread().invoke(() -> { - if (!isOnBoat()) { - if (Microbot.getClient().getTopLevelWorldView().getScene().isInstance()) { - LocalPoint l = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), Microbot.getClient().getLocalPlayer().getWorldLocation()); - return WorldPoint.fromLocalInstance(Microbot.getClient(), l); - } else { - return Microbot.getClient().getLocalPlayer().getWorldLocation(); - } - } - WorldPoint playerLocation = Microbot.getClient().getLocalPlayer().getWorldLocation(); - LocalPoint localPoint = LocalPoint.fromWorld( - Microbot.getClient().getLocalPlayer().getWorldView(), - playerLocation - ); - - var mainWorldProjection = Microbot.getClient() - .getLocalPlayer() - .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 static boolean boardBoat() { - if (isOnBoat()) { - return true; - } - int[] SAILING_GANGPLANKS = { - 59831, // DISEMBARK - 59832, // EMBARK - 59833, // MOORING_DISEMBARK - 59834, // MOORING_EMBARK - 59835, // PORT_SARIM - 59836, // THE_PANDEMONIUM - 59837, // LANDS_END - 59838, // MUSA_POINT - 59839, // HOSIDIUS - 59840, // RIMMINGTON - 59841, // CATHERBY - 59842, // PORT_PISCARILIUS - 59843, // BRIMHAVEN - 59844, // ARDOUGNE - 59845, // PORT_KHAZARD - 59846, // WITCHAVEN - 59847, // ENTRANA - 59848, // CIVITAS_ILLA_FORTIS - 59849, // CORSAIR_COVE - 59850, // CAIRN_ISLE - 59851, // SUNSET_COAST - 59852, // THE_SUMMER_SHORE - 59853, // ALDARIN - 59854, // RUINS_OF_UNKAH - 59855, // VOID_KNIGHTS_OUTPOST - 59856, // PORT_ROBERTS - 59857, // RED_ROCK - 59858, // RELLEKKA - 59859, // ETCETERIA - 59860, // PORT_TYRAS - 59861, // DEEPFIN_POINT - 59862, // JATIZSO - 59863, // NEITIZNOT - 59864, // PRIFDDINAS - 59865, // PISCATORIS - 59866 // LUNAR_ISLE - }; - Rs2GameObject.interact(SAILING_GANGPLANKS, "Board"); - sleepUntil(Rs2Sailing::isOnBoat, 5000); - return isOnBoat(); - } - - /** - * This will be removed once reworked with new api, for now it works - * @return - */ - public static boolean disembarkBoat() { - if (!isOnBoat()) { - return true; - } - int[] SAILING_GANGPLANKS = { - 59831, // DISEMBARK - 59832, // EMBARK - 59833, // MOORING_DISEMBARK - 59834, // MOORING_EMBARK - 59835, // PORT_SARIM - 59836, // THE_PANDEMONIUM - 59837, // LANDS_END - 59838, // MUSA_POINT - 59839, // HOSIDIUS - 59840, // RIMMINGTON - 59841, // CATHERBY - 59842, // PORT_PISCARILIUS - 59843, // BRIMHAVEN - 59844, // ARDOUGNE - 59845, // PORT_KHAZARD - 59846, // WITCHAVEN - 59847, // ENTRANA - 59848, // CIVITAS_ILLA_FORTIS - 59849, // CORSAIR_COVE - 59850, // CAIRN_ISLE - 59851, // SUNSET_COAST - 59852, // THE_SUMMER_SHORE - 59853, // ALDARIN - 59854, // RUINS_OF_UNKAH - 59855, // VOID_KNIGHTS_OUTPOST - 59856, // PORT_ROBERTS - 59857, // RED_ROCK - 59858, // RELLEKKA - 59859, // ETCETERIA - 59860, // PORT_TYRAS - 59861, // DEEPFIN_POINT - 59862, // JATIZSO - 59863, // NEITIZNOT - 59864, // PRIFDDINAS - 59865, // PISCATORIS - 59866 // LUNAR_ISLE - }; - 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 static boolean isMovingForward() { - final int movingForward = 2; - return Microbot.getVarbitValue(VarbitID.SAILING_SIDEPANEL_BOAT_MOVE_MODE) == movingForward; - } - - public static boolean isMovingBackward() { - final int movingBackward = 3; - return Microbot.getVarbitValue(VarbitID.SAILING_SIDEPANEL_BOAT_MOVE_MODE) == movingBackward; - } - - public static boolean isStandingStill() { - final int standingStill = 0; - return Microbot.getVarbitValue(VarbitID.SAILING_SIDEPANEL_BOAT_MOVE_MODE) == standingStill; - } - - public static boolean clickSailButton() { - var widget = Rs2Widget.getWidget(InterfaceID.SailingSidepanel.FACILITIES_ROWS); - var setSailButton = widget.getDynamicChildren()[0]; - return Rs2Widget.clickWidget(setSailButton); - } - - /** - * Set the sails to full speed ahead. - */ - public static void setSails() - { - if(!isNavigating()) - { - return; - } - Rs2Tab.switchTo(InterfaceTab.COMBAT); - if (!isMovingForward()) { - clickSailButton(); - sleepUntil(Rs2Sailing::isMovingForward, 2500); - } - } - - /** - * Set the sails to neutral. - */ - public static void unsetSails() - { - if(!isNavigating()) - { - return; - } - Rs2Tab.switchTo(InterfaceTab.COMBAT); - if (!isStandingStill()) { - clickSailButton(); - sleepUntil(Rs2Sailing::isStandingStill, 2500); - } - } - - public static void sailTo(WorldPoint target) { - if (!isOnBoat()) { - var result = boardBoat(); - if (!result) { - log("Failed to board boat."); - } - return; - } - if (!isNavigating()) { - var result = navigate(); - if (!result) { - log("Failed to navigate boat."); - } - return; - } - - var direction = getDirection(target); - var heading = Heading.getHeading(direction); - setHeading(heading); - - if (!isMovingForward()) { - setSails(); - } - } - - public static int getDirection(WorldPoint target) { - double angle = getAngle(target); - - double rotated = 270.0 - angle; - - // Normalize [0, 360) - rotated %= 360.0; - if (rotated < 0) { - rotated += 360.0; - } - - return (int) Math.round(rotated / 22.5) & 0xF; - } - - private static double getAngle(WorldPoint target) { - WorldPoint worldPoint = Rs2Sailing.getPlayerBoatLocation(); - 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)); - } - - /** - * Set the boat's heading. - * @param heading - */ - public static 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 static boolean trimSails() { - sleep(2500, 3500); - if (!isOnBoat()) { - return false; - } - System.out.println("interacting to trim sails"); - 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); - } - - /** - * Open the cargo hold of the boat. - * @return - */ - public static 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"); - } - - /** - * Get all port task varbits with values greater than 0. - * @return - */ - public static 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)); - } - - /** - * Get the PortTaskData for a specific varbit ID. - * @param varbitValue - * @return - */ - public static PortTaskData getPortTaskData(int varbitValue) - { - if (varbitValue <= 0) { - return null; - } - - return Arrays.stream(PortTaskData.values()) - .filter(x -> x.getId() == varbitValue) - .findFirst() - .orElse(null); - } +/** + * Legacy facade for sailing helpers. Prefer calling {@link Rs2Boat} directly. + */ +@Deprecated +public class Rs2Sailing +{ + private Rs2Sailing() + { + } + + public static void handleChatMessage(ChatMessage event) + { + Rs2Boat.handleChatMessage(event); + } + + public static BoatType getBoatType() + { + return Rs2Boat.getBoatType(); + } + + public static int getSteeringForBoatType() + { + return Rs2Boat.getSteeringForBoatType(); + } + + public static WorldEntity getBoat() + { + return Rs2Boat.getBoat(); + } + + public static boolean isNavigating() + { + return Rs2Boat.isNavigating(); + } + + public static boolean navigate() + { + return Rs2Boat.navigate(); + } + + public static boolean isOnBoat() + { + return Rs2Boat.isOnBoat(); + } + + public static WorldPoint getPlayerBoatLocation() + { + return Rs2Boat.getPlayerBoatLocation(); + } + + public static boolean boardBoat() + { + return Rs2Boat.boardBoat(); + } + + public static boolean disembarkBoat() + { + return Rs2Boat.disembarkBoat(); + } + + public static boolean isMovingForward() + { + return Rs2Boat.isMovingForward(); + } + + public static boolean isMovingBackward() + { + return Rs2Boat.isMovingBackward(); + } + + public static boolean isStandingStill() + { + return Rs2Boat.isStandingStill(); + } + + public static boolean clickSailButton() + { + return Rs2Boat.clickSailButton(); + } + + public static void setSails() + { + Rs2Boat.setSails(); + } + + public static void unsetSails() + { + Rs2Boat.unsetSails(); + } + + public static void sailTo(WorldPoint target) + { + Rs2Boat.sailTo(target); + } + + public static int getDirection(WorldPoint target) + { + return Rs2Boat.getDirection(target); + } + + public static void setHeading(Heading heading) + { + Rs2Boat.setHeading(heading); + } + + public static boolean trimSails() + { + return Rs2Boat.trimSails(); + } + + public static boolean openCargo() + { + return Rs2Boat.openCargo(); + } + + public static Map getPortTasksVarbits() + { + return Rs2Boat.getPortTasksVarbits(); + } + + public static PortTaskData getPortTaskData(int varbitValue) + { + return Rs2Boat.getPortTaskData(varbitValue); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/BoatPathFollower.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/BoatPathFollower.java index f01bfeb1102..8909e253597 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/BoatPathFollower.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/BoatPathFollower.java @@ -1,11 +1,11 @@ package net.runelite.client.plugins.microbot.util.sailing.data; import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.util.sailing.Rs2Sailing; +import net.runelite.client.plugins.microbot.api.boat.Rs2Boat; import java.util.List; -import static net.runelite.client.plugins.microbot.util.sailing.Rs2Sailing.sailTo; +import static net.runelite.client.plugins.microbot.api.boat.Rs2Boat.sailTo; public class BoatPathFollower { private final List path; @@ -19,7 +19,7 @@ public BoatPathFollower(List fullPath) { } public boolean loop() { - if (!Rs2Sailing.isOnBoat()) { + if (!Rs2Boat.isOnBoat()) { return false; } @@ -29,7 +29,7 @@ public boolean loop() { return true; } - WorldPoint boat = Rs2Sailing.getPlayerBoatLocation(); + WorldPoint boat = Rs2Boat.getPlayerBoatLocation(); if (boat == null) { return false; } @@ -48,7 +48,7 @@ public boolean loop() { } private int findStartingIndex() { - WorldPoint boat = Rs2Sailing.getPlayerBoatLocation(); + WorldPoint boat = Rs2Boat.getPlayerBoatLocation(); if (boat == null) return 0; int bestIndex = 0; @@ -66,7 +66,7 @@ private int findStartingIndex() { } private void stopFollowing() { - Rs2Sailing.unsetSails(); + Rs2Boat.unsetSails(); // e.g. clear some flag, stop the script, whatever your framework uses } From 91c1a22cd8dd181babaa232a1213d6fd32e7bc96 Mon Sep 17 00:00:00 2001 From: chsami Date: Mon, 8 Dec 2025 03:45:06 +0100 Subject: [PATCH 38/42] feat: boat object interactions --- .../plugins/microbot/api/boat/Rs2Boat.java | 645 ++++++++++++++++++ .../api/boat/models/Rs2BoatModel.java | 121 ++++ 2 files changed, 766 insertions(+) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/Rs2Boat.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/models/Rs2BoatModel.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/Rs2Boat.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/Rs2Boat.java new file mode 100644 index 00000000000..1e844f4c9ee --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/Rs2Boat.java @@ -0,0 +1,645 @@ +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.coords.WorldPoint; +import net.runelite.api.events.ChatMessage; +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.boat.models.Rs2BoatModel; +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.sailing.data.BoatType; +import net.runelite.client.plugins.microbot.util.sailing.data.Heading; +import net.runelite.client.plugins.microbot.util.sailing.data.PortTaskData; +import net.runelite.client.plugins.microbot.util.sailing.data.PortTaskVarbits; +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.ObjectID.*; +import static net.runelite.client.plugins.microbot.Microbot.log; +import static net.runelite.client.plugins.microbot.util.Global.sleep; +import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; + +@Slf4j +public final class Rs2Boat +{ + private static int lastCheckedOnBoat = 0; + private static WorldEntity boat = null; + + // Temp fix for disembark plank ids + public static 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 static Heading currentHeading = Heading.SOUTH; + + private Rs2Boat() + { + } + + public static void handleChatMessage(ChatMessage event) + { + if (event.getType() == ChatMessageType.GAMEMESSAGE) + { + String message = event.getMessage(); + if (message.contains("You feel a gust of wind")) + { + Microbot.getClientThread().runOnSeperateThread(Rs2Boat::trimSails); + } + } + } + + public static BoatType getBoatType() + { + if (Microbot.getVarbitPlayerValue(VarPlayerID.SAILING_SIDEPANEL_BOAT_TYPE) == 8110) + { + return BoatType.RAFT; + } + return BoatType.RAFT; + } + + public static 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 static WorldEntity getBoat() + { + if (lastCheckedOnBoat * 2 >= Microbot.getClient().getTickCount()) { + return 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 boat; + } + + public static Rs2BoatModel getCurrentBoat() + { + WorldEntity boat = getBoat(); + + if (boat == null) + { + return null; + } + + return new Rs2BoatModel(boat); + } + + public static boolean isOnBoat() + { + return getCurrentBoat() != null; + } + + public static boolean isNavigating() + { + return Microbot.getVarbitValue(VarbitID.SAILING_SIDEPANEL_PLAYER_AT_HELM) == 1; + } + + public static boolean navigate() + { + if (!isOnBoat()) + { + return false; + } + + if (isNavigating()) + { + return true; + } + + Rs2GameObject.interact(getSteeringForBoatType(), "Navigate"); + sleepUntil(Rs2Boat::isNavigating, 5000); + return isNavigating(); + } + + public static WorldPoint getPlayerBoatLocation() + { + var playerBoat = getBoat(); + if (playerBoat == 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 static 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(Rs2Boat::isOnBoat, 5000); + return isOnBoat(); + } + + public static 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 static boolean isMovingForward() + { + final int movingForward = 2; + return Microbot.getVarbitValue(VarbitID.SAILING_SIDEPANEL_BOAT_MOVE_MODE) == movingForward; + } + + public static boolean isMovingBackward() + { + final int movingBackward = 3; + return Microbot.getVarbitValue(VarbitID.SAILING_SIDEPANEL_BOAT_MOVE_MODE) == movingBackward; + } + + public static boolean isStandingStill() + { + final int standingStill = 0; + return Microbot.getVarbitValue(VarbitID.SAILING_SIDEPANEL_BOAT_MOVE_MODE) == standingStill; + } + + public static boolean clickSailButton() + { + var widget = Rs2Widget.getWidget(InterfaceID.SailingSidepanel.FACILITIES_ROWS); + var setSailButton = widget.getDynamicChildren()[0]; + return Rs2Widget.clickWidget(setSailButton); + } + + public static void setSails() + { + if (!isNavigating()) + { + return; + } + Rs2Tab.switchTo(InterfaceTab.COMBAT); + if (!isMovingForward()) + { + clickSailButton(); + sleepUntil(Rs2Boat::isMovingForward, 2500); + } + } + + public static void unsetSails() + { + if (!isNavigating()) + { + return; + } + Rs2Tab.switchTo(InterfaceTab.COMBAT); + if (!isStandingStill()) + { + clickSailButton(); + sleepUntil(Rs2Boat::isStandingStill, 2500); + } + } + + public static void sailTo(WorldPoint target) + { + if (!isOnBoat()) + { + var result = boardBoat(); + if (!result) + { + log("Failed to board boat."); + } + return; + } + if (!isNavigating()) + { + var result = navigate(); + if (!result) + { + log("Failed to navigate boat."); + } + return; + } + + var direction = getDirection(target); + var heading = Heading.getHeading(direction); + setHeading(heading); + + if (!isMovingForward()) + { + setSails(); + } + } + + public static 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 static double getAngle(WorldPoint target) + { + WorldPoint worldPoint = Rs2Boat.getPlayerBoatLocation(); + 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 static 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 static 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 static 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 static 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 static PortTaskData getPortTaskData(int varbitValue) + { + if (varbitValue <= 0) + { + return null; + } + + return Arrays.stream(PortTaskData.values()) + .filter(x -> x.getId() == varbitValue) + .findFirst() + .orElse(null); + } +} 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..82e91fa707b --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/models/Rs2BoatModel.java @@ -0,0 +1,121 @@ +package net.runelite.client.plugins.microbot.api.boat.models; + +import net.runelite.api.WorldEntity; +import net.runelite.api.WorldEntityConfig; +import net.runelite.api.WorldView; +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; + +public class Rs2BoatModel implements WorldEntity, IEntity +{ + protected final WorldEntity worldEntity; + + public Rs2BoatModel(WorldEntity worldEntity) + { + this.worldEntity = worldEntity; + } + + @Override + public WorldView getWorldView() + { + return Microbot.getClientThread().runOnClientThreadOptional(worldEntity::getWorldView).orElse(null); + } + + @Override + public LocalPoint getCameraFocus() + { + return Microbot.getClientThread().runOnClientThreadOptional(worldEntity::getCameraFocus).orElse(null); + } + + @Override + public LocalPoint getLocalLocation() + { + return Microbot.getClientThread().runOnClientThreadOptional(worldEntity::getLocalLocation).orElse(null); + } + + @Override + public WorldPoint getWorldLocation() + { + return Microbot.getClientThread().runOnClientThreadOptional(() -> + { + WorldView worldView = worldEntity.getWorldView(); + LocalPoint localLocation = worldEntity.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(worldEntity::getOrientation).orElse(0); + } + + @Override + public LocalPoint getTargetLocation() + { + return Microbot.getClientThread().runOnClientThreadOptional(worldEntity::getTargetLocation).orElse(null); + } + + @Override + public int getTargetOrientation() + { + return Microbot.getClientThread().runOnClientThreadOptional(worldEntity::getTargetOrientation).orElse(0); + } + + @Override + public LocalPoint transformToMainWorld(LocalPoint point) + { + return Microbot.getClientThread().runOnClientThreadOptional(() -> worldEntity.transformToMainWorld(point)).orElse(null); + } + + @Override + public boolean isHiddenForOverlap() + { + return Microbot.getClientThread().runOnClientThreadOptional(worldEntity::isHiddenForOverlap).orElse(false); + } + + @Override + public WorldEntityConfig getConfig() + { + return Microbot.getClientThread().runOnClientThreadOptional(worldEntity::getConfig).orElse(null); + } + + @Override + public int getOwnerType() + { + return Microbot.getClientThread().runOnClientThreadOptional(worldEntity::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; + } +} \ No newline at end of file From a98cb60876aba1208df0987df160c33fc6981e81 Mon Sep 17 00:00:00 2001 From: chsami Date: Sun, 14 Dec 2025 07:07:08 +0100 Subject: [PATCH 39/42] feat(boat): add BoatType enum and update related classes for sailing --- .../net/runelite/api/gameval/ObjectID1.java | 2 +- .../plugins/microbot/MicrobotPlugin.java | 3 +- .../microbot/api/AbstractEntityQueryable.java | 22 +- .../client/plugins/microbot/api/IEntity.java | 1 + .../microbot/api/IEntityQueryable.java | 2 + .../microbot/api/actor/Rs2ActorModel.java | 37 +- .../plugins/microbot/api/boat/Rs2Boat.java | 645 -------------- .../microbot/api/boat/Rs2BoatCache.java | 91 ++ .../microbot/api/boat/data/BoatType.java | 7 + .../sailing => api/boat}/data/Heading.java | 2 +- .../sailing => api/boat}/data/LedgerID.java | 2 +- .../boat}/data/PortLocation.java | 2 +- .../sailing => api/boat}/data/PortPaths.java | 2 +- .../boat}/data/PortTaskData.java | 2 +- .../boat}/data/PortTaskVarbits.java | 2 +- .../boat}/data/RelativeMove.java | 2 +- .../api/boat/models/Rs2BoatModel.java | 611 ++++++++++++- .../microbot/api/npc/models/Rs2NpcModel.java | 216 +++-- .../microbot/api/player/PlayerApiExample.java | 110 --- .../api/player/data/SalvagingAnimations.java | 80 ++ .../api/player/models/Rs2PlayerModel.java | 89 +- .../api/tileitem/Rs2TileItemCache.java | 28 +- .../api/tileitem/models/Rs2TileItemModel.java | 5 + .../api/tileobject/Rs2TileObjectCache.java | 1 + .../tileobject/models/Rs2TileObjectModel.java | 42 +- .../microbot/example/ExampleScript.java | 63 +- .../microbot/shortestpath/SailingPanel.java | 820 ------------------ .../shortestpath/SailingPathOverlay.java | 256 ------ .../shortestpath/ShortestPathPlugin.java | 19 - .../plugins/microbot/util/ActorModel.java | 7 +- .../util/gameobject/Rs2GameObject.java | 36 +- .../microbot/util/player/Rs2Player.java | 9 +- .../microbot/util/player/Rs2PlayerModel.java | 7 + .../microbot/util/sailing/Rs2Sailing.java | 138 --- .../util/sailing/data/BoatPathFollower.java | 79 -- .../microbot/util/sailing/data/BoatType.java | 7 - 36 files changed, 1119 insertions(+), 2328 deletions(-) delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/Rs2Boat.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/Rs2BoatCache.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/data/BoatType.java rename runelite-client/src/main/java/net/runelite/client/plugins/microbot/{util/sailing => api/boat}/data/Heading.java (91%) rename runelite-client/src/main/java/net/runelite/client/plugins/microbot/{util/sailing => api/boat}/data/LedgerID.java (98%) rename runelite-client/src/main/java/net/runelite/client/plugins/microbot/{util/sailing => api/boat}/data/PortLocation.java (98%) rename runelite-client/src/main/java/net/runelite/client/plugins/microbot/{util/sailing => api/boat}/data/PortPaths.java (99%) rename runelite-client/src/main/java/net/runelite/client/plugins/microbot/{util/sailing => api/boat}/data/PortTaskData.java (99%) rename runelite-client/src/main/java/net/runelite/client/plugins/microbot/{util/sailing => api/boat}/data/PortTaskVarbits.java (97%) rename runelite-client/src/main/java/net/runelite/client/plugins/microbot/{util/sailing => api/boat}/data/RelativeMove.java (92%) delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/data/SalvagingAnimations.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/SailingPanel.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/SailingPathOverlay.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/Rs2Sailing.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/BoatPathFollower.java delete mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/BoatType.java diff --git a/runelite-api/src/main/java/net/runelite/api/gameval/ObjectID1.java b/runelite-api/src/main/java/net/runelite/api/gameval/ObjectID1.java index 68253e1606f..89585b40e7c 100644 --- a/runelite-api/src/main/java/net/runelite/api/gameval/ObjectID1.java +++ b/runelite-api/src/main/java/net/runelite/api/gameval/ObjectID1.java @@ -2,7 +2,7 @@ package net.runelite.api.gameval; @SuppressWarnings("unused") -class ObjectID1 +public class ObjectID1 { /** diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java index b487d92a617..be926a9d28d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/MicrobotPlugin.java @@ -27,7 +27,7 @@ import net.runelite.client.plugins.microbot.util.overlay.GembagOverlay; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.reflection.Rs2Reflection; -import net.runelite.client.plugins.microbot.api.boat.Rs2Boat; +import net.runelite.client.plugins.microbot.api.boat.Rs2BoatCache; import net.runelite.client.plugins.microbot.util.shop.Rs2Shop; import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; import net.runelite.client.plugins.microbot.util.security.LoginManager; @@ -374,7 +374,6 @@ private void onChatMessage(ChatMessage event) } Microbot.getPouchScript().onChatMessage(event); Rs2Gembag.onChatMessage(event); - Rs2Boat.handleChatMessage(event); } @Subscribe diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java index abd2950cdc1..3c490dbc9c7 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/AbstractEntityQueryable.java @@ -1,5 +1,6 @@ package net.runelite.client.plugins.microbot.api; +import net.runelite.api.WorldView; import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; @@ -26,6 +27,21 @@ protected AbstractEntityQueryable() { 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) { @@ -150,8 +166,10 @@ public E nearestReachable(int maxDistance) { @Override public E nearest(int maxDistance) { - WorldPoint playerLoc = new Rs2PlayerModel().getWorldLocation(); - if (playerLoc == null) { + var player = new Rs2PlayerModel(); + WorldPoint playerLoc = player.getWorldLocation(); + WorldView worldView = player.getWorldView(); + if (playerLoc == null || worldView == null) { return null; } 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 index 46e2c9c641c..c7d3e62d1bc 100644 --- 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 @@ -10,6 +10,7 @@ public interface IEntity { String getName(); WorldPoint getWorldLocation(); LocalPoint getLocalLocation(); + WorldView getWorldView(); boolean click(); boolean click(String action); default boolean isReachable() { 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 index 211afc8566a..fe116ff1a68 100644 --- 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 @@ -1,11 +1,13 @@ 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(Predicate predicate); Q within(int distance); 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 index bb11972cecc..8a04bbabc1f 100644 --- 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 @@ -8,6 +8,7 @@ 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.*; @@ -23,7 +24,7 @@ public class Rs2ActorModel implements Actor @Override public WorldView getWorldView() { - return actor.getWorldView(); + return Microbot.getClientThread().invoke(actor::getWorldView); } @Override @@ -70,6 +71,10 @@ public int getHealthScale() @Override public WorldPoint getWorldLocation() { + if (getWorldView() != null && getWorldView().getId() != -1) { + return Microbot.getClientThread().invoke(this::projectActorLocationToMainWorld); + } + return actor.getWorldLocation(); } @@ -432,4 +437,34 @@ 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/Rs2Boat.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/Rs2Boat.java deleted file mode 100644 index 1e844f4c9ee..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/boat/Rs2Boat.java +++ /dev/null @@ -1,645 +0,0 @@ -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.coords.WorldPoint; -import net.runelite.api.events.ChatMessage; -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.boat.models.Rs2BoatModel; -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.sailing.data.BoatType; -import net.runelite.client.plugins.microbot.util.sailing.data.Heading; -import net.runelite.client.plugins.microbot.util.sailing.data.PortTaskData; -import net.runelite.client.plugins.microbot.util.sailing.data.PortTaskVarbits; -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.ObjectID.*; -import static net.runelite.client.plugins.microbot.Microbot.log; -import static net.runelite.client.plugins.microbot.util.Global.sleep; -import static net.runelite.client.plugins.microbot.util.Global.sleepUntil; - -@Slf4j -public final class Rs2Boat -{ - private static int lastCheckedOnBoat = 0; - private static WorldEntity boat = null; - - // Temp fix for disembark plank ids - public static 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 static Heading currentHeading = Heading.SOUTH; - - private Rs2Boat() - { - } - - public static void handleChatMessage(ChatMessage event) - { - if (event.getType() == ChatMessageType.GAMEMESSAGE) - { - String message = event.getMessage(); - if (message.contains("You feel a gust of wind")) - { - Microbot.getClientThread().runOnSeperateThread(Rs2Boat::trimSails); - } - } - } - - public static BoatType getBoatType() - { - if (Microbot.getVarbitPlayerValue(VarPlayerID.SAILING_SIDEPANEL_BOAT_TYPE) == 8110) - { - return BoatType.RAFT; - } - return BoatType.RAFT; - } - - public static 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 static WorldEntity getBoat() - { - if (lastCheckedOnBoat * 2 >= Microbot.getClient().getTickCount()) { - return 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 boat; - } - - public static Rs2BoatModel getCurrentBoat() - { - WorldEntity boat = getBoat(); - - if (boat == null) - { - return null; - } - - return new Rs2BoatModel(boat); - } - - public static boolean isOnBoat() - { - return getCurrentBoat() != null; - } - - public static boolean isNavigating() - { - return Microbot.getVarbitValue(VarbitID.SAILING_SIDEPANEL_PLAYER_AT_HELM) == 1; - } - - public static boolean navigate() - { - if (!isOnBoat()) - { - return false; - } - - if (isNavigating()) - { - return true; - } - - Rs2GameObject.interact(getSteeringForBoatType(), "Navigate"); - sleepUntil(Rs2Boat::isNavigating, 5000); - return isNavigating(); - } - - public static WorldPoint getPlayerBoatLocation() - { - var playerBoat = getBoat(); - if (playerBoat == 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 static 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(Rs2Boat::isOnBoat, 5000); - return isOnBoat(); - } - - public static 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 static boolean isMovingForward() - { - final int movingForward = 2; - return Microbot.getVarbitValue(VarbitID.SAILING_SIDEPANEL_BOAT_MOVE_MODE) == movingForward; - } - - public static boolean isMovingBackward() - { - final int movingBackward = 3; - return Microbot.getVarbitValue(VarbitID.SAILING_SIDEPANEL_BOAT_MOVE_MODE) == movingBackward; - } - - public static boolean isStandingStill() - { - final int standingStill = 0; - return Microbot.getVarbitValue(VarbitID.SAILING_SIDEPANEL_BOAT_MOVE_MODE) == standingStill; - } - - public static boolean clickSailButton() - { - var widget = Rs2Widget.getWidget(InterfaceID.SailingSidepanel.FACILITIES_ROWS); - var setSailButton = widget.getDynamicChildren()[0]; - return Rs2Widget.clickWidget(setSailButton); - } - - public static void setSails() - { - if (!isNavigating()) - { - return; - } - Rs2Tab.switchTo(InterfaceTab.COMBAT); - if (!isMovingForward()) - { - clickSailButton(); - sleepUntil(Rs2Boat::isMovingForward, 2500); - } - } - - public static void unsetSails() - { - if (!isNavigating()) - { - return; - } - Rs2Tab.switchTo(InterfaceTab.COMBAT); - if (!isStandingStill()) - { - clickSailButton(); - sleepUntil(Rs2Boat::isStandingStill, 2500); - } - } - - public static void sailTo(WorldPoint target) - { - if (!isOnBoat()) - { - var result = boardBoat(); - if (!result) - { - log("Failed to board boat."); - } - return; - } - if (!isNavigating()) - { - var result = navigate(); - if (!result) - { - log("Failed to navigate boat."); - } - return; - } - - var direction = getDirection(target); - var heading = Heading.getHeading(direction); - setHeading(heading); - - if (!isMovingForward()) - { - setSails(); - } - } - - public static 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 static double getAngle(WorldPoint target) - { - WorldPoint worldPoint = Rs2Boat.getPlayerBoatLocation(); - 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 static 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 static 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 static 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 static 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 static PortTaskData getPortTaskData(int varbitValue) - { - if (varbitValue <= 0) - { - return null; - } - - return Arrays.stream(PortTaskData.values()) - .filter(x -> x.getId() == varbitValue) - .findFirst() - .orElse(null); - } -} 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 index 82e91fa707b..0267669a4f5 100644 --- 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 @@ -1,38 +1,128 @@ package net.runelite.client.plugins.microbot.api.boat.models; -import net.runelite.api.WorldEntity; -import net.runelite.api.WorldEntityConfig; -import net.runelite.api.WorldView; +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; -public class Rs2BoatModel implements WorldEntity, IEntity -{ - protected final WorldEntity worldEntity; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; - public Rs2BoatModel(WorldEntity worldEntity) +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.worldEntity = worldEntity; + 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(worldEntity::getWorldView).orElse(null); + return Microbot.getClientThread().runOnClientThreadOptional(boat::getWorldView).orElse(null); } @Override public LocalPoint getCameraFocus() { - return Microbot.getClientThread().runOnClientThreadOptional(worldEntity::getCameraFocus).orElse(null); + return Microbot.getClientThread().runOnClientThreadOptional(boat::getCameraFocus).orElse(null); } @Override public LocalPoint getLocalLocation() { - return Microbot.getClientThread().runOnClientThreadOptional(worldEntity::getLocalLocation).orElse(null); + return Microbot.getClientThread().runOnClientThreadOptional(boat::getLocalLocation).orElse(null); } @Override @@ -40,8 +130,8 @@ public WorldPoint getWorldLocation() { return Microbot.getClientThread().runOnClientThreadOptional(() -> { - WorldView worldView = worldEntity.getWorldView(); - LocalPoint localLocation = worldEntity.getLocalLocation(); + WorldView worldView = boat.getWorldView(); + LocalPoint localLocation = boat.getLocalLocation(); if (worldView == null || localLocation == null) { @@ -55,43 +145,43 @@ public WorldPoint getWorldLocation() @Override public int getOrientation() { - return Microbot.getClientThread().runOnClientThreadOptional(worldEntity::getOrientation).orElse(0); + return Microbot.getClientThread().runOnClientThreadOptional(boat::getOrientation).orElse(0); } @Override public LocalPoint getTargetLocation() { - return Microbot.getClientThread().runOnClientThreadOptional(worldEntity::getTargetLocation).orElse(null); + return Microbot.getClientThread().runOnClientThreadOptional(boat::getTargetLocation).orElse(null); } @Override public int getTargetOrientation() { - return Microbot.getClientThread().runOnClientThreadOptional(worldEntity::getTargetOrientation).orElse(0); + return Microbot.getClientThread().runOnClientThreadOptional(boat::getTargetOrientation).orElse(0); } @Override public LocalPoint transformToMainWorld(LocalPoint point) { - return Microbot.getClientThread().runOnClientThreadOptional(() -> worldEntity.transformToMainWorld(point)).orElse(null); + return Microbot.getClientThread().runOnClientThreadOptional(() -> boat.transformToMainWorld(point)).orElse(null); } @Override public boolean isHiddenForOverlap() { - return Microbot.getClientThread().runOnClientThreadOptional(worldEntity::isHiddenForOverlap).orElse(false); + return Microbot.getClientThread().runOnClientThreadOptional(boat::isHiddenForOverlap).orElse(false); } @Override public WorldEntityConfig getConfig() { - return Microbot.getClientThread().runOnClientThreadOptional(worldEntity::getConfig).orElse(null); + return Microbot.getClientThread().runOnClientThreadOptional(boat::getConfig).orElse(null); } @Override public int getOwnerType() { - return Microbot.getClientThread().runOnClientThreadOptional(worldEntity::getOwnerType).orElse(OWNER_TYPE_NOT_PLAYER); + return Microbot.getClientThread().runOnClientThreadOptional(boat::getOwnerType).orElse(OWNER_TYPE_NOT_PLAYER); } @Override @@ -118,4 +208,485 @@ 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/models/Rs2NpcModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/npc/models/Rs2NpcModel.java index 505c0f9f75a..d3e392f0035 100644 --- 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 @@ -1,83 +1,45 @@ package net.runelite.client.plugins.microbot.api.npc.models; -import lombok.EqualsAndHashCode; import lombok.Getter; -import net.runelite.api.HeadIcon; -import net.runelite.api.NPC; -import net.runelite.api.NPCComposition; -import net.runelite.api.NpcOverrides; +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.ActorModel; -import org.apache.commons.lang3.NotImplementedException; -import org.jetbrains.annotations.Nullable; +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 -@EqualsAndHashCode(callSuper = true) // Ensure equality checks include ActorModel fields -public class Rs2NpcModel extends ActorModel implements NPC, IEntity +@Slf4j +public class Rs2NpcModel extends Rs2ActorModel implements IEntity { - private final NPC runeliteNpc; + private final NPC npc; public Rs2NpcModel(final NPC npc) { super(npc); - this.runeliteNpc = npc; + this.npc = npc; } @Override public int getId() { - return runeliteNpc.getId(); + return npc.getId(); } - @Override - public int getIndex() - { - return runeliteNpc.getIndex(); - } - - @Override - public NPCComposition getComposition() - { - return runeliteNpc.getComposition(); - } - - @Override - public @Nullable NPCComposition getTransformedComposition() - { - return runeliteNpc.getTransformedComposition(); - } - - @Override - public @Nullable NpcOverrides getModelOverrides() - { - return runeliteNpc.getModelOverrides(); - } - - @Override - public @Nullable NpcOverrides getChatheadOverrides() - { - return runeliteNpc.getChatheadOverrides(); - } - - @Override - public int @Nullable [] getOverheadArchiveIds() - { - return runeliteNpc.getOverheadArchiveIds(); - } - - @Override - public short @Nullable [] getOverheadSpriteIds() - { - return runeliteNpc.getOverheadSpriteIds(); - } - // Enhanced utility methods for cache operations /** @@ -116,7 +78,7 @@ public int getDistanceFromPlayer() { */ public boolean isWithinDistance(WorldPoint anchor, int maxDistance) { if (anchor == null) return false; - return this.getWorldLocation().distanceTo(anchor) <= maxDistance; + return getWorldLocation().distanceTo(anchor) <= maxDistance; } /** @@ -172,35 +134,163 @@ public static Predicate matches(boolean exact, String... names) { * @return */ public HeadIcon getHeadIcon() { - if (runeliteNpc == null) { + if (npc == null) { return null; } - if (runeliteNpc.getOverheadSpriteIds() == null) { + if (npc.getOverheadSpriteIds() == null) { Microbot.log("Failed to find the correct overhead prayer."); return null; } - for (int i = 0; i < runeliteNpc.getOverheadSpriteIds().length; i++) { - int overheadSpriteId = runeliteNpc.getOverheadSpriteIds()[i]; + 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(runeliteNpc.getOverheadSpriteIds()) + " but failed to find valid overhead prayer."); + 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 net.runelite.client.plugins.microbot.util.npc.Rs2Npc.interact(this); + return click(""); } @Override public boolean click(String action) { - return net.runelite.client.plugins.microbot.util.npc.Rs2Npc.interact(this, 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/PlayerApiExample.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java deleted file mode 100644 index a372e8a90c0..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/player/PlayerApiExample.java +++ /dev/null @@ -1,110 +0,0 @@ -package net.runelite.client.plugins.microbot.api.player; - -import net.runelite.client.plugins.microbot.api.player.models.Rs2PlayerModel; - -import java.util.List; -import java.util.stream.Collectors; - -/** - * Example usage of the Player API - * - * This demonstrates how to query players using the new API structure: - * - Rs2PlayerCache: Caches players for efficient querying - * - Rs2PlayerQueryable: Provides a fluent interface for filtering and querying players - */ -public class PlayerApiExample { - - public static void examples() { - // Create a new cache instance - Rs2PlayerCache cache = new Rs2PlayerCache(); - - // Example 1: Get the nearest player (excluding local player) - Rs2PlayerModel nearestPlayer = cache.query().nearest(); - - // Example 2: Get the nearest player within 10 tiles - Rs2PlayerModel nearestPlayerWithinRange = cache.query().nearest(10); - - // Example 3: Find a player by name - Rs2PlayerModel playerByName = cache.query().withName("PlayerName").nearest(); - - // Example 4: Find a player by multiple names - Rs2PlayerModel anyOfThesePlayers = cache.query().withNames("Player1", "Player2", "Player3").nearest(); - - // Example 5: Find a player by ID - Rs2PlayerModel playerById = cache.query().withId(12345).nearest(); - - // Example 6: Find a player by multiple IDs - Rs2PlayerModel playerByIds = cache.query().withIds(12345, 67890, 11111).nearest(); - - // Example 8: Find all friends - List friends = cache.query() - .where(Rs2PlayerModel::isFriend) - .toList(); - - // Example 9: Find all clan members - List clanMembers = cache.query() - .where(Rs2PlayerModel::isClanMember) - .toList(); - - // Example 10: Find all friends chat members - List fcMembers = cache.query() - .where(Rs2PlayerModel::isFriendsChatMember) - .toList(); - - // Example 11: Find players in combat (with health bar visible) - List playersInCombat = cache.query() - .where(player -> player.getHealthRatio() != -1) - .toList(); - - // Example 12: Find nearest player who is not in your clan - Rs2PlayerModel nearestNonClan = cache.query() - .where(player -> !player.isClanMember()) - .where(player -> !player.isFriend()) - .nearest(); - - // Example 13: Find players with skull (PvP) - List skulledPlayers = cache.query() - .where(player -> player.getSkullIcon() != -1) - .toList(); - - // Example 14: Find players with prayer active (overhead icon) - List playersWithPrayer = cache.query() - .where(player -> player.getOverheadIcon() != null) - .toList(); - - // Example 15: Find nearest player that is animating (doing something) - Rs2PlayerModel animatingPlayer = cache.query() - .where(player -> player.getAnimation() != -1) - .nearest(); - - // Example 16: Complex query - Find nearest low health enemy player within 5 tiles - Rs2PlayerModel target = cache.query() - .where(player -> !player.isFriend()) - .where(player -> !player.isClanMember()) - .where(player -> player.getHealthRatio() > 0) - .where(player -> player.getHealthRatio() < player.getHealthScale() / 2) - .nearest(5); - - // Example 17: Find all players on the same team (Castle Wars, etc.) - int myTeam = 1; // Example team ID - List teammates = cache.query() - .where(player -> player.getTeam() == myTeam) - .toList(); - - // Example 18: Static method to get stream directly - Rs2PlayerModel firstPlayer = Rs2PlayerCache.getPlayersStream() - .filter(player -> player.getName() != null) - .findFirst() - .orElse(null); - - // Example 19: Get all players - List allPlayersIncludingMe = Rs2PlayerCache.getPlayersStream() - .collect(Collectors.toList()); - - // Example 20: Find players by partial name match - Rs2PlayerModel playerContainingName = cache.query() - .where(player -> player.getName() != null && - player.getName().toLowerCase().contains("iron")) - .first(); - } -} 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 index 74918bfc1fd..0829cd117f9 100644 --- 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 @@ -6,15 +6,16 @@ import net.runelite.api.Player; import net.runelite.api.PlayerComposition; import net.runelite.api.coords.WorldPoint; -import net.runelite.api.gameval.AnimationID; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.api.IEntity; -import net.runelite.client.plugins.microbot.api.boat.Rs2Boat; +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; import org.apache.commons.lang3.NotImplementedException; @Getter -public class Rs2PlayerModel extends ActorModel implements Player, IEntity { +public class Rs2PlayerModel extends Rs2ActorModel implements IEntity { private final Player player; @@ -33,11 +34,7 @@ public Rs2PlayerModel(final Player player) @Override public WorldPoint getWorldLocation() { - if (Rs2Boat.isOnBoat()) { - return Rs2Boat.getPlayerBoatLocation(); - } else { - return super.getWorldLocation(); - } + return super.getWorldLocation(); } @Override @@ -46,65 +43,6 @@ public int getId() return player.getId(); } - @Override - public PlayerComposition getPlayerComposition() - { - return player.getPlayerComposition(); - } - - @Override - public Polygon[] getPolygons() - { - return player.getPolygons(); - } - - @Override - public int getTeam() - { - return player.getTeam(); - } - - @Override - public boolean isFriendsChatMember() - { - return player.isFriendsChatMember(); - } - - @Override - public boolean isFriend() - { - return player.isFriend(); - } - - @Override - public boolean isClanMember() - { - return player.isClanMember(); - } - - @Override - public HeadIcon getOverheadIcon() - { - return player.getOverheadIcon(); - } - - @Override - public int getSkullIcon() - { - return player.getSkullIcon(); - } - - @Override - public void setSkullIcon(int skullIcon) - { - player.setSkullIcon(skullIcon); - } - - @Override - public int getFootprintSize() - { - return 0; - } @Override public boolean click() { @@ -116,11 +54,14 @@ public boolean click(String action) { throw new NotImplementedException("click(String action) not implemented yet for Rs2PlayerModel - player interactions are not well-defined in the current codebase"); } - - //TODO: We need a method that will search the name of the animation in the list of ids - // example: "salvage" as animation name and compare it against the player.getAnimation() id - -/* public boolean isSalvaging() { - return player.getAnimation() == - }*/ + // 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/tileitem/Rs2TileItemCache.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/tileitem/Rs2TileItemCache.java index 120fc9359b4..4a165678c3c 100644 --- 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 @@ -45,24 +45,22 @@ public static Stream getGroundItemsStream() { List result = new ArrayList<>(); - for (var id : Microbot.getWorldViewIds()) { - WorldView worldView = Microbot.getClient().getWorldView(id); - if (worldView == null) { - continue; - } + WorldView worldView = Microbot.getClient().getTopLevelWorldView(); + if (worldView == null) { + return Stream.empty(); + } - Tile[][] tiles = worldView.getScene().getTiles()[worldView.getPlane()]; - for (Tile[] tileRow : tiles) { - for (Tile tile : tileRow) { - if (tile == 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; + List items = tile.getGroundItems(); + if (items == null || items.isEmpty()) continue; - for (TileItem item : items) { - if (item != null) { - result.add(new Rs2TileItemModel(tile, item)); - } + for (TileItem item : items) { + if (item != null) { + result.add(new Rs2TileItemModel(tile, item)); } } } 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 index bf9b78808ff..c691c6a222e 100644 --- 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 @@ -107,6 +107,11 @@ 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()); 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 index ba96145d8d6..243106d36c7 100644 --- 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 @@ -6,6 +6,7 @@ import net.runelite.api.WorldView; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel; +import net.runelite.client.plugins.microbot.example.ExampleScript; import java.util.ArrayList; import java.util.List; 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 index 0c24cfb3d91..652b8bb9e72 100644 --- 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 @@ -7,12 +7,11 @@ 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.Rs2Boat; +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 net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -91,34 +90,21 @@ public int getId() { @Override public @NotNull WorldPoint getWorldLocation() { - if (Rs2Boat.isOnBoat()) { - return Objects.requireNonNull(Microbot.getClientThread().invoke(() -> { - LocalPoint localPoint = LocalPoint.fromWorld( - getWorldView(), - this.tileObject.getWorldLocation() - ); - - var mainWorldProjection = - getWorldView() - .getMainWorldProjection(); - - if (mainWorldProjection == null) { - return this.tileObject.getWorldLocation(); - } + WorldPoint worldLocation = tileObject.getWorldLocation(); - 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 - )); - })); - } else { - return this.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() { 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 bc2b1d8fd27..c3259bc46d2 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 @@ -3,6 +3,7 @@ import lombok.extern.slf4j.Slf4j; 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.api.npc.Rs2NpcCache; @@ -10,11 +11,16 @@ 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.api.player.Rs2PlayerCache; +import net.runelite.client.plugins.microbot.util.player.Rs2PlayerModel; import net.runelite.client.plugins.microbot.util.reachable.Rs2Reachable; import javax.inject.Inject; +import java.util.ArrayList; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -62,8 +68,39 @@ public boolean run() { System.out.println(a); }*/ - rs2TileObjectCache.query().withIds(60493).nearest().click("Deploy"); + var shipwreck = rs2TileObjectCache.query().where(x -> x.getId() == ObjectID.SAILING_LARGE_SHIPWRECK).within(10).nearest(); + var inventoryCheck = Rs2Inventory.count() >= Rs2Random.between(24, 28); + if (inventoryCheck && Rs2Inventory.count("large salvage") > 0) { + // 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("large salvage") == 0, 20000); + } else if (inventoryCheck) { + dropJunk(); + } else { + var player = new Rs2PlayerModel(); + if (player.getAnimation() != -1) { + log.info("Currently salvaging, waiting..."); + sleep(5000, 10000); + return; + } + + if (shipwreck == null) { + log.info("No shipwreck found nearby"); + sleep(5000); + dropJunk(); + return; + } + + rs2TileObjectCache.query().fromWorldView().withIds(60493).nearest().click("Deploy"); + sleepUntil(() -> player.getAnimation() != -1, 5000); + + } } catch (Exception ex) { log.error("Error in performance test loop", ex); @@ -72,4 +109,28 @@ public boolean run() { 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"); + Rs2Inventory.dropAll(junkItems.toArray(new String[0])); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/SailingPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/SailingPanel.java deleted file mode 100644 index e66d0d23eaf..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/SailingPanel.java +++ /dev/null @@ -1,820 +0,0 @@ -package net.runelite.client.plugins.microbot.shortestpath; - -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.FlowLayout; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.image.BufferedImage; -import java.util.*; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import javax.swing.*; -import javax.swing.border.Border; -import javax.swing.border.TitledBorder; - -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.api.boat.Rs2Boat; -import net.runelite.client.plugins.microbot.util.sailing.data.BoatPathFollower; -import net.runelite.client.plugins.microbot.util.sailing.data.PortLocation; -import net.runelite.client.plugins.microbot.util.sailing.data.PortPaths; -import net.runelite.client.plugins.microbot.util.sailing.data.PortTaskData; -import net.runelite.client.plugins.microbot.util.sailing.data.PortTaskVarbits; -import net.runelite.client.ui.PluginPanel; -import net.runelite.client.util.ImageUtil; - -/** - * SailingPanel provides a navigation interface for sailing to port task destinations. - * It displays all currently available port tasks and allows the user to navigate - * their boat to the destination of the selected task. - */ -public class SailingPanel extends PluginPanel -{ - private static final int REFRESH_INTERVAL_MS = 2000; - private static final int START_LOCATION_TOLERANCE = 50; // tiles - - private JPanel tasksPanel; - private JLabel statusLabel; - private javax.swing.Timer refreshTimer; - private JComboBox startLocationDropdown; - private JComboBox endLocationDropdown; - private JLabel routeStatusLabel; - - // Active path following - private BoatPathFollower activePathFollower; - private ScheduledExecutorService sailingExecutor; - private ScheduledFuture sailingFuture; - private PortTaskData currentTask; - private PortPaths currentManualPath; - private boolean currentManualReverse; - - // Debug overlay - private boolean debugOverlayEnabled = false; - private List currentPath = null; - - public SailingPanel() - { - super(); - setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); - setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - - add(createInfoPanel()); - add(Box.createRigidArea(new Dimension(0, 10))); - add(createStatusPanel()); - add(Box.createRigidArea(new Dimension(0, 10))); - add(createTabbedPane()); - - // Start refresh timer - refreshTimer = new javax.swing.Timer(REFRESH_INTERVAL_MS, e -> refreshTaskList()); - refreshTimer.start(); - } - - private JPanel createInfoPanel() - { - JPanel panel = new JPanel(); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - panel.setBorder(createCenteredTitledBorder("Sailing Navigation", "sailing_icon.png")); - - JLabel infoLabel = new JLabel("

" - + "Boat Path Navigator

" - + "Navigate your boat using predefined routes. " - + "Use Manual tab to select custom routes, or " - + "Tasks tab for your active port deliveries.

" - + "Note: You must be on your boat and close to the starting location." - + "
"); - infoLabel.setHorizontalAlignment(SwingConstants.CENTER); - infoLabel.setAlignmentX(Component.CENTER_ALIGNMENT); - - - panel.add(infoLabel); - return panel; - } - - private JPanel createStatusPanel() - { - JPanel panel = new JPanel(); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - panel.setBorder(BorderFactory.createTitledBorder("Status")); - - statusLabel = new JLabel("Checking boat status..."); - statusLabel.setHorizontalAlignment(SwingConstants.CENTER); - statusLabel.setAlignmentX(Component.CENTER_ALIGNMENT); - - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); - JButton stopButton = new JButton("Stop Navigation"); - stopButton.addActionListener(e -> stopNavigation()); - buttonPanel.add(stopButton); - - // Debug overlay checkbox - JCheckBox debugCheckbox = new JCheckBox("Show Path Overlay"); - debugCheckbox.setSelected(debugOverlayEnabled); - debugCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT); - debugCheckbox.addActionListener(e -> debugOverlayEnabled = debugCheckbox.isSelected()); - - panel.add(statusLabel); - panel.add(Box.createRigidArea(new Dimension(0, 5))); - panel.add(buttonPanel); - panel.add(debugCheckbox); - - return panel; - } - - private JTabbedPane createTabbedPane() - { - JTabbedPane tabbedPane = new JTabbedPane(); - tabbedPane.setAlignmentX(Component.CENTER_ALIGNMENT); - - // Manual Navigation Tab - tabbedPane.addTab("Manual", createManualNavigationPanel()); - - // Port Tasks Tab - tabbedPane.addTab("Tasks", createTasksPanel()); - - return tabbedPane; - } - - private JPanel createManualNavigationPanel() - { - JPanel panel = new JPanel(); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - panel.setBorder(BorderFactory.createEmptyBorder(10, 5, 10, 5)); - - // Get all unique locations from PortPaths (excluding EMPTY/DEFAULT) - Set locationSet = new HashSet<>(); - for (PortPaths path : PortPaths.values()) - { - if (path != PortPaths.DEFAULT) - { - locationSet.add(path.getStart()); - locationSet.add(path.getEnd()); - } - } - - PortLocation[] locations = locationSet.stream() - .filter(loc -> loc != PortLocation.EMPTY) - .sorted(Comparator.comparing(PortLocation::getName)) - .toArray(PortLocation[]::new); - - // Custom renderer for location dropdowns - DefaultListCellRenderer locationRenderer = new DefaultListCellRenderer() - { - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) - { - super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - if (value instanceof PortLocation) - { - PortLocation loc = (PortLocation) value; - setText(loc.getName()); - } - return this; - } - }; - - // Starting location - JLabel startLabel = new JLabel("Starting Location:"); - startLabel.setAlignmentX(Component.CENTER_ALIGNMENT); - - startLocationDropdown = new JComboBox<>(locations); - startLocationDropdown.setMaximumSize(new Dimension(Integer.MAX_VALUE, 30)); - startLocationDropdown.setAlignmentX(Component.CENTER_ALIGNMENT); - startLocationDropdown.setRenderer(locationRenderer); - startLocationDropdown.addActionListener(e -> updateRouteStatus()); - - // Ending location - JLabel endLabel = new JLabel("Ending Location:"); - endLabel.setAlignmentX(Component.CENTER_ALIGNMENT); - - endLocationDropdown = new JComboBox<>(locations); - endLocationDropdown.setMaximumSize(new Dimension(Integer.MAX_VALUE, 30)); - endLocationDropdown.setAlignmentX(Component.CENTER_ALIGNMENT); - endLocationDropdown.setRenderer(locationRenderer); - endLocationDropdown.addActionListener(e -> updateRouteStatus()); - - if (locations.length > 1) - { - endLocationDropdown.setSelectedIndex(1); - } - - // Route status label - routeStatusLabel = new JLabel(" "); - routeStatusLabel.setAlignmentX(Component.CENTER_ALIGNMENT); - - updateRouteStatus(); - - // Navigate button - JButton navigateButton = new JButton("Navigate"); - navigateButton.addActionListener(e -> startManualNavigation()); - navigateButton.setAlignmentX(Component.CENTER_ALIGNMENT); - - // Add components - panel.add(startLabel); - panel.add(Box.createRigidArea(new Dimension(0, 3))); - panel.add(startLocationDropdown); - panel.add(Box.createRigidArea(new Dimension(0, 10))); - - panel.add(endLabel); - panel.add(Box.createRigidArea(new Dimension(0, 3))); - panel.add(endLocationDropdown); - panel.add(Box.createRigidArea(new Dimension(0, 10))); - - panel.add(routeStatusLabel); - panel.add(Box.createRigidArea(new Dimension(0, 10))); - - panel.add(navigateButton); - - // Add filler to push content to top - panel.add(Box.createVerticalGlue()); - - return panel; - } - - private JPanel createTasksPanel() - { - JPanel panel = new JPanel(new BorderLayout()); - panel.setBorder(BorderFactory.createEmptyBorder(10, 5, 10, 5)); - - tasksPanel = new JPanel(); - tasksPanel.setLayout(new BoxLayout(tasksPanel, BoxLayout.Y_AXIS)); - - JScrollPane scrollPane = new JScrollPane(tasksPanel); - scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); - scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - scrollPane.setBorder(null); - - // Refresh button at the bottom - JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); - JButton refreshButton = new JButton("Refresh Tasks"); - refreshButton.addActionListener(e -> refreshTaskList()); - bottomPanel.add(refreshButton); - - panel.add(scrollPane, BorderLayout.CENTER); - panel.add(bottomPanel, BorderLayout.SOUTH); - - return panel; - } - - private void updateRouteStatus() - { - // Guard against null during initialization - if (startLocationDropdown == null || endLocationDropdown == null || routeStatusLabel == null) - { - return; - } - - PortLocation start = (PortLocation) startLocationDropdown.getSelectedItem(); - PortLocation end = (PortLocation) endLocationDropdown.getSelectedItem(); - - if (start == null || end == null) - { - routeStatusLabel.setText(" "); - routeStatusLabel.setForeground(Color.GRAY); - return; - } - - if (start == end) - { - routeStatusLabel.setText("Start and end must be different"); - routeStatusLabel.setForeground(Color.RED); - return; - } - - // Find a matching path - PortPaths matchingPath = findPath(start, end); - if (matchingPath != null) - { - routeStatusLabel.setText("Route available"); - routeStatusLabel.setForeground(new Color(0, 150, 0)); - } - else - { - routeStatusLabel.setText("No direct route available"); - routeStatusLabel.setForeground(Color.RED); - } - } - - /** - * Finds a PortPath that connects the given start and end locations. - * Returns null if no path exists. - */ - private PortPaths findPath(PortLocation start, PortLocation end) - { - for (PortPaths path : PortPaths.values()) - { - if (path == PortPaths.DEFAULT) - { - continue; - } - - // Check forward direction - if (path.getStart() == start && path.getEnd() == end) - { - return path; - } - - // Check reverse direction - if (path.getStart() == end && path.getEnd() == start) - { - return path; - } - } - return null; - } - - /** - * Determines if the path needs to be reversed based on the selected start location. - */ - private boolean isPathReversed(PortPaths path, PortLocation selectedStart) - { - return path.getEnd() == selectedStart; - } - - private void refreshTaskList() - { - new SwingWorker() - { - @Override - protected RefreshResult doInBackground() - { - boolean onBoat = Rs2Boat.isOnBoat(); - boolean navigating = Rs2Boat.isNavigating(); - - Map activeTasks = Rs2Boat.getPortTasksVarbits(); - - java.util.List taskDataList = new ArrayList<>(); - if (activeTasks != null && !activeTasks.isEmpty()) - { - for (Map.Entry entry : activeTasks.entrySet()) - { - PortTaskData taskData = Rs2Boat.getPortTaskData(entry.getValue()); - if (taskData != null) - { - taskDataList.add(taskData); - } - } - } - - return new RefreshResult(onBoat, navigating, taskDataList); - } - - @Override - protected void done() - { - RefreshResult result; - try - { - result = get(); - } - catch (Exception ex) - { - ex.printStackTrace(); - return; - } - - boolean onBoat = result.onBoat; - boolean navigating = result.navigating; - java.util.List taskDataList = result.tasks; - - // Status label logic (unchanged) - if (currentTask != null && sailingFuture != null && !sailingFuture.isDone()) - { - statusLabel.setText("
Sailing to: " + currentTask.taskName + "
"); - statusLabel.setForeground(new Color(0, 150, 0)); - } - else if (currentManualPath != null && sailingFuture != null && !sailingFuture.isDone()) - { - String dest = currentManualReverse - ? currentManualPath.getStart().getName() - : currentManualPath.getEnd().getName(); - statusLabel.setText("
Sailing to: " + dest + "
"); - statusLabel.setForeground(new Color(0, 150, 0)); - } - else if (onBoat && navigating) - { - statusLabel.setText("On boat - At helm (Ready)"); - statusLabel.setForeground(new Color(0, 150, 0)); - } - else if (onBoat) - { - statusLabel.setText("On boat - Not at helm"); - statusLabel.setForeground(new Color(200, 150, 0)); - } - else - { - statusLabel.setText("Not on a boat"); - statusLabel.setForeground(Color.RED); - } - - tasksPanel.removeAll(); - - if (taskDataList.isEmpty()) - { - JLabel noTasksLabel = new JLabel( - "
No active port tasks found.

" + - "Visit a port notice board
to get delivery tasks.
" - ); - noTasksLabel.setHorizontalAlignment(SwingConstants.CENTER); - noTasksLabel.setAlignmentX(Component.CENTER_ALIGNMENT); - tasksPanel.add(Box.createVerticalGlue()); - tasksPanel.add(noTasksLabel); - tasksPanel.add(Box.createVerticalGlue()); - } - else - { - for (PortTaskData taskData : taskDataList) - { - tasksPanel.add(createTaskPanel(taskData)); - tasksPanel.add(Box.createRigidArea(new Dimension(0, 5))); - } - } - - tasksPanel.revalidate(); - tasksPanel.repaint(); - } - }.execute(); - } - - /** - * Simple DTO for background result - */ - private static final class RefreshResult - { - final boolean onBoat; - final boolean navigating; - final java.util.List tasks; - - RefreshResult(boolean onBoat, boolean navigating, java.util.List tasks) - { - this.onBoat = onBoat; - this.navigating = navigating; - this.tasks = tasks; - } - } - - private JPanel createTaskPanel(PortTaskData task) - { - JPanel panel = new JPanel(); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - panel.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createLineBorder(Color.GRAY), - BorderFactory.createEmptyBorder(8, 8, 8, 8) - )); - - JLabel nameLabel = new JLabel("" + formatTaskName(task.taskName) + ""); - nameLabel.setAlignmentX(Component.CENTER_ALIGNMENT); - - String startName = task.getDockMarkers().getStart().getName(); - String endName = task.getDockMarkers().getEnd().getName(); - String routeText = task.isReversePath() - ? String.format("%s -> %s", endName, startName) - : String.format("%s -> %s", startName, endName); - - JLabel routeLabel = new JLabel(routeText); - routeLabel.setAlignmentX(Component.CENTER_ALIGNMENT); - routeLabel.setFont(routeLabel.getFont().deriveFont(11f)); - routeLabel.setForeground(Color.GRAY); - - JButton navigateButton = new JButton("Navigate"); - navigateButton.setAlignmentX(Component.CENTER_ALIGNMENT); - navigateButton.addActionListener(e -> startNavigation(task)); - - if (currentTask != null && currentTask.equals(task) && sailingFuture != null && !sailingFuture.isDone()) - { - navigateButton.setText("Active"); - navigateButton.setEnabled(false); - panel.setBackground(new Color(200, 255, 200)); - panel.setOpaque(true); - } - - panel.add(nameLabel); - panel.add(Box.createRigidArea(new Dimension(0, 3))); - panel.add(routeLabel); - panel.add(Box.createRigidArea(new Dimension(0, 8))); - panel.add(navigateButton); - - return panel; - } - - private String formatTaskName(String taskName) - { - if (taskName == null || taskName.isEmpty()) - { - return taskName; - } - String[] words = taskName.split(" "); - StringBuilder result = new StringBuilder(); - for (String word : words) - { - if (!word.isEmpty()) - { - result.append(Character.toUpperCase(word.charAt(0))) - .append(word.substring(1).toLowerCase()) - .append(" "); - } - } - return result.toString().trim(); - } - - private void startManualNavigation() - { - PortLocation start = (PortLocation) startLocationDropdown.getSelectedItem(); - PortLocation end = (PortLocation) endLocationDropdown.getSelectedItem(); - - if (start == null || end == null) - { - Microbot.showMessage("Please select both starting and ending locations."); - return; - } - - if (start == end) - { - Microbot.showMessage("Starting and ending locations must be different."); - return; - } - - if (!Rs2Boat.isOnBoat()) - { - Microbot.showMessage("You must be on a boat to navigate!"); - return; - } - - // Find the path - PortPaths path = findPath(start, end); - if (path == null) - { - Microbot.showMessage("No route available between " + start.getName() + " and " + end.getName() + "."); - return; - } - - boolean reverse = isPathReversed(path, start); - - // Get start location based on reverse flag - WorldPoint startLocation = start.getNavigationLocation(); - - // Check if player is close enough to start location - WorldPoint boatLocation = Rs2Boat.getPlayerBoatLocation(); - if (boatLocation == null) - { - Microbot.showMessage("Could not determine boat location."); - return; - } - - int distance = boatLocation.distanceTo(startLocation); - if (distance > START_LOCATION_TOLERANCE) - { - Microbot.showMessage(String.format( - "You are too far from %s. Distance: %d tiles.", - start.getName(), distance)); - return; - } - - // Stop any existing navigation - stopNavigation(); - - currentManualPath = path; - currentManualReverse = reverse; - currentTask = null; - - // Get the full path - List fullPath = path.getFullPath(reverse); - - if (fullPath == null || fullPath.isEmpty()) - { - Microbot.showMessage("No valid path found for this route."); - currentManualPath = null; - return; - } - - Microbot.log("Starting sailing navigation from " + start.getName() + " to " + end.getName()); - Microbot.log("Path has " + fullPath.size() + " waypoints, reverse=" + reverse); - - // Store path for debug overlay - currentPath = fullPath; - - // Create the path follower - activePathFollower = new BoatPathFollower(fullPath); - - // Start the sailing loop - sailingExecutor = Executors.newSingleThreadScheduledExecutor(); - sailingFuture = sailingExecutor.scheduleWithFixedDelay(() -> { - try - { - if (!Rs2Boat.isOnBoat()) - { - Microbot.log("No longer on boat, stopping navigation."); - stopNavigation(); - return; - } - - boolean result = activePathFollower.loop(); - if (result) - { - Microbot.log("Reached destination: " + end.getName()); - sailingFuture.cancel(true); - } - } - catch (Exception e) - { - Microbot.log("Error during sailing navigation: " + e.getMessage()); - } - }, 0, 600, TimeUnit.MILLISECONDS); - - // Refresh UI to show active state - refreshTaskList(); - } - - private void startNavigation(PortTaskData task) - { - if (!Rs2Boat.isOnBoat()) - { - Microbot.showMessage("You must be on a boat to navigate!"); - return; - } - - PortPaths path = task.getDockMarkers(); - boolean reverse = task.isReversePath(); - - // Get start location based on reverse flag - WorldPoint startLocation = reverse - ? path.getEnd().getNavigationLocation() - : path.getStart().getNavigationLocation(); - - // Check if player is close enough to start location - WorldPoint boatLocation = Rs2Boat.getPlayerBoatLocation(); - if (boatLocation == null) - { - Microbot.showMessage("Could not determine boat location."); - return; - } - - int distance = boatLocation.distanceTo(startLocation); - if (distance > START_LOCATION_TOLERANCE) - { - String startName = reverse ? path.getEnd().getName() : path.getStart().getName(); - Microbot.showMessage(String.format( - "You are too far from %s. Distance: %d tiles.", - startName, distance)); - return; - } - - // Stop any existing navigation - stopNavigation(); - - currentTask = task; - currentManualPath = null; - - // Get the full path based on the reverse flag - List fullPath = path.getFullPath(reverse); - - if (fullPath == null || fullPath.isEmpty()) - { - Microbot.showMessage("No valid path found for this task."); - currentTask = null; - return; - } - - Microbot.log("Starting sailing navigation to: " + task.taskName); - Microbot.log("Path has " + fullPath.size() + " waypoints, reverse=" + reverse); - - // Store path for debug overlay - currentPath = fullPath; - - // Create the path follower - activePathFollower = new BoatPathFollower(fullPath); - - // Start the sailing loop - sailingExecutor = Executors.newSingleThreadScheduledExecutor(); - sailingFuture = sailingExecutor.scheduleWithFixedDelay(() -> { - try - { - if (!Rs2Boat.isOnBoat()) - { - Microbot.log("No longer on boat, stopping navigation."); - stopNavigation(); - return; - } - - boolean result = activePathFollower.loop(); - if (result) - { - Microbot.log("Reached destination for task: " + task.taskName); - sailingFuture.cancel(true); - } - } - catch (Exception e) - { - Microbot.log("Error during sailing navigation: " + e.getMessage()); - } - }, 0, 600, TimeUnit.MILLISECONDS); - - // Refresh UI to show active state - refreshTaskList(); - } - - private void stopNavigation() - { - if (sailingFuture != null) - { - sailingFuture.cancel(true); - sailingFuture = null; - } - - if (sailingExecutor != null) - { - sailingExecutor.shutdownNow(); - sailingExecutor = null; - } - - activePathFollower = null; - currentTask = null; - currentManualPath = null; - currentPath = null; - - // Stop the boat sails - if (Rs2Boat.isOnBoat() && Rs2Boat.isNavigating()) - { - Rs2Boat.unsetSails(); - } - - Microbot.log("Sailing navigation stopped."); - refreshTaskList(); - } - - private Border createCenteredTitledBorder(String title, String iconPath) - { - BufferedImage icon = ImageUtil.loadImageResource(ShortestPathPlugin.class, iconPath); - ImageIcon imageIcon = new ImageIcon(icon); - - JLabel titleLabel = new JLabel("" + title + "", imageIcon, JLabel.CENTER); - titleLabel.setHorizontalTextPosition(JLabel.RIGHT); - titleLabel.setVerticalTextPosition(JLabel.CENTER); - - Border emptyBorder = BorderFactory.createEmptyBorder(5, 5, 5, 5); - Border lineBorder = BorderFactory.createLineBorder(Color.GRAY); - - return BorderFactory.createCompoundBorder( - BorderFactory.createCompoundBorder( - lineBorder, - BorderFactory.createEmptyBorder(2, 2, 2, 2) - ), - new TitledBorder(emptyBorder, title, TitledBorder.CENTER, TitledBorder.TOP, null, null) - { - @Override - public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) - { - Graphics2D g2d = (Graphics2D) g.create(); - g2d.translate(x + width / 2 - titleLabel.getPreferredSize().width / 2, y); - titleLabel.setSize(titleLabel.getPreferredSize()); - titleLabel.paint(g2d); - g2d.dispose(); - } - } - ); - } - - /** - * Clean up resources when the panel is disposed. - */ - public void dispose() - { - if (refreshTimer != null) - { - refreshTimer.stop(); - refreshTimer = null; - } - stopNavigation(); - } - - /** - * Returns whether the debug overlay is enabled. - */ - public boolean isDebugOverlayEnabled() - { - return debugOverlayEnabled; - } - - /** - * Returns the current navigation path. - */ - public List getCurrentPath() - { - return currentPath; - } - - /** - * Returns the current waypoint index from the path follower. - */ - public int getCurrentWaypointIndex() - { - if (activePathFollower != null) - { - return activePathFollower.getCurrentWaypointIndex(); - } - return 0; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/SailingPathOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/SailingPathOverlay.java deleted file mode 100644 index 0baf62ea8c9..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/SailingPathOverlay.java +++ /dev/null @@ -1,256 +0,0 @@ -package net.runelite.client.plugins.microbot.shortestpath; - -import net.runelite.api.Client; -import net.runelite.api.Perspective; -import net.runelite.api.Point; -import net.runelite.api.coords.LocalPoint; -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.ui.overlay.Overlay; -import net.runelite.client.ui.overlay.OverlayLayer; -import net.runelite.client.ui.overlay.OverlayPosition; - -import javax.inject.Inject; -import java.awt.*; -import java.awt.geom.Ellipse2D; -import java.awt.geom.Line2D; -import java.util.List; - -/** - * Debug overlay that draws the sailing navigation path lines on the game world. - */ -public class SailingPathOverlay extends Overlay -{ - private final Client client; - private final ShortestPathPlugin plugin; - - private static final Color PATH_COLOR = new Color(0, 150, 255, 150); - private static final Color WAYPOINT_COLOR = new Color(255, 255, 0, 200); - private static final Color CURRENT_WAYPOINT_COLOR = new Color(0, 255, 0, 255); - private static final Color START_COLOR = new Color(0, 255, 0, 200); - private static final Color END_COLOR = new Color(255, 0, 0, 200); - private static final int WAYPOINT_RADIUS = 5; - - @Inject - public SailingPathOverlay(Client client, ShortestPathPlugin plugin) - { - this.client = client; - this.plugin = plugin; - setPosition(OverlayPosition.DYNAMIC); - setPriority(Overlay.PRIORITY_LOW); - setLayer(OverlayLayer.ABOVE_SCENE); - } - - @Override - public Dimension render(Graphics2D graphics) - { - SailingPanel sailingPanel = plugin.getSailingPanel(); - if (sailingPanel == null || !sailingPanel.isDebugOverlayEnabled()) - { - return null; - } - - List path = sailingPanel.getCurrentPath(); - if (path == null || path.isEmpty()) - { - return null; - } - - int currentWaypointIndex = sailingPanel.getCurrentWaypointIndex(); - - graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - // Draw lines between waypoints - for (int i = 1; i < path.size(); i++) - { - WorldPoint start = path.get(i - 1); - WorldPoint end = path.get(i); - - // Color based on whether we've passed this segment - Color lineColor; - if (i <= currentWaypointIndex) - { - // Already passed - dim color - lineColor = new Color(100, 100, 100, 100); - } - else if (i == currentWaypointIndex + 1) - { - // Current segment - bright color - lineColor = new Color(0, 255, 100, 200); - } - else - { - // Future segment - normal color - lineColor = PATH_COLOR; - } - - drawLine(graphics, start, end, lineColor); - } - - // Draw waypoints - for (int i = 0; i < path.size(); i++) - { - WorldPoint waypoint = path.get(i); - Color color; - int radius = WAYPOINT_RADIUS; - - if (i == 0) - { - // Start point - color = START_COLOR; - radius = 8; - } - else if (i == path.size() - 1) - { - // End point - color = END_COLOR; - radius = 8; - } - else if (i == currentWaypointIndex) - { - // Current target waypoint - color = CURRENT_WAYPOINT_COLOR; - radius = 7; - } - else if (i < currentWaypointIndex) - { - // Already passed - color = new Color(100, 100, 100, 150); - radius = 4; - } - else - { - // Future waypoint - color = WAYPOINT_COLOR; - } - - drawWaypoint(graphics, waypoint, color, radius, i); - } - - // Draw info text - drawInfoText(graphics, path, currentWaypointIndex); - - return null; - } - - private void drawLine(Graphics2D graphics, WorldPoint startLoc, WorldPoint endLoc, Color color) - { - LocalPoint lpStart = LocalPoint.fromWorld(client, startLoc); - LocalPoint lpEnd = LocalPoint.fromWorld(client, endLoc); - - if (lpStart == null || lpEnd == null) - { - return; - } - - final int z = client.getPlane(); - final int startHeight = Perspective.getTileHeight(client, lpStart, z); - final int endHeight = Perspective.getTileHeight(client, lpEnd, z); - - Point p1 = Perspective.localToCanvas(client, lpStart.getX(), lpStart.getY(), startHeight); - Point p2 = Perspective.localToCanvas(client, lpEnd.getX(), lpEnd.getY(), endHeight); - - if (p1 == null || p2 == null) - { - return; - } - - Line2D.Double line = new Line2D.Double(p1.getX(), p1.getY(), p2.getX(), p2.getY()); - - // Draw outline - graphics.setColor(Color.BLACK); - graphics.setStroke(new BasicStroke(5)); - graphics.draw(line); - - // Draw line - graphics.setColor(color); - graphics.setStroke(new BasicStroke(3)); - graphics.draw(line); - } - - private void drawWaypoint(Graphics2D graphics, WorldPoint location, Color color, int radius, int index) - { - LocalPoint lp = LocalPoint.fromWorld(client, location); - if (lp == null) - { - return; - } - - final int z = client.getPlane(); - final int height = Perspective.getTileHeight(client, lp, z); - - Point p = Perspective.localToCanvas(client, lp.getX(), lp.getY(), height); - if (p == null) - { - return; - } - - // Draw circle - Ellipse2D.Double circle = new Ellipse2D.Double( - p.getX() - radius, - p.getY() - radius, - radius * 2, - radius * 2 - ); - - // Outline - graphics.setColor(Color.BLACK); - graphics.setStroke(new BasicStroke(2)); - graphics.draw(circle); - - // Fill - graphics.setColor(color); - graphics.fill(circle); - - // Draw index number - String indexText = String.valueOf(index); - FontMetrics fm = graphics.getFontMetrics(); - int textWidth = fm.stringWidth(indexText); - int textHeight = fm.getHeight(); - - int textX = (int) p.getX() - textWidth / 2; - int textY = (int) p.getY() - radius - 5; - - // Text shadow - graphics.setColor(Color.BLACK); - graphics.drawString(indexText, textX + 1, textY + 1); - - // Text - graphics.setColor(Color.WHITE); - graphics.drawString(indexText, textX, textY); - } - - private void drawInfoText(Graphics2D graphics, List path, int currentWaypointIndex) - { - int x = 10; - int y = 50; - - graphics.setFont(graphics.getFont().deriveFont(Font.BOLD, 12f)); - - // Background - String[] lines = { - "Sailing Debug Overlay", - "Total Waypoints: " + path.size(), - "Current Target: " + (currentWaypointIndex < path.size() ? currentWaypointIndex : "Done"), - "Remaining: " + Math.max(0, path.size() - currentWaypointIndex) - }; - - int maxWidth = 0; - for (String line : lines) - { - maxWidth = Math.max(maxWidth, graphics.getFontMetrics().stringWidth(line)); - } - - graphics.setColor(new Color(0, 0, 0, 180)); - graphics.fillRoundRect(x - 5, y - 15, maxWidth + 15, lines.length * 18 + 10, 5, 5); - - // Text - graphics.setColor(Color.CYAN); - graphics.drawString(lines[0], x, y); - - graphics.setColor(Color.WHITE); - for (int i = 1; i < lines.length; i++) - { - graphics.drawString(lines[i], x, y + i * 18); - } - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPlugin.java index e7ffe69eae7..137fec7d94b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/shortestpath/ShortestPathPlugin.java @@ -129,9 +129,6 @@ public class ShortestPathPlugin extends Plugin implements KeyListener { @Inject private DebugOverlayPanel debugOverlayPanel; - @Inject - private SailingPathOverlay sailingPathOverlay; - @Inject private ETAOverlayPanel etaOverlayPanel; @@ -162,8 +159,6 @@ public class ShortestPathPlugin extends Plugin implements KeyListener { private ShortestPathPanel panel; private PohPanel pohPanel; @Getter - private SailingPanel sailingPanel; - @Getter @Setter public static WorldMapPoint marker; @Setter @@ -230,14 +225,6 @@ protected void startUp() { .build(); clientToolbar.addNavigation(pohNavButton); - sailingPanel = new SailingPanel(); - final BufferedImage sailingIcon = ImageUtil.loadImageResource(ShortestPathPlugin.class, "sailing_icon.png"); - sailingNavButton = NavigationButton.builder() - .tooltip("Sailing Navigation") - .icon(sailingIcon) - .priority(10) - .panel(sailingPanel) - .build(); clientToolbar.addNavigation(sailingNavButton); Rs2Walker.setConfig(config); @@ -255,7 +242,6 @@ protected void startUp() { if (config.drawDebugPanel()) { overlayManager.add(debugOverlayPanel); } - overlayManager.add(sailingPathOverlay); keyManager.registerKeyListener(this); } @@ -266,7 +252,6 @@ protected void shutDown() { overlayManager.remove(pathMapOverlay); overlayManager.remove(pathMapTooltipOverlay); overlayManager.remove(debugOverlayPanel); - overlayManager.remove(sailingPathOverlay); clientToolbar.removeNavigation(navButton); clientToolbar.removeNavigation(pohNavButton); clientToolbar.removeNavigation(sailingNavButton); @@ -279,10 +264,6 @@ protected void shutDown() { panel = null; PohPanel.instance = null; pohPanel = null; - if (sailingPanel != null) { - sailingPanel.dispose(); - } - sailingPanel = null; shortestPathScript.shutdown(); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/ActorModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/ActorModel.java index 15c46a96776..7f538df1147 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/ActorModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/ActorModel.java @@ -8,7 +8,6 @@ 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.boat.Rs2Boat; import org.jetbrains.annotations.Nullable; import java.awt.*; @@ -71,13 +70,13 @@ public int getHealthScale() @Override public WorldPoint getWorldLocation() { - return actor.getWorldLocation(); + return Microbot.getClientThread().invoke(actor::getWorldLocation); } @Override public LocalPoint getLocalLocation() { - return actor.getLocalLocation(); + return Microbot.getClientThread().invoke(actor::getLocalLocation); } @Override @@ -353,7 +352,7 @@ public Shape getConvexHull() @Override public WorldArea getWorldArea() { - return actor.getWorldArea(); + return Microbot.getClientThread().invoke(actor::getWorldArea); } @Override diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java index e137c1288f4..9d17a6e62b6 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/gameobject/Rs2GameObject.java @@ -17,7 +17,7 @@ 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.player.Rs2Player; -import net.runelite.client.plugins.microbot.api.boat.Rs2Boat; +import net.runelite.client.plugins.microbot.api.boat.Rs2BoatCache; import net.runelite.client.plugins.microbot.util.tile.Rs2Tile; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import org.apache.commons.lang3.tuple.Triple; @@ -38,6 +38,7 @@ */ @Deprecated(since = "2.1.0 - Use Rs2TileObjectQueryable instead", forRemoval = true) public class Rs2GameObject { + private Rs2BoatCache boatCache = new Rs2BoatCache(); /** * Extracts all {@link GameObject}s located on a given {@link Tile}. * @@ -529,14 +530,7 @@ public static List getAll(Predicate List getAll(Predicate predicate, int distance) { - Supplier s = Rs2Boat::isOnBoat; - var isOnBoat = Microbot.getClientThread().invoke(s); - WorldPoint worldPoint; - if (!isOnBoat) { - worldPoint = Microbot.getClient().getLocalPlayer().getWorldLocation(); - } else { - worldPoint = Rs2Boat.getPlayerBoatLocation(); - } + WorldPoint worldPoint = Microbot.getClient().getLocalPlayer().getWorldLocation(); return getAll(predicate, worldPoint, distance); } @@ -1526,25 +1520,14 @@ private static Stream getSceneObjects(Function { Player player = Microbot.getClient().getLocalPlayer(); - var worldView = player.getWorldView(); - - Scene scene; - if (worldView != null) { - scene = player.getWorldView().getScene(); - } else { - scene = Rs2Boat.isOnBoat() - ? Microbot.getClient().getTopLevelWorldView().getScene() - : player.getWorldView().getScene(); - } - - + Scene scene = player.getWorldView().getScene(); Tile[][][] tiles = scene.getTiles(); if (tiles == null) { return Triple.of(null, null, 0); } - int z = Rs2Boat.isOnBoat() ? 3 : player.getWorldView().getPlane(); + int z = player.getWorldView().getPlane(); return Triple.of(scene, tiles, z); }); @@ -1553,7 +1536,7 @@ private static Stream getSceneObjects(Function List getSceneObjects(Function 51) { + if (Microbot.getClient().getLocalPlayer().getWorldLocation().distanceTo(object.getWorldLocation()) > 51) { Microbot.log("Object with id " + object.getId() + " is not close enough to interact with. Walking to the object...."); Rs2Walker.walkTo(object.getWorldLocation()); return false; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java index 5ab1585ab9a..33400cb6cee 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2Player.java @@ -13,7 +13,7 @@ import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.WidgetInfo; import net.runelite.client.plugins.microbot.Microbot; -import net.runelite.client.plugins.microbot.api.boat.Rs2Boat; +import net.runelite.client.plugins.microbot.api.boat.Rs2BoatCache; import net.runelite.client.plugins.microbot.globval.enums.InterfaceTab; import net.runelite.client.plugins.microbot.util.coords.Rs2WorldPoint; import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; @@ -966,13 +966,8 @@ public static WorldPoint getWorldLocation() { if (Microbot.getClient().getTopLevelWorldView().getScene().isInstance()) { LocalPoint l = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), Microbot.getClient().getLocalPlayer().getWorldLocation()); return WorldPoint.fromLocalInstance(Microbot.getClient(), l); - } else { - if (Rs2Boat.isOnBoat()) { - return Rs2Boat.getPlayerBoatLocation(); - } else { - return Microbot.getClient().getLocalPlayer().getWorldLocation(); - } } + return Microbot.getClient().getLocalPlayer().getWorldLocation(); } /** diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerModel.java index 96dbd69894f..12f3d609270 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/player/Rs2PlayerModel.java @@ -5,6 +5,7 @@ import net.runelite.api.HeadIcon; import net.runelite.api.Player; import net.runelite.api.PlayerComposition; +import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.api.IEntity; import net.runelite.client.plugins.microbot.util.ActorModel; import org.apache.commons.lang3.NotImplementedException; @@ -14,6 +15,12 @@ public class Rs2PlayerModel extends ActorModel implements Player { private final Player player; + public Rs2PlayerModel() + { + super(Microbot.getClient().getLocalPlayer()); + this.player = Microbot.getClient().getLocalPlayer(); + } + public Rs2PlayerModel(final Player player) { super(player); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/Rs2Sailing.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/Rs2Sailing.java deleted file mode 100644 index 564895172fc..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/Rs2Sailing.java +++ /dev/null @@ -1,138 +0,0 @@ -package net.runelite.client.plugins.microbot.util.sailing; - -import net.runelite.api.WorldEntity; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.ChatMessage; -import net.runelite.client.plugins.microbot.api.boat.Rs2Boat; -import net.runelite.client.plugins.microbot.util.sailing.data.BoatType; -import net.runelite.client.plugins.microbot.util.sailing.data.Heading; -import net.runelite.client.plugins.microbot.util.sailing.data.PortTaskData; -import net.runelite.client.plugins.microbot.util.sailing.data.PortTaskVarbits; - -import java.util.Map; - -/** - * Legacy facade for sailing helpers. Prefer calling {@link Rs2Boat} directly. - */ -@Deprecated -public class Rs2Sailing -{ - private Rs2Sailing() - { - } - - public static void handleChatMessage(ChatMessage event) - { - Rs2Boat.handleChatMessage(event); - } - - public static BoatType getBoatType() - { - return Rs2Boat.getBoatType(); - } - - public static int getSteeringForBoatType() - { - return Rs2Boat.getSteeringForBoatType(); - } - - public static WorldEntity getBoat() - { - return Rs2Boat.getBoat(); - } - - public static boolean isNavigating() - { - return Rs2Boat.isNavigating(); - } - - public static boolean navigate() - { - return Rs2Boat.navigate(); - } - - public static boolean isOnBoat() - { - return Rs2Boat.isOnBoat(); - } - - public static WorldPoint getPlayerBoatLocation() - { - return Rs2Boat.getPlayerBoatLocation(); - } - - public static boolean boardBoat() - { - return Rs2Boat.boardBoat(); - } - - public static boolean disembarkBoat() - { - return Rs2Boat.disembarkBoat(); - } - - public static boolean isMovingForward() - { - return Rs2Boat.isMovingForward(); - } - - public static boolean isMovingBackward() - { - return Rs2Boat.isMovingBackward(); - } - - public static boolean isStandingStill() - { - return Rs2Boat.isStandingStill(); - } - - public static boolean clickSailButton() - { - return Rs2Boat.clickSailButton(); - } - - public static void setSails() - { - Rs2Boat.setSails(); - } - - public static void unsetSails() - { - Rs2Boat.unsetSails(); - } - - public static void sailTo(WorldPoint target) - { - Rs2Boat.sailTo(target); - } - - public static int getDirection(WorldPoint target) - { - return Rs2Boat.getDirection(target); - } - - public static void setHeading(Heading heading) - { - Rs2Boat.setHeading(heading); - } - - public static boolean trimSails() - { - return Rs2Boat.trimSails(); - } - - public static boolean openCargo() - { - return Rs2Boat.openCargo(); - } - - public static Map getPortTasksVarbits() - { - return Rs2Boat.getPortTasksVarbits(); - } - - public static PortTaskData getPortTaskData(int varbitValue) - { - return Rs2Boat.getPortTaskData(varbitValue); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/BoatPathFollower.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/BoatPathFollower.java deleted file mode 100644 index 8909e253597..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/BoatPathFollower.java +++ /dev/null @@ -1,79 +0,0 @@ -package net.runelite.client.plugins.microbot.util.sailing.data; - -import net.runelite.api.coords.WorldPoint; -import net.runelite.client.plugins.microbot.api.boat.Rs2Boat; - -import java.util.List; - -import static net.runelite.client.plugins.microbot.api.boat.Rs2Boat.sailTo; - -public class BoatPathFollower { - private final List path; - private int currentIndex = 0; - private static final int WAYPOINT_TOLERANCE = 2; // tiles - - public BoatPathFollower(List fullPath) { - this.path = fullPath; - // Optional: skip waypoints behind us - this.currentIndex = findStartingIndex(); - } - - public boolean loop() { - if (!Rs2Boat.isOnBoat()) { - return false; - } - - if (currentIndex >= path.size()) { - // Done, we reached the final destination - stopFollowing(); - return true; - } - - WorldPoint boat = Rs2Boat.getPlayerBoatLocation(); - if (boat == null) { - return false; - } - - WorldPoint target = path.get(currentIndex); - - // If we're close enough to this waypoint, go to the next one - if (boat.distanceTo(target) <= WAYPOINT_TOLERANCE) { - currentIndex++; - return false; - } - - // Actively sail towards the current waypoint - sailTo(target); - return false; - } - - private int findStartingIndex() { - WorldPoint boat = Rs2Boat.getPlayerBoatLocation(); - if (boat == null) return 0; - - int bestIndex = 0; - int bestDist = Integer.MAX_VALUE; - - for (int i = 0; i < path.size(); i++) { - int dist = boat.distanceTo(path.get(i)); - if (dist < bestDist) { - bestDist = dist; - bestIndex = i; - } - } - - return bestIndex; - } - - private void stopFollowing() { - Rs2Boat.unsetSails(); - // e.g. clear some flag, stop the script, whatever your framework uses - } - - /** - * Returns the current waypoint index in the path. - */ - public int getCurrentWaypointIndex() { - return currentIndex; - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/BoatType.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/BoatType.java deleted file mode 100644 index 78d8f6913d8..00000000000 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/sailing/data/BoatType.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.runelite.client.plugins.microbot.util.sailing.data; - -public enum BoatType { - RAFT, - SKIFF, - SLOOP, -} From 9beee3aa09cb2ba6dab78497fc3ef88e2c632ac2 Mon Sep 17 00:00:00 2001 From: chsami Date: Sun, 14 Dec 2025 21:59:25 +0100 Subject: [PATCH 40/42] fix(script): update shipwreck and salvage item checks for accuracy --- .../plugins/microbot/example/ExampleScript.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) 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 c3259bc46d2..9228e9208bd 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 @@ -68,10 +68,10 @@ public boolean run() { System.out.println(a); }*/ - var shipwreck = rs2TileObjectCache.query().where(x -> x.getId() == ObjectID.SAILING_LARGE_SHIPWRECK).within(10).nearest(); + var shipwreck = rs2TileObjectCache.query().where(x -> x.getName() != null && x.getName().toLowerCase().contains("shipwreck")).within(10).nearestOnClientThread(); var inventoryCheck = Rs2Inventory.count() >= Rs2Random.between(24, 28); - if (inventoryCheck && Rs2Inventory.count("large salvage") > 0) { + if (inventoryCheck && Rs2Inventory.count("salvage") > 0) { // Rs2Inventory.dropAll("large salvage"); rs2TileObjectCache.query() .fromWorldView() @@ -79,7 +79,7 @@ public boolean run() { .where(x -> x.getWorldView().getId() == new Rs2PlayerModel().getWorldView().getId()) .nearestOnClientThread() .click(); - sleepUntil(() -> Rs2Inventory.count("large salvage") == 0, 20000); + sleepUntil(() -> Rs2Inventory.count("salvage") == 0, 20000); } else if (inventoryCheck) { dropJunk(); } else { @@ -97,7 +97,7 @@ public boolean run() { return; } - rs2TileObjectCache.query().fromWorldView().withIds(60493).nearest().click("Deploy"); + rs2TileObjectCache.query().fromWorldView().where(x -> x.getName() != null && x.getName().toLowerCase().contains("salvaging hook")).nearestOnClientThread().click("Deploy"); sleepUntil(() -> player.getAnimation() != -1, 5000); } @@ -131,6 +131,12 @@ private void dropJunk() { junkItems.add("oak plank"); junkItems.add("hemp seed"); junkItems.add("flax seed"); - Rs2Inventory.dropAll(junkItems.toArray(new String[0])); + 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"); + Rs2Inventory.dropAll( junkItems.toArray(new String[0])); } } From 64d39771fa55b042e11f0b6e5ec006896c2baab5 Mon Sep 17 00:00:00 2001 From: chsami Date: Sat, 27 Dec 2025 13:56:05 +0100 Subject: [PATCH 41/42] refactor(api): simplify profile management in integration tests --- .run/Runelite.run.xml | 4 +- gradle/verification-metadata.xml | 20 + libs.versions.toml | 5 +- runelite-client/build.gradle.kts | 2 + .../client/plugins/microbot/AGENTS.md | 22 + .../client/plugins/microbot/CLAUDE.md | 2233 +++++++++++++++++ .../plugins/microbot/api/QUERYABLE_API.md | 1153 +++++++++ .../client/plugins/microbot/api/README.md | 145 ++ .../api/tileitem/Rs2TileItemCache.java | 1 - .../microbot/example/ExampleScript.java | 19 +- .../microbot/util/menu/NewMenuEntry.java | 67 + .../plugins/microbot/example/CHANGELOG.md | 149 ++ .../example/INTEGRATION_TEST_GUIDE.md | 173 ++ .../microbot/example/PROFILE_COMPARISON.md | 42 + .../client/plugins/microbot/example/README.md | 158 ++ 15 files changed, 4183 insertions(+), 10 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/AGENTS.md create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/CLAUDE.md create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/QUERYABLE_API.md create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/api/README.md create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/example/CHANGELOG.md create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/example/INTEGRATION_TEST_GUIDE.md create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/example/PROFILE_COMPARISON.md create mode 100644 runelite-client/src/test/java/net/runelite/client/plugins/microbot/example/README.md diff --git a/.run/Runelite.run.xml b/.run/Runelite.run.xml index 0b28a112a9e..355455be082 100644 --- a/.run/Runelite.run.xml +++ b/.run/Runelite.run.xml @@ -3,8 +3,8 @@